summaryrefslogtreecommitdiff
path: root/src/crypto/workers
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-12-05 19:38:19 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-12-05 19:38:19 +0100
commitf67d7f54f9d0fed97446898942e3dfee67ee2985 (patch)
tree2b81738025e8f61250ede10908cbf81071e16975 /src/crypto/workers
parent829acdd3d98f1014747f15ecb619b6fbaa06b640 (diff)
downloadwallet-core-f67d7f54f9d0fed97446898942e3dfee67ee2985.tar.gz
wallet-core-f67d7f54f9d0fed97446898942e3dfee67ee2985.tar.bz2
wallet-core-f67d7f54f9d0fed97446898942e3dfee67ee2985.zip
threads, retries and notifications WIP
Diffstat (limited to 'src/crypto/workers')
-rw-r--r--src/crypto/workers/browserWorkerEntry.ts70
-rw-r--r--src/crypto/workers/cryptoApi.ts455
-rw-r--r--src/crypto/workers/cryptoImplementation.ts608
-rw-r--r--src/crypto/workers/cryptoWorker.ts8
-rw-r--r--src/crypto/workers/nodeThreadWorker.ts175
-rw-r--r--src/crypto/workers/synchronousWorker.ts135
6 files changed, 1451 insertions, 0 deletions
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 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * 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<WorkerState>(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<T>(
+ operation: string,
+ priority: number,
+ ...args: any[]
+ ): Promise<T> {
+ const p: Promise<T> = new Promise<T>((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<PlanchetCreationResult> {
+ return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, req);
+ }
+
+ createTipPlanchet(denom: DenominationRecord): Promise<TipPlanchet> {
+ return this.doRpc<TipPlanchet>("createTipPlanchet", 1, denom);
+ }
+
+ hashString(str: string): Promise<string> {
+ return this.doRpc<string>("hashString", 1, str);
+ }
+
+ hashDenomPub(denomPub: string): Promise<string> {
+ return this.doRpc<string>("hashDenomPub", 1, denomPub);
+ }
+
+ isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> {
+ return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub);
+ }
+
+ isValidWireFee(
+ type: string,
+ wf: WireFee,
+ masterPub: string,
+ ): Promise<boolean> {
+ return this.doRpc<boolean>("isValidWireFee", 2, type, wf, masterPub);
+ }
+
+ isValidPaymentSignature(
+ sig: string,
+ contractHash: string,
+ merchantPub: string,
+ ): Promise<boolean> {
+ return this.doRpc<boolean>(
+ "isValidPaymentSignature",
+ 1,
+ sig,
+ contractHash,
+ merchantPub,
+ );
+ }
+
+ signDeposit(
+ contractTerms: ContractTerms,
+ cds: CoinWithDenom[],
+ totalAmount: AmountJson,
+ ): Promise<PayCoinInfo> {
+ return this.doRpc<PayCoinInfo>(
+ "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<string> {
+ return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk);
+ }
+
+ rsaVerify(hm: string, sig: string, pk: string): Promise<boolean> {
+ return this.doRpc<boolean>("rsaVerify", 4, hm, sig, pk);
+ }
+
+ createPaybackRequest(coin: CoinRecord): Promise<PaybackRequest> {
+ return this.doRpc<PaybackRequest>("createPaybackRequest", 1, coin);
+ }
+
+ createRefreshSession(
+ exchangeBaseUrl: string,
+ kappa: number,
+ meltCoin: CoinRecord,
+ newCoinDenoms: DenominationRecord[],
+ meltFee: AmountJson,
+ ): Promise<RefreshSessionRecord> {
+ return this.doRpc<RefreshSessionRecord>(
+ "createRefreshSession",
+ 4,
+ exchangeBaseUrl,
+ kappa,
+ meltCoin,
+ newCoinDenoms,
+ meltFee,
+ );
+ }
+
+ signCoinLink(
+ oldCoinPriv: string,
+ newDenomHash: string,
+ oldCoinPub: string,
+ transferPub: string,
+ coinEv: string,
+ ): Promise<string> {
+ return this.doRpc<string>(
+ "signCoinLink",
+ 4,
+ oldCoinPriv,
+ newDenomHash,
+ oldCoinPub,
+ transferPub,
+ coinEv,
+ );
+ }
+
+ benchmark(repetitions: number): Promise<BenchmarkResult> {
+ return this.doRpc<BenchmarkResult>("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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+// 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 <http://www.gnu.org/licenses/>
+ */
+
+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.
+ }
+}