taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit f936b3e62ff5b34a7e044ff67c7d6c6f80196835
parent e0a55769190ada8b35be7a71b7b3e27c98cbc1cb
Author: Florian Dold <dold@taler.net>
Date:   Sun,  9 Aug 2026 22:23:24 +0200

util: use Node native crypto primitives

Diffstat:
Mpackages/taler-util/package.json | 5+++++
Apackages/taler-util/src/chacha20poly1305.ts | 241+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-util/src/crypto-platform.fallback.ts | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-util/src/crypto-platform.node.ts | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-util/src/crypto-platform.test.ts | 296+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/kdf.ts | 9++++-----
Mpackages/taler-util/src/taler-crypto.ts | 261+++++++++++--------------------------------------------------------------------
7 files changed, 899 insertions(+), 231 deletions(-)

diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json @@ -60,6 +60,11 @@ "browser": "./lib/argon2-impl.wasm.js", "webpack": "./lib/argon2-impl.wasm.js", "default": "./lib/argon2-impl.missing.js" + }, + "#crypto-platform": { + "types": "./lib/crypto-platform.node.d.ts", + "node": "./lib/crypto-platform.node.js", + "default": "./lib/crypto-platform.fallback.js" } }, "scripts": { diff --git a/packages/taler-util/src/chacha20poly1305.ts b/packages/taler-util/src/chacha20poly1305.ts @@ -0,0 +1,241 @@ +/* + This file is part of GNU Taler + Copyright (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + */ + +import * as nacl from "./nacl-fast.js"; + +function invariant(condition: boolean): asserts condition { + if (!condition) { + throw new Error("invariant failed"); + } +} + +// RFC 8439 ChaCha20-Poly1305 (IETF variants) + +function chacha20_toUint32(data: Uint8Array | number[], index: number): number { + return ( + data[index++] ^ + (data[index++] << 8) ^ + (data[index++] << 16) ^ + (data[index] << 24) + ); +} + +function chacha20_rotl(data: number, shift: number): number { + return (data << shift) | (data >>> (32 - shift)); +} + +export function chacha20_quarterround( + out: number[], + a: number, + b: number, + c: number, + d: number, +) { + out[d] = chacha20_rotl(out[d] ^ (out[a] += out[b]), 16); + out[b] = chacha20_rotl(out[b] ^ (out[c] += out[d]), 12); + out[d] = chacha20_rotl(out[d] ^ (out[a] += out[b]), 8); + out[b] = chacha20_rotl(out[b] ^ (out[c] += out[d]), 7); + + out[a] >>>= 0; + out[b] >>>= 0; + out[c] >>>= 0; + out[d] >>>= 0; +} + +export function chacha20_block(input: number[]): Uint8Array { + const out = Array<number>(64).fill(0); + // copy param array to x + const x = Array.from(input); + var i = 0; + var bytesWritten = 0; + + // 10 loops × 2 rounds/loop = 20 rounds + for (i = 0; i < 20; i += 2) { + // Odd round + chacha20_quarterround(x, 0, 4, 8, 12); + chacha20_quarterround(x, 1, 5, 9, 13); + chacha20_quarterround(x, 2, 6, 10, 14); + chacha20_quarterround(x, 3, 7, 11, 15); + + // Even round + chacha20_quarterround(x, 0, 5, 10, 15); + chacha20_quarterround(x, 1, 6, 11, 12); + chacha20_quarterround(x, 2, 7, 8, 13); + chacha20_quarterround(x, 3, 4, 9, 14); + } + + for (i = 0; i < 16; i++) { + // out[i] = x[i] + in[i] + let tmp = x[i] + input[i]; + + // update pad + out[bytesWritten++] = tmp & 0xff; + out[bytesWritten++] = (tmp >>> 8) & 0xff; + out[bytesWritten++] = (tmp >>> 16) & 0xff; + out[bytesWritten++] = (tmp >>> 24) & 0xff; + } + return new Uint8Array([...out]); +} + +export function chacha20_ietf_xor( + key: Uint8Array, + nonce: Uint8Array, + m: Uint8Array, + c?: number, +): Uint8Array { + invariant(0 != m.length); + var bytesWritten = 0; + const out = new Uint8Array(m.length); + const sigma: number[] = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]; + const keybytes = [ + chacha20_toUint32(key, 0), + chacha20_toUint32(key, 4), + chacha20_toUint32(key, 8), + chacha20_toUint32(key, 12), + chacha20_toUint32(key, 16), + chacha20_toUint32(key, 20), + chacha20_toUint32(key, 24), + chacha20_toUint32(key, 28), + ]; + const noncebytes = [ + chacha20_toUint32(nonce, 0), + chacha20_toUint32(nonce, 4), + chacha20_toUint32(nonce, 8), + ]; + const param: number[] = [ + ...sigma, + ...keybytes, + c ? c : 0, // Counter, index is 12 + ...noncebytes, + ]; + for (let i = 0; i < m.length; i++) { + var pad; + if (bytesWritten === 0 || bytesWritten === 64) { + // generate new block // + + pad = chacha20_block(param); + // counter increment + param[12]++; + + // bytes counter for wrap around + bytesWritten = 0; + } + invariant(pad != undefined); + out[i] = m[i] ^ pad[bytesWritten++]; + } + + return out; +} + +export function chacha20_ietf( + outBytes: number, + key: Uint8Array, + nonce: Uint8Array, +): Uint8Array { + var bytesWritten = 0; + const m = Array<number>(outBytes).fill(0); + const out = new Uint8Array(m.length); + const sigma: number[] = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]; + const keybytes = [ + chacha20_toUint32(key, 0), + chacha20_toUint32(key, 4), + chacha20_toUint32(key, 8), + chacha20_toUint32(key, 12), + chacha20_toUint32(key, 16), + chacha20_toUint32(key, 20), + chacha20_toUint32(key, 24), + chacha20_toUint32(key, 28), + ]; + const noncebytes = [ + chacha20_toUint32(nonce, 0), + chacha20_toUint32(nonce, 4), + chacha20_toUint32(nonce, 8), + ]; + const param: number[] = [ + ...sigma, + ...keybytes, + 0, // Counter, index is 12 + ...noncebytes, + ]; + for (let i = 0; i < m.length; i++) { + var pad; + if (bytesWritten === 0 || bytesWritten === 64) { + // generate new block // + + pad = chacha20_block(param); + // counter increment + param[12]++; + + // bytes counter for wrap around + bytesWritten = 0; + } + invariant(pad != undefined); + out[i] = m[i] ^ pad[bytesWritten++]; + } + + return out; +} + +export function chacha20poly1305_ietf_encrypt( + m: Uint8Array, + ad: Uint8Array, + npub: Uint8Array, + k: Uint8Array, +): Uint8Array { + invariant(k.length == 32); + invariant(npub.length == 12); + const slenBuf = new ArrayBuffer(8); + const slenDv = new DataView(slenBuf); + const pad0 = new Uint8Array(16).fill(0); + const block0 = chacha20_ietf(64, k, npub); + const tag = new Uint8Array(16); + const p = new nacl.poly1305(block0); + p.update(ad, 0, ad.length); + p.update(pad0, 0, (0x10 - ad.length) & 0xf); + const ct = chacha20_ietf_xor(k, npub, m, 1); + p.update(ct, 0, ct.length); + p.update(pad0, 0, (0x10 - m.length) & 0xf); + slenDv.setBigUint64(0, BigInt(ad.length), true); + p.update(new Uint8Array(slenBuf), 0, 8); + slenDv.setBigUint64(0, BigInt(ct.length), true); + p.update(new Uint8Array(slenBuf), 0, 8); + p.finish(tag, 0); + return new Uint8Array([...ct, ...tag]); +} + +export function chacha20poly1305_ietf_decrypt( + ct: Uint8Array, + ad: Uint8Array, + npub: Uint8Array, + k: Uint8Array, +): Uint8Array | undefined { + invariant(k.length == 32); + invariant(npub.length == 12); + const slenBuf = new ArrayBuffer(8); + const slenDv = new DataView(slenBuf); + const pad0 = new Uint8Array(16).fill(0); + const block0 = chacha20_ietf(64, k, npub); + const tag = new Uint8Array(16); + const p = new nacl.poly1305(block0); + const mlen = ct.length - tag.length; + p.update(ad, 0, ad.length); + p.update(pad0, 0, (0x10 - ad.length) & 0xf); + p.update(ct, 0, mlen); + p.update(pad0, 0, (0x10 - mlen) & 0xf); + slenDv.setBigUint64(0, BigInt(ad.length), true); + p.update(new Uint8Array(slenBuf), 0, 8); + slenDv.setBigUint64(0, BigInt(mlen), true); + p.update(new Uint8Array(slenBuf), 0, 8); + p.finish(tag, 0); + if (nacl.crypto_verify_16(tag, 0, ct, mlen) !== 0) { + return undefined; + } + const m = chacha20_ietf_xor(k, npub, ct.slice(0, mlen), 1); + return m; +} diff --git a/packages/taler-util/src/crypto-platform.fallback.ts b/packages/taler-util/src/crypto-platform.fallback.ts @@ -0,0 +1,116 @@ +/* + This file is part of GNU Taler + Copyright (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + */ + +import * as nacl from "./nacl-fast.js"; +import { sha256 } from "./sha256.js"; +import { + chacha20poly1305_ietf_decrypt, + chacha20poly1305_ietf_encrypt, +} from "./chacha20poly1305.js"; + +function hmac( + digest: (data: Uint8Array) => Uint8Array, + blockSize: number, + key: Uint8Array, + message: Uint8Array, +): Uint8Array { + if (key.byteLength > blockSize) { + key = digest(key); + } + if (key.byteLength < blockSize) { + const paddedKey = new Uint8Array(blockSize); + paddedKey.set(key); + key = paddedKey; + } + const outerKeyPad = new Uint8Array(blockSize); + const innerKeyPad = new Uint8Array(blockSize); + for (let i = 0; i < blockSize; i++) { + innerKeyPad[i] = key[i] ^ 0x36; + outerKeyPad[i] = key[i] ^ 0x5c; + } + const innerInput = new Uint8Array(blockSize + message.byteLength); + innerInput.set(innerKeyPad); + innerInput.set(message, blockSize); + const innerHash = digest(innerInput); + const outerInput = new Uint8Array(blockSize + innerHash.byteLength); + outerInput.set(outerKeyPad); + outerInput.set(innerHash, blockSize); + return digest(outerInput); +} + +export function hashSha256(data: Uint8Array): Uint8Array { + return sha256(data); +} + +export function hashSha512(data: Uint8Array): Uint8Array { + return nacl.hash(data); +} + +export function hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array { + return hmac(hashSha256, 64, key, message); +} + +export function hmacSha512(key: Uint8Array, message: Uint8Array): Uint8Array { + return hmac(hashSha512, 128, key, message); +} + +export function eddsaGetPublic(seed: Uint8Array): Uint8Array { + return nacl.crypto_sign_keyPair_fromSeed(seed).publicKey; +} + +export function eddsaSign(message: Uint8Array, seed: Uint8Array): Uint8Array { + const keyPair = nacl.crypto_sign_keyPair_fromSeed(seed); + return nacl.sign_detached(message, keyPair.secretKey); +} + +export function eddsaVerify( + message: Uint8Array, + signature: Uint8Array, + publicKey: Uint8Array, +): boolean { + return nacl.sign_detached_verify(message, signature, publicKey); +} + +export function x25519GetPublic(privateKey: Uint8Array): Uint8Array { + return nacl.scalarMult_base(privateKey); +} + +export function x25519( + privateKey: Uint8Array, + publicKey: Uint8Array, +): Uint8Array { + return nacl.scalarMult(privateKey, publicKey); +} + +export function chacha20Poly1305Encrypt( + message: Uint8Array, + additionalData: Uint8Array, + nonce: Uint8Array, + key: Uint8Array, +): Uint8Array { + return chacha20poly1305_ietf_encrypt(message, additionalData, nonce, key); +} + +export function chacha20Poly1305Decrypt( + ciphertext: Uint8Array, + additionalData: Uint8Array, + nonce: Uint8Array, + key: Uint8Array, +): Uint8Array | undefined { + return chacha20poly1305_ietf_decrypt(ciphertext, additionalData, nonce, key); +} + +export interface Sha512Context { + update(data: Uint8Array): void; + finish(): Uint8Array; +} + +export function createSha512Context(): Sha512Context { + return new nacl.HashState(); +} diff --git a/packages/taler-util/src/crypto-platform.node.ts b/packages/taler-util/src/crypto-platform.node.ts @@ -0,0 +1,202 @@ +/* + This file is part of GNU Taler + Copyright (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + */ + +import { + createCipheriv, + createDecipheriv, + createHash, + createHmac, + createPrivateKey, + createPublicKey, + diffieHellman, + sign, + verify, +} from "node:crypto"; +import { Buffer } from "node:buffer"; + +// RFC 8410 PKCS#8 / SPKI prefixes for raw Ed25519 and X25519 keys. +const ed25519Pkcs8Prefix = Uint8Array.from([ + 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, + 0x22, 0x04, 0x20, +]); +const ed25519SpkiPrefix = Uint8Array.from([ + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, +]); +const x25519Pkcs8Prefix = Uint8Array.from([ + 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x04, + 0x22, 0x04, 0x20, +]); +const x25519SpkiPrefix = Uint8Array.from([ + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00, +]); + +function checkLength(value: Uint8Array, length: number, name: string): void { + if (value.byteLength !== length) { + throw new Error(`bad ${name} size: ${value.byteLength}`); + } +} + +function privateKey(prefix: Uint8Array, rawKey: Uint8Array) { + checkLength(rawKey, 32, "private key"); + return createPrivateKey({ + key: Buffer.concat([prefix, rawKey]), + format: "der", + type: "pkcs8", + }); +} + +function publicKey(prefix: Uint8Array, rawKey: Uint8Array) { + checkLength(rawKey, 32, "public key"); + return createPublicKey({ + key: Buffer.concat([prefix, rawKey]), + format: "der", + type: "spki", + }); +} + +function rawPublicKey(der: Uint8Array): Uint8Array { + return new Uint8Array(der.subarray(der.byteLength - 32)); +} + +export function hashSha256(data: Uint8Array): Uint8Array { + return new Uint8Array(createHash("sha256").update(data).digest()); +} + +export function hashSha512(data: Uint8Array): Uint8Array { + return new Uint8Array(createHash("sha512").update(data).digest()); +} + +export function hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array { + return new Uint8Array(createHmac("sha256", key).update(message).digest()); +} + +export function hmacSha512(key: Uint8Array, message: Uint8Array): Uint8Array { + return new Uint8Array(createHmac("sha512", key).update(message).digest()); +} + +export function eddsaGetPublic(seed: Uint8Array): Uint8Array { + const privateKeyObject = privateKey(ed25519Pkcs8Prefix, seed); + const der = createPublicKey(privateKeyObject).export({ + format: "der", + type: "spki", + }); + return rawPublicKey(der); +} + +export function eddsaSign(message: Uint8Array, seed: Uint8Array): Uint8Array { + return new Uint8Array( + sign(null, message, privateKey(ed25519Pkcs8Prefix, seed)), + ); +} + +export function eddsaVerify( + message: Uint8Array, + signature: Uint8Array, + rawPublicKey: Uint8Array, +): boolean { + checkLength(signature, 64, "signature"); + return verify( + null, + message, + publicKey(ed25519SpkiPrefix, rawPublicKey), + signature, + ); +} + +export function x25519GetPublic(rawPrivateKey: Uint8Array): Uint8Array { + const privateKeyObject = privateKey(x25519Pkcs8Prefix, rawPrivateKey); + const der = createPublicKey(privateKeyObject).export({ + format: "der", + type: "spki", + }); + return rawPublicKey(der); +} + +export function x25519( + rawPrivateKey: Uint8Array, + rawPublicKey: Uint8Array, +): Uint8Array { + return new Uint8Array( + diffieHellman({ + privateKey: privateKey(x25519Pkcs8Prefix, rawPrivateKey), + publicKey: publicKey(x25519SpkiPrefix, rawPublicKey), + }), + ); +} + +export function chacha20Poly1305Encrypt( + message: Uint8Array, + additionalData: Uint8Array, + nonce: Uint8Array, + key: Uint8Array, +): Uint8Array { + checkLength(key, 32, "key"); + checkLength(nonce, 12, "nonce"); + const cipher = createCipheriv("chacha20-poly1305", key, nonce, { + authTagLength: 16, + }); + cipher.setAAD(additionalData, { plaintextLength: message.byteLength }); + return new Uint8Array( + Buffer.concat([ + cipher.update(message), + cipher.final(), + cipher.getAuthTag(), + ]), + ); +} + +export function chacha20Poly1305Decrypt( + ciphertext: Uint8Array, + additionalData: Uint8Array, + nonce: Uint8Array, + key: Uint8Array, +): Uint8Array | undefined { + checkLength(key, 32, "key"); + checkLength(nonce, 12, "nonce"); + if (ciphertext.byteLength < 16) { + return undefined; + } + const tagOffset = ciphertext.byteLength - 16; + const encrypted = ciphertext.subarray(0, tagOffset); + const tag = ciphertext.subarray(tagOffset); + try { + const decipher = createDecipheriv("chacha20-poly1305", key, nonce, { + authTagLength: 16, + }); + decipher.setAAD(additionalData, { plaintextLength: encrypted.byteLength }); + decipher.setAuthTag(tag); + return new Uint8Array( + Buffer.concat([decipher.update(encrypted), decipher.final()]), + ); + } catch { + return undefined; + } +} + +export interface Sha512Context { + update(data: Uint8Array): void; + finish(): Uint8Array; +} + +export function createSha512Context(): Sha512Context { + const hash = createHash("sha512"); + let digest: Uint8Array | undefined; + return { + update(data: Uint8Array): void { + if (digest) { + throw new Error("SHA-512 context is already finalized"); + } + hash.update(data); + }, + finish(): Uint8Array { + digest ??= new Uint8Array(hash.digest()); + return new Uint8Array(digest); + }, + }; +} diff --git a/packages/taler-util/src/crypto-platform.test.ts b/packages/taler-util/src/crypto-platform.test.ts @@ -0,0 +1,296 @@ +/* + This file is part of GNU Taler + Copyright (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + */ + +import assert from "node:assert"; +import { test } from "node:test"; +import * as fallbackCrypto from "./crypto-platform.fallback.js"; +import * as nodeCrypto from "./crypto-platform.node.js"; + +type CryptoImplementation = typeof nodeCrypto; + +const implementations: [string, CryptoImplementation][] = [ + ["node", nodeCrypto], + ["fallback", fallbackCrypto], +]; + +function fromHex(value: string): Uint8Array { + const normalized = value.replaceAll(/\s/g, ""); + assert.strictEqual(normalized.length % 2, 0); + const result = new Uint8Array(normalized.length / 2); + for (let i = 0; i < result.byteLength; i++) { + result[i] = Number.parseInt(normalized.slice(i * 2, i * 2 + 2), 16); + } + return result; +} + +function assertBytesEqual(actual: Uint8Array, expected: Uint8Array): void { + assert.deepStrictEqual(Array.from(actual), Array.from(expected)); +} + +const abc = new TextEncoder().encode("abc"); + +for (const [name, implementation] of implementations) { + test(`${name} SHA-256 matches the FIPS 180-4 vector`, () => { + assertBytesEqual( + implementation.hashSha256(abc), + fromHex( + "ba7816bf8f01cfea414140de5dae2223" + "b00361a396177a9cb410ff61f20015ad", + ), + ); + }); + + test(`${name} SHA-512 matches the FIPS 180-4 vector`, () => { + assertBytesEqual( + implementation.hashSha512(abc), + fromHex( + "ddaf35a193617abacc417349ae204131" + + "12e6fa4e89a97ea20a9eeee64b55d39a" + + "2192992a274fc1a836ba3c23a3feebbd" + + "454d4423643ce80e2a9ac94fa54ca49f", + ), + ); + }); + + test(`${name} incremental SHA-512 matches the one-shot hash`, () => { + const context = implementation.createSha512Context(); + context.update(abc.subarray(0, 1)); + context.update(abc.subarray(1)); + assertBytesEqual(context.finish(), implementation.hashSha512(abc)); + }); + + test(`${name} HMAC-SHA-256 matches RFC 4231 case 1`, () => { + assertBytesEqual( + implementation.hmacSha256( + new Uint8Array(20).fill(0x0b), + new TextEncoder().encode("Hi There"), + ), + fromHex( + "b0344c61d8db38535ca8afceaf0bf12b" + "881dc200c9833da726e9376c2e32cff7", + ), + ); + }); + + test(`${name} HMAC-SHA-512 matches RFC 4231 case 1`, () => { + assertBytesEqual( + implementation.hmacSha512( + new Uint8Array(20).fill(0x0b), + new TextEncoder().encode("Hi There"), + ), + fromHex( + "87aa7cdea5ef619d4ff0b4241a1d6cb0" + + "2379f4e2ce4ec2787ad0b30545e17cde" + + "daa833b7d6b8a702038b274eaea3f4e4" + + "be9d914eeb61f1702e696c203a126854", + ), + ); + }); + + test(`${name} Ed25519 matches RFC 8032 test vector 1`, () => { + const seed = fromHex( + "9d61b19deffd5a60ba844af492ec2cc4" + "4449c5697b326919703bac031cae7f60", + ); + const publicKey = fromHex( + "d75a980182b10ab7d54bfed3c964073a" + "0ee172f3daa62325af021a68f707511a", + ); + const signature = fromHex( + "e5564300c360ac729086e2cc806e828a" + + "84877f1eb8e5d974d873e06522490155" + + "5fb8821590a33bacc61e39701cf9b46b" + + "d25bf5f0595bbe24655141438e7a100b", + ); + const message = new Uint8Array(); + + assertBytesEqual(implementation.eddsaGetPublic(seed), publicKey); + assertBytesEqual(implementation.eddsaSign(message, seed), signature); + assert.strictEqual( + implementation.eddsaVerify(message, signature, publicKey), + true, + ); + signature[0] ^= 1; + assert.strictEqual( + implementation.eddsaVerify(message, signature, publicKey), + false, + ); + }); + + test(`${name} X25519 matches RFC 7748 Alice's key and shared secret`, () => { + const alicePrivate = fromHex( + "77076d0a7318a57d3c16c17251b26645" + "df4c2f87ebc0992ab177fba51db92c2a", + ); + const alicePublic = fromHex( + "8520f0098930a754748b7ddcb43ef75a" + "0dbf3a0d26381af4eba4a98eaa9b4e6a", + ); + const bobPublic = fromHex( + "de9edb7d7b7dc1b4d35b61c2ece43537" + "3f8343c85b78674dadfc7e146f882b4f", + ); + const sharedSecret = fromHex( + "4a5d9d5ba4ce2de1728e3bf480350f25" + "e07e21c947d19e3376f09b3c1e161742", + ); + + assertBytesEqual(implementation.x25519GetPublic(alicePrivate), alicePublic); + assertBytesEqual( + implementation.x25519(alicePrivate, bobPublic), + sharedSecret, + ); + }); + + test(`${name} ChaCha20-Poly1305 matches RFC 8439 section 2.8.2`, () => { + const key = fromHex( + "808182838485868788898a8b8c8d8e8f" + "909192939495969798999a9b9c9d9e9f", + ); + const nonce = fromHex("070000004041424344454647"); + const additionalData = fromHex("50515253c0c1c2c3c4c5c6c7"); + const message = fromHex( + "4c616469657320616e642047656e746c" + + "656d656e206f662074686520636c6173" + + "73206f66202739393a20496620492063" + + "6f756c64206f6666657220796f75206f" + + "6e6c79206f6e652074697020666f7220" + + "746865206675747572652c2073756e73" + + "637265656e20776f756c642062652069" + + "742e", + ); + const ciphertext = fromHex( + "d31a8d34648e60db7b86afbc53ef7ec2" + + "a4aded51296e08fea9e2b5a736ee62d6" + + "3dbea45e8ca9671282fafb69da92728b" + + "1a71de0a9e060b2905d6a5b67ecd3b36" + + "92ddbd7f2d778b8c9803aee328091b58" + + "fab324e4fad675945585808b4831d7bc" + + "3ff4def08e4b7a9de576d26586cec64b" + + "61161ae10b594f09e26a7e902ecbd0600691", + ); + + assertBytesEqual( + implementation.chacha20Poly1305Encrypt( + message, + additionalData, + nonce, + key, + ), + ciphertext, + ); + assertBytesEqual( + implementation.chacha20Poly1305Decrypt( + ciphertext, + additionalData, + nonce, + key, + )!, + message, + ); + ciphertext[0] ^= 1; + assert.strictEqual( + implementation.chacha20Poly1305Decrypt( + ciphertext, + additionalData, + nonce, + key, + ), + undefined, + ); + }); +} + +test("Node and fallback primitives produce identical results", () => { + const message = Uint8Array.from({ length: 257 }, (_, i) => (i * 31) & 0xff); + const key = Uint8Array.from({ length: 131 }, (_, i) => (i * 17) & 0xff); + const ed25519Seed = message.subarray(7, 39); + const x25519Private = message.subarray(41, 73); + const otherX25519Private = message.subarray(101, 133); + const otherX25519Public = fallbackCrypto.x25519GetPublic(otherX25519Private); + + assertBytesEqual( + nodeCrypto.hashSha256(message), + fallbackCrypto.hashSha256(message), + ); + assertBytesEqual( + nodeCrypto.hashSha512(message), + fallbackCrypto.hashSha512(message), + ); + assertBytesEqual( + nodeCrypto.hmacSha256(key, message), + fallbackCrypto.hmacSha256(key, message), + ); + assertBytesEqual( + nodeCrypto.hmacSha512(key, message), + fallbackCrypto.hmacSha512(key, message), + ); + + const nodePublic = nodeCrypto.eddsaGetPublic(ed25519Seed); + const fallbackPublic = fallbackCrypto.eddsaGetPublic(ed25519Seed); + const nodeSignature = nodeCrypto.eddsaSign(message, ed25519Seed); + const fallbackSignature = fallbackCrypto.eddsaSign(message, ed25519Seed); + assertBytesEqual(nodePublic, fallbackPublic); + assertBytesEqual(nodeSignature, fallbackSignature); + assert.strictEqual( + fallbackCrypto.eddsaVerify(message, nodeSignature, nodePublic), + true, + ); + assert.strictEqual( + nodeCrypto.eddsaVerify(message, fallbackSignature, fallbackPublic), + true, + ); + + assertBytesEqual( + nodeCrypto.x25519GetPublic(x25519Private), + fallbackCrypto.x25519GetPublic(x25519Private), + ); + assertBytesEqual( + nodeCrypto.x25519(x25519Private, otherX25519Public), + fallbackCrypto.x25519(x25519Private, otherX25519Public), + ); + + const nonce = message.subarray(73, 85); + const aeadKey = message.subarray(85, 117); + const additionalData = message.subarray(117, 181); + const nodeCiphertext = nodeCrypto.chacha20Poly1305Encrypt( + message, + additionalData, + nonce, + aeadKey, + ); + const fallbackCiphertext = fallbackCrypto.chacha20Poly1305Encrypt( + message, + additionalData, + nonce, + aeadKey, + ); + assertBytesEqual(nodeCiphertext, fallbackCiphertext); + assertBytesEqual( + nodeCrypto.chacha20Poly1305Decrypt( + fallbackCiphertext, + additionalData, + nonce, + aeadKey, + )!, + message, + ); + assertBytesEqual( + fallbackCrypto.chacha20Poly1305Decrypt( + nodeCiphertext, + additionalData, + nonce, + aeadKey, + )!, + message, + ); + + const nodeContext = nodeCrypto.createSha512Context(); + const fallbackContext = fallbackCrypto.createSha512Context(); + for (const chunk of [ + message.subarray(0, 3), + message.subarray(3, 129), + message.subarray(129), + ]) { + nodeContext.update(chunk); + fallbackContext.update(chunk); + } + assertBytesEqual(nodeContext.finish(), fallbackContext.finish()); +}); diff --git a/packages/taler-util/src/kdf.ts b/packages/taler-util/src/kdf.ts @@ -14,11 +14,10 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import * as nacl from "./nacl-fast.js"; -import { sha256 } from "./sha256.js"; +import * as platformCrypto from "#crypto-platform"; export function sha512(data: Uint8Array): Uint8Array { - return nacl.hash(data); + return platformCrypto.hashSha512(data); } export function hmac( @@ -52,9 +51,9 @@ export function hmac( } export function hmacSha512(key: Uint8Array, message: Uint8Array): Uint8Array { - return hmac(sha512, 128, key, message); + return platformCrypto.hmacSha512(key, message); } export function hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array { - return hmac(sha256, 64, key, message); + return platformCrypto.hmacSha256(key, message); } diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts @@ -23,6 +23,7 @@ */ import bigint from "big-integer"; import * as fflate from "fflate"; +import * as platformCrypto from "#crypto-platform"; import { AmountLike, Amounts } from "./amounts.js"; import * as argon2 from "./argon2.js"; import { canonicalJson } from "./helpers.js"; @@ -390,15 +391,14 @@ export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array { if (tart) { return tart.eddsaGetPublic(eddsaPriv); } - const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv); - return pair.publicKey; + return platformCrypto.eddsaGetPublic(eddsaPriv); } export function ecdhGetPublic(ecdhePriv: Uint8Array): Uint8Array { if (tart) { return tart.ecdheGetPublic(ecdhePriv); } - return nacl.scalarMult_base(ecdhePriv); + return platformCrypto.x25519GetPublic(ecdhePriv); } export function keyExchangeEddsaEcdh( @@ -413,7 +413,7 @@ export function keyExchangeEddsaEcdh( for (let i = 0; i < 32; i++) { a[i] = ph[i]; } - const x = nacl.scalarMult(a, ecdhPub); + const x = platformCrypto.x25519(a, ecdhPub); return hash(x); } @@ -425,7 +425,7 @@ export function keyExchangeEcdhEddsa( return tart.keyExchangeEcdhEddsa(ecdhPriv, eddsaPub); } const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub); - const x = nacl.scalarMult(ecdhPriv, curve25519Pub); + const x = platformCrypto.x25519(ecdhPriv, curve25519Pub); return hash(x); } @@ -867,7 +867,7 @@ export function hash(d: Uint8Array): Uint8Array { if (tart) { return tart.hash(d); } - return nacl.hash(d); + return platformCrypto.hashSha512(d); } /** @@ -1011,8 +1011,7 @@ export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array { if (tart) { return tart.eddsaSign(msg, eddsaPriv); } - const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv); - return nacl.sign_detached(msg, pair.secretKey); + return platformCrypto.eddsaSign(msg, eddsaPriv); } export function eddsaVerify( @@ -1023,7 +1022,7 @@ export function eddsaVerify( if (tart) { return tart.eddsaVerify(msg, sig, eddsaPub); } - return nacl.sign_detached_verify(msg, sig, eddsaPub); + return platformCrypto.eddsaVerify(msg, sig, eddsaPub); } export interface TalerHashState { @@ -1040,7 +1039,7 @@ export function createHashContext(): TalerHashState { update: (d) => t.hashStateUpdate(st, d), }; } - return new nacl.HashState(); + return platformCrypto.createSha512Context(); } export interface FreshCoin { @@ -1910,7 +1909,7 @@ export function ecdh_x25519( ecdhPub: Uint8Array, ): Uint8Array { var checkbyte = 0; - const res = nacl.scalarMult(ecdhPriv, ecdhPub); + const res = platformCrypto.x25519(ecdhPriv, ecdhPub); for (let i = 0; i < res.length; i++) { checkbyte = res[i] | checkbyte; } @@ -2152,227 +2151,37 @@ export function hpkeCreateSecretKey(): HpkeSecretKey { return keypair.ecdhePriv as HpkeSecretKey; } -// RFC 8439 ChaCha20-Poly1305 (IETF variants) +export { + chacha20_block, + chacha20_ietf, + chacha20_ietf_xor, + chacha20_quarterround, +} from "./chacha20poly1305.js"; -function chacha20_toUint32(data: Uint8Array | number[], index: number): number { - return ( - data[index++] ^ - (data[index++] << 8) ^ - (data[index++] << 16) ^ - (data[index] << 24) - ); -} - -function chacha20_rotl(data: number, shift: number): number { - return (data << shift) | (data >>> (32 - shift)); -} - -export function chacha20_quarterround( - out: number[], - a: number, - b: number, - c: number, - d: number, -) { - out[d] = chacha20_rotl(out[d] ^ (out[a] += out[b]), 16); - out[b] = chacha20_rotl(out[b] ^ (out[c] += out[d]), 12); - out[d] = chacha20_rotl(out[d] ^ (out[a] += out[b]), 8); - out[b] = chacha20_rotl(out[b] ^ (out[c] += out[d]), 7); - - out[a] >>>= 0; - out[b] >>>= 0; - out[c] >>>= 0; - out[d] >>>= 0; -} - -export function chacha20_block(input: number[]): Uint8Array { - const out = Array<number>(64).fill(0); - // copy param array to x - const x = Array.from(input); - var i = 0; - var bytesWritten = 0; - - // 10 loops × 2 rounds/loop = 20 rounds - for (i = 0; i < 20; i += 2) { - // Odd round - chacha20_quarterround(x, 0, 4, 8, 12); - chacha20_quarterround(x, 1, 5, 9, 13); - chacha20_quarterround(x, 2, 6, 10, 14); - chacha20_quarterround(x, 3, 7, 11, 15); - - // Even round - chacha20_quarterround(x, 0, 5, 10, 15); - chacha20_quarterround(x, 1, 6, 11, 12); - chacha20_quarterround(x, 2, 7, 8, 13); - chacha20_quarterround(x, 3, 4, 9, 14); - } - - for (i = 0; i < 16; i++) { - // out[i] = x[i] + in[i] - let tmp = x[i] + input[i]; - - // update pad - out[bytesWritten++] = tmp & 0xff; - out[bytesWritten++] = (tmp >>> 8) & 0xff; - out[bytesWritten++] = (tmp >>> 16) & 0xff; - out[bytesWritten++] = (tmp >>> 24) & 0xff; - } - return new Uint8Array([...out]); -} - -export function chacha20_ietf_xor( - key: Uint8Array, +export function chacha20poly1305_ietf_encrypt( + message: Uint8Array, + additionalData: Uint8Array, nonce: Uint8Array, - m: Uint8Array, - c?: number, -): Uint8Array { - invariant(0 != m.length); - var bytesWritten = 0; - const out = new Uint8Array(m.length); - const sigma: number[] = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]; - const keybytes = [ - chacha20_toUint32(key, 0), - chacha20_toUint32(key, 4), - chacha20_toUint32(key, 8), - chacha20_toUint32(key, 12), - chacha20_toUint32(key, 16), - chacha20_toUint32(key, 20), - chacha20_toUint32(key, 24), - chacha20_toUint32(key, 28), - ]; - const noncebytes = [ - chacha20_toUint32(nonce, 0), - chacha20_toUint32(nonce, 4), - chacha20_toUint32(nonce, 8), - ]; - const param: number[] = [ - ...sigma, - ...keybytes, - c ? c : 0, // Counter, index is 12 - ...noncebytes, - ]; - for (let i = 0; i < m.length; i++) { - var pad; - if (bytesWritten === 0 || bytesWritten === 64) { - // generate new block // - - pad = chacha20_block(param); - // counter increment - param[12]++; - - // bytes counter for wrap around - bytesWritten = 0; - } - invariant(pad != undefined); - out[i] = m[i] ^ pad[bytesWritten++]; - } - - return out; -} - -export function chacha20_ietf( - outBytes: number, key: Uint8Array, - nonce: Uint8Array, ): Uint8Array { - var bytesWritten = 0; - const m = Array<number>(outBytes).fill(0); - const out = new Uint8Array(m.length); - const sigma: number[] = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]; - const keybytes = [ - chacha20_toUint32(key, 0), - chacha20_toUint32(key, 4), - chacha20_toUint32(key, 8), - chacha20_toUint32(key, 12), - chacha20_toUint32(key, 16), - chacha20_toUint32(key, 20), - chacha20_toUint32(key, 24), - chacha20_toUint32(key, 28), - ]; - const noncebytes = [ - chacha20_toUint32(nonce, 0), - chacha20_toUint32(nonce, 4), - chacha20_toUint32(nonce, 8), - ]; - const param: number[] = [ - ...sigma, - ...keybytes, - 0, // Counter, index is 12 - ...noncebytes, - ]; - for (let i = 0; i < m.length; i++) { - var pad; - if (bytesWritten === 0 || bytesWritten === 64) { - // generate new block // - - pad = chacha20_block(param); - // counter increment - param[12]++; - - // bytes counter for wrap around - bytesWritten = 0; - } - invariant(pad != undefined); - out[i] = m[i] ^ pad[bytesWritten++]; - } - - return out; -} - -export function chacha20poly1305_ietf_encrypt( - m: Uint8Array, - ad: Uint8Array, - npub: Uint8Array, - k: Uint8Array, -): Uint8Array { - invariant(k.length == 32); - invariant(npub.length == 12); - const slenBuf = new ArrayBuffer(8); - const slenDv = new DataView(slenBuf); - const pad0 = new Uint8Array(16).fill(0); - const block0 = chacha20_ietf(64, k, npub); - const tag = new Uint8Array(16); - const p = new nacl.poly1305(block0); - p.update(ad, 0, ad.length); - p.update(pad0, 0, (0x10 - ad.length) & 0xf); - const ct = chacha20_ietf_xor(k, npub, m, 1); - p.update(ct, 0, ct.length); - p.update(pad0, 0, (0x10 - m.length) & 0xf); - slenDv.setBigUint64(0, BigInt(ad.length), true); - p.update(new Uint8Array(slenBuf), 0, 8); - slenDv.setBigUint64(0, BigInt(ct.length), true); - p.update(new Uint8Array(slenBuf), 0, 8); - p.finish(tag, 0); - return new Uint8Array([...ct, ...tag]); + return platformCrypto.chacha20Poly1305Encrypt( + message, + additionalData, + nonce, + key, + ); } export function chacha20poly1305_ietf_decrypt( - ct: Uint8Array, - ad: Uint8Array, - npub: Uint8Array, - k: Uint8Array, + ciphertext: Uint8Array, + additionalData: Uint8Array, + nonce: Uint8Array, + key: Uint8Array, ): Uint8Array | undefined { - invariant(k.length == 32); - invariant(npub.length == 12); - const slenBuf = new ArrayBuffer(8); - const slenDv = new DataView(slenBuf); - const pad0 = new Uint8Array(16).fill(0); - const block0 = chacha20_ietf(64, k, npub); - const tag = new Uint8Array(16); - const p = new nacl.poly1305(block0); - const mlen = ct.length - tag.length; - p.update(ad, 0, ad.length); - p.update(pad0, 0, (0x10 - ad.length) & 0xf); - p.update(ct, 0, mlen); - p.update(pad0, 0, (0x10 - mlen) & 0xf); - slenDv.setBigUint64(0, BigInt(ad.length), true); - p.update(new Uint8Array(slenBuf), 0, 8); - slenDv.setBigUint64(0, BigInt(mlen), true); - p.update(new Uint8Array(slenBuf), 0, 8); - p.finish(tag, 0); - if (nacl.crypto_verify_16(tag, 0, ct, mlen) !== 0) { - return undefined; - } - const m = chacha20_ietf_xor(k, npub, ct.slice(0, mlen), 1); - return m; + return platformCrypto.chacha20Poly1305Decrypt( + ciphertext, + additionalData, + nonce, + key, + ); }