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
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import { finalizeEvent } from 'nostr-tools/pure';
import { unwrapEvent } from 'nostr-tools/nip17';
import * as libnip44 from '$lib/nip44';
import { u } from '$lib/nip44';
import { eventToSign, signer } from '$lib/store';
import { get } from 'svelte/store';
// 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 NIP-07 extension is available and matches user
if (window.nostr) {
try {
const extPubkey = await window.nostr.getPublicKey();
if (extPubkey === userPubkey) {
return null; // Use NIP-07
}
} catch (e) {
// Extension didn't work, fall through to signer
}
}
// Check if signer already has a valid sk (64 hex chars = 32 bytes)
const currentSigner = get(signer);
if (currentSigner?.ready && currentSigner?.params?.sk && currentSigner.params.sk.length === 64) {
return hexToBytes(currentSigner.params.sk);
}
// 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: object, user: object): object => {
const sk = await ensureSigner(user.pubkey);
if (sk === null) {
return window.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: object, user: object, receiverPK: string): string => {
const senderSK = await ensureSigner(user.pubkey);
if (senderSK === null) {
return window.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: object, receiverPK: string): object => {
const senderSK = await ensureSigner(user.pubkey);
if (senderSK === null) {
return JSON.parse(await window.nostr.nip44.decrypt(
receiverPK, encrypted));
} else {
const senderSKString = bytesToHex(senderSK);
const ckey = u.getConversationKey(senderSKString, receiverPK);
return JSON.parse(libnip44.decrypt(encrypted, ckey));
}
}
|