From 7a9d4326862926a2da7277d5cb01d4d559a01bc2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 25 Jan 2024 12:14:51 -0300 Subject: [PATCH] add nip49 key encryption and decryption. --- nip19.ts | 4 +-- nip49.test.ts | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++ nip49.ts | 45 ++++++++++++++++++++++++++ package.json | 7 +++- 4 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 nip49.test.ts create mode 100644 nip49.ts diff --git a/nip19.ts b/nip19.ts index bf416cbb..8ad7c466 100644 --- a/nip19.ts +++ b/nip19.ts @@ -3,7 +3,7 @@ import { bech32 } from '@scure/base' import { utf8Decoder, utf8Encoder } from './utils.ts' -const Bech32MaxSize = 5000 +export const Bech32MaxSize = 5000 /** * Bech32 regex. @@ -175,7 +175,7 @@ function encodeBech32(prefix: Prefix, data: Uint8Array): return bech32.encode(prefix, words, Bech32MaxSize) as `${Prefix}1${string}` } -function encodeBytes(prefix: Prefix, bytes: Uint8Array): `${Prefix}1${string}` { +export function encodeBytes(prefix: Prefix, bytes: Uint8Array): `${Prefix}1${string}` { return encodeBech32(prefix, bytes) } diff --git a/nip49.test.ts b/nip49.test.ts new file mode 100644 index 00000000..857e8f9f --- /dev/null +++ b/nip49.test.ts @@ -0,0 +1,88 @@ +import { test, expect } from 'bun:test' +import { decrypt, encrypt } from './nip49' +import { hexToBytes } from '@noble/hashes/utils' + +test('encrypt and decrypt', () => { + for (let i = 0; i < vectors.length; i++) { + let [password, secret, logn, ksb, ncryptsec] = vectors[i] + let sec = hexToBytes(secret) + let there = encrypt(sec, password, logn, ksb) + let back = decrypt(there, password) + let again = decrypt(ncryptsec, password) + expect(back).toEqual(again) + expect(again).toEqual(sec) + } +}) + +const vectors: [string, string, number, 0x00 | 0x01 | 0x02, string][] = [ + [ + '.ksjabdk.aselqwe', + '14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a', + 1, + 0x00, + 'ncryptsec1qgqeya6cggg2chdaf48s9evsr0czq3dw059t2khf5nvmq03yeckywqmspcc037l9ajjsq2p08480afuc5hq2zq3rtt454c2epjqxcxll0eff3u7ln2t349t7rc04029q63u28mkeuj4tdazsqqk6p5ky', + ], + [ + 'skjdaklrnçurbç l', + 'f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab', + 2, + 0x01, + 'ncryptsec1qgp86t7az0u5w0wp8nrjnxu9xhullqt39wvfsljz8289gyxg0thrlzv3k40dsqu32vcqza3m7srzm27mkg929gmv6hv5ctay59jf0h8vsj5pjmylvupkdtvy7fy88et3fhe6m3d84t9m8j2umq0j75lw', + ], + [ + '777z7z7z7z7z7z7z', + '11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944', + 3, + 0x02, + 'ncryptsec1qgpc7jmmzmds376r8slazywlagrm5eerlrx7njnjenweggq2atjl0h9vmpk8f9gad0tqy3pwch8e49kyj5qtehp4mjwpzlshx5f5cce8feukst08w52zf4a7gssdqvt3eselup7x4zzezlme3ydxpjaf', + ], + [ + '.ksjabdk.aselqwe', + '14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a', + 7, + 0x00, + 'ncryptsec1qgrss6ycqptee05e5anq33x2vz6ljr0rqunsy9xj5gypkp0lucatdf8yhexrztqcy76sqweuzk8yqzep9mugp988vznz5df8urnyrmaa7l7fvvskp4t0ydjtz0zeajtumul8cnsjcksp68xhxggmy4dz', + ], + [ + 'skjdaklrnçurbç l', + 'f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab', + 8, + 0x01, + 'ncryptsec1qgy0gg98z4wvl35eqlraxf7cyxhfs4968teq59vm97e94gpycmcy6znsc8z82dy5rk8sz0r499ue7xfmd0yuyvzxagtfyxtnwcrcsjavkch8lfseejukwdq7mdcpm43znffngw7texdc5pdujywszhrr', + ], + [ + '777z7z7z7z7z7z7z', + '11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944', + 9, + 0x02, + 'ncryptsec1qgyskhh7mpr0zspg95kv4eefm8233hyz46xyr6s52s6qvan906c2u24gl3dc5f7wytzq9njx7sqksd7snagce3kqth7tv4ug4avlxd5su4vthsh54vk62m88whkazavyc6yefnegf4tx473afssxw4p9', + ], + [ + '', + 'f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab', + 4, + 0x00, + 'ncryptsec1qgzv73a9ktnwmgyvv24x2xtr6grup2v6an96xgs64z3pmh5etg2k4yryachtlu3tpqwqphhm0pjnq9zmftr0qf4p5lmah4rlz02ucjkawr2s9quau67p3jq3d7yp3kreghs0wdcqpf6pkc8jcgsqrn5l', + ], + [ + '', + '11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944', + 5, + 0x01, + 'ncryptsec1qgzs50vjjhewdrxnm0z4y77w7juycf6crny9q0kzeg7vxv3erw77qpauthaf7sfwsgnszjzcqh7zql74m8yxnhcj07dry3v5fgr5x42mpzxvfl76gpuayccvk2nczc7ner3q842rj9v033nykvja6cql', + ], + [ + '', + 'f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab', + 1, + 0x00, + 'ncryptsec1qgqnx59n7duv6ec3hhrvn33q25u2qfd7m69vv6plsg7spnw6d4r9hq0ayjsnlw99eghqqzj8ps7vfwx40nqp9gpw7yzyy09jmwkq3a3z8q0ph5jahs2hap5k6h2wfrme7w2nuek4jnwpzfht4q3u79ra', + ], + [ + '', + '11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944', + 9, + 0x01, + 'ncryptsec1qgylzyeunu2j85au05ae0g0sly03xu54tgnjemr6g9w0eqwuuczya7k0f4juqve64vzsrlxqxmcekzrpvg2a8qu4q6wtjxe0dvy3xkjh5smmz4uy59jg0jay9vnf28e3rc6jq2kd26h6g3fejyw6cype', + ], +] diff --git a/nip49.ts b/nip49.ts new file mode 100644 index 00000000..470e766c --- /dev/null +++ b/nip49.ts @@ -0,0 +1,45 @@ +import { scrypt } from '@noble/hashes/scrypt' +import { xchacha20poly1305 } from '@noble/ciphers/chacha' +import { concatBytes, randomBytes } from '@noble/hashes/utils' +import { Bech32MaxSize, encodeBytes } from './nip19' +import { bech32 } from '@scure/base' + +export function encrypt(sec: Uint8Array, password: string, logn: number = 16, ksb: 0x00 | 0x01 | 0x02 = 0x02): string { + let salt = randomBytes(16) + let n = 2 ** logn + let key = scrypt(password, salt, { N: n, r: 8, p: 1, dkLen: 32 }) + let nonce = randomBytes(24) + let aad = Uint8Array.from([ksb]) + let xc2p1 = xchacha20poly1305(key, nonce, aad) + let ciphertext = xc2p1.encrypt(sec) + let b = concatBytes(Uint8Array.from([0x02]), Uint8Array.from([logn]), salt, nonce, aad, ciphertext) + return encodeBytes('ncryptsec', b) +} + +export function decrypt(ncryptsec: string, password: string): Uint8Array { + let { prefix, words } = bech32.decode(ncryptsec, Bech32MaxSize) + if (prefix !== 'ncryptsec') { + throw new Error(`invalid prefix ${prefix}, expected 'ncryptsec'`) + } + let b = new Uint8Array(bech32.fromWords(words)) + + let version = b[0] + if (version !== 0x02) { + throw new Error(`invalid version ${version}, expected 0x02`) + } + + let logn = b[1] + let n = 2 ** logn + + let salt = b.slice(2, 2 + 16) + let nonce = b.slice(2 + 16, 2 + 16 + 24) + let ksb = b[2 + 16 + 24] + let aad = Uint8Array.from([ksb]) + let ciphertext = b.slice(2 + 16 + 24 + 1) + + let key = scrypt(password, salt, { N: n, r: 8, p: 1, dkLen: 32 }) + let xc2p1 = xchacha20poly1305(key, nonce, aad) + let sec = xc2p1.decrypt(ciphertext) + + return sec +} diff --git a/package.json b/package.json index 7b175719..48e34e7a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "nostr-tools", - "version": "2.1.5", + "version": "2.1.6", "description": "Tools for making a Nostr client.", "repository": { "type": "git", @@ -150,6 +150,11 @@ "require": "./lib/cjs/nip44.js", "types": "./lib/types/nip44.d.ts" }, + "./nip49": { + "import": "./lib/esm/nip49.js", + "require": "./lib/cjs/nip49.js", + "types": "./lib/types/nip49.d.ts" + }, "./nip57": { "import": "./lib/esm/nip57.js", "require": "./lib/cjs/nip57.js",