aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdrien Hopkins <[email protected]>2026-02-24 11:37:26 -0500
committerAdrien Hopkins <[email protected]>2026-02-24 11:37:26 -0500
commit017677414e1dd9c73c339fccea55a41376e863d2 (patch)
tree79d7bb673fcde5f253b9b04a1137edef36291b81 /src
parent7f5b762ba56ae731ae1c81ee49e70d11d2e95477 (diff)
downloadcoinos-ui-017677414e1dd9c73c339fccea55a41376e863d2.tar.gz
coinos-ui-017677414e1dd9c73c339fccea55a41376e863d2.zip
Always use ensureSigner for nostr signing
This fixes a problem that occurs with muting users using secret keys.
Diffstat (limited to 'src')
-rw-r--r--src/lib/nip07.ts77
-rw-r--r--src/lib/nip17.ts62
2 files changed, 70 insertions, 69 deletions
diff --git a/src/lib/nip07.ts b/src/lib/nip07.ts
index 9f675950..ec635f24 100644
--- a/src/lib/nip07.ts
+++ b/src/lib/nip07.ts
@@ -1,27 +1,88 @@
-import { bytesToHex } from '@noble/hashes/utils';
+import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import { finalizeEvent } from 'nostr-tools/pure';
import { unwrapEvent } from 'nostr-tools/nip17';
-import { getPrivateKey } from '$lib/nostr';
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 => {
- if (await window.nostr.getPublicKey() === user.pubkey) {
+ const sk = await ensureSigner(user.pubkey);
+ if (sk === null) {
return window.nostr.signEvent(event);
} else {
- const sk = await getPrivateKey();
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 => {
- if (await window.nostr.getPublicKey() === user.pubkey) {
+ const senderSK = await ensureSigner(user.pubkey);
+ if (senderSK === null) {
return window.nostr.nip44.encrypt(
receiverPK, JSON.stringify(json));
} else {
- const senderSK = await getPrivateKey();
const senderSKString = bytesToHex(senderSK);
const ckey = u.getConversationKey(senderSKString, receiverPK);
return libnip44.encrypt(JSON.stringify(json), ckey);
@@ -30,11 +91,11 @@ export const encrypt = async (json: object, user: object, receiverPK: string): s
// Decrypts an event in NIP-44 using NIP-07 if available, otherwise getPrivateKey()
export const decrypt = async (encrypted: string, user: object, receiverPK: string): object => {
- if (await window.nostr.getPublicKey() === user.pubkey) {
+ const senderSK = await ensureSigner(user.pubkey);
+ if (senderSK === null) {
return JSON.parse(await window.nostr.nip44.decrypt(
receiverPK, encrypted));
} else {
- const senderSK = await getPrivateKey();
const senderSKString = bytesToHex(senderSK);
const ckey = u.getConversationKey(senderSKString, receiverPK);
return JSON.parse(libnip44.decrypt(encrypted, ckey));
diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts
index acfa8fc5..70741786 100644
--- a/src/lib/nip17.ts
+++ b/src/lib/nip17.ts
@@ -5,11 +5,10 @@ import { SimplePool } from 'nostr-tools/pool';
import { Relay } from 'nostr-tools/relay';
import { unwrapEvent } from 'nostr-tools/nip17';
+import { ensureSigner } from '$lib/nip07';
import { relaysSupporting } from '$lib/nip11';
import { expired, expiration } from '$lib/nip40';
import { encrypt, u } from '$lib/nip44';
-import { eventToSign, signer } from '$lib/store';
-import { get } from 'svelte/store';
const TWO_DAYS = 2 * 24 * 60 * 60;
const randomTimestamp = () => Math.floor(
@@ -21,65 +20,6 @@ import { PUBLIC_DM_RELAYS } from '$env/static/public';
const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(',');
const DM_FETCH_LIMIT = 65536;
-// 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.
-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');
-}
-
// Create a NIP-17 message using a provided sender secret key.
// If wrapPK is specified and different than receiverPK,
// the message will be gift-wrapped to wrapPK