From d13eecad4a91ebe7800f58ccc1f0a77c280df9a1 Mon Sep 17 00:00:00 2001 From: Jonathan Staab Date: Fri, 11 Aug 2023 17:18:00 -0700 Subject: [PATCH] Add support for nip44 --- index.ts | 1 + nip44.test.ts | 19 +++++++++++++++++ nip44.ts | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + yarn.lock | 44 ++++++++++++++++++++++++--------------- 5 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 nip44.test.ts create mode 100644 nip44.ts diff --git a/index.ts b/index.ts index 16211a7..c9dc368 100644 --- a/index.ts +++ b/index.ts @@ -19,6 +19,7 @@ export * as nip27 from './nip27.ts' export * as nip28 from './nip28.ts' export * as nip39 from './nip39.ts' export * as nip42 from './nip42.ts' +export * as nip44 from './nip44.ts' export * as nip57 from './nip57.ts' export * as nip98 from './nip98.ts' diff --git a/nip44.test.ts b/nip44.test.ts new file mode 100644 index 0000000..1dfe4b8 --- /dev/null +++ b/nip44.test.ts @@ -0,0 +1,19 @@ +import crypto from 'node:crypto' + +import {encrypt, decrypt} from './nip44.ts' +import {getPublicKey, generatePrivateKey} from './keys.ts' + +// @ts-ignore +// eslint-disable-next-line no-undef +globalThis.crypto = crypto + +test('encrypt and decrypt message', async () => { + let sk1 = generatePrivateKey() + let sk2 = generatePrivateKey() + let pk1 = getPublicKey(sk1) + let pk2 = getPublicKey(sk2) + + expect(await decrypt(sk2, pk1, await encrypt(sk1, pk2, 'hello'))).toEqual( + 'hello' + ) +}) diff --git a/nip44.ts b/nip44.ts new file mode 100644 index 0000000..0e75164 --- /dev/null +++ b/nip44.ts @@ -0,0 +1,57 @@ +import {base64} from '@scure/base' +import {randomBytes} from '@noble/hashes/utils' +import {secp256k1} from '@noble/curves/secp256k1' +import {sha256} from '@noble/hashes/sha256' +import {xchacha20} from '@noble/ciphers/chacha' + +import {utf8Decoder, utf8Encoder} from './utils.ts' + +// @ts-ignore +if (typeof crypto !== 'undefined' && !crypto.subtle && crypto.webcrypto) { + // @ts-ignore + crypto.subtle = crypto.webcrypto.subtle +} + +export const getSharedSecret = (privkey: string, pubkey: string): Uint8Array => + sha256(secp256k1.getSharedSecret(privkey, '02' + pubkey).subarray(1, 33)) + +export function encrypt(privkey: string, pubkey: string, text: string, v = 1) { + if (v !== 1) { + throw new Error('NIP44: unknown encryption version') + } + + const key = getSharedSecret(privkey, pubkey) + const nonce = randomBytes(24) + const plaintext = utf8Encoder.encode(text) + const ciphertext = xchacha20(key, nonce, plaintext) + + return JSON.stringify({ + ciphertext: base64.encode(ciphertext), + nonce: base64.encode(nonce), + v + }) +} + +export function decrypt(privkey: string, pubkey: string, payload: string) { + let data + try { + data = JSON.parse(payload) as { + ciphertext: string + nonce: string + v: number + } + } catch (e) { + throw new Error('NIP44: failed to parse payload') + } + + if (data.v !== 1) { + throw new Error('NIP44: unknown encryption version') + } + + const key = getSharedSecret(privkey, pubkey) + const nonce = base64.decode(data.nonce) + const ciphertext = base64.decode(data.ciphertext) + const plaintext = xchacha20(key, nonce, ciphertext) + + return utf8Decoder.decode(plaintext) +} diff --git a/package.json b/package.json index e027964..ae1c4b9 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "@noble/curves": "1.1.0", "@noble/hashes": "1.3.1", + "@noble/ciphers": "^0.2.0", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1" diff --git a/yarn.lock b/yarn.lock index 29b5734..3b9cf00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -705,14 +705,24 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@noble/curves@1.0.0", "@noble/curves@~1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932" - integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw== - dependencies: - "@noble/hashes" "1.3.0" +"@noble/ciphers@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.2.0.tgz#a12cda60f3cf1ab5d7c77068c3711d2366649ed7" + integrity sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw== -"@noble/hashes@1.3.0", "@noble/hashes@~1.3.0": +"@noble/curves@1.1.0", "@noble/curves@~1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" + +"@noble/hashes@1.3.1", "@noble/hashes@~1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + +"@noble/hashes@~1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== @@ -743,19 +753,19 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== -"@scure/bip32@1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.0.tgz#6c8d980ef3f290987736acd0ee2e0f0d50068d87" - integrity sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q== +"@scure/bip32@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" + integrity sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A== dependencies: - "@noble/curves" "~1.0.0" - "@noble/hashes" "~1.3.0" + "@noble/curves" "~1.1.0" + "@noble/hashes" "~1.3.1" "@scure/base" "~1.1.0" -"@scure/bip39@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.0.tgz#a207e2ef96de354de7d0002292ba1503538fc77b" - integrity sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg== +"@scure/bip39@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" + integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== dependencies: "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0"