From f67d7f54f9d0fed97446898942e3dfee67ee2985 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 5 Dec 2019 19:38:19 +0100 Subject: threads, retries and notifications WIP --- src/crypto/browserWorkerEntry.ts | 70 ---- src/crypto/cryptoApi.ts | 444 --------------------- src/crypto/cryptoImplementation.ts | 596 ---------------------------- src/crypto/cryptoWorker.ts | 8 - src/crypto/nodeProcessWorker.ts | 118 ------ src/crypto/nodeWorkerEntry.ts | 69 ---- src/crypto/synchronousWorker.ts | 135 ------- src/crypto/workers/browserWorkerEntry.ts | 70 ++++ src/crypto/workers/cryptoApi.ts | 455 +++++++++++++++++++++ src/crypto/workers/cryptoImplementation.ts | 608 +++++++++++++++++++++++++++++ src/crypto/workers/cryptoWorker.ts | 8 + src/crypto/workers/nodeThreadWorker.ts | 175 +++++++++ src/crypto/workers/synchronousWorker.ts | 135 +++++++ 13 files changed, 1451 insertions(+), 1440 deletions(-) delete mode 100644 src/crypto/browserWorkerEntry.ts delete mode 100644 src/crypto/cryptoApi.ts delete mode 100644 src/crypto/cryptoImplementation.ts delete mode 100644 src/crypto/cryptoWorker.ts delete mode 100644 src/crypto/nodeProcessWorker.ts delete mode 100644 src/crypto/nodeWorkerEntry.ts delete mode 100644 src/crypto/synchronousWorker.ts create mode 100644 src/crypto/workers/browserWorkerEntry.ts create mode 100644 src/crypto/workers/cryptoApi.ts create mode 100644 src/crypto/workers/cryptoImplementation.ts create mode 100644 src/crypto/workers/cryptoWorker.ts create mode 100644 src/crypto/workers/nodeThreadWorker.ts create mode 100644 src/crypto/workers/synchronousWorker.ts (limited to 'src/crypto') diff --git a/src/crypto/browserWorkerEntry.ts b/src/crypto/browserWorkerEntry.ts deleted file mode 100644 index 5ac762c13..000000000 --- a/src/crypto/browserWorkerEntry.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - 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. - - 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 - TALER; see the file COPYING. If not, see -*/ - -/** - * Web worker for crypto operations. - */ - -/** - * Imports. - */ - -import { CryptoImplementation } from "./cryptoImplementation"; - -const worker: Worker = (self as any) as Worker; - -async function handleRequest(operation: string, id: number, args: string[]) { - const impl = new CryptoImplementation(); - - if (!(operation in impl)) { - console.error(`crypto operation '${operation}' not found`); - return; - } - - try { - const result = (impl as any)[operation](...args); - worker.postMessage({ result, id }); - } catch (e) { - console.log("error during operation", e); - return; - } -} - -worker.onmessage = (msg: MessageEvent) => { - const args = msg.data.args; - if (!Array.isArray(args)) { - console.error("args must be array"); - return; - } - const id = msg.data.id; - if (typeof id !== "number") { - console.error("RPC id must be number"); - return; - } - const operation = msg.data.operation; - if (typeof operation !== "string") { - console.error("RPC operation must be string"); - return; - } - - if (CryptoImplementation.enableTracing) { - console.log("onmessage with", operation); - } - - handleRequest(operation, id, args).catch((e) => { - console.error("error in browsere worker", e); - }); -}; diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts deleted file mode 100644 index 2521b54ea..000000000 --- a/src/crypto/cryptoApi.ts +++ /dev/null @@ -1,444 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - 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. - - 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 - TALER; see the file COPYING. If not, see - */ - -/** - * API to access the Taler crypto worker thread. - * @author Florian Dold - */ - -/** - * Imports. - */ -import { AmountJson } from "../util/amounts"; - -import { - CoinRecord, - DenominationRecord, - RefreshSessionRecord, - TipPlanchet, - WireFee, -} from "../dbTypes"; - -import { CryptoWorker } from "./cryptoWorker"; - -import { ContractTerms, PaybackRequest } from "../talerTypes"; - -import { BenchmarkResult, CoinWithDenom, PayCoinInfo, PlanchetCreationResult, PlanchetCreationRequest } from "../walletTypes"; - -import * as timer from "../util/timer"; - -/** - * State of a crypto worker. - */ -interface WorkerState { - /** - * The actual worker thread. - */ - w: CryptoWorker | null; - - /** - * Work we're currently executing or null if not busy. - */ - currentWorkItem: WorkItem | null; - - /** - * Timer to terminate the worker if it's not busy enough. - */ - terminationTimerHandle: timer.TimerHandle | null; -} - -interface WorkItem { - operation: string; - args: any[]; - resolve: any; - reject: any; - - /** - * Serial id to identify a matching response. - */ - rpcId: number; - - /** - * Time when the work was submitted to a (non-busy) worker thread. - */ - startTime: number; -} - -/** - * Number of different priorities. Each priority p - * must be 0 <= p < NUM_PRIO. - */ -const NUM_PRIO = 5; - -export interface CryptoWorkerFactory { - /** - * Start a new worker. - */ - startWorker(): CryptoWorker; - - /** - * Query the number of workers that should be - * run at the same time. - */ - getConcurrency(): number; -} - -export class BrowserCryptoWorkerFactory implements CryptoWorkerFactory { - startWorker(): CryptoWorker { - const workerCtor = Worker; - const workerPath = "/dist/cryptoWorker-bundle.js"; - return new workerCtor(workerPath) as CryptoWorker; - } - - getConcurrency(): number { - let concurrency = 2; - try { - // only works in the browser - // tslint:disable-next-line:no-string-literal - concurrency = (navigator as any)["hardwareConcurrency"]; - concurrency = Math.max(1, Math.ceil(concurrency / 2)); - } catch (e) { - concurrency = 2; - } - return concurrency; - } -} - -/** - * Crypto API that interfaces manages a background crypto thread - * for the execution of expensive operations. - */ -export class CryptoApi { - private nextRpcId: number = 1; - private workers: WorkerState[]; - private workQueues: WorkItem[][]; - - private workerFactory: CryptoWorkerFactory; - - /** - * Number of busy workers. - */ - private numBusy: number = 0; - - /** - * Did we stop accepting new requests? - */ - private stopped: boolean = false; - - static enableTracing = false; - - /** - * Terminate all worker threads. - */ - terminateWorkers() { - for (let worker of this.workers) { - if (worker.w) { - CryptoApi.enableTracing && console.log("terminating worker"); - worker.w.terminate(); - if (worker.terminationTimerHandle) { - worker.terminationTimerHandle.clear(); - worker.terminationTimerHandle = null; - } - if (worker.currentWorkItem) { - worker.currentWorkItem.reject(Error("explicitly terminated")); - worker.currentWorkItem = null; - } - worker.w = null; - } - } - } - - stop() { - this.terminateWorkers(); - this.stopped = true; - } - - /** - * Start a worker (if not started) and set as busy. - */ - wake(ws: WorkerState, work: WorkItem): void { - if (this.stopped) { - console.log("cryptoApi is stopped"); - CryptoApi.enableTracing && console.log("not waking, as cryptoApi is stopped"); - return; - } - if (ws.currentWorkItem !== null) { - throw Error("assertion failed"); - } - ws.currentWorkItem = work; - this.numBusy++; - if (!ws.w) { - const w = this.workerFactory.startWorker(); - w.onmessage = (m: MessageEvent) => this.handleWorkerMessage(ws, m); - w.onerror = (e: ErrorEvent) => this.handleWorkerError(ws, e); - ws.w = w; - } - - const msg: any = { - args: work.args, - id: work.rpcId, - operation: work.operation, - }; - this.resetWorkerTimeout(ws); - work.startTime = timer.performanceNow(); - setImmediate(() => ws.w!.postMessage(msg)); - } - - resetWorkerTimeout(ws: WorkerState) { - if (ws.terminationTimerHandle !== null) { - ws.terminationTimerHandle.clear(); - ws.terminationTimerHandle = null; - } - const destroy = () => { - // terminate worker if it's idle - if (ws.w && ws.currentWorkItem === null) { - ws.w!.terminate(); - ws.w = null; - } - }; - ws.terminationTimerHandle = timer.after(15 * 1000, destroy); - } - - handleWorkerError(ws: WorkerState, e: ErrorEvent) { - if (ws.currentWorkItem) { - console.error( - `error in worker during ${ws.currentWorkItem!.operation}`, - e, - ); - } else { - console.error("error in worker", e); - } - console.error(e.message); - try { - ws.w!.terminate(); - ws.w = null; - } catch (e) { - console.error(e); - } - if (ws.currentWorkItem !== null) { - ws.currentWorkItem.reject(e); - ws.currentWorkItem = null; - this.numBusy--; - } - this.findWork(ws); - } - - private findWork(ws: WorkerState) { - // try to find more work for this worker - for (let i = 0; i < NUM_PRIO; i++) { - const q = this.workQueues[NUM_PRIO - i - 1]; - if (q.length !== 0) { - const work: WorkItem = q.shift()!; - this.wake(ws, work); - return; - } - } - } - - handleWorkerMessage(ws: WorkerState, msg: MessageEvent) { - const id = msg.data.id; - if (typeof id !== "number") { - console.error("rpc id must be number"); - return; - } - const currentWorkItem = ws.currentWorkItem; - ws.currentWorkItem = null; - this.numBusy--; - this.findWork(ws); - if (!currentWorkItem) { - console.error("unsolicited response from worker"); - return; - } - if (id !== currentWorkItem.rpcId) { - console.error(`RPC with id ${id} has no registry entry`); - return; - } - - CryptoApi.enableTracing && - console.log( - `rpc ${currentWorkItem.operation} took ${timer.performanceNow() - - currentWorkItem.startTime}ms`, - ); - currentWorkItem.resolve(msg.data.result); - } - - constructor(workerFactory: CryptoWorkerFactory) { - this.workerFactory = workerFactory; - this.workers = new Array(workerFactory.getConcurrency()); - - for (let i = 0; i < this.workers.length; i++) { - this.workers[i] = { - currentWorkItem: null, - terminationTimerHandle: null, - w: null, - }; - } - - this.workQueues = []; - for (let i = 0; i < NUM_PRIO; i++) { - this.workQueues.push([]); - } - } - - private doRpc( - operation: string, - priority: number, - ...args: any[] - ): Promise { - const p: Promise = new Promise((resolve, reject) => { - const rpcId = this.nextRpcId++; - const workItem: WorkItem = { - operation, - args, - resolve, - reject, - rpcId, - startTime: 0, - }; - - if (this.numBusy === this.workers.length) { - const q = this.workQueues[priority]; - if (!q) { - throw Error("assertion failed"); - } - this.workQueues[priority].push(workItem); - return; - } - - for (const ws of this.workers) { - if (ws.currentWorkItem !== null) { - continue; - } - this.wake(ws, workItem); - return; - } - - throw Error("assertion failed"); - }); - - return p; - } - - createPlanchet( - req: PlanchetCreationRequest - ): Promise { - return this.doRpc("createPlanchet", 1, req); - } - - createTipPlanchet(denom: DenominationRecord): Promise { - return this.doRpc("createTipPlanchet", 1, denom); - } - - hashString(str: string): Promise { - return this.doRpc("hashString", 1, str); - } - - hashDenomPub(denomPub: string): Promise { - return this.doRpc("hashDenomPub", 1, denomPub); - } - - isValidDenom(denom: DenominationRecord, masterPub: string): Promise { - return this.doRpc("isValidDenom", 2, denom, masterPub); - } - - isValidWireFee( - type: string, - wf: WireFee, - masterPub: string, - ): Promise { - return this.doRpc("isValidWireFee", 2, type, wf, masterPub); - } - - isValidPaymentSignature( - sig: string, - contractHash: string, - merchantPub: string, - ): Promise { - return this.doRpc( - "isValidPaymentSignature", - 1, - sig, - contractHash, - merchantPub, - ); - } - - signDeposit( - contractTerms: ContractTerms, - cds: CoinWithDenom[], - totalAmount: AmountJson, - ): Promise { - return this.doRpc( - "signDeposit", - 3, - contractTerms, - cds, - totalAmount, - ); - } - - createEddsaKeypair(): Promise<{ priv: string; pub: string }> { - return this.doRpc<{ priv: string; pub: string }>("createEddsaKeypair", 1); - } - - rsaUnblind(sig: string, bk: string, pk: string): Promise { - return this.doRpc("rsaUnblind", 4, sig, bk, pk); - } - - createPaybackRequest(coin: CoinRecord): Promise { - return this.doRpc("createPaybackRequest", 1, coin); - } - - createRefreshSession( - exchangeBaseUrl: string, - kappa: number, - meltCoin: CoinRecord, - newCoinDenoms: DenominationRecord[], - meltFee: AmountJson, - ): Promise { - return this.doRpc( - "createRefreshSession", - 4, - exchangeBaseUrl, - kappa, - meltCoin, - newCoinDenoms, - meltFee, - ); - } - - signCoinLink( - oldCoinPriv: string, - newDenomHash: string, - oldCoinPub: string, - transferPub: string, - coinEv: string, - ): Promise { - return this.doRpc( - "signCoinLink", - 4, - oldCoinPriv, - newDenomHash, - oldCoinPub, - transferPub, - coinEv, - ); - } - - benchmark(repetitions: number): Promise { - return this.doRpc("benchmark", 1, repetitions); - } -} diff --git a/src/crypto/cryptoImplementation.ts b/src/crypto/cryptoImplementation.ts deleted file mode 100644 index faebbaa4a..000000000 --- a/src/crypto/cryptoImplementation.ts +++ /dev/null @@ -1,596 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - 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. - - 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 - TALER; see the file COPYING. If not, see - */ - -/** - * Synchronous implementation of crypto-related functions for the wallet. - * - * The functionality is parameterized over an Emscripten environment. - */ - -/** - * Imports. - */ - -import { - CoinRecord, - CoinStatus, - DenominationRecord, - RefreshPlanchetRecord, - RefreshSessionRecord, - ReserveRecord, - TipPlanchet, - WireFee, -} from "../dbTypes"; - -import { CoinPaySig, ContractTerms, PaybackRequest } from "../talerTypes"; -import { - BenchmarkResult, - CoinWithDenom, - PayCoinInfo, - Timestamp, - PlanchetCreationResult, - PlanchetCreationRequest, -} from "../walletTypes"; -import { canonicalJson, getTalerStampSec } from "../util/helpers"; -import { AmountJson } from "../util/amounts"; -import * as Amounts from "../util/amounts"; -import * as timer from "../util/timer"; -import { - getRandomBytes, - encodeCrock, - decodeCrock, - createEddsaKeyPair, - createBlindingKeySecret, - hash, - rsaBlind, - eddsaVerify, - eddsaSign, - rsaUnblind, - stringToBytes, - createHashContext, - createEcdheKeyPair, - keyExchangeEcdheEddsa, - setupRefreshPlanchet, -} from "./talerCrypto"; -import { randomBytes } from "./primitives/nacl-fast"; - -enum SignaturePurpose { - RESERVE_WITHDRAW = 1200, - WALLET_COIN_DEPOSIT = 1201, - MASTER_DENOMINATION_KEY_VALIDITY = 1025, - WALLET_COIN_MELT = 1202, - TEST = 4242, - MERCHANT_PAYMENT_OK = 1104, - MASTER_WIRE_FEES = 1028, - WALLET_COIN_PAYBACK = 1203, - WALLET_COIN_LINK = 1204, -} - -function amountToBuffer(amount: AmountJson): Uint8Array { - const buffer = new ArrayBuffer(8 + 4 + 12); - const dvbuf = new DataView(buffer); - const u8buf = new Uint8Array(buffer); - const te = new TextEncoder(); - const curr = te.encode(amount.currency); - dvbuf.setBigUint64(0, BigInt(amount.value)); - dvbuf.setUint32(8, amount.fraction); - u8buf.set(curr, 8 + 4); - - return u8buf; -} - -function timestampToBuffer(ts: Timestamp): Uint8Array { - const b = new ArrayBuffer(8); - const v = new DataView(b); - const s = BigInt(ts.t_ms) * BigInt(1000); - v.setBigUint64(0, s); - return new Uint8Array(b); -} - -function talerTimestampStringToBuffer(ts: string): Uint8Array { - const t_sec = getTalerStampSec(ts); - if (t_sec === null || t_sec === undefined) { - // Should have been validated before! - throw Error("invalid timestamp"); - } - const buffer = new ArrayBuffer(8); - const dvbuf = new DataView(buffer); - const s = BigInt(t_sec) * BigInt(1000 * 1000); - dvbuf.setBigUint64(0, s); - return new Uint8Array(buffer); -} - -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 (let c of this.chunks) { - payloadLen += c.byteLength; - } - const buf = new ArrayBuffer(4 + 4 + payloadLen); - const u8buf = new Uint8Array(buf); - let p = 8; - for (let 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; - } -} - -function buildSigPS(purposeNum: number): SignaturePurposeBuilder { - return new SignaturePurposeBuilder(purposeNum); -} - -export class CryptoImplementation { - static enableTracing: boolean = false; - - constructor() {} - - /** - * Create a pre-coin of the given denomination to be withdrawn from then given - * reserve. - */ - createPlanchet( - req: PlanchetCreationRequest, - ): PlanchetCreationResult { - const reservePub = decodeCrock(req.reservePub); - const reservePriv = decodeCrock(req.reservePriv); - const denomPub = decodeCrock(req.denomPub); - const coinKeyPair = createEddsaKeyPair(); - const blindingFactor = createBlindingKeySecret(); - const coinPubHash = hash(coinKeyPair.eddsaPub); - const ev = rsaBlind(coinPubHash, blindingFactor, denomPub); - const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount; - const denomPubHash = hash(denomPub); - const evHash = hash(ev); - - const withdrawRequest = buildSigPS(SignaturePurpose.RESERVE_WITHDRAW) - .put(reservePub) - .put(amountToBuffer(amountWithFee)) - .put(amountToBuffer(req.feeWithdraw)) - .put(denomPubHash) - .put(evHash) - .build(); - - const sig = eddsaSign(withdrawRequest, reservePriv); - - const planchet: PlanchetCreationResult = { - blindingKey: encodeCrock(blindingFactor), - coinEv: encodeCrock(ev), - coinPriv: encodeCrock(coinKeyPair.eddsaPriv), - coinPub: encodeCrock(coinKeyPair.eddsaPub), - coinValue: req.value, - denomPub: encodeCrock(denomPub), - denomPubHash: encodeCrock(denomPubHash), - reservePub: encodeCrock(reservePub), - withdrawSig: encodeCrock(sig), - }; - return planchet; - } - - /** - * Create a planchet used for tipping, including the private keys. - */ - createTipPlanchet(denom: DenominationRecord): TipPlanchet { - const denomPub = decodeCrock(denom.denomPub); - const coinKeyPair = createEddsaKeyPair(); - const blindingFactor = createBlindingKeySecret(); - const coinPubHash = hash(coinKeyPair.eddsaPub); - const ev = rsaBlind(coinPubHash, blindingFactor, denomPub); - - const tipPlanchet: TipPlanchet = { - blindingKey: encodeCrock(blindingFactor), - coinEv: encodeCrock(ev), - coinPriv: encodeCrock(coinKeyPair.eddsaPriv), - coinPub: encodeCrock(coinKeyPair.eddsaPub), - coinValue: denom.value, - denomPub: encodeCrock(denomPub), - denomPubHash: encodeCrock(hash(denomPub)), - }; - return tipPlanchet; - } - - /** - * Create and sign a message to request payback for a coin. - */ - createPaybackRequest(coin: CoinRecord): PaybackRequest { - const p = buildSigPS(SignaturePurpose.WALLET_COIN_PAYBACK) - .put(decodeCrock(coin.coinPub)) - .put(decodeCrock(coin.denomPubHash)) - .put(decodeCrock(coin.blindingKey)) - .build(); - - const coinPriv = decodeCrock(coin.coinPriv); - const coinSig = eddsaSign(p, coinPriv); - const paybackRequest: PaybackRequest = { - coin_blind_key_secret: coin.blindingKey, - coin_pub: coin.coinPub, - coin_sig: encodeCrock(coinSig), - denom_pub: coin.denomPub, - denom_sig: coin.denomSig, - }; - return paybackRequest; - } - - /** - * Check if a payment signature is valid. - */ - isValidPaymentSignature( - sig: string, - contractHash: string, - merchantPub: string, - ): boolean { - const p = buildSigPS(SignaturePurpose.MERCHANT_PAYMENT_OK) - .put(decodeCrock(contractHash)) - .build(); - const sigBytes = decodeCrock(sig); - const pubBytes = decodeCrock(merchantPub); - return eddsaVerify(p, sigBytes, pubBytes); - } - - /** - * Check if a wire fee is correctly signed. - */ - isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean { - const p = buildSigPS(SignaturePurpose.MASTER_WIRE_FEES) - .put(hash(stringToBytes(type + "\0"))) - .put(timestampToBuffer(wf.startStamp)) - .put(timestampToBuffer(wf.endStamp)) - .put(amountToBuffer(wf.wireFee)) - .build(); - const sig = decodeCrock(wf.sig); - const pub = decodeCrock(masterPub); - return eddsaVerify(p, sig, pub); - } - - /** - * Check if the signature of a denomination is valid. - */ - isValidDenom(denom: DenominationRecord, masterPub: string): boolean { - const p = buildSigPS(SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY) - .put(decodeCrock(masterPub)) - .put(timestampToBuffer(denom.stampStart)) - .put(timestampToBuffer(denom.stampExpireWithdraw)) - .put(timestampToBuffer(denom.stampExpireDeposit)) - .put(timestampToBuffer(denom.stampExpireLegal)) - .put(amountToBuffer(denom.value)) - .put(amountToBuffer(denom.feeWithdraw)) - .put(amountToBuffer(denom.feeDeposit)) - .put(amountToBuffer(denom.feeRefresh)) - .put(amountToBuffer(denom.feeRefund)) - .put(decodeCrock(denom.denomPubHash)) - .build(); - const sig = decodeCrock(denom.masterSig); - const pub = decodeCrock(masterPub); - return eddsaVerify(p, sig, pub); - } - - /** - * Create a new EdDSA key pair. - */ - createEddsaKeypair(): { priv: string; pub: string } { - const pair = createEddsaKeyPair(); - return { - priv: encodeCrock(pair.eddsaPriv), - pub: encodeCrock(pair.eddsaPub), - }; - } - - /** - * Unblind a blindly signed value. - */ - rsaUnblind(sig: string, bk: string, pk: string): string { - const denomSig = rsaUnblind( - decodeCrock(sig), - decodeCrock(pk), - decodeCrock(bk), - ); - return encodeCrock(denomSig); - } - - /** - * Generate updated coins (to store in the database) - * and deposit permissions for each given coin. - */ - signDeposit( - contractTerms: ContractTerms, - cds: CoinWithDenom[], - totalAmount: AmountJson, - ): PayCoinInfo { - const ret: PayCoinInfo = { - originalCoins: [], - sigs: [], - updatedCoins: [], - }; - - const contractTermsHash = this.hashString(canonicalJson(contractTerms)); - - const feeList: AmountJson[] = cds.map(x => x.denom.feeDeposit); - let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList) - .amount; - // okay if saturates - fees = Amounts.sub(fees, Amounts.parseOrThrow(contractTerms.max_fee)) - .amount; - const total = Amounts.add(fees, totalAmount).amount; - - let amountSpent = Amounts.getZero(cds[0].coin.currentAmount.currency); - let amountRemaining = total; - - for (const cd of cds) { - const originalCoin = { ...cd.coin }; - - if (amountRemaining.value === 0 && amountRemaining.fraction === 0) { - break; - } - - let coinSpend: AmountJson; - if (Amounts.cmp(amountRemaining, cd.coin.currentAmount) < 0) { - coinSpend = amountRemaining; - } else { - coinSpend = cd.coin.currentAmount; - } - - amountSpent = Amounts.add(amountSpent, coinSpend).amount; - - const feeDeposit = cd.denom.feeDeposit; - - // Give the merchant at least the deposit fee, otherwise it'll reject - // the coin. - - if (Amounts.cmp(coinSpend, feeDeposit) < 0) { - coinSpend = feeDeposit; - } - - const newAmount = Amounts.sub(cd.coin.currentAmount, coinSpend).amount; - cd.coin.currentAmount = newAmount; - cd.coin.status = CoinStatus.Dirty; - - const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT) - .put(decodeCrock(contractTermsHash)) - .put(decodeCrock(contractTerms.H_wire)) - .put(talerTimestampStringToBuffer(contractTerms.timestamp)) - .put(talerTimestampStringToBuffer(contractTerms.refund_deadline)) - .put(amountToBuffer(coinSpend)) - .put(amountToBuffer(cd.denom.feeDeposit)) - .put(decodeCrock(contractTerms.merchant_pub)) - .put(decodeCrock(cd.coin.coinPub)) - .build(); - const coinSig = eddsaSign(d, decodeCrock(cd.coin.coinPriv)); - - const s: CoinPaySig = { - coin_pub: cd.coin.coinPub, - coin_sig: encodeCrock(coinSig), - contribution: Amounts.toString(coinSpend), - denom_pub: cd.coin.denomPub, - exchange_url: cd.denom.exchangeBaseUrl, - ub_sig: cd.coin.denomSig, - }; - ret.sigs.push(s); - ret.updatedCoins.push(cd.coin); - ret.originalCoins.push(originalCoin); - } - return ret; - } - - /** - * Create a new refresh session. - */ - createRefreshSession( - exchangeBaseUrl: string, - kappa: number, - meltCoin: CoinRecord, - newCoinDenoms: DenominationRecord[], - meltFee: AmountJson, - ): RefreshSessionRecord { - let valueWithFee = Amounts.getZero(newCoinDenoms[0].value.currency); - - for (const ncd of newCoinDenoms) { - valueWithFee = Amounts.add(valueWithFee, ncd.value, ncd.feeWithdraw) - .amount; - } - - // melt fee - valueWithFee = Amounts.add(valueWithFee, meltFee).amount; - - const sessionHc = createHashContext(); - - const transferPubs: string[] = []; - const transferPrivs: string[] = []; - - const planchetsForGammas: RefreshPlanchetRecord[][] = []; - - for (let i = 0; i < kappa; i++) { - const transferKeyPair = createEcdheKeyPair(); - sessionHc.update(transferKeyPair.ecdhePub); - transferPrivs.push(encodeCrock(transferKeyPair.ecdhePriv)); - transferPubs.push(encodeCrock(transferKeyPair.ecdhePub)); - } - - for (const denom of newCoinDenoms) { - const r = decodeCrock(denom.denomPub); - sessionHc.update(r); - } - - sessionHc.update(decodeCrock(meltCoin.coinPub)); - sessionHc.update(amountToBuffer(valueWithFee)); - - for (let i = 0; i < kappa; i++) { - const planchets: RefreshPlanchetRecord[] = []; - for (let j = 0; j < newCoinDenoms.length; j++) { - const transferPriv = decodeCrock(transferPrivs[i]); - const oldCoinPub = decodeCrock(meltCoin.coinPub); - const transferSecret = keyExchangeEcdheEddsa(transferPriv, oldCoinPub); - - const fresh = setupRefreshPlanchet(transferSecret, j); - - const coinPriv = fresh.coinPriv; - const coinPub = fresh.coinPub; - const blindingFactor = fresh.bks; - const pubHash = hash(coinPub); - const denomPub = decodeCrock(newCoinDenoms[j].denomPub); - const ev = rsaBlind(pubHash, blindingFactor, denomPub); - const planchet: RefreshPlanchetRecord = { - blindingKey: encodeCrock(blindingFactor), - coinEv: encodeCrock(ev), - privateKey: encodeCrock(coinPriv), - publicKey: encodeCrock(coinPub), - }; - planchets.push(planchet); - sessionHc.update(ev); - } - planchetsForGammas.push(planchets); - } - - const sessionHash = sessionHc.finish(); - - const confirmData = buildSigPS(SignaturePurpose.WALLET_COIN_MELT) - .put(sessionHash) - .put(amountToBuffer(valueWithFee)) - .put(amountToBuffer(meltFee)) - .put(decodeCrock(meltCoin.coinPub)) - .build(); - - const confirmSig = eddsaSign(confirmData, decodeCrock(meltCoin.coinPriv)); - - let valueOutput = Amounts.getZero(newCoinDenoms[0].value.currency); - for (const denom of newCoinDenoms) { - valueOutput = Amounts.add(valueOutput, denom.value).amount; - } - - const refreshSessionId = encodeCrock(getRandomBytes(32)); - - const refreshSession: RefreshSessionRecord = { - refreshSessionId, - confirmSig: encodeCrock(confirmSig), - exchangeBaseUrl, - finished: false, - hash: encodeCrock(sessionHash), - meltCoinPub: meltCoin.coinPub, - newDenomHashes: newCoinDenoms.map(d => d.denomPubHash), - newDenoms: newCoinDenoms.map(d => d.denomPub), - norevealIndex: undefined, - planchetsForGammas: planchetsForGammas, - transferPrivs, - transferPubs, - valueOutput, - valueWithFee, - }; - - return refreshSession; - } - - /** - * Hash a string including the zero terminator. - */ - hashString(str: string): string { - const ts = new TextEncoder(); - const b = ts.encode(str + "\0"); - return encodeCrock(hash(b)); - } - - /** - * Hash a denomination public key. - */ - hashDenomPub(denomPub: string): string { - return encodeCrock(hash(decodeCrock(denomPub))); - } - - signCoinLink( - oldCoinPriv: string, - newDenomHash: string, - oldCoinPub: string, - transferPub: string, - coinEv: string, - ): string { - const coinEvHash = hash(decodeCrock(coinEv)); - const coinLink = buildSigPS(SignaturePurpose.WALLET_COIN_LINK) - .put(decodeCrock(newDenomHash)) - .put(decodeCrock(oldCoinPub)) - .put(decodeCrock(transferPub)) - .put(coinEvHash) - .build(); - const coinPriv = decodeCrock(oldCoinPriv); - const sig = eddsaSign(coinLink, coinPriv); - return encodeCrock(sig); - } - - benchmark(repetitions: number): BenchmarkResult { - let time_hash = 0; - for (let i = 0; i < repetitions; i++) { - const start = timer.performanceNow(); - this.hashString("hello world"); - time_hash += timer.performanceNow() - start; - } - - let time_hash_big = 0; - for (let i = 0; i < repetitions; i++) { - const ba = randomBytes(4096); - const start = timer.performanceNow(); - hash(ba); - time_hash_big += timer.performanceNow() - start; - } - - let time_eddsa_create = 0; - for (let i = 0; i < repetitions; i++) { - const start = timer.performanceNow(); - const pair = createEddsaKeyPair(); - time_eddsa_create += timer.performanceNow() - start; - } - - let time_eddsa_sign = 0; - const p = randomBytes(4096); - - const pair = createEddsaKeyPair(); - - for (let i = 0; i < repetitions; i++) { - const start = timer.performanceNow(); - eddsaSign(p, pair.eddsaPriv); - time_eddsa_sign += timer.performanceNow() - start; - } - - const sig = eddsaSign(p, pair.eddsaPriv); - - let time_eddsa_verify = 0; - for (let i = 0; i < repetitions; i++) { - const start = timer.performanceNow(); - eddsaVerify(p, sig, pair.eddsaPub); - time_eddsa_verify += timer.performanceNow() - start; - } - - return { - repetitions, - time: { - hash_small: time_hash, - hash_big: time_hash_big, - eddsa_create: time_eddsa_create, - eddsa_sign: time_eddsa_sign, - eddsa_verify: time_eddsa_verify, - }, - }; - } -} diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts deleted file mode 100644 index 0ea641dde..000000000 --- a/src/crypto/cryptoWorker.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface CryptoWorker { - postMessage(message: any): void; - - terminate(): void; - - onmessage: (m: any) => void; - onerror: (m: any) => void; -} \ No newline at end of file diff --git a/src/crypto/nodeProcessWorker.ts b/src/crypto/nodeProcessWorker.ts deleted file mode 100644 index 8ff149788..000000000 --- a/src/crypto/nodeProcessWorker.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { CryptoWorkerFactory } from "./cryptoApi"; - -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - 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. - - 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 - TALER; see the file COPYING. If not, see - */ - - -// tslint:disable:no-var-requires - -import { CryptoWorker } from "./cryptoWorker"; - -import path = require("path"); -import child_process = require("child_process"); - -const nodeWorkerEntry = path.join(__dirname, "nodeWorkerEntry.js"); - - -export class NodeCryptoWorkerFactory implements CryptoWorkerFactory { - startWorker(): CryptoWorker { - if (typeof require === "undefined") { - throw Error("cannot make worker, require(...) not defined"); - } - const workerCtor = require("./nodeProcessWorker").Worker; - const workerPath = __dirname + "/cryptoWorker.js"; - return new workerCtor(workerPath); - } - - getConcurrency(): number { - return 4; - } -} - -/** - * Worker implementation that uses node subprocesses. - */ -export class Worker { - private child: any; - - /** - * Function to be called when we receive a message from the worker thread. - */ - onmessage: undefined | ((m: any) => void); - - /** - * Function to be called when we receive an error from the worker thread. - */ - onerror: undefined | ((m: any) => void); - - private dispatchMessage(msg: any) { - if (this.onmessage) { - this.onmessage({ data: msg }); - } else { - console.warn("no handler for worker event 'message' defined") - } - } - - private dispatchError(msg: any) { - if (this.onerror) { - this.onerror({ data: msg }); - } else { - console.warn("no handler for worker event 'error' defined") - } - } - - constructor() { - this.child = child_process.fork(nodeWorkerEntry); - this.onerror = undefined; - this.onmessage = undefined; - - this.child.on("error", (e: any) => { - this.dispatchError(e); - }); - - this.child.on("message", (msg: any) => { - this.dispatchMessage(msg); - }); - } - - /** - * Add an event listener for either an "error" or "message" event. - */ - addEventListener(event: "message" | "error", fn: (x: any) => void): void { - switch (event) { - case "message": - this.onmessage = fn; - break; - case "error": - this.onerror = fn; - break; - } - } - - /** - * Send a message to the worker thread. - */ - postMessage (msg: any) { - this.child.send(JSON.stringify({data: msg})); - } - - /** - * Forcibly terminate the worker thread. - */ - terminate () { - this.child.kill("SIGINT"); - } -} diff --git a/src/crypto/nodeWorkerEntry.ts b/src/crypto/nodeWorkerEntry.ts deleted file mode 100644 index 1e088d987..000000000 --- a/src/crypto/nodeWorkerEntry.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - 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. - - 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 - TALER; see the file COPYING. If not, see - */ - -// tslint:disable:no-var-requires - -import { CryptoImplementation } from "./cryptoImplementation"; - -async function handleRequest(operation: string, id: number, args: string[]) { - - const impl = new CryptoImplementation(); - - if (!(operation in impl)) { - console.error(`crypto operation '${operation}' not found`); - return; - } - - try { - const result = (impl as any)[operation](...args); - if (process.send) { - process.send({ result, id }); - } else { - console.error("process.send not available"); - } - } catch (e) { - console.error("error during operation", e); - return; - } -} - -process.on("message", (msgStr: any) => { - const msg = JSON.parse(msgStr); - - const args = msg.data.args; - if (!Array.isArray(args)) { - console.error("args must be array"); - return; - } - const id = msg.data.id; - if (typeof id !== "number") { - console.error("RPC id must be number"); - return; - } - const operation = msg.data.operation; - if (typeof operation !== "string") { - console.error("RPC operation must be string"); - return; - } - - handleRequest(operation, id, args).catch((e) => { - console.error("error in node worker", e); - }); -}); - -process.on("uncaughtException", (err: any) => { - console.error("uncaught exception in node worker entry", err); -}); diff --git a/src/crypto/synchronousWorker.ts b/src/crypto/synchronousWorker.ts deleted file mode 100644 index 12eecde9a..000000000 --- a/src/crypto/synchronousWorker.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - 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 - */ - -import { CryptoImplementation } from "./cryptoImplementation"; - -import { CryptoWorkerFactory } from "./cryptoApi"; -import { CryptoWorker } from "./cryptoWorker"; - -/** - * The synchronous crypto worker produced by this factory doesn't run in the - * background, but actually blocks the caller until the operation is done. - */ -export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory { - startWorker(): CryptoWorker { - if (typeof require === "undefined") { - throw Error("cannot make worker, require(...) not defined"); - } - const workerCtor = require("./synchronousWorker").SynchronousCryptoWorker; - return new workerCtor(); - } - - getConcurrency(): number { - return 1; - } -} - - -/** - * Worker implementation that uses node subprocesses. - */ -export class SynchronousCryptoWorker { - - /** - * Function to be called when we receive a message from the worker thread. - */ - onmessage: undefined | ((m: any) => void); - - /** - * Function to be called when we receive an error from the worker thread. - */ - onerror: undefined | ((m: any) => void); - - constructor() { - this.onerror = undefined; - this.onmessage = undefined; - } - - /** - * Add an event listener for either an "error" or "message" event. - */ - addEventListener(event: "message" | "error", fn: (x: any) => void): void { - switch (event) { - case "message": - this.onmessage = fn; - break; - case "error": - this.onerror = fn; - break; - } - } - - private dispatchMessage(msg: any) { - if (this.onmessage) { - this.onmessage({ data: msg }); - } - } - - private async handleRequest(operation: string, id: number, args: string[]) { - const impl = new CryptoImplementation(); - - if (!(operation in impl)) { - console.error(`crypto operation '${operation}' not found`); - return; - } - - let result: any; - try { - result = (impl as any)[operation](...args); - } catch (e) { - console.log("error during operation", e); - return; - } - - try { - setImmediate(() => this.dispatchMessage({ result, id })); - } catch (e) { - console.log("got error during dispatch", e); - } - } - - /** - * Send a message to the worker thread. - */ - postMessage(msg: any) { - const args = msg.args; - if (!Array.isArray(args)) { - console.error("args must be array"); - return; - } - const id = msg.id; - if (typeof id !== "number") { - console.error("RPC id must be number"); - return; - } - const operation = msg.operation; - if (typeof operation !== "string") { - console.error("RPC operation must be string"); - return; - } - - this.handleRequest(operation, id, args).catch(e => { - console.error("Error while handling crypto request:", e); - }); - } - - /** - * Forcibly terminate the worker thread. - */ - terminate() { - // This is a no-op. - } -} diff --git a/src/crypto/workers/browserWorkerEntry.ts b/src/crypto/workers/browserWorkerEntry.ts new file mode 100644 index 000000000..5ac762c13 --- /dev/null +++ b/src/crypto/workers/browserWorkerEntry.ts @@ -0,0 +1,70 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + 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. + + 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 + TALER; see the file COPYING. If not, see +*/ + +/** + * Web worker for crypto operations. + */ + +/** + * Imports. + */ + +import { CryptoImplementation } from "./cryptoImplementation"; + +const worker: Worker = (self as any) as Worker; + +async function handleRequest(operation: string, id: number, args: string[]) { + const impl = new CryptoImplementation(); + + if (!(operation in impl)) { + console.error(`crypto operation '${operation}' not found`); + return; + } + + try { + const result = (impl as any)[operation](...args); + worker.postMessage({ result, id }); + } catch (e) { + console.log("error during operation", e); + return; + } +} + +worker.onmessage = (msg: MessageEvent) => { + const args = msg.data.args; + if (!Array.isArray(args)) { + console.error("args must be array"); + return; + } + const id = msg.data.id; + if (typeof id !== "number") { + console.error("RPC id must be number"); + return; + } + const operation = msg.data.operation; + if (typeof operation !== "string") { + console.error("RPC operation must be string"); + return; + } + + if (CryptoImplementation.enableTracing) { + console.log("onmessage with", operation); + } + + handleRequest(operation, id, args).catch((e) => { + console.error("error in browsere worker", e); + }); +}; diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts new file mode 100644 index 000000000..5537bb39f --- /dev/null +++ b/src/crypto/workers/cryptoApi.ts @@ -0,0 +1,455 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + 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. + + 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 + TALER; see the file COPYING. If not, see + */ + +/** + * API to access the Taler crypto worker thread. + * @author Florian Dold + */ + +/** + * Imports. + */ +import { AmountJson } from "../../util/amounts"; + +import { + CoinRecord, + DenominationRecord, + RefreshSessionRecord, + TipPlanchet, + WireFee, +} from "../../dbTypes"; + +import { CryptoWorker } from "./cryptoWorker"; + +import { ContractTerms, PaybackRequest } from "../../talerTypes"; + +import { + BenchmarkResult, + CoinWithDenom, + PayCoinInfo, + PlanchetCreationResult, + PlanchetCreationRequest, +} from "../../walletTypes"; + +import * as timer from "../../util/timer"; + +/** + * State of a crypto worker. + */ +interface WorkerState { + /** + * The actual worker thread. + */ + w: CryptoWorker | null; + + /** + * Work we're currently executing or null if not busy. + */ + currentWorkItem: WorkItem | null; + + /** + * Timer to terminate the worker if it's not busy enough. + */ + terminationTimerHandle: timer.TimerHandle | null; +} + +interface WorkItem { + operation: string; + args: any[]; + resolve: any; + reject: any; + + /** + * Serial id to identify a matching response. + */ + rpcId: number; + + /** + * Time when the work was submitted to a (non-busy) worker thread. + */ + startTime: number; +} + +/** + * Number of different priorities. Each priority p + * must be 0 <= p < NUM_PRIO. + */ +const NUM_PRIO = 5; + +export interface CryptoWorkerFactory { + /** + * Start a new worker. + */ + startWorker(): CryptoWorker; + + /** + * Query the number of workers that should be + * run at the same time. + */ + getConcurrency(): number; +} + +export class BrowserCryptoWorkerFactory implements CryptoWorkerFactory { + startWorker(): CryptoWorker { + const workerCtor = Worker; + const workerPath = "/dist/cryptoWorker-bundle.js"; + return new workerCtor(workerPath) as CryptoWorker; + } + + getConcurrency(): number { + let concurrency = 2; + try { + // only works in the browser + // tslint:disable-next-line:no-string-literal + concurrency = (navigator as any)["hardwareConcurrency"]; + concurrency = Math.max(1, Math.ceil(concurrency / 2)); + } catch (e) { + concurrency = 2; + } + return concurrency; + } +} + +/** + * Crypto API that interfaces manages a background crypto thread + * for the execution of expensive operations. + */ +export class CryptoApi { + private nextRpcId: number = 1; + private workers: WorkerState[]; + private workQueues: WorkItem[][]; + + private workerFactory: CryptoWorkerFactory; + + /** + * Number of busy workers. + */ + private numBusy: number = 0; + + /** + * Did we stop accepting new requests? + */ + private stopped: boolean = false; + + static enableTracing = false; + + /** + * Terminate all worker threads. + */ + terminateWorkers() { + for (let worker of this.workers) { + if (worker.w) { + CryptoApi.enableTracing && console.log("terminating worker"); + worker.w.terminate(); + if (worker.terminationTimerHandle) { + worker.terminationTimerHandle.clear(); + worker.terminationTimerHandle = null; + } + if (worker.currentWorkItem) { + worker.currentWorkItem.reject(Error("explicitly terminated")); + worker.currentWorkItem = null; + } + worker.w = null; + } + } + } + + stop() { + this.terminateWorkers(); + this.stopped = true; + } + + /** + * Start a worker (if not started) and set as busy. + */ + wake(ws: WorkerState, work: WorkItem): void { + if (this.stopped) { + console.log("cryptoApi is stopped"); + CryptoApi.enableTracing && + console.log("not waking, as cryptoApi is stopped"); + return; + } + if (ws.currentWorkItem !== null) { + throw Error("assertion failed"); + } + ws.currentWorkItem = work; + this.numBusy++; + if (!ws.w) { + const w = this.workerFactory.startWorker(); + w.onmessage = (m: MessageEvent) => this.handleWorkerMessage(ws, m); + w.onerror = (e: ErrorEvent) => this.handleWorkerError(ws, e); + ws.w = w; + } + + const msg: any = { + args: work.args, + id: work.rpcId, + operation: work.operation, + }; + this.resetWorkerTimeout(ws); + work.startTime = timer.performanceNow(); + setImmediate(() => ws.w!.postMessage(msg)); + } + + resetWorkerTimeout(ws: WorkerState) { + if (ws.terminationTimerHandle !== null) { + ws.terminationTimerHandle.clear(); + ws.terminationTimerHandle = null; + } + const destroy = () => { + // terminate worker if it's idle + if (ws.w && ws.currentWorkItem === null) { + ws.w!.terminate(); + ws.w = null; + } + }; + ws.terminationTimerHandle = timer.after(15 * 1000, destroy); + } + + handleWorkerError(ws: WorkerState, e: ErrorEvent) { + if (ws.currentWorkItem) { + console.error( + `error in worker during ${ws.currentWorkItem!.operation}`, + e, + ); + } else { + console.error("error in worker", e); + } + console.error(e.message); + try { + ws.w!.terminate(); + ws.w = null; + } catch (e) { + console.error(e); + } + if (ws.currentWorkItem !== null) { + ws.currentWorkItem.reject(e); + ws.currentWorkItem = null; + this.numBusy--; + } + this.findWork(ws); + } + + private findWork(ws: WorkerState) { + // try to find more work for this worker + for (let i = 0; i < NUM_PRIO; i++) { + const q = this.workQueues[NUM_PRIO - i - 1]; + if (q.length !== 0) { + const work: WorkItem = q.shift()!; + this.wake(ws, work); + return; + } + } + } + + handleWorkerMessage(ws: WorkerState, msg: MessageEvent) { + const id = msg.data.id; + if (typeof id !== "number") { + console.error("rpc id must be number"); + return; + } + const currentWorkItem = ws.currentWorkItem; + ws.currentWorkItem = null; + this.numBusy--; + this.findWork(ws); + if (!currentWorkItem) { + console.error("unsolicited response from worker"); + return; + } + if (id !== currentWorkItem.rpcId) { + console.error(`RPC with id ${id} has no registry entry`); + return; + } + + CryptoApi.enableTracing && + console.log( + `rpc ${currentWorkItem.operation} took ${timer.performanceNow() - + currentWorkItem.startTime}ms`, + ); + currentWorkItem.resolve(msg.data.result); + } + + constructor(workerFactory: CryptoWorkerFactory) { + this.workerFactory = workerFactory; + this.workers = new Array(workerFactory.getConcurrency()); + + for (let i = 0; i < this.workers.length; i++) { + this.workers[i] = { + currentWorkItem: null, + terminationTimerHandle: null, + w: null, + }; + } + + this.workQueues = []; + for (let i = 0; i < NUM_PRIO; i++) { + this.workQueues.push([]); + } + } + + private doRpc( + operation: string, + priority: number, + ...args: any[] + ): Promise { + const p: Promise = new Promise((resolve, reject) => { + const rpcId = this.nextRpcId++; + const workItem: WorkItem = { + operation, + args, + resolve, + reject, + rpcId, + startTime: 0, + }; + + if (this.numBusy === this.workers.length) { + const q = this.workQueues[priority]; + if (!q) { + throw Error("assertion failed"); + } + this.workQueues[priority].push(workItem); + return; + } + + for (const ws of this.workers) { + if (ws.currentWorkItem !== null) { + continue; + } + this.wake(ws, workItem); + return; + } + + throw Error("assertion failed"); + }); + + return p; + } + + createPlanchet( + req: PlanchetCreationRequest, + ): Promise { + return this.doRpc("createPlanchet", 1, req); + } + + createTipPlanchet(denom: DenominationRecord): Promise { + return this.doRpc("createTipPlanchet", 1, denom); + } + + hashString(str: string): Promise { + return this.doRpc("hashString", 1, str); + } + + hashDenomPub(denomPub: string): Promise { + return this.doRpc("hashDenomPub", 1, denomPub); + } + + isValidDenom(denom: DenominationRecord, masterPub: string): Promise { + return this.doRpc("isValidDenom", 2, denom, masterPub); + } + + isValidWireFee( + type: string, + wf: WireFee, + masterPub: string, + ): Promise { + return this.doRpc("isValidWireFee", 2, type, wf, masterPub); + } + + isValidPaymentSignature( + sig: string, + contractHash: string, + merchantPub: string, + ): Promise { + return this.doRpc( + "isValidPaymentSignature", + 1, + sig, + contractHash, + merchantPub, + ); + } + + signDeposit( + contractTerms: ContractTerms, + cds: CoinWithDenom[], + totalAmount: AmountJson, + ): Promise { + return this.doRpc( + "signDeposit", + 3, + contractTerms, + cds, + totalAmount, + ); + } + + createEddsaKeypair(): Promise<{ priv: string; pub: string }> { + return this.doRpc<{ priv: string; pub: string }>("createEddsaKeypair", 1); + } + + rsaUnblind(sig: string, bk: string, pk: string): Promise { + return this.doRpc("rsaUnblind", 4, sig, bk, pk); + } + + rsaVerify(hm: string, sig: string, pk: string): Promise { + return this.doRpc("rsaVerify", 4, hm, sig, pk); + } + + createPaybackRequest(coin: CoinRecord): Promise { + return this.doRpc("createPaybackRequest", 1, coin); + } + + createRefreshSession( + exchangeBaseUrl: string, + kappa: number, + meltCoin: CoinRecord, + newCoinDenoms: DenominationRecord[], + meltFee: AmountJson, + ): Promise { + return this.doRpc( + "createRefreshSession", + 4, + exchangeBaseUrl, + kappa, + meltCoin, + newCoinDenoms, + meltFee, + ); + } + + signCoinLink( + oldCoinPriv: string, + newDenomHash: string, + oldCoinPub: string, + transferPub: string, + coinEv: string, + ): Promise { + return this.doRpc( + "signCoinLink", + 4, + oldCoinPriv, + newDenomHash, + oldCoinPub, + transferPub, + coinEv, + ); + } + + benchmark(repetitions: number): Promise { + return this.doRpc("benchmark", 1, repetitions); + } +} diff --git a/src/crypto/workers/cryptoImplementation.ts b/src/crypto/workers/cryptoImplementation.ts new file mode 100644 index 000000000..00d81ce27 --- /dev/null +++ b/src/crypto/workers/cryptoImplementation.ts @@ -0,0 +1,608 @@ +/* + This file is part of GNU Taler + (C) 2019 GNUnet e.V. + + 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. + + 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 + TALER; see the file COPYING. If not, see + */ + +/** + * Synchronous implementation of crypto-related functions for the wallet. + * + * The functionality is parameterized over an Emscripten environment. + */ + +/** + * Imports. + */ + +import { + CoinRecord, + CoinStatus, + DenominationRecord, + RefreshPlanchetRecord, + RefreshSessionRecord, + TipPlanchet, + WireFee, + initRetryInfo, +} from "../../dbTypes"; + +import { CoinPaySig, ContractTerms, PaybackRequest } from "../../talerTypes"; +import { + BenchmarkResult, + CoinWithDenom, + PayCoinInfo, + Timestamp, + PlanchetCreationResult, + PlanchetCreationRequest, + getTimestampNow, +} from "../../walletTypes"; +import { canonicalJson, getTalerStampSec } from "../../util/helpers"; +import { AmountJson } from "../../util/amounts"; +import * as Amounts from "../../util/amounts"; +import * as timer from "../../util/timer"; +import { + getRandomBytes, + encodeCrock, + decodeCrock, + createEddsaKeyPair, + createBlindingKeySecret, + hash, + rsaBlind, + eddsaVerify, + eddsaSign, + rsaUnblind, + stringToBytes, + createHashContext, + createEcdheKeyPair, + keyExchangeEcdheEddsa, + setupRefreshPlanchet, + rsaVerify, +} from "../talerCrypto"; +import { randomBytes } from "../primitives/nacl-fast"; + +enum SignaturePurpose { + RESERVE_WITHDRAW = 1200, + WALLET_COIN_DEPOSIT = 1201, + MASTER_DENOMINATION_KEY_VALIDITY = 1025, + WALLET_COIN_MELT = 1202, + TEST = 4242, + MERCHANT_PAYMENT_OK = 1104, + MASTER_WIRE_FEES = 1028, + WALLET_COIN_PAYBACK = 1203, + WALLET_COIN_LINK = 1204, +} + +function amountToBuffer(amount: AmountJson): Uint8Array { + const buffer = new ArrayBuffer(8 + 4 + 12); + const dvbuf = new DataView(buffer); + const u8buf = new Uint8Array(buffer); + const te = new TextEncoder(); + const curr = te.encode(amount.currency); + dvbuf.setBigUint64(0, BigInt(amount.value)); + dvbuf.setUint32(8, amount.fraction); + u8buf.set(curr, 8 + 4); + + return u8buf; +} + +function timestampToBuffer(ts: Timestamp): Uint8Array { + const b = new ArrayBuffer(8); + const v = new DataView(b); + const s = BigInt(ts.t_ms) * BigInt(1000); + v.setBigUint64(0, s); + return new Uint8Array(b); +} + +function talerTimestampStringToBuffer(ts: string): Uint8Array { + const t_sec = getTalerStampSec(ts); + if (t_sec === null || t_sec === undefined) { + // Should have been validated before! + throw Error("invalid timestamp"); + } + const buffer = new ArrayBuffer(8); + const dvbuf = new DataView(buffer); + const s = BigInt(t_sec) * BigInt(1000 * 1000); + dvbuf.setBigUint64(0, s); + return new Uint8Array(buffer); +} + +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 (let c of this.chunks) { + payloadLen += c.byteLength; + } + const buf = new ArrayBuffer(4 + 4 + payloadLen); + const u8buf = new Uint8Array(buf); + let p = 8; + for (let 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; + } +} + +function buildSigPS(purposeNum: number): SignaturePurposeBuilder { + return new SignaturePurposeBuilder(purposeNum); +} + +export class CryptoImplementation { + static enableTracing: boolean = false; + + constructor() {} + + /** + * Create a pre-coin of the given denomination to be withdrawn from then given + * reserve. + */ + createPlanchet( + req: PlanchetCreationRequest, + ): PlanchetCreationResult { + const reservePub = decodeCrock(req.reservePub); + const reservePriv = decodeCrock(req.reservePriv); + const denomPub = decodeCrock(req.denomPub); + const coinKeyPair = createEddsaKeyPair(); + const blindingFactor = createBlindingKeySecret(); + const coinPubHash = hash(coinKeyPair.eddsaPub); + const ev = rsaBlind(coinPubHash, blindingFactor, denomPub); + const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount; + const denomPubHash = hash(denomPub); + const evHash = hash(ev); + + const withdrawRequest = buildSigPS(SignaturePurpose.RESERVE_WITHDRAW) + .put(reservePub) + .put(amountToBuffer(amountWithFee)) + .put(amountToBuffer(req.feeWithdraw)) + .put(denomPubHash) + .put(evHash) + .build(); + + const sig = eddsaSign(withdrawRequest, reservePriv); + + const planchet: PlanchetCreationResult = { + blindingKey: encodeCrock(blindingFactor), + coinEv: encodeCrock(ev), + coinPriv: encodeCrock(coinKeyPair.eddsaPriv), + coinPub: encodeCrock(coinKeyPair.eddsaPub), + coinValue: req.value, + denomPub: encodeCrock(denomPub), + denomPubHash: encodeCrock(denomPubHash), + reservePub: encodeCrock(reservePub), + withdrawSig: encodeCrock(sig), + }; + return planchet; + } + + /** + * Create a planchet used for tipping, including the private keys. + */ + createTipPlanchet(denom: DenominationRecord): TipPlanchet { + const denomPub = decodeCrock(denom.denomPub); + const coinKeyPair = createEddsaKeyPair(); + const blindingFactor = createBlindingKeySecret(); + const coinPubHash = hash(coinKeyPair.eddsaPub); + const ev = rsaBlind(coinPubHash, blindingFactor, denomPub); + + const tipPlanchet: TipPlanchet = { + blindingKey: encodeCrock(blindingFactor), + coinEv: encodeCrock(ev), + coinPriv: encodeCrock(coinKeyPair.eddsaPriv), + coinPub: encodeCrock(coinKeyPair.eddsaPub), + coinValue: denom.value, + denomPub: encodeCrock(denomPub), + denomPubHash: encodeCrock(hash(denomPub)), + }; + return tipPlanchet; + } + + /** + * Create and sign a message to request payback for a coin. + */ + createPaybackRequest(coin: CoinRecord): PaybackRequest { + const p = buildSigPS(SignaturePurpose.WALLET_COIN_PAYBACK) + .put(decodeCrock(coin.coinPub)) + .put(decodeCrock(coin.denomPubHash)) + .put(decodeCrock(coin.blindingKey)) + .build(); + + const coinPriv = decodeCrock(coin.coinPriv); + const coinSig = eddsaSign(p, coinPriv); + const paybackRequest: PaybackRequest = { + coin_blind_key_secret: coin.blindingKey, + coin_pub: coin.coinPub, + coin_sig: encodeCrock(coinSig), + denom_pub: coin.denomPub, + denom_sig: coin.denomSig, + }; + return paybackRequest; + } + + /** + * Check if a payment signature is valid. + */ + isValidPaymentSignature( + sig: string, + contractHash: string, + merchantPub: string, + ): boolean { + const p = buildSigPS(SignaturePurpose.MERCHANT_PAYMENT_OK) + .put(decodeCrock(contractHash)) + .build(); + const sigBytes = decodeCrock(sig); + const pubBytes = decodeCrock(merchantPub); + return eddsaVerify(p, sigBytes, pubBytes); + } + + /** + * Check if a wire fee is correctly signed. + */ + isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean { + const p = buildSigPS(SignaturePurpose.MASTER_WIRE_FEES) + .put(hash(stringToBytes(type + "\0"))) + .put(timestampToBuffer(wf.startStamp)) + .put(timestampToBuffer(wf.endStamp)) + .put(amountToBuffer(wf.wireFee)) + .build(); + const sig = decodeCrock(wf.sig); + const pub = decodeCrock(masterPub); + return eddsaVerify(p, sig, pub); + } + + /** + * Check if the signature of a denomination is valid. + */ + isValidDenom(denom: DenominationRecord, masterPub: string): boolean { + const p = buildSigPS(SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY) + .put(decodeCrock(masterPub)) + .put(timestampToBuffer(denom.stampStart)) + .put(timestampToBuffer(denom.stampExpireWithdraw)) + .put(timestampToBuffer(denom.stampExpireDeposit)) + .put(timestampToBuffer(denom.stampExpireLegal)) + .put(amountToBuffer(denom.value)) + .put(amountToBuffer(denom.feeWithdraw)) + .put(amountToBuffer(denom.feeDeposit)) + .put(amountToBuffer(denom.feeRefresh)) + .put(amountToBuffer(denom.feeRefund)) + .put(decodeCrock(denom.denomPubHash)) + .build(); + const sig = decodeCrock(denom.masterSig); + const pub = decodeCrock(masterPub); + return eddsaVerify(p, sig, pub); + } + + /** + * Create a new EdDSA key pair. + */ + createEddsaKeypair(): { priv: string; pub: string } { + const pair = createEddsaKeyPair(); + return { + priv: encodeCrock(pair.eddsaPriv), + pub: encodeCrock(pair.eddsaPub), + }; + } + + /** + * Unblind a blindly signed value. + */ + rsaUnblind(blindedSig: string, bk: string, pk: string): string { + const denomSig = rsaUnblind( + decodeCrock(blindedSig), + decodeCrock(pk), + decodeCrock(bk), + ); + return encodeCrock(denomSig); + } + + /** + * Unblind a blindly signed value. + */ + rsaVerify(hm: string, sig: string, pk: string): boolean { + return rsaVerify(hash(decodeCrock(hm)), decodeCrock(sig), decodeCrock(pk)); + } + + /** + * Generate updated coins (to store in the database) + * and deposit permissions for each given coin. + */ + signDeposit( + contractTerms: ContractTerms, + cds: CoinWithDenom[], + totalAmount: AmountJson, + ): PayCoinInfo { + const ret: PayCoinInfo = { + originalCoins: [], + sigs: [], + updatedCoins: [], + }; + + const contractTermsHash = this.hashString(canonicalJson(contractTerms)); + + const feeList: AmountJson[] = cds.map(x => x.denom.feeDeposit); + let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList) + .amount; + // okay if saturates + fees = Amounts.sub(fees, Amounts.parseOrThrow(contractTerms.max_fee)) + .amount; + const total = Amounts.add(fees, totalAmount).amount; + + let amountSpent = Amounts.getZero(cds[0].coin.currentAmount.currency); + let amountRemaining = total; + + for (const cd of cds) { + const originalCoin = { ...cd.coin }; + + if (amountRemaining.value === 0 && amountRemaining.fraction === 0) { + break; + } + + let coinSpend: AmountJson; + if (Amounts.cmp(amountRemaining, cd.coin.currentAmount) < 0) { + coinSpend = amountRemaining; + } else { + coinSpend = cd.coin.currentAmount; + } + + amountSpent = Amounts.add(amountSpent, coinSpend).amount; + + const feeDeposit = cd.denom.feeDeposit; + + // Give the merchant at least the deposit fee, otherwise it'll reject + // the coin. + + if (Amounts.cmp(coinSpend, feeDeposit) < 0) { + coinSpend = feeDeposit; + } + + const newAmount = Amounts.sub(cd.coin.currentAmount, coinSpend).amount; + cd.coin.currentAmount = newAmount; + cd.coin.status = CoinStatus.Dirty; + + const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT) + .put(decodeCrock(contractTermsHash)) + .put(decodeCrock(contractTerms.H_wire)) + .put(talerTimestampStringToBuffer(contractTerms.timestamp)) + .put(talerTimestampStringToBuffer(contractTerms.refund_deadline)) + .put(amountToBuffer(coinSpend)) + .put(amountToBuffer(cd.denom.feeDeposit)) + .put(decodeCrock(contractTerms.merchant_pub)) + .put(decodeCrock(cd.coin.coinPub)) + .build(); + const coinSig = eddsaSign(d, decodeCrock(cd.coin.coinPriv)); + + const s: CoinPaySig = { + coin_pub: cd.coin.coinPub, + coin_sig: encodeCrock(coinSig), + contribution: Amounts.toString(coinSpend), + denom_pub: cd.coin.denomPub, + exchange_url: cd.denom.exchangeBaseUrl, + ub_sig: cd.coin.denomSig, + }; + ret.sigs.push(s); + ret.updatedCoins.push(cd.coin); + ret.originalCoins.push(originalCoin); + } + return ret; + } + + /** + * Create a new refresh session. + */ + createRefreshSession( + exchangeBaseUrl: string, + kappa: number, + meltCoin: CoinRecord, + newCoinDenoms: DenominationRecord[], + meltFee: AmountJson, + ): RefreshSessionRecord { + let valueWithFee = Amounts.getZero(newCoinDenoms[0].value.currency); + + for (const ncd of newCoinDenoms) { + valueWithFee = Amounts.add(valueWithFee, ncd.value, ncd.feeWithdraw) + .amount; + } + + // melt fee + valueWithFee = Amounts.add(valueWithFee, meltFee).amount; + + const sessionHc = createHashContext(); + + const transferPubs: string[] = []; + const transferPrivs: string[] = []; + + const planchetsForGammas: RefreshPlanchetRecord[][] = []; + + for (let i = 0; i < kappa; i++) { + const transferKeyPair = createEcdheKeyPair(); + sessionHc.update(transferKeyPair.ecdhePub); + transferPrivs.push(encodeCrock(transferKeyPair.ecdhePriv)); + transferPubs.push(encodeCrock(transferKeyPair.ecdhePub)); + } + + for (const denom of newCoinDenoms) { + const r = decodeCrock(denom.denomPub); + sessionHc.update(r); + } + + sessionHc.update(decodeCrock(meltCoin.coinPub)); + sessionHc.update(amountToBuffer(valueWithFee)); + + for (let i = 0; i < kappa; i++) { + const planchets: RefreshPlanchetRecord[] = []; + for (let j = 0; j < newCoinDenoms.length; j++) { + const transferPriv = decodeCrock(transferPrivs[i]); + const oldCoinPub = decodeCrock(meltCoin.coinPub); + const transferSecret = keyExchangeEcdheEddsa(transferPriv, oldCoinPub); + + const fresh = setupRefreshPlanchet(transferSecret, j); + + const coinPriv = fresh.coinPriv; + const coinPub = fresh.coinPub; + const blindingFactor = fresh.bks; + const pubHash = hash(coinPub); + const denomPub = decodeCrock(newCoinDenoms[j].denomPub); + const ev = rsaBlind(pubHash, blindingFactor, denomPub); + const planchet: RefreshPlanchetRecord = { + blindingKey: encodeCrock(blindingFactor), + coinEv: encodeCrock(ev), + privateKey: encodeCrock(coinPriv), + publicKey: encodeCrock(coinPub), + }; + planchets.push(planchet); + sessionHc.update(ev); + } + planchetsForGammas.push(planchets); + } + + const sessionHash = sessionHc.finish(); + + const confirmData = buildSigPS(SignaturePurpose.WALLET_COIN_MELT) + .put(sessionHash) + .put(amountToBuffer(valueWithFee)) + .put(amountToBuffer(meltFee)) + .put(decodeCrock(meltCoin.coinPub)) + .build(); + + const confirmSig = eddsaSign(confirmData, decodeCrock(meltCoin.coinPriv)); + + let valueOutput = Amounts.getZero(newCoinDenoms[0].value.currency); + for (const denom of newCoinDenoms) { + valueOutput = Amounts.add(valueOutput, denom.value).amount; + } + + const refreshSessionId = encodeCrock(getRandomBytes(32)); + + const refreshSession: RefreshSessionRecord = { + refreshSessionId, + confirmSig: encodeCrock(confirmSig), + exchangeBaseUrl, + hash: encodeCrock(sessionHash), + meltCoinPub: meltCoin.coinPub, + newDenomHashes: newCoinDenoms.map(d => d.denomPubHash), + newDenoms: newCoinDenoms.map(d => d.denomPub), + norevealIndex: undefined, + planchetsForGammas: planchetsForGammas, + transferPrivs, + transferPubs, + valueOutput, + valueWithFee, + created: getTimestampNow(), + retryInfo: initRetryInfo(), + finishedTimestamp: undefined, + lastError: undefined, + }; + + return refreshSession; + } + + /** + * Hash a string including the zero terminator. + */ + hashString(str: string): string { + const ts = new TextEncoder(); + const b = ts.encode(str + "\0"); + return encodeCrock(hash(b)); + } + + /** + * Hash a denomination public key. + */ + hashDenomPub(denomPub: string): string { + return encodeCrock(hash(decodeCrock(denomPub))); + } + + signCoinLink( + oldCoinPriv: string, + newDenomHash: string, + oldCoinPub: string, + transferPub: string, + coinEv: string, + ): string { + const coinEvHash = hash(decodeCrock(coinEv)); + const coinLink = buildSigPS(SignaturePurpose.WALLET_COIN_LINK) + .put(decodeCrock(newDenomHash)) + .put(decodeCrock(oldCoinPub)) + .put(decodeCrock(transferPub)) + .put(coinEvHash) + .build(); + const coinPriv = decodeCrock(oldCoinPriv); + const sig = eddsaSign(coinLink, coinPriv); + return encodeCrock(sig); + } + + benchmark(repetitions: number): BenchmarkResult { + let time_hash = 0; + for (let i = 0; i < repetitions; i++) { + const start = timer.performanceNow(); + this.hashString("hello world"); + time_hash += timer.performanceNow() - start; + } + + let time_hash_big = 0; + for (let i = 0; i < repetitions; i++) { + const ba = randomBytes(4096); + const start = timer.performanceNow(); + hash(ba); + time_hash_big += timer.performanceNow() - start; + } + + let time_eddsa_create = 0; + for (let i = 0; i < repetitions; i++) { + const start = timer.performanceNow(); + const pair = createEddsaKeyPair(); + time_eddsa_create += timer.performanceNow() - start; + } + + let time_eddsa_sign = 0; + const p = randomBytes(4096); + + const pair = createEddsaKeyPair(); + + for (let i = 0; i < repetitions; i++) { + const start = timer.performanceNow(); + eddsaSign(p, pair.eddsaPriv); + time_eddsa_sign += timer.performanceNow() - start; + } + + const sig = eddsaSign(p, pair.eddsaPriv); + + let time_eddsa_verify = 0; + for (let i = 0; i < repetitions; i++) { + const start = timer.performanceNow(); + eddsaVerify(p, sig, pair.eddsaPub); + time_eddsa_verify += timer.performanceNow() - start; + } + + return { + repetitions, + time: { + hash_small: time_hash, + hash_big: time_hash_big, + eddsa_create: time_eddsa_create, + eddsa_sign: time_eddsa_sign, + eddsa_verify: time_eddsa_verify, + }, + }; + } +} diff --git a/src/crypto/workers/cryptoWorker.ts b/src/crypto/workers/cryptoWorker.ts new file mode 100644 index 000000000..d4449f4a2 --- /dev/null +++ b/src/crypto/workers/cryptoWorker.ts @@ -0,0 +1,8 @@ +export interface CryptoWorker { + postMessage(message: any): void; + + terminate(): void; + + onmessage: ((m: any) => void) | undefined; + onerror: ((m: any) => void) | undefined; +} \ No newline at end of file diff --git a/src/crypto/workers/nodeThreadWorker.ts b/src/crypto/workers/nodeThreadWorker.ts new file mode 100644 index 000000000..b42031c40 --- /dev/null +++ b/src/crypto/workers/nodeThreadWorker.ts @@ -0,0 +1,175 @@ +import { CryptoWorkerFactory } from "./cryptoApi"; + +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + 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. + + 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 + TALER; see the file COPYING. If not, see + */ + +// tslint:disable:no-var-requires + +import { CryptoWorker } from "./cryptoWorker"; + +import worker_threads = require("worker_threads"); +import os = require("os"); +import { CryptoImplementation } from "./cryptoImplementation"; + +const f = __filename; + +const workerCode = ` + const worker_threads = require('worker_threads'); + const parentPort = worker_threads.parentPort; + let tw; + try { + tw = require("${f}"); + } catch (e) { + console.log("could not load from ${f}"); + } + if (!tw) { + try { + tw = require("taler-wallet-android"); + } catch (e) { + console.log("could not load taler-wallet-android either"); + throw e; + } + } + parentPort.on("message", tw.handleWorkerMessage); + parentPort.on("error", tw.handleWorkerError); +`; + +/** + * This function is executed in the worker thread to handle + * a message. + */ +export function handleWorkerMessage(msg: any) { + const args = msg.args; + if (!Array.isArray(args)) { + console.error("args must be array"); + return; + } + const id = msg.id; + if (typeof id !== "number") { + console.error("RPC id must be number"); + return; + } + const operation = msg.operation; + if (typeof operation !== "string") { + console.error("RPC operation must be string"); + return; + } + + const handleRequest = async () => { + const impl = new CryptoImplementation(); + + if (!(operation in impl)) { + console.error(`crypto operation '${operation}' not found`); + return; + } + + try { + const result = (impl as any)[operation](...args); + const p = worker_threads.parentPort; + worker_threads.parentPort?.postMessage; + if (p) { + p.postMessage({ data: { result, id } }); + } else { + console.error("parent port not available (not running in thread?"); + } + } catch (e) { + console.error("error during operation", e); + return; + } + }; + + handleRequest().catch(e => { + console.error("error in node worker", e); + }); +} + +export function handleWorkerError(e: Error) { + console.log("got error from worker", e); +} + +export class NodeThreadCryptoWorkerFactory implements CryptoWorkerFactory { + startWorker(): CryptoWorker { + if (typeof require === "undefined") { + throw Error("cannot make worker, require(...) not defined"); + } + return new NodeThreadCryptoWorker(); + } + + getConcurrency(): number { + return Math.max(1, os.cpus().length - 1); + } +} + +/** + * Worker implementation that uses node subprocesses. + */ +class NodeThreadCryptoWorker implements CryptoWorker { + /** + * Function to be called when we receive a message from the worker thread. + */ + onmessage: undefined | ((m: any) => void); + + /** + * Function to be called when we receive an error from the worker thread. + */ + onerror: undefined | ((m: any) => void); + + private nodeWorker: worker_threads.Worker; + + constructor() { + this.nodeWorker = new worker_threads.Worker(workerCode, { eval: true }); + this.nodeWorker.on("error", (err: Error) => { + console.error("error in node worker:", err); + if (this.onerror) { + this.onerror(err); + } + }); + this.nodeWorker.on("message", (v: any) => { + if (this.onmessage) { + this.onmessage(v); + } + }); + this.nodeWorker.unref(); + } + + /** + * Add an event listener for either an "error" or "message" event. + */ + addEventListener(event: "message" | "error", fn: (x: any) => void): void { + switch (event) { + case "message": + this.onmessage = fn; + break; + case "error": + this.onerror = fn; + break; + } + } + + /** + * Send a message to the worker thread. + */ + postMessage(msg: any) { + this.nodeWorker.postMessage(msg); + } + + /** + * Forcibly terminate the worker thread. + */ + terminate() { + this.nodeWorker.terminate(); + } +} diff --git a/src/crypto/workers/synchronousWorker.ts b/src/crypto/workers/synchronousWorker.ts new file mode 100644 index 000000000..12eecde9a --- /dev/null +++ b/src/crypto/workers/synchronousWorker.ts @@ -0,0 +1,135 @@ +/* + 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 + */ + +import { CryptoImplementation } from "./cryptoImplementation"; + +import { CryptoWorkerFactory } from "./cryptoApi"; +import { CryptoWorker } from "./cryptoWorker"; + +/** + * The synchronous crypto worker produced by this factory doesn't run in the + * background, but actually blocks the caller until the operation is done. + */ +export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory { + startWorker(): CryptoWorker { + if (typeof require === "undefined") { + throw Error("cannot make worker, require(...) not defined"); + } + const workerCtor = require("./synchronousWorker").SynchronousCryptoWorker; + return new workerCtor(); + } + + getConcurrency(): number { + return 1; + } +} + + +/** + * Worker implementation that uses node subprocesses. + */ +export class SynchronousCryptoWorker { + + /** + * Function to be called when we receive a message from the worker thread. + */ + onmessage: undefined | ((m: any) => void); + + /** + * Function to be called when we receive an error from the worker thread. + */ + onerror: undefined | ((m: any) => void); + + constructor() { + this.onerror = undefined; + this.onmessage = undefined; + } + + /** + * Add an event listener for either an "error" or "message" event. + */ + addEventListener(event: "message" | "error", fn: (x: any) => void): void { + switch (event) { + case "message": + this.onmessage = fn; + break; + case "error": + this.onerror = fn; + break; + } + } + + private dispatchMessage(msg: any) { + if (this.onmessage) { + this.onmessage({ data: msg }); + } + } + + private async handleRequest(operation: string, id: number, args: string[]) { + const impl = new CryptoImplementation(); + + if (!(operation in impl)) { + console.error(`crypto operation '${operation}' not found`); + return; + } + + let result: any; + try { + result = (impl as any)[operation](...args); + } catch (e) { + console.log("error during operation", e); + return; + } + + try { + setImmediate(() => this.dispatchMessage({ result, id })); + } catch (e) { + console.log("got error during dispatch", e); + } + } + + /** + * Send a message to the worker thread. + */ + postMessage(msg: any) { + const args = msg.args; + if (!Array.isArray(args)) { + console.error("args must be array"); + return; + } + const id = msg.id; + if (typeof id !== "number") { + console.error("RPC id must be number"); + return; + } + const operation = msg.operation; + if (typeof operation !== "string") { + console.error("RPC operation must be string"); + return; + } + + this.handleRequest(operation, id, args).catch(e => { + console.error("Error while handling crypto request:", e); + }); + } + + /** + * Forcibly terminate the worker thread. + */ + terminate() { + // This is a no-op. + } +} -- cgit v1.2.3