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:
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,
+ );
}