diff --git a/nsecbunker-account/pom.xml b/nsecbunker-account/pom.xml index 0ec21eb..3793d1f 100644 --- a/nsecbunker-account/pom.xml +++ b/nsecbunker-account/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-account diff --git a/nsecbunker-admin/pom.xml b/nsecbunker-admin/pom.xml index c40c51f..26ce241 100644 --- a/nsecbunker-admin/pom.xml +++ b/nsecbunker-admin/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-admin diff --git a/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManager.java b/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManager.java index 5c08217..45eb4e5 100644 --- a/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManager.java +++ b/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManager.java @@ -58,20 +58,22 @@ public DefaultKeyManager(NsecBunkerAdminClient adminClient, ObjectMapper objectM @Override public CompletableFuture createKey(String name, String nsec, String passphrase) { validateName(name); - requirePassphrase(passphrase); if (nsec == null || nsec.isBlank()) { throw new IllegalArgumentException("nsec must not be null or blank"); } - return sendForKey(METHOD_CREATE_NEW_KEY, List.of(name, passphrase, nsec), name); + // Empty passphrase is allowed - nsecbunkerd will store the key unencrypted + String normalizedPassphrase = normalizePassphrase(passphrase); + return sendForKey(METHOD_CREATE_NEW_KEY, List.of(name, normalizedPassphrase, nsec), name); } @Override public CompletableFuture createKey(String name, String passphrase) { validateName(name); - requirePassphrase(passphrase); - return sendForKey(METHOD_CREATE_NEW_KEY, List.of(name, passphrase), name); + // Empty passphrase is allowed - nsecbunkerd will store the key unencrypted + String normalizedPassphrase = normalizePassphrase(passphrase); + return sendForKey(METHOD_CREATE_NEW_KEY, List.of(name, normalizedPassphrase), name); } @Override @@ -83,9 +85,10 @@ public CompletableFuture> listKeys() { @Override public CompletableFuture unlockKey(String name, String passphrase) { validateName(name); - requirePassphrase(passphrase); - return sendForResult(METHOD_UNLOCK_KEY, List.of(name, passphrase), "unlock key " + name) + // Empty passphrase is allowed - for keys stored without encryption + String normalizedPassphrase = normalizePassphrase(passphrase); + return sendForResult(METHOD_UNLOCK_KEY, List.of(name, normalizedPassphrase), "unlock key " + name) .thenApply(ignored -> Boolean.TRUE); } @@ -108,9 +111,9 @@ public CompletableFuture getKeyDetails(String name) { public CompletableFuture rotateKey(String oldName, String newName, String passphrase) { validateName(oldName); validateName(newName); - requirePassphrase(passphrase); - return sendForKey(METHOD_ROTATE_KEY, List.of(oldName, newName, passphrase), newName); + String normalizedPassphrase = normalizePassphrase(passphrase); + return sendForKey(METHOD_ROTATE_KEY, List.of(oldName, newName, normalizedPassphrase), newName); } private CompletableFuture sendForResult(String method, List params, String description) { @@ -185,9 +188,14 @@ private void validateName(String name) { } } - private void requirePassphrase(String passphrase) { - if (passphrase == null || passphrase.isBlank()) { - throw new IllegalArgumentException("Passphrase must not be null or blank"); - } + /** + * Normalizes a passphrase to an empty string if null or blank. + * This allows creating/unlocking keys without encryption. + * + * @param passphrase the passphrase to normalize + * @return the passphrase or empty string if null/blank + */ + private String normalizePassphrase(String passphrase) { + return passphrase != null && !passphrase.isBlank() ? passphrase : ""; } } diff --git a/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManagerTest.java b/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManagerTest.java index 2cbf27b..6ecac68 100644 --- a/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManagerTest.java +++ b/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/key/DefaultKeyManagerTest.java @@ -213,6 +213,45 @@ void shouldRotateKey() throws Exception { assertThat(request.getParams()).containsExactlyElementsOf(List.of("cashu-old", "cashu-rotated", TEST_PASSPHRASE)); } + /** + * Ensures creating a key with null passphrase normalizes it to empty string. + */ + @Test + void shouldCreateKeyWithNullPassphraseNormalizedToEmpty() { + // Arrange + String npub = "npub1nopwd"; + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Nip46Request.class); + when(adminClient.sendRequest(requestCaptor.capture())) + .thenReturn(CompletableFuture.completedFuture(Nip46Response.success("1", npub))); + + // Act + BunkerKey result = keyManager.createKey("key-no-passphrase", null).join(); + + // Assert + assertThat(result.getName()).isEqualTo("key-no-passphrase"); + Nip46Request request = requestCaptor.getValue(); + assertThat(request.getParams()).containsExactlyElementsOf(List.of("key-no-passphrase", "")); + } + + /** + * Ensures unlocking a key with null passphrase normalizes it to empty string. + */ + @Test + void shouldUnlockKeyWithNullPassphraseNormalizedToEmpty() { + // Arrange + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Nip46Request.class); + when(adminClient.sendRequest(requestCaptor.capture())) + .thenReturn(CompletableFuture.completedFuture(Nip46Response.success("1", "ok"))); + + // Act + boolean result = keyManager.unlockKey("key-unencrypted", null).join(); + + // Assert + assertThat(result).isTrue(); + Nip46Request request = requestCaptor.getValue(); + assertThat(request.getParams()).containsExactlyElementsOf(List.of("key-unencrypted", "")); + } + /** * Ensures NIP-46 errors are surfaced as AdminException through the future. */ diff --git a/nsecbunker-client/pom.xml b/nsecbunker-client/pom.xml index bbc8833..2e98e8c 100644 --- a/nsecbunker-client/pom.xml +++ b/nsecbunker-client/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-client diff --git a/nsecbunker-connection/pom.xml b/nsecbunker-connection/pom.xml index 99f1516..4ea604e 100644 --- a/nsecbunker-connection/pom.xml +++ b/nsecbunker-connection/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-connection diff --git a/nsecbunker-core/pom.xml b/nsecbunker-core/pom.xml index 2484c19..c58de7b 100644 --- a/nsecbunker-core/pom.xml +++ b/nsecbunker-core/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-core diff --git a/nsecbunker-monitoring/pom.xml b/nsecbunker-monitoring/pom.xml index a7dd48b..587a720 100644 --- a/nsecbunker-monitoring/pom.xml +++ b/nsecbunker-monitoring/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-monitoring diff --git a/nsecbunker-protocol/pom.xml b/nsecbunker-protocol/pom.xml index ff65d98..5570120 100644 --- a/nsecbunker-protocol/pom.xml +++ b/nsecbunker-protocol/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-protocol diff --git a/nsecbunker-spring-boot-starter/pom.xml b/nsecbunker-spring-boot-starter/pom.xml index eb44dc5..6115842 100644 --- a/nsecbunker-spring-boot-starter/pom.xml +++ b/nsecbunker-spring-boot-starter/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-spring-boot-starter diff --git a/nsecbunker-tests/nsecbunker-chaos/pom.xml b/nsecbunker-tests/nsecbunker-chaos/pom.xml index f085853..4a62193 100644 --- a/nsecbunker-tests/nsecbunker-chaos/pom.xml +++ b/nsecbunker-tests/nsecbunker-chaos/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0 + 0.1.1 nsecbunker-chaos diff --git a/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java b/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java index c0d1c43..c7a746f 100644 --- a/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java +++ b/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java @@ -89,9 +89,34 @@ void shouldStopAfterMaxReconnectAttempts() throws Exception { } /** - * No-op WebSocket listener for MockWebServer upgrades. + * WebSocket listener for MockWebServer upgrades that properly handles lifecycle. + * + *

A completely empty listener causes NPE in OkHttp's RealWebSocket.loopReader + * because the reader loop expects proper lifecycle handling. This implementation + * handles onOpen, onMessage, and onClosing to prevent the MockWebServer crash. */ private static final class NoopWebSocketListener extends okhttp3.WebSocketListener { + + @Override + public void onOpen(okhttp3.WebSocket webSocket, okhttp3.Response response) { + // Connection opened - no action needed for test + } + + @Override + public void onMessage(okhttp3.WebSocket webSocket, String text) { + // Message received - no action needed for test + } + + @Override + public void onClosing(okhttp3.WebSocket webSocket, int code, String reason) { + // Server closing - echo back close to complete handshake + webSocket.close(code, reason); + } + + @Override + public void onFailure(okhttp3.WebSocket webSocket, Throwable t, okhttp3.Response response) { + // Connection failed - no action needed for test + } } /** diff --git a/nsecbunker-tests/nsecbunker-e2e/pom.xml b/nsecbunker-tests/nsecbunker-e2e/pom.xml index 1b5071d..d922128 100644 --- a/nsecbunker-tests/nsecbunker-e2e/pom.xml +++ b/nsecbunker-tests/nsecbunker-e2e/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0 + 0.1.1 nsecbunker-e2e diff --git a/nsecbunker-tests/nsecbunker-it/pom.xml b/nsecbunker-tests/nsecbunker-it/pom.xml index 48bc51a..e826aa0 100644 --- a/nsecbunker-tests/nsecbunker-it/pom.xml +++ b/nsecbunker-tests/nsecbunker-it/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0 + 0.1.1 nsecbunker-it diff --git a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java index c7ba252..3909575 100644 --- a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java +++ b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java @@ -86,6 +86,8 @@ void connectIsIdempotentWhenConnected() throws Exception { client.connect(); assertThat(client.isConnected()).isTrue(); + // Wait for server to register the connection + assertThat(mockRelay.awaitConnection(5, TimeUnit.SECONDS)).isTrue(); // Second connect should not throw client.connect(); diff --git a/nsecbunker-tests/nsecbunker-perf/pom.xml b/nsecbunker-tests/nsecbunker-perf/pom.xml index a7863f6..d279be8 100644 --- a/nsecbunker-tests/nsecbunker-perf/pom.xml +++ b/nsecbunker-tests/nsecbunker-perf/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0 + 0.1.1 nsecbunker-perf diff --git a/nsecbunker-tests/nsecbunker-security/pom.xml b/nsecbunker-tests/nsecbunker-security/pom.xml index babd250..d8536bc 100644 --- a/nsecbunker-tests/nsecbunker-security/pom.xml +++ b/nsecbunker-tests/nsecbunker-security/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0 + 0.1.1 nsecbunker-security diff --git a/nsecbunker-tests/pom.xml b/nsecbunker-tests/pom.xml index 82b6f47..f4c2c17 100644 --- a/nsecbunker-tests/pom.xml +++ b/nsecbunker-tests/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 nsecbunker-tests diff --git a/pom.xml b/pom.xml index a3c3c22..8fb1850 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0 + 0.1.1 pom nsecBunker Java Library