(payload: string, conversationKey: Buffer)
| 114 | * Validates version byte, payload sizes, and MAC before decrypting. |
| 115 | */ |
| 116 | export function nip44Decrypt(payload: string, conversationKey: Buffer): string { |
| 117 | if (!payload || payload[0] === '#') { |
| 118 | throw new Error('unknown version') |
| 119 | } |
| 120 | if (payload.length < 132 || payload.length > 87472) { |
| 121 | throw new Error('invalid payload size') |
| 122 | } |
| 123 | |
| 124 | const data = Buffer.from(payload, 'base64') |
| 125 | if (data.length < 99 || data.length > 65603) { |
| 126 | throw new Error('invalid data size') |
| 127 | } |
| 128 | |
| 129 | const version = data[0] |
| 130 | if (version !== 2) { |
| 131 | throw new Error(`unknown version ${version}`) |
| 132 | } |
| 133 | |
| 134 | const nonce = data.subarray(1, 33) |
| 135 | const ciphertext = data.subarray(33, data.length - 32) |
| 136 | const mac = data.subarray(data.length - 32) |
| 137 | |
| 138 | const { chachaKey, chachaNonce, hmacKey } = getMessageKeys(conversationKey, nonce) |
| 139 | |
| 140 | const expectedMac = createHmac('sha256', hmacKey).update(nonce).update(ciphertext).digest() |
| 141 | if (!timingSafeEqual(expectedMac, mac)) { |
| 142 | throw new Error('invalid MAC') |
| 143 | } |
| 144 | |
| 145 | const iv = Buffer.concat([Buffer.alloc(4), chachaNonce]) |
| 146 | const decipher = createDecipheriv('chacha20', chachaKey, iv) |
| 147 | const padded = Buffer.concat([decipher.update(ciphertext), decipher.final()]) |
| 148 | |
| 149 | return unpad(padded) |
| 150 | } |
| 151 | |
| 152 | /** |
| 153 | * Validate the structural format of a NIP-44 v2 payload without decrypting it. |
no test coverage detected