aboutsummaryrefslogtreecommitdiffstats
path: root/src/nostr.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-28 09:31:51 -0700
committerAdam Soltys <[email protected]>2026-06-28 09:31:51 -0700
commit0fbeb3c55413fb221d890b3bf3464ebd2aed9fa1 (patch)
treef8f034c2ba00d0f78a34e1df1972a46712487151 /src/nostr.js
parent96d9b7ee6914b157a17e078cfe597cb40f35c9e2 (diff)
downloadcoinosv3-0fbeb3c55413fb221d890b3bf3464ebd2aed9fa1.tar.gz
coinosv3-0fbeb3c55413fb221d890b3bf3464ebd2aed9fa1.zip
Locked gifts: let NIP-07 extension users decrypt in-place, skip the code
The gift payload is now encrypted two ways in the link: under the one-time code (the DM / manual path) AND to the recipient's nostr pubkey via an ephemeral key. When a NIP-07 browser extension (window.nostr) is present, the claim view offers 'Claim with nostr extension' — it checks the extension's pubkey matches the gift, decrypts ctKey via window.nostr.nip44.decrypt, and claims into a fresh wallet with no code. The pasted-code path remains as the fallback for everyone else. Verified: both decrypt paths recover the payload from the blob; a different extension account is rejected. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/nostr.js')
-rw-r--r--src/nostr.js21
1 files changed, 14 insertions, 7 deletions
diff --git a/src/nostr.js b/src/nostr.js
index 378cec9..0ebfe92 100644
--- a/src/nostr.js
+++ b/src/nostr.js
@@ -12,7 +12,7 @@
import * as nip06 from 'nostr-tools/nip06';
import * as nip44 from 'nostr-tools/nip44';
import * as nip04 from 'nostr-tools/nip04';
-import { getPublicKey, finalizeEvent } from 'nostr-tools/pure';
+import { getPublicKey, finalizeEvent, generateSecretKey } from 'nostr-tools/pure';
import { decode as nip19decode, npubEncode } from 'nostr-tools/nip19';
import { Relay } from 'nostr-tools/relay';
import { randomBytes } from '@noble/hashes/utils';
@@ -28,12 +28,19 @@ export function parseNostrPubkey(input) {
}
export function npubOf(pkHex) { try { return npubEncode(pkHex); } catch { return null; } }
-// Encrypt a payload under a fresh one-time code (32 random bytes → base64url).
-// The code travels to the recipient out-of-band (a nostr DM), so the public link
-// holding the ciphertext can't be claimed without it. Returns { code, ct }.
-export function encryptWithCode(plaintext) {
- const key = randomBytes(32);
- return { code: base64urlnopad.encode(key), ct: nip44.encrypt(plaintext, key) };
+// Encrypt a gift payload two ways: (1) under a fresh one-time code, delivered to
+// the recipient out-of-band via a nostr DM — the manual path; and (2) to the
+// recipient's nostr pubkey via an ephemeral key, so a NIP-07 browser extension
+// can decrypt it in-place with no code. Both decrypt to the same payload.
+export function encryptGiftPayload(plaintext, recipientPkHex) {
+ const codeKey = randomBytes(32);
+ const ephSk = generateSecretKey();
+ return {
+ code: base64urlnopad.encode(codeKey),
+ ctCode: nip44.encrypt(plaintext, codeKey),
+ eph: getPublicKey(ephSk),
+ ctKey: nip44.encrypt(plaintext, nip44.getConversationKey(ephSk, recipientPkHex)),
+ };
}
export function decryptWithCode(code, ct) {
return nip44.decrypt(ct, base64urlnopad.decode((code || '').trim()));