nips icon indicating copy to clipboard operation
nips copied to clipboard

Key Wrapping to solve NIP-44 latency in NIP-46 clients (NIP-60 extension)

Open cypherpunk21 opened this issue 1 month ago • 6 comments

Tags / Keywords: NIP-60, NIP-44, NIP-46, Encryption, Performance, Cashu, Key Wrapping

Abstract This proposal addresses a critical performance bottleneck when using NIP-44 encryption in combination with NIP-46 remote signers (e.g., Amber, Bunker, Keyston). By implementing a "Key Wrapping" architecture, we can reduce the number of remote signer calls from $O(n)$ to $O(1)$ during wallet initialization, reducing load times from 30s+ to sub-second speeds.

The Problem Current specifications (like NIP-60 for Cashu) often require decrypting multiple events (e.g., Token Events) individually using the user's Identity Key. For clients using NIP-46 (Remote Signing):

  1. Fetching 50 token events requires 50 separate WebSocket calls to the signer to decrypt the NIP-44 payloads.
  2. This introduces massive latency and potential points of failure (timeouts).
  3. It forces the user to approve decryption repeatedly or rely on "auto-approve" policies which might be too broad.

The Proposed Solution: Ephemeral Data Keys Instead of encrypting data directly to the Identity Key (NIP-44), clients should:

  1. Generate a local, random Data Key (Wallet Key).
  2. Encrypt this Data Key once using the Identity Key (NIP-44) and store it in a configuration event (e.g., NIP-60 Kind 17375).
  3. Encrypt all payload data (Tokens, History) using this Data Key locally.

Benefits

  • Performance: Only one NIP-46 call is needed at startup (to unwrap the Data Key). All subsequent NIP-44 operations happen locally in memory (0ms latency).
  • UX: The user only sees one "Decrypt Config" request instead of spamming requests.
  • Security: The Identity Key is exposed less frequently. The Data Key can be rotated easily without changing the user's identity.

Reference Implementation (Logic)

Here is a simplified TypeScript implementation demonstrating the flow used in our Progressive Web App (PWA):

// 1. Initialization (The only NIP-46 Call)
async function initializeWallet(signer: RemoteSigner) {
  // Fetch the "Wrapper" Config (Kind 17375)
  const configEvent = await fetchConfigEvent();
  
  let walletKey: string;
  
  if (configEvent) {
    // UNWRAP: Use Remote Signer (NIP-46) - ONE TIME ONLY
    const decrypted = await signer.nip44Decrypt(configEvent.content);
    walletKey = JSON.parse(decrypted).privkey;
  } else {
    // Generate new local key if none exists
    walletKey = generateRandomKey(); 
    // Wrap it and save to relays
    const wrapped = await signer.nip44Encrypt(JSON.stringify({ privkey: walletKey }));
    await publishConfig(wrapped);
  }
  
  // Initialize a local signer with the unwrapped key
  return new LocalSigner(walletKey);
}

// 2. Usage (Zero Latency)
async function loadTokens(localSigner: LocalSigner) {
  const events = await fetchTokenEvents();
  
  // DECRYPT: Happens locally in memory using the Data Key
  // No network calls to Amber/Bunker needed here!
  const tokens = await Promise.all(events.map(evt => 
    localSigner.nip44Decrypt(evt.content)
  ));
  
  return tokens;
}

cypherpunk21 avatar Dec 15 '25 01:12 cypherpunk21

I think the long term solution is: https://github.com/nostr-protocol/nips/pull/1647

reyamir avatar Dec 15 '25 02:12 reyamir

Since it's always the same key, NIP-46 could also just return the get_conversation_key once from NIP-44 and let the client decrypt everything based on that shared key.

vitorpamplona avatar Dec 15 '25 18:12 vitorpamplona

Yes, you're right, your approach is much simpler. Does NIP-46/amber support get_conversation_key?

cypherpunk21 avatar Dec 16 '25 13:12 cypherpunk21

Why was get_conversation_key removed from the NIP-46 spec?

fiatjaf avatar Dec 17 '25 02:12 fiatjaf

Why was get_conversation_key removed from the NIP-46 spec?

People were chickens

vitorpamplona avatar Dec 17 '25 02:12 vitorpamplona

I'm still a chicken, but now I'm building something that returns conversation keys to the client (because you can't do encryption with frost), so consider my objections invalidated now.

staab avatar Dec 17 '25 18:12 staab