From a1f8a82e73dc8324c00c707a89265966a8dc99c4 Mon Sep 17 00:00:00 2001 From: Jonathan Staab Date: Mon, 14 Aug 2023 17:42:33 -0700 Subject: [PATCH] Switch from JSON to custom TLV for nip 44 --- 44.md | 93 ++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/44.md b/44.md index 66bf5656..1ff5a8a1 100644 --- a/44.md +++ b/44.md @@ -24,12 +24,13 @@ Params: Example: +- Alice's private key: `5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a` +- Bob's private key: `4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d` + +Encrypting the message `hello` from Alice to Bob results in the base-64 encoded tlv payload: + ``` -{ - "ciphertext": "FvQi1H4atMwU+FzUR/0CJ7kowjs+", - "nonce": "3dBKd83Pg2Q4Tu2A2e8N++c+ZW2IBc2f", - "v": 1 -} +AAEBARgeI8gcP/4mnw3mKgtMvD8aGYUnGBlhopoCBd94Ev9i ``` # Other Notes @@ -46,52 +47,98 @@ This encryption scheme replaces the one described in NIP-04, which is not secure import {xchacha20} from "@noble/ciphers/chacha" import {secp256k1} from "@noble/curves/secp256k1" import {sha256} from "@noble/hashes/sha256" -import {randomBytes} from "@noble/hashes/utils" +import {randomBytes, concatBytes} from "@noble/hashes/utils" import {base64} from "@scure/base" export const utf8Decoder = new TextDecoder() export const utf8Encoder = new TextEncoder() +export type TLV = {[t: number]: Uint8Array[]} + +export function parseTLV(data: Uint8Array): TLV { + let result: TLV = {} + let rest = data + while (rest.length > 0) { + let t = rest[0] + let l = rest[1] + if (!l) throw new Error(`malformed TLV ${t}`) + let v = rest.slice(2, 2 + l) + rest = rest.slice(2 + l) + if (v.length < l) throw new Error(`not enough data to read on TLV ${t}`) + result[t] = result[t] || [] + result[t].push(v) + } + return result +} + +export function encodeTLV(tlv: TLV): Uint8Array { + let entries: Uint8Array[] = [] + + Object.entries(tlv).forEach(([t, vs]) => { + vs.forEach(v => { + let entry = new Uint8Array(v.length + 2) + entry.set([parseInt(t)], 0) + entry.set([v.length], 1) + entry.set(v, 2) + entries.push(entry) + }) + }) + + return concatBytes(...entries) +} + 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") + 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, + const tlv = encodeTLV({ + 0: [new Uint8Array([1])], + 1: [nonce], + 2: [ciphertext] }) + + return base64.encode(tlv) } export function decrypt(privkey: string, pubkey: string, payload: string) { - let data + let byteArray try { - data = JSON.parse(payload) as { - ciphertext: string - nonce: string - v: number - } + byteArray = base64.decode(payload) } catch (e) { - throw new Error("NIP44: failed to parse payload") + throw new Error(`NIP44: failed to base64 decode payload: ${e}`) } - if (data.v !== 1) { - throw new Error("NIP44: unknown encryption version") + let tlv + try { + tlv = parseTLV(byteArray) + } catch (e) { + throw new Error(`NIP44: failed to decode tlv: ${e}`) } + if (tlv[0]?.[0]?.[0] !== 1) { + throw new Error(`NIP44: invalid version: ${tlv[0]?.[0]?.[0]}`) + } + + if (tlv[1]?.[0]?.length !== 24) { + throw new Error(`NIP44: invalid nonce: ${tlv[1]?.[0]}`) + } + + if (!tlv[2]?.[0]) { + throw new Error(`NIP44: missing ciphertext`) + } + + const nonce = tlv[1][0] + const ciphertext = tlv[2][0] 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)