summaryrefslogtreecommitdiff
path: root/packages/taler-util/src/taler-crypto.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src/taler-crypto.ts')
-rw-r--r--packages/taler-util/src/taler-crypto.ts1662
1 files changed, 1662 insertions, 0 deletions
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
new file mode 100644
index 000000000..e587773e2
--- /dev/null
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -0,0 +1,1662 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Native implementation of GNU Taler crypto primitives.
+ */
+
+/**
+ * Imports.
+ */
+import * as nacl from "./nacl-fast.js";
+import { hmacSha256, hmacSha512 } from "./kdf.js";
+import bigint from "big-integer";
+import * as argon2 from "./argon2.js";
+import {
+ CoinEnvelope,
+ CoinPublicKeyString,
+ DenominationPubKey,
+ DenomKeyType,
+ HashCodeString,
+} from "./taler-types.js";
+import { Logger } from "./logging.js";
+import { secretbox } from "./nacl-fast.js";
+import * as fflate from "fflate";
+import { canonicalJson } from "./helpers.js";
+import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
+import { AmountLike, Amounts } from "./amounts.js";
+
+export type Flavor<T, FlavorT extends string> = T & {
+ _flavor?: `taler.${FlavorT}`;
+};
+
+export type FlavorP<T, FlavorT extends string, S extends number> = T & {
+ _flavor?: `taler.${FlavorT}`;
+ _size?: S;
+};
+
+export function getRandomBytes(n: number): Uint8Array {
+ return nacl.randomBytes(n);
+}
+
+export function getRandomBytesF<T extends number, N extends string>(
+ n: T,
+): FlavorP<Uint8Array, N, T> {
+ 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<Uint8Array> {
+ 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<CsSignature> {
+ 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<boolean> {
+ 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<FreshCoin> {
+ 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_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_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,
+ TALER_SIGNATURE_AML_DECISION = 1350,
+ TALER_SIGNATURE_AML_QUERY = 1351,
+ TALER_SIGNATURE_MASTER_AML_KEY = 1017,
+ 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<Uint8Array, any>;
+export type Edx25519PublicKey = FlavorP<Uint8Array, "Edx25519PublicKey", 32>;
+export type Edx25519PrivateKey = FlavorP<Uint8Array, "Edx25519PrivateKey", 64>;
+export type Edx25519Signature = FlavorP<Uint8Array, "Edx25519Signature", 64>;
+
+export type Edx25519PublicKeyEnc = FlavorP<string, "Edx25519PublicKeyEnc", 32>;
+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<Edx25519PrivateKey> {
+ return nacl.crypto_edx25519_private_key_create_from_seed(seed);
+ }
+
+ export async function keyCreate(): Promise<Edx25519PrivateKey> {
+ return nacl.crypto_edx25519_private_key_create();
+ }
+
+ export async function getPublic(
+ priv: Edx25519PrivateKey,
+ ): Promise<Edx25519PublicKey> {
+ return nacl.crypto_edx25519_get_public(priv);
+ }
+
+ export function sign(
+ msg: OpaqueData,
+ key: Edx25519PrivateKey,
+ ): Promise<Edx25519Signature> {
+ throw Error("not implemented");
+ }
+
+ async function deriveFactor(
+ pub: Edx25519PublicKey,
+ seed: OpaqueData,
+ ): Promise<OpaqueData> {
+ const res = kdfKw({
+ outputLength: 64,
+ salt: seed,
+ ikm: pub,
+ info: stringToBytes("edx25519-derivation"),
+ });
+
+ return res;
+ }
+
+ export async function privateKeyDerive(
+ priv: Edx25519PrivateKey,
+ seed: OpaqueData,
+ ): Promise<Edx25519PrivateKey> {
+ 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<Edx25519PublicKey> {
+ 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<AgeCommitmentProof> {
+ 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<AgeCommitmentProof> {
+ 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<boolean> {
+ 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<AgeCommitmentProof> {
+ 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<Uint8Array, "EncryptionNonce", 24>;
+
+async function deriveKey(
+ keySeed: OpaqueData,
+ nonce: EncryptionNonce,
+ salt: string,
+): Promise<Uint8Array> {
+ return kdfKw({
+ outputLength: 32,
+ salt: nonce,
+ ikm: keySeed,
+ info: stringToBytes(salt),
+ });
+}
+
+export async function encryptWithDerivedKey(
+ nonce: EncryptionNonce,
+ keySeed: OpaqueData,
+ plaintext: OpaqueData,
+ salt: string,
+): Promise<OpaqueData> {
+ 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<OpaqueData> {
+ 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<Uint8Array, "PursePublicKey", 32> &
+ MaterialEddsaPub;
+
+type ContractPrivateKey = FlavorP<Uint8Array, "ContractPrivateKey", 32> &
+ MaterialEcdhePriv;
+
+type MergePrivateKey = FlavorP<Uint8Array, "MergePrivateKey", 32> &
+ 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<OpaqueData> {
+ 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<OpaqueData> {
+ 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<DecryptForMergeResult> {
+ 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<DecryptForDepositResult> {
+ 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);
+}