/*
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 * as nacl from "./nacl-fast.js";
import { hmacSha256, hmacSha512 } from "./kdf.js";
import bigint from "big-integer";
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";
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;
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 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 globa/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 publick 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 publick 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_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,
EXCHANGE_CONFIRM_RECOUP = 1039,
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
ANASTASIS_POLICY_UPLOAD = 1400,
ANASTASIS_POLICY_DOWNLOAD = 1401,
SYNC_BACKUP_UPLOAD = 1450,
}
export const 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)),
},
};
}
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 < numPubs; i++) {
const privSeed = await kdfKw({
outputLength: 32,
ikm: seed,
info: stringToBytes("age-restriction-commit"),
salt: bufferForUint32(i),
});
const priv = await Edx25519.keyCreateFromSeed(privSeed);
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)),
},
};
}
/**
* 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!
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,
): 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(getRandomBytesF(24), key, data, mergeSalt);
}
export function encryptContractForDeposit(
pursePub: PursePublicKey,
contractPriv: ContractPrivateKey,
contractTerms: any,
): 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(getRandomBytesF(24), 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),
};
}