/* 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. */ import * as nacl from "./primitives/nacl-fast"; import bigint from "big-integer"; import { kdf } from "./primitives/kdf"; // @ts-ignore const decoder = new TextDecoder(); if (typeof decoder !== "object") { throw Error("FATAL: TextDecoder not available"); } // @ts-ignore const encoder = new TextEncoder(); if (typeof encoder !== "object") { throw Error("FATAL: TextEncoder not available"); } export function getRandomBytes(n: number): Uint8Array { return nacl.randomBytes(n); } 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 { 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 decodeCrock(encoded: string): Uint8Array { 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 { const pair = nacl.sign_keyPair_fromSeed(eddsaPriv); return pair.publicKey; } export function ecdheGetPublic(ecdhePriv: Uint8Array): Uint8Array { return nacl.scalarMult_base(ecdhePriv); } export function keyExchangeEddsaEcdhe( eddsaPriv: Uint8Array, ecdhePub: Uint8Array, ): Uint8Array { const ph = nacl.hash(eddsaPriv); const a = new Uint8Array(32); for (let i = 0; i < 32; i++) { a[i] = ph[i]; } const x = nacl.scalarMult(a, ecdhePub); return nacl.hash(x); } export function keyExchangeEcdheEddsa( ecdhePriv: Uint8Array, eddsaPub: Uint8Array, ): Uint8Array { const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub); const x = nacl.scalarMult(ecdhePriv, curve25519Pub); return nacl.hash(x); } interface RsaPub { N: bigint.BigInteger; e: bigint.BigInteger; } interface RsaBlindingKey { r: 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++; } } export function stringToBytes(s: string): Uint8Array { return encoder.encode(s); } export function bytesToString(b: Uint8Array): string { 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 { 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 { 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 { 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 interface EddsaKeyPair { eddsaPub: Uint8Array; eddsaPriv: Uint8Array; } export interface EcdheKeyPair { ecdhePub: Uint8Array; ecdhePriv: Uint8Array; } 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 = ecdheGetPublic(ecdhePriv); return { ecdhePriv, ecdhePub }; } export function hash(d: Uint8Array): Uint8Array { return nacl.hash(d); } export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array { const pair = nacl.sign_keyPair_fromSeed(eddsaPriv); return nacl.sign_detached(msg, pair.secretKey); } export function eddsaVerify( msg: Uint8Array, sig: Uint8Array, eddsaPub: Uint8Array, ): boolean { return nacl.sign_detached_verify(msg, sig, eddsaPub); } export function createHashContext(): nacl.HashState { return new nacl.HashState(); } export interface FreshCoin { coinPub: Uint8Array; coinPriv: Uint8Array; bks: Uint8Array; } export function setupRefreshPlanchet( secretSeed: Uint8Array, coinNumber: number, ): FreshCoin { const info = stringToBytes("taler-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); return { bks, coinPriv, coinPub: eddsaGetPublic(coinPriv), }; } export function setupWithdrawPlanchet( secretSeed: Uint8Array, coinNumber: number, ): FreshCoin { const info = stringToBytes("taler-withdrawal-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); return { bks, coinPriv, coinPub: eddsaGetPublic(coinPriv), }; } export function setupTipPlanchet( secretSeed: Uint8Array, coinNumber: number, ): 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); return { bks, coinPriv, coinPub: eddsaGetPublic(coinPriv), }; } export function setupRefreshTransferPub( secretSeed: Uint8Array, transferPubIndex: number, ): EcdheKeyPair { const info = stringToBytes("taler-transfer-pub-derivation"); const saltArrBuf = new ArrayBuffer(4); const salt = new Uint8Array(saltArrBuf); const saltDataView = new DataView(saltArrBuf); saltDataView.setUint32(0, transferPubIndex); const out = kdf(32, secretSeed, salt, info); return { ecdhePriv: out, ecdhePub: ecdheGetPublic(out), }; }