/* This file is part of GNU Taler (C) 2019 GNUnet e.V. 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. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ /** * Native implementation of GNU Taler crypto primitives. */ /** * Imports. */ import bigint from "big-integer"; import * as fflate from "fflate"; import { AmountLike, Amounts } from "./amounts.js"; import * as argon2 from "./argon2.js"; import { canonicalJson } from "./helpers.js"; import { hmacSha256, hmacSha512 } from "./kdf.js"; import { Logger } from "./logging.js"; import * as nacl from "./nacl-fast.js"; import { secretbox } from "./nacl-fast.js"; import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js"; import { CoinPublicKeyString, HashCodeString } from "./types-taler-common.js"; import { CoinEnvelope, DenomKeyType, DenominationPubKey, } from "./types-taler-exchange.js"; export type Flavor = T & { _flavor?: `taler.${FlavorT}`; }; export type FlavorP = T & { _flavor?: `taler.${FlavorT}`; _size?: S; }; export function getRandomBytes(n: number): Uint8Array { return nacl.randomBytes(n); } export function getRandomBytesF( n: T, ): FlavorP { return nacl.randomBytes(n); } export const useNative = true; /** * Interface of the native Taler runtime library. */ interface NativeTartLib { decodeUtf8(buf: Uint8Array): string; decodeUtf8(str: string): Uint8Array; randomBytes(n: number): Uint8Array; encodeCrock(buf: Uint8Array | ArrayBuffer): string; decodeCrock(str: string): Uint8Array; hash(buf: Uint8Array): Uint8Array; hashArgon2id( password: Uint8Array, salt: Uint8Array, iterations: number, memorySize: number, hashLength: number, ): Uint8Array; eddsaGetPublic(buf: Uint8Array): Uint8Array; ecdheGetPublic(buf: Uint8Array): Uint8Array; eddsaSign(msg: Uint8Array, priv: Uint8Array): Uint8Array; eddsaVerify(msg: Uint8Array, sig: Uint8Array, pub: Uint8Array): boolean; kdf( outLen: number, ikm: Uint8Array, salt?: Uint8Array, info?: Uint8Array, ): Uint8Array; keyExchangeEcdhEddsa(ecdhPriv: Uint8Array, eddsaPub: Uint8Array): Uint8Array; keyExchangeEddsaEcdh(eddsaPriv: Uint8Array, ecdhPub: Uint8Array): Uint8Array; rsaBlind(hmsg: Uint8Array, bks: Uint8Array, rsaPub: Uint8Array): Uint8Array; rsaUnblind( blindSig: Uint8Array, rsaPub: Uint8Array, bks: Uint8Array, ): Uint8Array; rsaVerify(hmsg: Uint8Array, rsaSig: Uint8Array, rsaPub: Uint8Array): boolean; hashStateInit(): any; hashStateUpdate(st: any, data: Uint8Array): any; hashStateFinish(st: any): Uint8Array; } // @ts-ignore let tart: NativeTartLib | undefined; if (useNative) { // @ts-ignore tart = globalThis._tart; } const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; class EncodingError extends Error { constructor() { super("Encoding error"); Object.setPrototypeOf(this, EncodingError.prototype); } } function getValue(chr: string): number { let a = chr; switch (chr) { case "O": case "o": a = "0"; break; case "i": case "I": case "l": case "L": a = "1"; break; case "u": case "U": a = "V"; } if (a >= "0" && a <= "9") { return a.charCodeAt(0) - "0".charCodeAt(0); } if (a >= "a" && a <= "z") a = a.toUpperCase(); let dec = 0; if (a >= "A" && a <= "Z") { if ("I" < a) dec++; if ("L" < a) dec++; if ("O" < a) dec++; if ("U" < a) dec++; return a.charCodeAt(0) - "A".charCodeAt(0) + 10 - dec; } throw new EncodingError(); } export function encodeCrock(data: ArrayBuffer): string { if (tart) { return tart.encodeCrock(data); } const dataBytes = new Uint8Array(data); let sb = ""; const size = data.byteLength; let bitBuf = 0; let numBits = 0; let pos = 0; while (pos < size || numBits > 0) { if (pos < size && numBits < 5) { const d = dataBytes[pos++]; bitBuf = (bitBuf << 8) | d; numBits += 8; } if (numBits < 5) { // zero-padding bitBuf = bitBuf << (5 - numBits); numBits = 5; } const v = (bitBuf >>> (numBits - 5)) & 31; sb += encTable[v]; numBits -= 5; } return sb; } export function kdf( outputLength: number, ikm: Uint8Array, salt?: Uint8Array, info?: Uint8Array, ): Uint8Array { if (tart) { return tart.kdf(outputLength, ikm, salt, info); } salt = salt ?? new Uint8Array(64); // extract const prk = hmacSha512(salt, ikm); info = info ?? new Uint8Array(0); // expand const N = Math.ceil(outputLength / 32); const output = new Uint8Array(N * 32); for (let i = 0; i < N; i++) { let buf; if (i == 0) { buf = new Uint8Array(info.byteLength + 1); buf.set(info, 0); } else { buf = new Uint8Array(info.byteLength + 1 + 32); for (let j = 0; j < 32; j++) { buf[j] = output[(i - 1) * 32 + j]; } buf.set(info, 32); } buf[buf.length - 1] = i + 1; const chunk = hmacSha256(prk, buf); output.set(chunk, i * 32); } return output.slice(0, outputLength); } /** * HMAC-SHA512-SHA256 (see RFC 5869). */ export function kdfKw(args: { outputLength: number; ikm: Uint8Array; salt?: Uint8Array; info?: Uint8Array; }) { return kdf(args.outputLength, args.ikm, args.salt, args.info); } export function decodeCrock(encoded: string): Uint8Array { if (tart) { return tart.decodeCrock(encoded); } const size = encoded.length; let bitpos = 0; let bitbuf = 0; let readPosition = 0; const outLen = Math.floor((size * 5) / 8); const out = new Uint8Array(outLen); let outPos = 0; while (readPosition < size || bitpos > 0) { if (readPosition < size) { const v = getValue(encoded[readPosition++]); bitbuf = (bitbuf << 5) | v; bitpos += 5; } while (bitpos >= 8) { const d = (bitbuf >>> (bitpos - 8)) & 0xff; out[outPos++] = d; bitpos -= 8; } if (readPosition == size && bitpos > 0) { bitbuf = (bitbuf << (8 - bitpos)) & 0xff; bitpos = bitbuf == 0 ? 0 : 8; } } return out; } export async function hashArgon2id( password: Uint8Array, salt: Uint8Array, iterations: number, memorySize: number, hashLength: number, ): Promise { if (tart) { return tart.hashArgon2id( password, salt, iterations, memorySize, hashLength, ); } return await argon2.hashArgon2id( password, salt, iterations, memorySize, hashLength, ); } export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array { if (tart) { return tart.eddsaGetPublic(eddsaPriv); } const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv); return pair.publicKey; } export function ecdhGetPublic(ecdhePriv: Uint8Array): Uint8Array { if (tart) { return tart.ecdheGetPublic(ecdhePriv); } return nacl.scalarMult_base(ecdhePriv); } export function keyExchangeEddsaEcdh( eddsaPriv: Uint8Array, ecdhPub: Uint8Array, ): Uint8Array { if (tart) { return tart.keyExchangeEddsaEcdh(eddsaPriv, ecdhPub); } const ph = hash(eddsaPriv); const a = new Uint8Array(32); for (let i = 0; i < 32; i++) { a[i] = ph[i]; } const x = nacl.scalarMult(a, ecdhPub); return hash(x); } export function keyExchangeEcdhEddsa( ecdhPriv: Uint8Array & MaterialEcdhePriv, eddsaPub: Uint8Array & MaterialEddsaPub, ): Uint8Array { if (tart) { return tart.keyExchangeEcdhEddsa(ecdhPriv, eddsaPub); } const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub); const x = nacl.scalarMult(ecdhPriv, curve25519Pub); return hash(x); } interface RsaPub { N: bigint.BigInteger; e: bigint.BigInteger; } /** * KDF modulo a big integer. */ function kdfMod( n: bigint.BigInteger, ikm: Uint8Array, salt: Uint8Array, info: Uint8Array, ): bigint.BigInteger { const nbits = n.bitLength().toJSNumber(); const buflen = Math.floor((nbits - 1) / 8 + 1); const mask = (1 << (8 - (buflen * 8 - nbits))) - 1; let counter = 0; while (true) { const ctx = new Uint8Array(info.byteLength + 2); ctx.set(info, 0); ctx[ctx.length - 2] = (counter >>> 8) & 0xff; ctx[ctx.length - 1] = counter & 0xff; const buf = kdf(buflen, ikm, salt, ctx); const arr = Array.from(buf); arr[0] = arr[0] & mask; const r = bigint.fromArray(arr, 256, false); if (r.lt(n)) { return r; } counter++; } } function csKdfMod( n: bigint.BigInteger, ikm: Uint8Array, salt: Uint8Array, info: Uint8Array, ): Uint8Array { const nbits = n.bitLength().toJSNumber(); const buflen = Math.floor((nbits - 1) / 8 + 1); const mask = (1 << (8 - (buflen * 8 - nbits))) - 1; let counter = 0; while (true) { const ctx = new Uint8Array(info.byteLength + 2); ctx.set(info, 0); ctx[ctx.length - 2] = (counter >>> 8) & 0xff; ctx[ctx.length - 1] = counter & 0xff; const buf = kdf(buflen, ikm, salt, ctx); const arr = Array.from(buf); arr[0] = arr[0] & mask; const r = bigint.fromArray(arr, 256, false); if (r.lt(n)) { return new Uint8Array(arr); } counter++; } } // Newer versions of node have TextEncoder and TextDecoder as a global, // just like modern browsers. // In older versions of node or environments that do not have these // globals, they must be polyfilled (by adding them to global/globalThis) // before stringToBytes or bytesToString is called the first time. let encoder: any; let decoder: any; export function stringToBytes(s: string): Uint8Array { if (!encoder) { encoder = new TextEncoder(); } return encoder.encode(s); } export function bytesToString(b: Uint8Array): string { if (!decoder) { decoder = new TextDecoder(); } return decoder.decode(b); } function loadBigInt(arr: Uint8Array): bigint.BigInteger { return bigint.fromArray(Array.from(arr), 256, false); } function rsaBlindingKeyDerive( rsaPub: RsaPub, bks: Uint8Array, ): bigint.BigInteger { const salt = stringToBytes("Blinding KDF extractor HMAC key"); const info = stringToBytes("Blinding KDF"); return kdfMod(rsaPub.N, bks, salt, info); } /* * Test for malicious RSA key. * * Assuming n is an RSA modulous and r is generated using a call to * GNUNET_CRYPTO_kdf_mod_mpi, if gcd(r,n) != 1 then n must be a * malicious RSA key designed to deanomize the user. * * @param r KDF result * @param n RSA modulus of the public key */ function rsaGcdValidate(r: bigint.BigInteger, n: bigint.BigInteger): void { const t = bigint.gcd(r, n); if (!t.equals(bigint.one)) { throw Error("malicious RSA public key"); } } function rsaFullDomainHash(hm: Uint8Array, rsaPub: RsaPub): bigint.BigInteger { const info = stringToBytes("RSA-FDA FTpsW!"); const salt = rsaPubEncode(rsaPub); const r = kdfMod(rsaPub.N, hm, salt, info); rsaGcdValidate(r, rsaPub.N); return r; } function rsaPubDecode(rsaPub: Uint8Array): RsaPub { const modulusLength = (rsaPub[0] << 8) | rsaPub[1]; const exponentLength = (rsaPub[2] << 8) | rsaPub[3]; if (4 + exponentLength + modulusLength != rsaPub.length) { throw Error("invalid RSA public key (format wrong)"); } const modulus = rsaPub.slice(4, 4 + modulusLength); const exponent = rsaPub.slice( 4 + modulusLength, 4 + modulusLength + exponentLength, ); const res = { N: loadBigInt(modulus), e: loadBigInt(exponent), }; return res; } function rsaPubEncode(rsaPub: RsaPub): Uint8Array { const mb = rsaPub.N.toArray(256).value; const eb = rsaPub.e.toArray(256).value; const out = new Uint8Array(4 + mb.length + eb.length); out[0] = (mb.length >>> 8) & 0xff; out[1] = mb.length & 0xff; out[2] = (eb.length >>> 8) & 0xff; out[3] = eb.length & 0xff; out.set(mb, 4); out.set(eb, 4 + mb.length); return out; } export function rsaBlind( hm: Uint8Array, bks: Uint8Array, rsaPubEnc: Uint8Array, ): Uint8Array { if (tart) { return tart.rsaBlind(hm, bks, rsaPubEnc); } const rsaPub = rsaPubDecode(rsaPubEnc); const data = rsaFullDomainHash(hm, rsaPub); const r = rsaBlindingKeyDerive(rsaPub, bks); const r_e = r.modPow(rsaPub.e, rsaPub.N); const bm = r_e.multiply(data).mod(rsaPub.N); return new Uint8Array(bm.toArray(256).value); } export function rsaUnblind( sig: Uint8Array, rsaPubEnc: Uint8Array, bks: Uint8Array, ): Uint8Array { if (tart) { return tart.rsaUnblind(sig, rsaPubEnc, bks); } const rsaPub = rsaPubDecode(rsaPubEnc); const blinded_s = loadBigInt(sig); const r = rsaBlindingKeyDerive(rsaPub, bks); const r_inv = r.modInv(rsaPub.N); const s = blinded_s.multiply(r_inv).mod(rsaPub.N); return new Uint8Array(s.toArray(256).value); } export function rsaVerify( hm: Uint8Array, rsaSig: Uint8Array, rsaPubEnc: Uint8Array, ): boolean { if (tart) { return tart.rsaVerify(hm, rsaSig, rsaPubEnc); } const rsaPub = rsaPubDecode(rsaPubEnc); const d = rsaFullDomainHash(hm, rsaPub); const sig = loadBigInt(rsaSig); const sig_e = sig.modPow(rsaPub.e, rsaPub.N); return sig_e.equals(d); } export type CsSignature = { s: Uint8Array; rPub: Uint8Array; }; export type CsBlindSignature = { sBlind: Uint8Array; rPubBlind: Uint8Array; }; export type CsBlindingSecrets = { alpha: [Uint8Array, Uint8Array]; beta: [Uint8Array, Uint8Array]; }; export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array { let payloadLen = 0; for (const c of chunks) { payloadLen += c.byteLength; } const buf = new ArrayBuffer(payloadLen); const u8buf = new Uint8Array(buf); let p = 0; for (const c of chunks) { u8buf.set(c, p); p += c.byteLength; } return u8buf; } /** * Map to scalar subgroup function * perform clamping as described in RFC7748 * @param scalar */ function mtoSS(scalar: Uint8Array): Uint8Array { scalar[0] &= 248; scalar[31] &= 127; scalar[31] |= 64; return scalar; } /** * The function returns the CS blinding secrets from a seed * @param bseed seed to derive blinding secrets * @returns blinding secrets */ export function deriveSecrets(bseed: Uint8Array): CsBlindingSecrets { const outLen = 130; const salt = stringToBytes("alphabeta"); const rndout = kdf(outLen, bseed, salt); const secrets: CsBlindingSecrets = { alpha: [mtoSS(rndout.slice(0, 32)), mtoSS(rndout.slice(64, 96))], beta: [mtoSS(rndout.slice(32, 64)), mtoSS(rndout.slice(96, 128))], }; return secrets; } /** * calculation of the blinded public point R in CS * @param csPub denomination publik key * @param secrets client blinding secrets * @param rPub public R received from /csr API */ export async function calcRBlind( csPub: Uint8Array, secrets: CsBlindingSecrets, rPub: [Uint8Array, Uint8Array], ): Promise<[Uint8Array, Uint8Array]> { const aG0 = nacl.crypto_scalarmult_ed25519_base_noclamp(secrets.alpha[0]); const aG1 = nacl.crypto_scalarmult_ed25519_base_noclamp(secrets.alpha[1]); const bDp0 = nacl.crypto_scalarmult_ed25519_noclamp(secrets.beta[0], csPub); const bDp1 = nacl.crypto_scalarmult_ed25519_noclamp(secrets.beta[1], csPub); const res0 = nacl.crypto_core_ed25519_add(aG0, bDp0); const res1 = nacl.crypto_core_ed25519_add(aG1, bDp1); return [ nacl.crypto_core_ed25519_add(rPub[0], res0), nacl.crypto_core_ed25519_add(rPub[1], res1), ]; } /** * FDH function used in CS * @param hm message hash * @param rPub public R included in FDH * @param csPub denomination public key as context * @returns mapped Curve25519 scalar */ function csFDH( hm: Uint8Array, rPub: Uint8Array, csPub: Uint8Array, ): Uint8Array { const lMod = Array.from( new Uint8Array([ 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6, 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed, ]), ); const L = bigint.fromArray(lMod, 256, false); const info = stringToBytes("Curve25519FDH"); const preshash = hash(typedArrayConcat([rPub, hm])); return csKdfMod(L, preshash, csPub, info).reverse(); } /** * blinding seed derived from coin private key * @param coinPriv private key of the corresponding coin * @param rPub public R received from /csr API * @returns blinding seed */ export function deriveBSeed( coinPriv: Uint8Array, rPub: [Uint8Array, Uint8Array], ): Uint8Array { const outLen = 32; const salt = stringToBytes("b-seed"); const ikm = typedArrayConcat([coinPriv, rPub[0], rPub[1]]); return kdf(outLen, ikm, salt); } /** * Derive withdraw nonce, used in /csr request * Note: In withdraw protocol, the nonce is chosen randomly * @param coinPriv coin private key * @returns nonce */ export function deriveWithdrawNonce(coinPriv: Uint8Array): Uint8Array { const outLen = 32; const salt = stringToBytes("n"); return kdf(outLen, coinPriv, salt); } /** * Blind operation for CS signatures, used after /csr call * @param bseed blinding seed to derive blinding secrets * @param rPub public R received from /csr * @param csPub denomination public key * @param hm message to blind * @returns two blinded c */ export async function csBlind( bseed: Uint8Array, rPub: [Uint8Array, Uint8Array], csPub: Uint8Array, hm: Uint8Array, ): Promise<[Uint8Array, Uint8Array]> { const secrets = deriveSecrets(bseed); const rPubBlind = await calcRBlind(csPub, secrets, rPub); const c_0 = csFDH(hm, rPubBlind[0], csPub); const c_1 = csFDH(hm, rPubBlind[1], csPub); return [ nacl.crypto_core_ed25519_scalar_add(c_0, secrets.beta[0]), nacl.crypto_core_ed25519_scalar_add(c_1, secrets.beta[1]), ]; } /** * Unblind operation to unblind the signature * @param bseed seed to derive secrets * @param rPub public R received from /csr * @param csPub denomination public key * @param b returned from exchange to select c * @param csSig blinded signature * @returns unblinded signature */ export async function csUnblind( bseed: Uint8Array, rPub: [Uint8Array, Uint8Array], csPub: Uint8Array, b: number, csSig: CsBlindSignature, ): Promise { if (b != 0 && b != 1) { throw new Error(); } const secrets = deriveSecrets(bseed); const rPubDash = (await calcRBlind(csPub, secrets, rPub))[b]; const sig: CsSignature = { s: nacl.crypto_core_ed25519_scalar_add(csSig.sBlind, secrets.alpha[b]), rPub: rPubDash, }; return sig; } /** * Verification algorithm for CS signatures * @param hm message signed * @param csSig unblinded signature * @param csPub denomination public key * @returns true if valid, false if invalid */ export async function csVerify( hm: Uint8Array, csSig: CsSignature, csPub: Uint8Array, ): Promise { const cDash = csFDH(hm, csSig.rPub, csPub); const sG = nacl.crypto_scalarmult_ed25519_base_noclamp(csSig.s); const cbDp = nacl.crypto_scalarmult_ed25519_noclamp(cDash, csPub); const sGeq = nacl.crypto_core_ed25519_add(csSig.rPub, cbDp); return nacl.verify(sG, sGeq); } export interface EddsaKeyPair { eddsaPub: Uint8Array; eddsaPriv: Uint8Array; } export interface EcdheKeyPair { ecdhePub: Uint8Array; ecdhePriv: Uint8Array; } export interface Edx25519Keypair { edxPub: string; edxPriv: string; } export function createEddsaKeyPair(): EddsaKeyPair { const eddsaPriv = nacl.randomBytes(32); const eddsaPub = eddsaGetPublic(eddsaPriv); return { eddsaPriv, eddsaPub }; } export function createEcdheKeyPair(): EcdheKeyPair { const ecdhePriv = nacl.randomBytes(32); const ecdhePub = ecdhGetPublic(ecdhePriv); return { ecdhePriv, ecdhePub }; } export function hash(d: Uint8Array): Uint8Array { if (tart) { return tart.hash(d); } return nacl.hash(d); } /** * Hash the input with SHA-512 and truncate the result * to 32 bytes. */ export function hashTruncate32(d: Uint8Array): Uint8Array { const sha512HashCode = hash(d); return sha512HashCode.subarray(0, 32); } export function hashCoinEv( coinEv: CoinEnvelope, denomPubHash: HashCodeString, ): Uint8Array { const hashContext = createHashContext(); hashContext.update(decodeCrock(denomPubHash)); hashCoinEvInner(coinEv, hashContext); return hashContext.finish(); } const logger = new Logger("talerCrypto.ts"); export function hashCoinEvInner( coinEv: CoinEnvelope, hashState: TalerHashState, ): void { const hashInputBuf = new ArrayBuffer(4); const uint8ArrayBuf = new Uint8Array(hashInputBuf); const dv = new DataView(hashInputBuf); dv.setUint32(0, DenomKeyType.toIntTag(coinEv.cipher)); hashState.update(uint8ArrayBuf); switch (coinEv.cipher) { case DenomKeyType.Rsa: hashState.update(decodeCrock(coinEv.rsa_blinded_planchet)); return; default: throw new Error(); } } export function hashCoinPub( coinPub: CoinPublicKeyString, ach?: HashCodeString, ): Uint8Array { if (!ach) { return hash(decodeCrock(coinPub)); } return hash(typedArrayConcat([decodeCrock(coinPub), decodeCrock(ach)])); } /** * Hash a denomination public key. */ export function hashDenomPub(pub: DenominationPubKey): Uint8Array { if (pub.cipher === DenomKeyType.Rsa) { const pubBuf = decodeCrock(pub.rsa_public_key); const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4); const uint8ArrayBuf = new Uint8Array(hashInputBuf); const dv = new DataView(hashInputBuf); dv.setUint32(0, pub.age_mask ?? 0); dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher)); uint8ArrayBuf.set(pubBuf, 8); return hash(uint8ArrayBuf); } else if (pub.cipher === DenomKeyType.ClauseSchnorr) { const pubBuf = decodeCrock(pub.cs_public_key); const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4); const uint8ArrayBuf = new Uint8Array(hashInputBuf); const dv = new DataView(hashInputBuf); dv.setUint32(0, pub.age_mask ?? 0); dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher)); uint8ArrayBuf.set(pubBuf, 8); return hash(uint8ArrayBuf); } else { throw Error( `unsupported cipher (${ (pub as DenominationPubKey).cipher }), unable to hash`, ); } } 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); } export function eddsaVerify( msg: Uint8Array, sig: Uint8Array, eddsaPub: Uint8Array, ): boolean { if (tart) { return tart.eddsaVerify(msg, sig, eddsaPub); } return nacl.sign_detached_verify(msg, sig, eddsaPub); } export interface TalerHashState { update(data: Uint8Array): void; finish(): Uint8Array; } export function createHashContext(): TalerHashState { if (tart) { const t = tart; const st = tart.hashStateInit(); return { finish: () => t.hashStateFinish(st), update: (d) => t.hashStateUpdate(st, d), }; } return new nacl.HashState(); } export interface FreshCoin { coinPub: Uint8Array; coinPriv: Uint8Array; bks: Uint8Array; maxAge: number; ageCommitmentProof: AgeCommitmentProof | undefined; } export function bufferForUint32(n: number): Uint8Array { const arrBuf = new ArrayBuffer(4); const buf = new Uint8Array(arrBuf); const dv = new DataView(arrBuf); dv.setUint32(0, n); return buf; } /** * This makes the assumption that the uint64 fits a float, * which should be true for all Taler protocol messages. */ export function bufferForUint64(n: number): Uint8Array { const arrBuf = new ArrayBuffer(8); const buf = new Uint8Array(arrBuf); const dv = new DataView(arrBuf); if (n < 0 || !Number.isInteger(n)) { throw Error("non-negative integer expected"); } dv.setBigUint64(0, BigInt(n)); return buf; } export function bufferForUint8(n: number): Uint8Array { const arrBuf = new ArrayBuffer(1); const buf = new Uint8Array(arrBuf); const dv = new DataView(arrBuf); dv.setUint8(0, n); return buf; } export async function setupTipPlanchet( secretSeed: Uint8Array, denomPub: DenominationPubKey, coinNumber: number, ): Promise { const info = stringToBytes("taler-tip-coin-derivation"); const saltArrBuf = new ArrayBuffer(4); const salt = new Uint8Array(saltArrBuf); const saltDataView = new DataView(saltArrBuf); saltDataView.setUint32(0, coinNumber); const out = kdf(64, secretSeed, salt, info); const coinPriv = out.slice(0, 32); const bks = out.slice(32, 64); let maybeAcp: AgeCommitmentProof | undefined; if (denomPub.age_mask != 0) { maybeAcp = await AgeRestriction.restrictionCommitSeeded( denomPub.age_mask, AgeRestriction.AGE_UNRESTRICTED, secretSeed, ); } return { bks, coinPriv, coinPub: eddsaGetPublic(coinPriv), maxAge: AgeRestriction.AGE_UNRESTRICTED, ageCommitmentProof: maybeAcp, }; } /** * * @param paytoUri * @param salt 16-byte salt * @returns */ export function hashWire(paytoUri: string, salt: string): string { const r = kdf( 64, stringToBytes(paytoUri + "\0"), decodeCrock(salt), stringToBytes("merchant-wire-signature"), ); return encodeCrock(r); } export enum TalerSignaturePurpose { MERCHANT_TRACK_TRANSACTION = 1103, WALLET_RESERVE_WITHDRAW = 1200, WALLET_RESERVE_HISTORY = 1208, WALLET_COIN_DEPOSIT = 1201, GLOBAL_FEES = 1022, MASTER_DENOMINATION_KEY_VALIDITY = 1025, MASTER_WIRE_FEES = 1028, MASTER_WIRE_DETAILS = 1030, WALLET_COIN_MELT = 1202, TEST = 4242, MERCHANT_PAYMENT_OK = 1104, MERCHANT_CONTRACT = 1101, MERCHANT_REFUND = 1102, WALLET_COIN_RECOUP = 1203, WALLET_COIN_LINK = 1204, WALLET_ACCOUNT_SETUP = 1205, WALLET_COIN_RECOUP_REFRESH = 1206, WALLET_AGE_ATTESTATION = 1207, WALLET_PURSE_CREATE = 1210, WALLET_PURSE_DEPOSIT = 1211, WALLET_PURSE_MERGE = 1213, WALLET_ACCOUNT_MERGE = 1214, WALLET_PURSE_ECONTRACT = 1216, WALLET_PURSE_DELETE = 1220, WALLET_COIN_HISTORY = 1209, EXCHANGE_CONFIRM_RECOUP = 1039, EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041, AML_DECISION = 1350, AML_QUERY = 1351, MASTER_AML_KEY = 1017, KYC_AUTH = 1360, ANASTASIS_POLICY_UPLOAD = 1400, ANASTASIS_POLICY_DOWNLOAD = 1401, SYNC_BACKUP_UPLOAD = 1450, } export enum WalletAccountMergeFlags { /** * Not a legal mode! */ None = 0, /** * We are merging a fully paid-up purse into a reserve. */ MergeFullyPaidPurse = 1, CreateFromPurseQuota = 2, CreateWithPurseFee = 3, } export class SignaturePurposeBuilder { private chunks: Uint8Array[] = []; constructor(private purposeNum: number) {} put(bytes: Uint8Array): SignaturePurposeBuilder { this.chunks.push(Uint8Array.from(bytes)); return this; } build(): Uint8Array { let payloadLen = 0; for (const c of this.chunks) { payloadLen += c.byteLength; } const buf = new ArrayBuffer(4 + 4 + payloadLen); const u8buf = new Uint8Array(buf); let p = 8; for (const c of this.chunks) { u8buf.set(c, p); p += c.byteLength; } const dvbuf = new DataView(buf); dvbuf.setUint32(0, payloadLen + 4 + 4); dvbuf.setUint32(4, this.purposeNum); return u8buf; } } export function buildSigPS(purposeNum: number): SignaturePurposeBuilder { return new SignaturePurposeBuilder(purposeNum); } export type OpaqueData = Flavor; export type Edx25519PublicKey = FlavorP; export type Edx25519PrivateKey = FlavorP; export type Edx25519Signature = FlavorP; export type Edx25519PublicKeyEnc = FlavorP; export type Edx25519PrivateKeyEnc = FlavorP< string, "Edx25519PrivateKeyEnc", 64 >; /** * Convert a big integer to a fixed-size, little-endian array. */ export function bigintToNaclArr( x: bigint.BigInteger, size: number, ): Uint8Array { const byteArr = new Uint8Array(size); const arr = x.toArray(256).value.reverse(); byteArr.set(arr, 0); return byteArr; } export function bigintFromNaclArr(arr: Uint8Array): bigint.BigInteger { let rev = new Uint8Array(arr); rev = rev.reverse(); return bigint.fromArray(Array.from(rev), 256, false); } export namespace Edx25519 { const revL = [ 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, ]; const L = bigint.fromArray(revL.reverse(), 256, false); export async function keyCreateFromSeed( seed: OpaqueData, ): Promise { return nacl.crypto_edx25519_private_key_create_from_seed(seed); } export async function keyCreate(): Promise { return nacl.crypto_edx25519_private_key_create(); } export async function getPublic( priv: Edx25519PrivateKey, ): Promise { return nacl.crypto_edx25519_get_public(priv); } export function sign( msg: OpaqueData, key: Edx25519PrivateKey, ): Promise { throw Error("not implemented"); } async function deriveFactor( pub: Edx25519PublicKey, seed: OpaqueData, ): Promise { const res = kdfKw({ outputLength: 64, salt: seed, ikm: pub, info: stringToBytes("edx25519-derivation"), }); return res; } export async function privateKeyDerive( priv: Edx25519PrivateKey, seed: OpaqueData, ): Promise { const pub = await getPublic(priv); const privDec = priv; const a = bigintFromNaclArr(privDec.subarray(0, 32)); const factorEnc = await deriveFactor(pub, seed); const factorModL = bigintFromNaclArr(factorEnc).mod(L); const aPrime = a.divide(8).multiply(factorModL).mod(L).multiply(8).mod(L); const bPrime = nacl .hash(typedArrayConcat([privDec.subarray(32, 64), factorEnc])) .subarray(0, 32); const newPriv = typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]); return newPriv; } export async function publicKeyDerive( pub: Edx25519PublicKey, seed: OpaqueData, ): Promise { const factorEnc = await deriveFactor(pub, seed); const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(factorEnc); const res = nacl.crypto_scalarmult_ed25519_noclamp(factorReduced, pub); return res; } } export interface AgeCommitment { mask: number; /** * Public keys, one for each age group specified in the age mask. */ publicKeys: Edx25519PublicKeyEnc[]; } export interface AgeProof { /** * Private keys. Typically smaller than the number of public keys, * because we drop private keys from age groups that are restricted. */ privateKeys: Edx25519PrivateKeyEnc[]; } export interface AgeCommitmentProof { commitment: AgeCommitment; proof: AgeProof; } function invariant(cond: boolean): asserts cond { if (!cond) { throw Error("invariant failed"); } } export namespace AgeRestriction { /** * Smallest age value that the protocol considers "unrestricted". */ export const AGE_UNRESTRICTED = 32; export function hashCommitment(ac: AgeCommitment): HashCodeString { const hc = new nacl.HashState(); for (const pub of ac.publicKeys) { hc.update(decodeCrock(pub)); } return encodeCrock(hc.finish().subarray(0, 32)); } export function countAgeGroups(mask: number): number { let count = 0; let m = mask; while (m > 0) { count += m & 1; m = m >> 1; } return count; } /** * Get the starting points for age groups in the mask. */ export function getAgeGroupsFromMask(mask: number): number[] { const groups: number[] = []; let age = 1; let m = mask >> 1; while (m > 0) { if (m & 1) { groups.push(age); } m = m >> 1; age++; } return groups; } export function getAgeGroupIndex(mask: number, age: number): number { invariant((mask & 1) === 1); let i = 0; let m = mask; let a = age; while (m > 0) { if (a <= 0) { break; } m = m >> 1; i += m & 1; a--; } return i; } export function ageGroupSpecToMask(ageGroupSpec: string): number { throw Error("not implemented"); } export async function restrictionCommit( ageMask: number, age: number, ): Promise { invariant((ageMask & 1) === 1); const numPubs = countAgeGroups(ageMask) - 1; const numPrivs = getAgeGroupIndex(ageMask, age); const pubs: Edx25519PublicKey[] = []; const privs: Edx25519PrivateKey[] = []; for (let i = 0; i < numPubs; i++) { const priv = await Edx25519.keyCreate(); const pub = await Edx25519.getPublic(priv); pubs.push(pub); if (i < numPrivs) { privs.push(priv); } } return { commitment: { mask: ageMask, publicKeys: pubs.map((x) => encodeCrock(x)), }, proof: { privateKeys: privs.map((x) => encodeCrock(x)), }, }; } const PublishedAgeRestrictionBaseKey: Edx25519PublicKey = decodeCrock( "CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG", ); export async function restrictionCommitSeeded( ageMask: number, age: number, seed: Uint8Array, ): Promise { invariant((ageMask & 1) === 1); const numPubs = countAgeGroups(ageMask) - 1; const numPrivs = getAgeGroupIndex(ageMask, age); const pubs: Edx25519PublicKey[] = []; const privs: Edx25519PrivateKey[] = []; for (let i = 0; i < numPrivs; i++) { const privSeed = await kdfKw({ outputLength: 32, ikm: seed, info: stringToBytes("age-commitment"), salt: bufferForUint32(i), }); const priv = await Edx25519.keyCreateFromSeed(privSeed); const pub = await Edx25519.getPublic(priv); pubs.push(pub); privs.push(priv); } for (let i = numPrivs; i < numPubs; i++) { const deriveSeed = await kdfKw({ outputLength: 32, ikm: seed, info: stringToBytes("age-factor"), salt: bufferForUint32(i), }); const pub = await Edx25519.publicKeyDerive( PublishedAgeRestrictionBaseKey, deriveSeed, ); pubs.push(pub); } return { commitment: { mask: ageMask, publicKeys: pubs.map((x) => encodeCrock(x)), }, proof: { privateKeys: privs.map((x) => encodeCrock(x)), }, }; } /** * Check that c1 = c2*salt */ export async function commitCompare( c1: AgeCommitment, c2: AgeCommitment, salt: OpaqueData, ): Promise { if (c1.publicKeys.length != c2.publicKeys.length) { return false; } for (let i = 0; i < c1.publicKeys.length; i++) { const k1 = decodeCrock(c1.publicKeys[i]); const k2 = await Edx25519.publicKeyDerive( decodeCrock(c2.publicKeys[i]), salt, ); if (k1 != k2) { return false; } } return true; } export async function commitmentDerive( commitmentProof: AgeCommitmentProof, salt: OpaqueData, ): Promise { const newPrivs: Edx25519PrivateKey[] = []; const newPubs: Edx25519PublicKey[] = []; for (const oldPub of commitmentProof.commitment.publicKeys) { newPubs.push(await Edx25519.publicKeyDerive(decodeCrock(oldPub), salt)); } for (const oldPriv of commitmentProof.proof.privateKeys) { newPrivs.push( await Edx25519.privateKeyDerive(decodeCrock(oldPriv), salt), ); } return { commitment: { mask: commitmentProof.commitment.mask, publicKeys: newPubs.map((x) => encodeCrock(x)), }, proof: { privateKeys: newPrivs.map((x) => encodeCrock(x)), }, }; } export function commitmentAttest( commitmentProof: AgeCommitmentProof, age: number, ): Edx25519Signature { const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION) .put(bufferForUint32(commitmentProof.commitment.mask)) .put(bufferForUint32(age)) .build(); const group = getAgeGroupIndex(commitmentProof.commitment.mask, age); if (group === 0) { // No attestation required. return new Uint8Array(64); } const priv = commitmentProof.proof.privateKeys[group - 1]; const pub = commitmentProof.commitment.publicKeys[group - 1]; const sig = nacl.crypto_edx25519_sign_detached( d, decodeCrock(priv), decodeCrock(pub), ); return sig; } export function commitmentVerify( commitment: AgeCommitment, sig: string, age: number, ): boolean { const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION) .put(bufferForUint32(commitment.mask)) .put(bufferForUint32(age)) .build(); const group = getAgeGroupIndex(commitment.mask, age); if (group === 0) { // No attestation required. return true; } const pub = commitment.publicKeys[group - 1]; return nacl.crypto_edx25519_sign_detached_verify( d, decodeCrock(sig), decodeCrock(pub), ); } } // FIXME: make it a branded type! export type EncryptionNonce = FlavorP; async function deriveKey( keySeed: OpaqueData, nonce: EncryptionNonce, salt: string, ): Promise { return kdfKw({ outputLength: 32, salt: nonce, ikm: keySeed, info: stringToBytes(salt), }); } export async function encryptWithDerivedKey( nonce: EncryptionNonce, keySeed: OpaqueData, plaintext: OpaqueData, salt: string, ): Promise { const key = await deriveKey(keySeed, nonce, salt); const cipherText = secretbox(plaintext, nonce, key); return typedArrayConcat([nonce, cipherText]); } const nonceSize = 24; export async function decryptWithDerivedKey( ciphertext: OpaqueData, keySeed: OpaqueData, salt: string, ): Promise { const ctBuf = ciphertext; const nonceBuf = ctBuf.slice(0, nonceSize); const enc = ctBuf.slice(nonceSize); const key = await deriveKey(keySeed, nonceBuf, salt); const clearText = nacl.secretbox_open(enc, nonceBuf, key); if (!clearText) { throw Error("could not decrypt"); } return clearText; } enum ContractFormatTag { PaymentOffer = 0, PaymentRequest = 1, } type MaterialEddsaPub = { _materialType?: "eddsa-pub"; _size?: 32; }; type MaterialEddsaPriv = { _materialType?: "ecdhe-priv"; _size?: 32; }; type MaterialEcdhePub = { _materialType?: "ecdhe-pub"; _size?: 32; }; type MaterialEcdhePriv = { _materialType?: "ecdhe-priv"; _size?: 32; }; type PursePublicKey = FlavorP & MaterialEddsaPub; type ContractPrivateKey = FlavorP & MaterialEcdhePriv; type MergePrivateKey = FlavorP & MaterialEddsaPriv; const mergeSalt = "p2p-merge-contract"; const depositSalt = "p2p-deposit-contract"; export function encryptContractForMerge( pursePub: PursePublicKey, contractPriv: ContractPrivateKey, mergePriv: MergePrivateKey, contractTerms: any, nonce: EncryptionNonce, ): Promise { const contractTermsCanon = canonicalJson(contractTerms) + "\0"; const contractTermsBytes = stringToBytes(contractTermsCanon); const contractTermsCompressed = fflate.zlibSync(contractTermsBytes); const data = typedArrayConcat([ bufferForUint32(ContractFormatTag.PaymentOffer), bufferForUint32(contractTermsBytes.length), mergePriv, contractTermsCompressed, ]); const key = keyExchangeEcdhEddsa(contractPriv, pursePub); return encryptWithDerivedKey(nonce, key, data, mergeSalt); } export function encryptContractForDeposit( pursePub: PursePublicKey, contractPriv: ContractPrivateKey, contractTerms: any, nonce: EncryptionNonce, ): Promise { const contractTermsCanon = canonicalJson(contractTerms) + "\0"; const contractTermsBytes = stringToBytes(contractTermsCanon); const contractTermsCompressed = fflate.zlibSync(contractTermsBytes); const data = typedArrayConcat([ bufferForUint32(ContractFormatTag.PaymentRequest), bufferForUint32(contractTermsBytes.length), contractTermsCompressed, ]); const key = keyExchangeEcdhEddsa(contractPriv, pursePub); return encryptWithDerivedKey(nonce, key, data, depositSalt); } export interface DecryptForMergeResult { contractTerms: any; mergePriv: Uint8Array; } export interface DecryptForDepositResult { contractTerms: any; } export async function decryptContractForMerge( enc: OpaqueData, pursePub: PursePublicKey, contractPriv: ContractPrivateKey, ): Promise { const key = keyExchangeEcdhEddsa(contractPriv, pursePub); const dec = await decryptWithDerivedKey(enc, key, mergeSalt); const mergePriv = dec.slice(8, 8 + 32); const contractTermsCompressed = dec.slice(8 + 32); const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); // Slice of the '\0' at the end and decode to a string const contractTermsString = bytesToString( contractTermsBuf.slice(0, contractTermsBuf.length - 1), ); return { mergePriv: mergePriv, contractTerms: JSON.parse(contractTermsString), }; } export async function decryptContractForDeposit( enc: OpaqueData, pursePub: PursePublicKey, contractPriv: ContractPrivateKey, ): Promise { const key = keyExchangeEcdhEddsa(contractPriv, pursePub); const dec = await decryptWithDerivedKey(enc, key, depositSalt); const contractTermsCompressed = dec.slice(8); const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); // Slice of the '\0' at the end and decode to a string const contractTermsString = bytesToString( contractTermsBuf.slice(0, contractTermsBuf.length - 1), ); return { contractTerms: JSON.parse(contractTermsString), }; } export function amountToBuffer(amount: AmountLike): Uint8Array { const amountJ = Amounts.jsonifyAmount(amount); const buffer = new ArrayBuffer(8 + 4 + 12); const dvbuf = new DataView(buffer); const u8buf = new Uint8Array(buffer); const curr = stringToBytes(amountJ.currency); if (typeof dvbuf.setBigUint64 !== "undefined") { dvbuf.setBigUint64(0, BigInt(amountJ.value)); } else { const arr = bigint(amountJ.value).toArray(2 ** 8).value; let offset = 8 - arr.length; for (let i = 0; i < arr.length; i++) { dvbuf.setUint8(offset++, arr[i]); } } dvbuf.setUint32(8, amountJ.fraction); u8buf.set(curr, 8 + 4); return u8buf; } export function timestampRoundedToBuffer( ts: TalerProtocolTimestamp, ): Uint8Array { const b = new ArrayBuffer(8); const v = new DataView(b); // The buffer we sign over represents the timestamp in microseconds. if (typeof v.setBigUint64 !== "undefined") { const s = BigInt(ts.t_s) * BigInt(1000 * 1000); v.setBigUint64(0, s); } else { const s = ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000); const arr = s.toArray(2 ** 8).value; let offset = 8 - arr.length; for (let i = 0; i < arr.length; i++) { v.setUint8(offset++, arr[i]); } } return new Uint8Array(b); } export function durationRoundedToBuffer(ts: TalerProtocolDuration): Uint8Array { const b = new ArrayBuffer(8); const v = new DataView(b); // The buffer we sign over represents the timestamp in microseconds. if (typeof v.setBigUint64 !== "undefined") { const s = BigInt(ts.d_us); v.setBigUint64(0, s); } else { const s = ts.d_us === "forever" ? bigint.zero : bigint(ts.d_us); const arr = s.toArray(2 ** 8).value; let offset = 8 - arr.length; for (let i = 0; i < arr.length; i++) { v.setUint8(offset++, arr[i]); } } return new Uint8Array(b); }