/*
 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 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";
import { TokenEnvelope, TokenIssuePublicKey } from "./types-taler-merchant.js";
import { PayWalletData } from "./types-taler-wallet.js";

const isEddsaPubP: unique symbol = Symbol("isEddsaPubP");
type FlavorEddsaPubP = {
  readonly flavor?: typeof isEddsaPubP;
  readonly _size?: 32;
};

const isEddsaPrivP: unique symbol = Symbol("isEddsaPrivP");
type FlavorEddsaPrivP = {
  readonly flavor?: typeof isEddsaPrivP;
  readonly _size?: 32;
};

const isEddsaSigP: unique symbol = Symbol("isEddsaSigP");
type FlavorEddsaSigP = {
  readonly flavor?: typeof isEddsaSigP;
  readonly _size?: 64;
};

const isEdx25519PublicKey: unique symbol = Symbol("isEdx25519PublicKey");
type FlavorEdx25519PublicKey = {
  readonly flavor?: typeof isEdx25519PublicKey;
  readonly _size?: 32;
};

const isEdx25519PrivateKey: unique symbol = Symbol("isEdx25519PrivateKey");
type FlavorEdx25519PrivateKey = {
  readonly flavor?: typeof isEdx25519PrivateKey;
  readonly _size?: 64;
};

const isEcdhePrivP: unique symbol = Symbol("isEcdhePrivP");
type FlavorEcdhePrivP = {
  readonly flavor?: typeof isEcdhePrivP;
  readonly _size?: 32;
};

const isEdx25519Signature: unique symbol = Symbol("isEdx25519Signature");
type FlavorEdx25519Signature = {
  readonly flavor?: typeof isEdx25519Signature;
  readonly _size?: 64;
};

const isEdx25519PublicKeyEnc: unique symbol = Symbol("isEdx25519PublicKeyEnc");
type FlavorEdx25519PublicKeyEnc = {
  readonly [isEdx25519PublicKeyEnc]?: true;
};

const isEdx25519PrivateKeyEnc: unique symbol = Symbol(
  "isEdx25519PrivateKeyEnc",
);
type FlavorEdx25519PrivateKeyEnc = {
  readonly [isEdx25519PrivateKeyEnc]?: true;
};

const isEncryptionNonce: unique symbol = Symbol("isEncryptionNone");
type FlavorEncryptionNonceP = {
  readonly flavor?: typeof isEncryptionNonce;
};

type Sized<T> = { readonly _size?: T };

export type EddsaPubP = Uint8Array & FlavorEddsaPubP;
export type EddsaPrivP = Uint8Array & FlavorEddsaPrivP;
export type EddsaSigP = Uint8Array & FlavorEddsaSigP;

export type EcdhePrivP = Uint8Array & FlavorEcdhePrivP;

export type OpaqueData = Uint8Array;
export type Edx25519PublicKey = Uint8Array & FlavorEdx25519PublicKey;
export type Edx25519PrivateKey = Uint8Array & FlavorEdx25519PrivateKey;
export type Edx25519Signature = Uint8Array & FlavorEdx25519Signature;

export type Edx25519PublicKeyEnc = string & FlavorEdx25519PublicKeyEnc;
export type Edx25519PrivateKeyEnc = string & FlavorEdx25519PrivateKeyEnc;

export type EncryptionNonceP = Uint8Array & FlavorEncryptionNonceP;

export type PursePublicKey = EddsaPubP;

export type ContractPrivateKey = EcdhePrivP;
export type MergePrivateKeyP = Uint8Array & EddsaPrivP;

export function getRandomBytes<N extends number>(n: N): Uint8Array & Sized<N> {
  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 & FlavorEcdhePrivP,
  eddsaPub: Uint8Array & FlavorEddsaPubP,
): 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`,
    );
  }
}

/**
 * Hash a token issue public key.
 */
export function hashTokenIssuePub(pub: TokenIssuePublicKey): Uint8Array {
  if (pub.cipher === DenomKeyType.Rsa) {
    const dec = decodeCrock(pub.rsa_pub);
    return hash(dec);
  } else if (pub.cipher === DenomKeyType.ClauseSchnorr) {
    const dec = decodeCrock(pub.cs_pub);
    return hash(dec);
  } else {
    throw Error(
      `unsupported cipher (${
        (pub as TokenIssuePublicKey).cipher
      }), unable to hash`,
    );
  }
}

/**
 * Hash a token envelope.
 */
export function hashTokenEv(
  tokenEv: TokenEnvelope,
  tokenIssuePubHash: HashCodeString,
): Uint8Array {
  const hashContext = createHashContext();
  hashContext.update(decodeCrock(tokenIssuePubHash));
  hashTokenEvInner(tokenEv, hashContext);
  return hashContext.finish();
}

export function hashTokenEvInner(
  tokenEv: TokenEnvelope,
  hashState: TalerHashState,
): void {
  const hashInputBuf = new ArrayBuffer(4);
  const uint8ArrayBuf = new Uint8Array(hashInputBuf);
  const dv = new DataView(hashInputBuf);
  dv.setUint32(0, DenomKeyType.toIntTag(tokenEv.cipher));
  hashState.update(uint8ArrayBuf);
  switch (tokenEv.cipher) {
    case DenomKeyType.Rsa:
      hashState.update(decodeCrock(tokenEv.rsa_blinded_planchet));
      return;
    default:
      throw new Error();
  }
}

export function hashPayWalletData(walletData: PayWalletData): Uint8Array {
  const canon = canonicalJson(walletData) + "\0";
  const bytes = stringToBytes(canon);
  return hash(bytes);
}

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: EddsaSigP,
  eddsaPub: EddsaPubP,
): 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_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_TOKEN_USE = 1222,
  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);
}

/**
 * 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,
      ikm: pub,
      salt: stringToBytes("edx25519-derivation"),
      info: seed,
    });

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

async function deriveKey(
  keySeed: OpaqueData,
  nonce: EncryptionNonceP,
  salt: string,
): Promise<Uint8Array> {
  return kdfKw({
    outputLength: 32,
    salt: nonce,
    ikm: keySeed,
    info: stringToBytes(salt),
  });
}

export async function encryptWithDerivedKey(
  nonce: EncryptionNonceP,
  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,
}

const mergeSalt = "p2p-merge-contract";
const depositSalt = "p2p-deposit-contract";

export function encryptContractForMerge(
  pursePub: PursePublicKey,
  contractPriv: ContractPrivateKey,
  mergePriv: MergePrivateKeyP,
  contractTerms: any,
  nonce: EncryptionNonceP,
): 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: EncryptionNonceP,
): 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 bufferFromAmount(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;
}

const foreverNum = 2n ** 64n - 1n;

export function timestampRoundedToBuffer(
  ts: TalerProtocolTimestamp,
): Uint8Array {
  const b = new ArrayBuffer(8);
  const v = new DataView(b);
  const numVal =
    ts.t_s === "never" ? foreverNum : BigInt(ts.t_s) * 1000n * 1000n;
  // The buffer we sign over represents the timestamp in microseconds.
  if (typeof v.setBigUint64 !== "undefined") {
    v.setBigUint64(0, numVal);
  } else {
    const s =
      ts.t_s === "never"
        ? bigint(foreverNum)
        : 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);
}

export function toHexString(byteArray: Uint8Array) {
  return byteArray.reduce(
    (output, elem) => output + ("0" + elem.toString(16)).slice(-2),
    "",
  );
}
