getKeys throws on passphrase-encrypted entries — nip19.decode({iv, data}) fails #5

Open
opened 2026-05-25 21:55:14 +00:00 by padreug · 0 comments
Owner

Symptom

After successfully calling create_new_key(keyName, passphrase) once, every subsequent get_keys admin RPC times out from the client side (no response event arrives). Verified against a freshly-built bunker via Python admin-RPC harness over the same relay channel — ping works, create_new_key works, but get_keys after a create silently fails.

Root cause

The bug is in the bunker source, not in NDK or the relay. Trace:

  1. src/commands/add.ts:saveEncrypted writes:

    currentConfig.keys[name] = { iv, data };
    

    So config.keys[name] becomes a plain object of shape {iv: string, data: string}.

  2. src/daemon/run.ts:getKeys reads:

    for (const [name, nsec] of Object.entries(config.keys)) {
      const hexpk = nip19.decode(nsec).data as string;  // ← nsec is {iv, data}, not a string!
      const user = await new NDKPrivateKeySigner(hexpk).user();
      ...
    }
    

    nip19.decode({iv, data}) throws because it expects a bech32 string. The exception propagates up through handleRequest's catch, which calls sendResponse(... "error", NDKKind.NostrConnectAdmin, err?.message).

  3. The error response is published but apparently doesn't reach the client subscription — same shape as the NDK echo issue in #4. Hard to confirm without instrumenting NDK directly.

Reproduction

# Pseudocode against a fresh nsecbunker on a relay
admin_rpc("ping", [])               # → {"result": "ok"}
admin_rpc("get_keys", [])           # → {"result": "[]"}
admin_rpc("create_new_key", ["alice", "pass"])
                                     # → {"result": "{\"npub\":\"npub1...\"}"}
admin_rpc("get_keys", [])           # → TimeoutError (no response)

Real fix

getKeys should only iterate keys that are actually plaintext nsecs:

for (const [name, value] of Object.entries(config.keys)) {
  // Skip encrypted entries — they're {iv, data} objects awaiting unlock,
  // not raw nsec strings. They show up in lockedKeyNames below.
  if (typeof value !== "string") continue;
  
  const hexpk = nip19.decode(value).data as string;
  ...
}

This matches the existing semantics — encrypted-but-locked keys go into lockedKeyNames and get returned as {name} (no npub), unlocked keys go in with {name, npub, userCount, tokenCount}. The bug is just that the type guard is missing.

Impact

For our use case (LNbits-as-IdP): does not block us. LNbits stores the target pubkey at create_new_key time and never asks the bunker for "what keys do you have." So get_keys is never called on the steady-state hot path.

For interactive admin via Pablo's web UI: probably blocks the UI from showing the post-create state of the keystore. nsecbunker.com might have a different code path that hides this.

Acceptance

  • Fix getKeys to skip non-string entries.
  • Add a regression test: create_new_key + get_keys round-trip should return the new key.
  • Investigate whether the same type-confusion affects other config-iteration sites.

Cross-refs

  • Discovered during aiolabs/lnbits#18 phase 2 spike (see Follow-up #3 in ~/dev/lnbits/nsec-bunker-spike-findings.md).
  • Related: #4 (NDK echo issue) is what hides the error response from the admin client.
## Symptom After successfully calling `create_new_key(keyName, passphrase)` once, every subsequent `get_keys` admin RPC times out from the client side (no response event arrives). Verified against a freshly-built bunker via Python admin-RPC harness over the same relay channel — `ping` works, `create_new_key` works, but `get_keys` after a create silently fails. ## Root cause The bug is in the bunker source, not in NDK or the relay. Trace: 1. **`src/commands/add.ts:saveEncrypted` writes**: ```typescript currentConfig.keys[name] = { iv, data }; ``` So `config.keys[name]` becomes a **plain object** of shape `{iv: string, data: string}`. 2. **`src/daemon/run.ts:getKeys` reads**: ```typescript for (const [name, nsec] of Object.entries(config.keys)) { const hexpk = nip19.decode(nsec).data as string; // ← nsec is {iv, data}, not a string! const user = await new NDKPrivateKeySigner(hexpk).user(); ... } ``` `nip19.decode({iv, data})` throws because it expects a bech32 string. The exception propagates up through `handleRequest`'s catch, which calls `sendResponse(... "error", NDKKind.NostrConnectAdmin, err?.message)`. 3. **The error response is published but apparently doesn't reach the client subscription** — same shape as the NDK echo issue in #4. Hard to confirm without instrumenting NDK directly. ## Reproduction ```python # Pseudocode against a fresh nsecbunker on a relay admin_rpc("ping", []) # → {"result": "ok"} admin_rpc("get_keys", []) # → {"result": "[]"} admin_rpc("create_new_key", ["alice", "pass"]) # → {"result": "{\"npub\":\"npub1...\"}"} admin_rpc("get_keys", []) # → TimeoutError (no response) ``` ## Real fix `getKeys` should only iterate keys that are actually plaintext nsecs: ```typescript for (const [name, value] of Object.entries(config.keys)) { // Skip encrypted entries — they're {iv, data} objects awaiting unlock, // not raw nsec strings. They show up in lockedKeyNames below. if (typeof value !== "string") continue; const hexpk = nip19.decode(value).data as string; ... } ``` This matches the existing semantics — encrypted-but-locked keys go into `lockedKeyNames` and get returned as `{name}` (no npub), unlocked keys go in with `{name, npub, userCount, tokenCount}`. The bug is just that the type guard is missing. ## Impact For our use case (LNbits-as-IdP): **does not block us**. LNbits stores the target pubkey at `create_new_key` time and never asks the bunker for "what keys do you have." So `get_keys` is never called on the steady-state hot path. For interactive admin via Pablo's web UI: probably blocks the UI from showing the post-create state of the keystore. nsecbunker.com might have a different code path that hides this. ## Acceptance - [ ] Fix `getKeys` to skip non-string entries. - [ ] Add a regression test: create_new_key + get_keys round-trip should return the new key. - [ ] Investigate whether the same type-confusion affects other config-iteration sites. ## Cross-refs - Discovered during `aiolabs/lnbits#18` phase 2 spike (see Follow-up #3 in `~/dev/lnbits/nsec-bunker-spike-findings.md`). - Related: #4 (NDK echo issue) is what hides the error response from the admin client.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/nsecbunkerd#5
No description provided.