From 27a536f41d040e33eda81b5c21df519e038bbb76 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 11 Mar 2024 11:16:04 -0500 Subject: [PATCH] NIP44: fix slow types --- README.md | 6 +++- nip44.ts | 85 ++++++++++++++++++++++++++++--------------------------- 2 files changed, 49 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 67c254e..07d47d9 100644 --- a/README.md +++ b/README.md @@ -266,4 +266,8 @@ This is free and unencumbered software released into the public domain. By submi ## Contributing to this repository -Use NIP-34 to send your patches to `naddr1qq9kummnw3ez6ar0dak8xqg5waehxw309aex2mrp0yhxummnw3ezucn8qyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqpzemhxue69uhhyetvv9ujuurjd9kkzmpwdejhgq3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmejdv00jq`. +Use NIP-34 to send your patches to: + +``` +naddr1qq9kummnw3ez6ar0dak8xqg5waehxw309aex2mrp0yhxummnw3ezucn8qyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqpzemhxue69uhhyetvv9ujuurjd9kkzmpwdejhgq3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmejdv00jq +``` diff --git a/nip44.ts b/nip44.ts index b770fc3..6557ad0 100644 --- a/nip44.ts +++ b/nip44.ts @@ -8,54 +8,59 @@ import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils' import { base64 } from '@scure/base' const decoder = new TextDecoder() -const u = { - minPlaintextSize: 0x0001, // 1b msg => padded to 32b - maxPlaintextSize: 0xffff, // 65535 (64kb-1) => padded to 64kb - utf8Encode: utf8ToBytes, - utf8Decode(bytes: Uint8Array): string { +class u { + static minPlaintextSize = 0x0001 // 1b msg => padded to 32b + static maxPlaintextSize = 0xffff // 65535 (64kb-1) => padded to 64kb + + static utf8Encode = utf8ToBytes + + static utf8Decode(bytes: Uint8Array): string { return decoder.decode(bytes) - }, + } - getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array { + static getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array { const sharedX = secp256k1.getSharedSecret(privkeyA, '02' + pubkeyB).subarray(1, 33) return hkdf_extract(sha256, sharedX, 'nip44-v2') - }, + } - getMessageKeys(conversationKey: Uint8Array, nonce: Uint8Array) { + static getMessageKeys( + conversationKey: Uint8Array, + nonce: Uint8Array, + ): { chacha_key: Uint8Array; chacha_nonce: Uint8Array; hmac_key: Uint8Array } { const keys = hkdf_expand(sha256, conversationKey, nonce, 76) return { chacha_key: keys.subarray(0, 32), chacha_nonce: keys.subarray(32, 44), hmac_key: keys.subarray(44, 76), } - }, + } - calcPaddedLen(len: number): number { + static calcPaddedLen(len: number): number { if (!Number.isSafeInteger(len) || len < 1) throw new Error('expected positive integer') if (len <= 32) return 32 const nextPower = 1 << (Math.floor(Math.log2(len - 1)) + 1) const chunk = nextPower <= 256 ? 32 : nextPower / 8 return chunk * (Math.floor((len - 1) / chunk) + 1) - }, + } - writeU16BE(num: number): Uint8Array { + static writeU16BE(num: number): Uint8Array { if (!Number.isSafeInteger(num) || num < u.minPlaintextSize || num > u.maxPlaintextSize) throw new Error('invalid plaintext size: must be between 1 and 65535 bytes') const arr = new Uint8Array(2) new DataView(arr.buffer).setUint16(0, num, false) return arr - }, + } - pad(plaintext: string): Uint8Array { + static pad(plaintext: string): Uint8Array { const unpadded = u.utf8Encode(plaintext) const unpaddedLen = unpadded.length const prefix = u.writeU16BE(unpaddedLen) const suffix = new Uint8Array(u.calcPaddedLen(unpaddedLen) - unpaddedLen) return concatBytes(prefix, unpadded, suffix) - }, + } - unpad(padded: Uint8Array): string { + static unpad(padded: Uint8Array): string { const unpaddedLen = new DataView(padded.buffer).getUint16(0) const unpadded = padded.subarray(2, 2 + unpaddedLen) if ( @@ -66,13 +71,13 @@ const u = { ) throw new Error('invalid padding') return u.utf8Decode(unpadded) - }, + } - hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array): Uint8Array { + static hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array): Uint8Array { if (aad.length !== 32) throw new Error('AAD associated data must be 32 bytes') const combined = concatBytes(aad, message) return hmac(sha256, key, combined) - }, + } // metadata: always 65b (version: 1b, nonce: 32b, max: 32b) // plaintext: 1b to 0xffff @@ -80,7 +85,7 @@ const u = { // ciphertext: 32b+2 to 0xffff+2 // raw payload: 99 (65+32+2) to 65603 (65+0xffff+2) // compressed payload (base64): 132b to 87472b - decodePayload(payload: string): { nonce: Uint8Array; ciphertext: Uint8Array; mac: Uint8Array } { + static decodePayload(payload: string): { nonce: Uint8Array; ciphertext: Uint8Array; mac: Uint8Array } { if (typeof payload !== 'string') throw new Error('payload must be a valid string') const plen = payload.length if (plen < 132 || plen > 87472) throw new Error('invalid payload length: ' + plen) @@ -100,30 +105,28 @@ const u = { ciphertext: data.subarray(33, -32), mac: data.subarray(-32), } - }, + } } -function encrypt(plaintext: string, conversationKey: Uint8Array, nonce: Uint8Array = randomBytes(32)): string { - const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce) - const padded = u.pad(plaintext) - const ciphertext = chacha20(chacha_key, chacha_nonce, padded) - const mac = u.hmacAad(hmac_key, ciphertext, nonce) - return base64.encode(concatBytes(new Uint8Array([2]), nonce, ciphertext, mac)) -} +export class v2 { + static utils = u -function decrypt(payload: string, conversationKey: Uint8Array): string { - const { nonce, ciphertext, mac } = u.decodePayload(payload) - const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce) - const calculatedMac = u.hmacAad(hmac_key, ciphertext, nonce) - if (!equalBytes(calculatedMac, mac)) throw new Error('invalid MAC') - const padded = chacha20(chacha_key, chacha_nonce, ciphertext) - return u.unpad(padded) -} + static encrypt(plaintext: string, conversationKey: Uint8Array, nonce: Uint8Array = randomBytes(32)): string { + const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce) + const padded = u.pad(plaintext) + const ciphertext = chacha20(chacha_key, chacha_nonce, padded) + const mac = u.hmacAad(hmac_key, ciphertext, nonce) + return base64.encode(concatBytes(new Uint8Array([2]), nonce, ciphertext, mac)) + } -export const v2 = { - utils: u, - encrypt, - decrypt, + static decrypt(payload: string, conversationKey: Uint8Array): string { + const { nonce, ciphertext, mac } = u.decodePayload(payload) + const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce) + const calculatedMac = u.hmacAad(hmac_key, ciphertext, nonce) + if (!equalBytes(calculatedMac, mac)) throw new Error('invalid MAC') + const padded = chacha20(chacha_key, chacha_nonce, ciphertext) + return u.unpad(padded) + } } export default { v2 }