1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
import { finalizeEvent, getPublicKey } from "nostr-tools/pure";
import * as libnip44 from "$lib/nip44";
import { u } from "$lib/nip44";
import { eventToSign, signer } from "$lib/store";
import { get } from "svelte/store";
// Try to load sk from localStorage nsec and populate the signer store.
const tryNsecFromStorage = async (): Promise<Uint8Array | null> => {
const nsec = localStorage.getItem("nsec");
if (!nsec) return null;
try {
const nip19 = await import("nostr-tools/nip19");
const { type, data } = nip19.decode(nsec);
if (type !== "nsec" || !(data instanceof Uint8Array)) return null;
const sk = data;
const pk = getPublicKey(sk);
signer.set({
method: "nsec",
params: { sk: bytesToHex(sk), pk },
ready: true,
});
return sk;
} catch {
return null;
}
};
// Ensures we have access to a secret key, triggering the signer modal if needed.
// Returns the sk as Uint8Array, or null if using NIP-07 extension.
export const ensureSigner = async (userPubkey: string): Promise<Uint8Array | null> => {
// Check if signer already has a valid sk that matches the user's pubkey
const currentSigner = get(signer);
if (currentSigner?.ready && currentSigner?.params?.sk && currentSigner.params.sk.length === 64) {
const sk = hexToBytes(currentSigner.params.sk);
if (getPublicKey(sk) === userPubkey) return sk;
// Stale signer — clear it so we re-derive below
signer.set(undefined);
}
// Try to load from localStorage nsec before triggering the modal
const skFromStorage = await tryNsecFromStorage();
if (skFromStorage) {
if (getPublicKey(skFromStorage) === userPubkey) return skFromStorage;
// localStorage nsec doesn't match — clear it
localStorage.removeItem("nsec");
signer.set(undefined);
}
// Try to derive nostr key from cached canonical key (e.g. passkey accounts)
try {
const { autoUnlockNostr } = await import("$lib/seed");
await autoUnlockNostr(userPubkey);
const skFromDerived = await tryNsecFromStorage();
if (skFromDerived) return skFromDerived;
} catch {}
// Fall back to NIP-07 extension if available and matches user
if (window.nostr) {
try {
const extPubkey = await window.nostr.getPublicKey();
if (extPubkey === userPubkey) {
return null; // Use NIP-07
}
} catch {
// Extension didn't work, fall through to signer modal
}
}
// Clear invalid signer state before triggering modal
if (
currentSigner?.ready &&
(!currentSigner?.params?.sk || currentSigner.params.sk.length !== 64)
) {
signer.set(undefined);
}
// Trigger the signer modal with a dummy event
const dummyEvent = {
kind: 14,
content: "DM encryption",
tags: [],
created_at: Math.floor(Date.now() / 1000),
};
eventToSign.set(dummyEvent);
// Wait for signer to be ready
const signerValue = await new Promise<any>((resolve, reject) => {
let unsubscribe: () => void;
unsubscribe = signer.subscribe((v) => {
if (v === "cancel") {
setTimeout(() => unsubscribe?.());
eventToSign.set(null);
reject(new Error("cancelled"));
}
if (v?.ready) {
setTimeout(() => unsubscribe?.());
eventToSign.set(null);
resolve(v);
}
});
});
if (signerValue?.params?.sk) {
return hexToBytes(signerValue.params.sk);
}
// If extension method was selected
if (signerValue?.method === "extension") {
return null; // Use NIP-07
}
throw new Error("No signing method available");
};
// Signs an event using NIP-07 if available, otherwise getPrivateKey()
export const signEvent = async (event: any, user: any): Promise<any> => {
const sk = await ensureSigner(user.pubkey);
if (sk === null) {
return (window as any).nostr.signEvent(event);
} else {
return finalizeEvent(event, sk);
}
};
// Encrypts an event in NIP-44 using NIP-07 if available, otherwise getPrivateKey()
export const encrypt = async (json: any, user: any, receiverPK: string): Promise<string> => {
const senderSK = await ensureSigner(user.pubkey);
if (senderSK === null) {
return (window as any).nostr.nip44.encrypt(receiverPK, JSON.stringify(json));
} else {
const senderSKString = bytesToHex(senderSK);
const ckey = u.getConversationKey(senderSKString, receiverPK);
return libnip44.encrypt(JSON.stringify(json), ckey);
}
};
// Decrypts an event in NIP-44 using NIP-07 if available, otherwise getPrivateKey()
export const decrypt = async (encrypted: string, user: any, receiverPK: string): Promise<any> => {
const senderSK = await ensureSigner(user.pubkey);
if (senderSK === null) {
return JSON.parse(await (window as any).nostr.nip44.decrypt(receiverPK, encrypted));
} else {
const senderSKString = bytesToHex(senderSK);
const ckey = u.getConversationKey(senderSKString, receiverPK);
return JSON.parse(libnip44.decrypt(encrypted, ckey));
}
};
|