diff options
| author | Adam Soltys <[email protected]> | 2026-06-28 09:31:51 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-28 09:31:51 -0700 |
| commit | 0fbeb3c55413fb221d890b3bf3464ebd2aed9fa1 (patch) | |
| tree | f8f034c2ba00d0f78a34e1df1972a46712487151 /src/nostr.js | |
| parent | 96d9b7ee6914b157a17e078cfe597cb40f35c9e2 (diff) | |
| download | coinosv3-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.js | 21 |
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())); |
