diff options
| author | Jonathan Staab <shtaab@gmail.com> | 2023-08-14 17:42:33 -0700 |
|---|---|---|
| committer | Jonathan Staab <shtaab@gmail.com> | 2023-08-14 17:42:33 -0700 |
| commit | a1f8a82e73dc8324c00c707a89265966a8dc99c4 (patch) | |
| tree | bece1f738518955da3039fa963ab6f50ae2da25e | |
| parent | 3a37d7c8b96ffd6bcdddba3bfd472c379ac6b89f (diff) | |
Switch from JSON to custom TLV for nip 44
| -rw-r--r-- | 44.md | 93 |
1 files changed, 70 insertions, 23 deletions
| @@ -24,12 +24,13 @@ Params: | |||
| 24 | 24 | ||
| 25 | Example: | 25 | Example: |
| 26 | 26 | ||
| 27 | - Alice's private key: `5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a` | ||
| 28 | - Bob's private key: `4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d` | ||
| 29 | |||
| 30 | Encrypting the message `hello` from Alice to Bob results in the base-64 encoded tlv payload: | ||
| 31 | |||
| 27 | ``` | 32 | ``` |
| 28 | { | 33 | AAEBARgeI8gcP/4mnw3mKgtMvD8aGYUnGBlhopoCBd94Ev9i |
| 29 | "ciphertext": "FvQi1H4atMwU+FzUR/0CJ7kowjs+", | ||
| 30 | "nonce": "3dBKd83Pg2Q4Tu2A2e8N++c+ZW2IBc2f", | ||
| 31 | "v": 1 | ||
| 32 | } | ||
| 33 | ``` | 34 | ``` |
| 34 | 35 | ||
| 35 | # Other Notes | 36 | # Other Notes |
| @@ -46,52 +47,98 @@ This encryption scheme replaces the one described in NIP-04, which is not secure | |||
| 46 | import {xchacha20} from "@noble/ciphers/chacha" | 47 | import {xchacha20} from "@noble/ciphers/chacha" |
| 47 | import {secp256k1} from "@noble/curves/secp256k1" | 48 | import {secp256k1} from "@noble/curves/secp256k1" |
| 48 | import {sha256} from "@noble/hashes/sha256" | 49 | import {sha256} from "@noble/hashes/sha256" |
| 49 | import {randomBytes} from "@noble/hashes/utils" | 50 | import {randomBytes, concatBytes} from "@noble/hashes/utils" |
| 50 | import {base64} from "@scure/base" | 51 | import {base64} from "@scure/base" |
| 51 | 52 | ||
| 52 | export const utf8Decoder = new TextDecoder() | 53 | export const utf8Decoder = new TextDecoder() |
| 53 | 54 | ||
| 54 | export const utf8Encoder = new TextEncoder() | 55 | export const utf8Encoder = new TextEncoder() |
| 55 | 56 | ||
| 57 | export type TLV = {[t: number]: Uint8Array[]} | ||
| 58 | |||
| 59 | export function parseTLV(data: Uint8Array): TLV { | ||
| 60 | let result: TLV = {} | ||
| 61 | let rest = data | ||
| 62 | while (rest.length > 0) { | ||
| 63 | let t = rest[0] | ||
| 64 | let l = rest[1] | ||
| 65 | if (!l) throw new Error(`malformed TLV ${t}`) | ||
| 66 | let v = rest.slice(2, 2 + l) | ||
| 67 | rest = rest.slice(2 + l) | ||
| 68 | if (v.length < l) throw new Error(`not enough data to read on TLV ${t}`) | ||
| 69 | result[t] = result[t] || [] | ||
| 70 | result[t].push(v) | ||
| 71 | } | ||
| 72 | return result | ||
| 73 | } | ||
| 74 | |||
| 75 | export function encodeTLV(tlv: TLV): Uint8Array { | ||
| 76 | let entries: Uint8Array[] = [] | ||
| 77 | |||
| 78 | Object.entries(tlv).forEach(([t, vs]) => { | ||
| 79 | vs.forEach(v => { | ||
| 80 | let entry = new Uint8Array(v.length + 2) | ||
| 81 | entry.set([parseInt(t)], 0) | ||
| 82 | entry.set([v.length], 1) | ||
| 83 | entry.set(v, 2) | ||
| 84 | entries.push(entry) | ||
| 85 | }) | ||
| 86 | }) | ||
| 87 | |||
| 88 | return concatBytes(...entries) | ||
| 89 | } | ||
| 90 | |||
| 56 | export const getSharedSecret = (privkey: string, pubkey: string): Uint8Array => | 91 | export const getSharedSecret = (privkey: string, pubkey: string): Uint8Array => |
| 57 | sha256(secp256k1.getSharedSecret(privkey, "02" + pubkey).subarray(1, 33)) | 92 | sha256(secp256k1.getSharedSecret(privkey, "02" + pubkey).subarray(1, 33)) |
| 58 | 93 | ||
| 59 | export function encrypt(privkey: string, pubkey: string, text: string, v = 1) { | 94 | export function encrypt(privkey: string, pubkey: string, text: string, v = 1) { |
| 60 | if (v !== 1) { | 95 | if (v !== 1) { |
| 61 | throw new Error("NIP44: unknown encryption version") | 96 | throw new Error('NIP44: unknown encryption version') |
| 62 | } | 97 | } |
| 63 | 98 | ||
| 64 | const key = getSharedSecret(privkey, pubkey) | 99 | const key = getSharedSecret(privkey, pubkey) |
| 65 | const nonce = randomBytes(24) | 100 | const nonce = randomBytes(24) |
| 66 | const plaintext = utf8Encoder.encode(text) | 101 | const plaintext = utf8Encoder.encode(text) |
| 67 | const ciphertext = xchacha20(key, nonce, plaintext) | 102 | const ciphertext = xchacha20(key, nonce, plaintext) |
| 68 | 103 | const tlv = encodeTLV({ | |
| 69 | return JSON.stringify({ | 104 | 0: [new Uint8Array([1])], |
| 70 | ciphertext: base64.encode(ciphertext), | 105 | 1: [nonce], |
| 71 | nonce: base64.encode(nonce), | 106 | 2: [ciphertext] |
| 72 | v, | ||
| 73 | }) | 107 | }) |
| 108 | |||
| 109 | return base64.encode(tlv) | ||
| 74 | } | 110 | } |
| 75 | 111 | ||
| 76 | export function decrypt(privkey: string, pubkey: string, payload: string) { | 112 | export function decrypt(privkey: string, pubkey: string, payload: string) { |
| 77 | let data | 113 | let byteArray |
| 114 | try { | ||
| 115 | byteArray = base64.decode(payload) | ||
| 116 | } catch (e) { | ||
| 117 | throw new Error(`NIP44: failed to base64 decode payload: ${e}`) | ||
| 118 | } | ||
| 119 | |||
| 120 | let tlv | ||
| 78 | try { | 121 | try { |
| 79 | data = JSON.parse(payload) as { | 122 | tlv = parseTLV(byteArray) |
| 80 | ciphertext: string | ||
| 81 | nonce: string | ||
| 82 | v: number | ||
| 83 | } | ||
| 84 | } catch (e) { | 123 | } catch (e) { |
| 85 | throw new Error("NIP44: failed to parse payload") | 124 | throw new Error(`NIP44: failed to decode tlv: ${e}`) |
| 125 | } | ||
| 126 | |||
| 127 | if (tlv[0]?.[0]?.[0] !== 1) { | ||
| 128 | throw new Error(`NIP44: invalid version: ${tlv[0]?.[0]?.[0]}`) | ||
| 129 | } | ||
| 130 | |||
| 131 | if (tlv[1]?.[0]?.length !== 24) { | ||
| 132 | throw new Error(`NIP44: invalid nonce: ${tlv[1]?.[0]}`) | ||
| 86 | } | 133 | } |
| 87 | 134 | ||
| 88 | if (data.v !== 1) { | 135 | if (!tlv[2]?.[0]) { |
| 89 | throw new Error("NIP44: unknown encryption version") | 136 | throw new Error(`NIP44: missing ciphertext`) |
| 90 | } | 137 | } |
| 91 | 138 | ||
| 139 | const nonce = tlv[1][0] | ||
| 140 | const ciphertext = tlv[2][0] | ||
| 92 | const key = getSharedSecret(privkey, pubkey) | 141 | const key = getSharedSecret(privkey, pubkey) |
| 93 | const nonce = base64.decode(data.nonce) | ||
| 94 | const ciphertext = base64.decode(data.ciphertext) | ||
| 95 | const plaintext = xchacha20(key, nonce, ciphertext) | 142 | const plaintext = xchacha20(key, nonce, ciphertext) |
| 96 | 143 | ||
| 97 | return utf8Decoder.decode(plaintext) | 144 | return utf8Decoder.decode(plaintext) |