Add NIP-04 support for drop-in NIP-07 parity

signEvent() already handles any event kind and NIP-44 was already
implemented, but window.nostr.nip04 (and the equivalent element-scoped
nip04Encrypt/nip04Decrypt) didn't exist anywhere -- every real NIP-07
extension exposes it, and sites with legacy DM code paths still expect
it. A site whose own code called window.nostr.nip04.encrypt(...) while
BitLogin was the active signer got a bare "Cannot read properties of
undefined" instead of a working call.

- packages/core/src/crypto/nip04.ts: AES-256-CBC + secp256k1 ECDH
  shared secret (raw X coordinate, no HKDF -- this is legacy NIP-04,
  not NIP-44), "<ciphertext>?iv=<iv>" payload shape, cross-checked
  against the NIP-04 spec's algorithm description
- NostrSigner.nip04Encrypt/nip04Decrypt, mirroring the existing
  nip44Encrypt/nip44Decrypt methods
- crypto worker: new nip04Encrypt/nip04Decrypt actions and protocol
  payload types
- <bitlogin-auth>.nip04Encrypt/nip04Decrypt (element-scoped, like the
  existing nip44 methods) and window.nostr.nip04.encrypt/decrypt

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
This commit is contained in:
thePR0M3TH3ANandClaude Sonnet 5 committed 2026-07-21 23:28:51 -04:00
1 parent 66cae69c7e
commit 283840261f
10 files changed
+167 -1

No files matched your search

+11
View File
@@ -165,6 +165,17 @@ export class BitLoginAuthElement extends HTMLElement {
async nip44Decrypt(peerPublicKey: string, payload: string): Promise<string> {
return (await this.worker.nip44Decrypt({ peerPublicKey, payload })).plaintext;
}
/**
* Legacy relative to nip44Encrypt/nip44Decrypt above, but still what a real NIP-07
* extension exposes as window.nostr.nip04 -- implemented for drop-in parity with sites
* (or their older DM code paths) that still expect NIP-04 rather than NIP-44.
*/
async nip04Encrypt(peerPublicKey: string, plaintext: string): Promise<string> {
return (await this.worker.nip04Encrypt({ peerPublicKey, plaintext })).ciphertext;
}
async nip04Decrypt(peerPublicKey: string, payload: string): Promise<string> {
return (await this.worker.nip04Decrypt({ peerPublicKey, payload })).plaintext;
}
async logout(): Promise<void> {
await this.worker.logout();
this.session = null;
+16
View File
@@ -17,6 +17,12 @@ export interface Nip07Provider {
encrypt(peerPublicKey: string, plaintext: string): Promise<string>;
decrypt(peerPublicKey: string, payload: string): Promise<string>;
};
/** Legacy relative to nip44 above, but present on every real NIP-07 extension -- kept for
* drop-in parity with sites (or their older DM code paths) that still expect it. */
nip04: {
encrypt(peerPublicKey: string, plaintext: string): Promise<string>;
decrypt(peerPublicKey: string, payload: string): Promise<string>;
};
/**
* Identifies this provider as BitLogin's, distinct from a browser extension (Alby, nos2x,
* …) that may also implement window.nostr. Only one provider can occupy window.nostr at a
@@ -62,6 +68,16 @@ export function createNip07Provider(workerClient: WorkerClient, configuredRelays
return plaintext;
}
},
nip04: {
async encrypt(peerPublicKey: string, plaintext: string): Promise<string> {
const { ciphertext } = await wrapUnlockError(workerClient.nip04Encrypt({ peerPublicKey, plaintext }));
return ciphertext;
},
async decrypt(peerPublicKey: string, payload: string): Promise<string> {
const { plaintext } = await wrapUnlockError(workerClient.nip04Decrypt({ peerPublicKey, payload }));
return plaintext;
}
},
_bitlogin: true
};
}
@@ -34,6 +34,8 @@ import type {
SignEventPayload,
Nip44EncryptPayload,
Nip44DecryptPayload,
Nip04EncryptPayload,
Nip04DecryptPayload,
PreviewImportKeyPayload
} from "./protocol.js";
@@ -259,6 +261,18 @@ async function handle(action: string, payload: unknown): Promise<unknown> {
return { plaintext: signer.nip44Decrypt(p.peerPublicKey, p.payload) };
}
case "nip04Encrypt": {
const { signer } = requireUnlocked();
const p = payload as Nip04EncryptPayload;
return { ciphertext: signer.nip04Encrypt(p.peerPublicKey, p.plaintext) };
}
case "nip04Decrypt": {
const { signer } = requireUnlocked();
const p = payload as Nip04DecryptPayload;
return { plaintext: signer.nip04Decrypt(p.peerPublicKey, p.payload) };
}
case "exportIdentity": {
const { everydayPrivateKey, signer } = requireUnlocked();
return { nsec: encodeNsec(everydayPrivateKey), npub: encodeNpub(signer.getPublicKey()) };
+11
View File
@@ -132,6 +132,15 @@ export interface Nip44DecryptPayload {
payload: string;
}
export interface Nip04EncryptPayload {
peerPublicKey: string;
plaintext: string;
}
export interface Nip04DecryptPayload {
peerPublicKey: string;
payload: string;
}
export interface ExportIdentityResult {
nsec: string;
npub: string;
@@ -161,6 +170,8 @@ export type WorkerActionMap = {
signEvent: [SignEventPayload, NostrEvent];
nip44Encrypt: [Nip44EncryptPayload, { ciphertext: string }];
nip44Decrypt: [Nip44DecryptPayload, { plaintext: string }];
nip04Encrypt: [Nip04EncryptPayload, { ciphertext: string }];
nip04Decrypt: [Nip04DecryptPayload, { plaintext: string }];
exportIdentity: [Record<string, never>, ExportIdentityResult];
buildRecoveryExport: [Record<string, never>, unknown];
repairReplicas: [Record<string, never>, RepairReplicasResult];
+9 -1
View File
@@ -14,7 +14,9 @@ import type {
PublishProfilePayload,
SignEventPayload,
Nip44EncryptPayload,
Nip44DecryptPayload
Nip44DecryptPayload,
Nip04EncryptPayload,
Nip04DecryptPayload
} from "./protocol.js";
export class WorkerClient {
@@ -92,6 +94,12 @@ export class WorkerClient {
nip44Decrypt(payload: Nip44DecryptPayload) {
return this.call("nip44Decrypt", payload);
}
nip04Encrypt(payload: Nip04EncryptPayload) {
return this.call("nip04Encrypt", payload);
}
nip04Decrypt(payload: Nip04DecryptPayload) {
return this.call("nip04Decrypt", payload);
}
exportIdentity() {
return this.call("exportIdentity", {});
}