aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/crypto/workers
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/crypto/workers')
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoApi.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts446
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts579
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoWorker.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts8
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts183
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts136
9 files changed, 1356 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.d.ts.map b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.d.ts.map
new file mode 100644
index 000000000..d8ab05823
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"cryptoApi.d.ts","sourceRoot":"","sources":["cryptoApi.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
new file mode 100644
index 000000000..a272d5724
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
@@ -0,0 +1,446 @@
+/*
+ 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,
+ DenominationSelectionInfo,
+} from "../../types/dbTypes";
+
+import { CryptoWorker } from "./cryptoWorker";
+
+import { RecoupRequest, CoinDepositPermission } from "../../types/talerTypes";
+
+import {
+ BenchmarkResult,
+ PlanchetCreationResult,
+ PlanchetCreationRequest,
+ DepositInfo,
+} from "../../types/walletTypes";
+
+import * as timer from "../../util/timer";
+import { Logger } from "../../util/logging";
+
+const logger = new Logger("cryptoApi.ts");
+
+/**
+ * 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;
+}
+
+/**
+ * Crypto API that interfaces manages a background crypto thread
+ * for the execution of expensive operations.
+ */
+export class CryptoApi {
+ private nextRpcId = 1;
+ private workers: WorkerState[];
+ private workQueues: WorkItem[][];
+
+ private workerFactory: CryptoWorkerFactory;
+
+ /**
+ * Number of busy workers.
+ */
+ private numBusy = 0;
+
+ /**
+ * Did we stop accepting new requests?
+ */
+ private stopped = false;
+
+ /**
+ * Terminate all worker threads.
+ */
+ terminateWorkers(): void {
+ for (const worker of this.workers) {
+ if (worker.w) {
+ logger.trace("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(): void {
+ this.terminateWorkers();
+ this.stopped = true;
+ }
+
+ /**
+ * Start a worker (if not started) and set as busy.
+ */
+ wake(ws: WorkerState, work: WorkItem): void {
+ if (this.stopped) {
+ logger.trace("cryptoApi is stopped");
+ return;
+ }
+ if (ws.currentWorkItem !== null) {
+ throw Error("assertion failed");
+ }
+ ws.currentWorkItem = work;
+ this.numBusy++;
+ let worker: CryptoWorker;
+ if (!ws.w) {
+ worker = this.workerFactory.startWorker();
+ worker.onmessage = (m: any) => this.handleWorkerMessage(ws, m);
+ worker.onerror = (e: any) => this.handleWorkerError(ws, e);
+ ws.w = worker;
+ } else {
+ worker = ws.w;
+ }
+
+ const msg: any = {
+ args: work.args,
+ id: work.rpcId,
+ operation: work.operation,
+ };
+ this.resetWorkerTimeout(ws);
+ work.startTime = timer.performanceNow();
+ setTimeout(() => worker.postMessage(msg), 0);
+ }
+
+ resetWorkerTimeout(ws: WorkerState): void {
+ if (ws.terminationTimerHandle !== null) {
+ ws.terminationTimerHandle.clear();
+ ws.terminationTimerHandle = null;
+ }
+ const destroy = (): void => {
+ // 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: any): void {
+ 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 {
+ if (ws.w) {
+ 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): void {
+ // 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 | undefined = q.shift();
+ if (!work) {
+ continue;
+ }
+ this.wake(ws, work);
+ return;
+ }
+ }
+ }
+
+ handleWorkerMessage(ws: WorkerState, msg: any): void {
+ 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;
+ }
+
+ 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);
+ }
+
+ hashEncoded(encodedBytes: string): Promise<string> {
+ return this.doRpc<string>("hashEncoded", 1, encodedBytes);
+ }
+
+ 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,
+ );
+ }
+
+ signDepositPermission(
+ depositInfo: DepositInfo,
+ ): Promise<CoinDepositPermission> {
+ return this.doRpc<CoinDepositPermission>(
+ "signDepositPermission",
+ 3,
+ depositInfo,
+ );
+ }
+
+ 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);
+ }
+
+ isValidWireAccount(
+ paytoUri: string,
+ sig: string,
+ masterPub: string,
+ ): Promise<boolean> {
+ return this.doRpc<boolean>(
+ "isValidWireAccount",
+ 4,
+ paytoUri,
+ sig,
+ masterPub,
+ );
+ }
+
+ createRecoupRequest(coin: CoinRecord): Promise<RecoupRequest> {
+ return this.doRpc<RecoupRequest>("createRecoupRequest", 1, coin);
+ }
+
+ createRefreshSession(
+ exchangeBaseUrl: string,
+ kappa: number,
+ meltCoin: CoinRecord,
+ newCoinDenoms: DenominationSelectionInfo,
+ 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/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.d.ts.map b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.d.ts.map
new file mode 100644
index 000000000..192c54d05
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"cryptoImplementation.d.ts","sourceRoot":"","sources":["cryptoImplementation.ts"],"names":[],"mappings":"AAgBA;;;;;;GAMG;AAEH;;GAEG;AAEH,OAAO,EACL,UAAU,EACV,kBAAkB,EAElB,oBAAoB,EACpB,WAAW,EACX,OAAO,EAEP,yBAAyB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,uBAAuB,EACvB,WAAW,EACZ,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,UAAU,EAAW,MAAM,oBAAoB,CAAC;AAgGzD,qBAAa,oBAAoB;IAC/B,MAAM,CAAC,aAAa,UAAS;IAE7B;;;OAGG;IACH,cAAc,CAAC,GAAG,EAAE,uBAAuB,GAAG,sBAAsB;IAoCpE;;OAEG;IACH,iBAAiB,CAAC,KAAK,EAAE,kBAAkB,GAAG,WAAW;IAmBzD;;OAEG;IACH,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa;IAoBpD;;OAEG;IACH,uBAAuB,CACrB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAClB,OAAO;IASV;;OAEG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAarE;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAmBnE,kBAAkB,CAChB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,OAAO;IAWV;;OAEG;IACH,kBAAkB,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAQnD;;OAEG;IACH,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM;IAS9D;;OAEG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO;IAIvD;;;OAGG;IACH,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,qBAAqB;IAyBtE;;OAEG;IACH,oBAAoB,CAClB,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,UAAU,EACpB,aAAa,EAAE,yBAAyB,EACxC,OAAO,EAAE,UAAU,GAClB,oBAAoB;IA2HvB;;OAEG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAK/B;;OAEG;IACH,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAIzC,YAAY,CACV,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,MAAM;IAaT,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe;CAsDhD"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
new file mode 100644
index 000000000..4195ebded
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
@@ -0,0 +1,579 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2020 Taler Systems SA
+
+ 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.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+
+import {
+ CoinRecord,
+ DenominationRecord,
+ RefreshPlanchetRecord,
+ RefreshSessionRecord,
+ TipPlanchet,
+ WireFee,
+ CoinSourceType,
+ DenominationSelectionInfo,
+} from "../../types/dbTypes";
+
+import { CoinDepositPermission, RecoupRequest } from "../../types/talerTypes";
+import {
+ BenchmarkResult,
+ PlanchetCreationResult,
+ PlanchetCreationRequest,
+ DepositInfo,
+} from "../../types/walletTypes";
+import { AmountJson, Amounts } from "../../util/amounts";
+import * as timer from "../../util/timer";
+import {
+ encodeCrock,
+ decodeCrock,
+ createEddsaKeyPair,
+ createBlindingKeySecret,
+ hash,
+ rsaBlind,
+ eddsaVerify,
+ eddsaSign,
+ rsaUnblind,
+ stringToBytes,
+ createHashContext,
+ createEcdheKeyPair,
+ keyExchangeEcdheEddsa,
+ setupRefreshPlanchet,
+ rsaVerify,
+} from "../talerCrypto";
+import { randomBytes } from "../primitives/nacl-fast";
+import { kdf } from "../primitives/kdf";
+import {
+ Timestamp,
+ getTimestampNow,
+ timestampTruncateToSecond,
+} from "../../util/time";
+
+enum SignaturePurpose {
+ WALLET_RESERVE_WITHDRAW = 1200,
+ WALLET_COIN_DEPOSIT = 1201,
+ MASTER_DENOMINATION_KEY_VALIDITY = 1025,
+ MASTER_WIRE_FEES = 1028,
+ MASTER_WIRE_DETAILS = 1030,
+ WALLET_COIN_MELT = 1202,
+ TEST = 4242,
+ MERCHANT_PAYMENT_OK = 1104,
+ WALLET_COIN_RECOUP = 1203,
+ WALLET_COIN_LINK = 1204,
+ EXCHANGE_CONFIRM_RECOUP = 1039,
+ EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
+}
+
+function amountToBuffer(amount: AmountJson): Uint8Array {
+ const buffer = new ArrayBuffer(8 + 4 + 12);
+ const dvbuf = new DataView(buffer);
+ const u8buf = new Uint8Array(buffer);
+ const curr = stringToBytes(amount.currency);
+ dvbuf.setBigUint64(0, BigInt(amount.value));
+ dvbuf.setUint32(8, amount.fraction);
+ u8buf.set(curr, 8 + 4);
+
+ return u8buf;
+}
+
+function timestampRoundedToBuffer(ts: Timestamp): Uint8Array {
+ const b = new ArrayBuffer(8);
+ const v = new DataView(b);
+ const tsRounded = timestampTruncateToSecond(ts);
+ const s = BigInt(tsRounded.t_ms) * BigInt(1000);
+ v.setBigUint64(0, s);
+ return new Uint8Array(b);
+}
+
+class SignaturePurposeBuilder {
+ private chunks: Uint8Array[] = [];
+
+ constructor(private purposeNum: number) {}
+
+ put(bytes: Uint8Array): SignaturePurposeBuilder {
+ this.chunks.push(Uint8Array.from(bytes));
+ return this;
+ }
+
+ build(): Uint8Array {
+ let payloadLen = 0;
+ for (const c of this.chunks) {
+ payloadLen += c.byteLength;
+ }
+ const buf = new ArrayBuffer(4 + 4 + payloadLen);
+ const u8buf = new Uint8Array(buf);
+ let p = 8;
+ for (const c of this.chunks) {
+ u8buf.set(c, p);
+ p += c.byteLength;
+ }
+ const dvbuf = new DataView(buf);
+ dvbuf.setUint32(0, payloadLen + 4 + 4);
+ dvbuf.setUint32(4, this.purposeNum);
+ return u8buf;
+ }
+}
+
+function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
+ return new SignaturePurposeBuilder(purposeNum);
+}
+
+export class CryptoImplementation {
+ static enableTracing = false;
+
+ /**
+ * 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.WALLET_RESERVE_WITHDRAW)
+ .put(reservePub)
+ .put(amountToBuffer(amountWithFee))
+ .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),
+ coinEvHash: encodeCrock(evHash),
+ };
+ 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 recoup a coin.
+ */
+ createRecoupRequest(coin: CoinRecord): RecoupRequest {
+ const p = buildSigPS(SignaturePurpose.WALLET_COIN_RECOUP)
+ .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: RecoupRequest = {
+ coin_blind_key_secret: coin.blindingKey,
+ coin_pub: coin.coinPub,
+ coin_sig: encodeCrock(coinSig),
+ denom_pub_hash: coin.denomPubHash,
+ denom_sig: coin.denomSig,
+ refreshed: coin.coinSource.type === CoinSourceType.Refresh,
+ };
+ 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(timestampRoundedToBuffer(wf.startStamp))
+ .put(timestampRoundedToBuffer(wf.endStamp))
+ .put(amountToBuffer(wf.wireFee))
+ .put(amountToBuffer(wf.closingFee))
+ .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(timestampRoundedToBuffer(denom.stampStart))
+ .put(timestampRoundedToBuffer(denom.stampExpireWithdraw))
+ .put(timestampRoundedToBuffer(denom.stampExpireDeposit))
+ .put(timestampRoundedToBuffer(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);
+ }
+
+ isValidWireAccount(
+ paytoUri: string,
+ sig: string,
+ masterPub: string,
+ ): boolean {
+ const h = kdf(
+ 64,
+ stringToBytes("exchange-wire-signature"),
+ stringToBytes(paytoUri + "\0"),
+ new Uint8Array(0),
+ );
+ const p = buildSigPS(SignaturePurpose.MASTER_WIRE_DETAILS).put(h).build();
+ return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub));
+ }
+
+ /**
+ * 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.
+ */
+ signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
+ const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
+ .put(decodeCrock(depositInfo.contractTermsHash))
+ .put(decodeCrock(depositInfo.wireInfoHash))
+ .put(decodeCrock(depositInfo.denomPubHash))
+ .put(timestampRoundedToBuffer(depositInfo.timestamp))
+ .put(timestampRoundedToBuffer(depositInfo.refundDeadline))
+ .put(amountToBuffer(depositInfo.spendAmount))
+ .put(amountToBuffer(depositInfo.feeDeposit))
+ .put(decodeCrock(depositInfo.merchantPub))
+ .put(decodeCrock(depositInfo.coinPub))
+ .build();
+ const coinSig = eddsaSign(d, decodeCrock(depositInfo.coinPriv));
+
+ const s: CoinDepositPermission = {
+ coin_pub: depositInfo.coinPub,
+ coin_sig: encodeCrock(coinSig),
+ contribution: Amounts.stringify(depositInfo.spendAmount),
+ h_denom: depositInfo.denomPubHash,
+ exchange_url: depositInfo.exchangeBaseUrl,
+ ub_sig: depositInfo.denomSig,
+ };
+ return s;
+ }
+
+ /**
+ * Create a new refresh session.
+ */
+ createRefreshSession(
+ exchangeBaseUrl: string,
+ kappa: number,
+ meltCoin: CoinRecord,
+ newCoinDenoms: DenominationSelectionInfo,
+ meltFee: AmountJson,
+ ): RefreshSessionRecord {
+ const currency = newCoinDenoms.selectedDenoms[0].denom.value.currency;
+ let valueWithFee = Amounts.getZero(currency);
+
+ for (const ncd of newCoinDenoms.selectedDenoms) {
+ const t = Amounts.add(ncd.denom.value, ncd.denom.feeWithdraw).amount;
+ valueWithFee = Amounts.add(
+ valueWithFee,
+ Amounts.mult(t, ncd.count).amount,
+ ).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 denomSel of newCoinDenoms.selectedDenoms) {
+ for (let i = 0; i < denomSel.count; i++) {
+ const r = decodeCrock(denomSel.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.selectedDenoms.length; j++) {
+ const denomSel = newCoinDenoms.selectedDenoms[j];
+ for (let k = 0; k < denomSel.count; k++) {
+ const coinNumber = planchets.length;
+ const transferPriv = decodeCrock(transferPrivs[i]);
+ const oldCoinPub = decodeCrock(meltCoin.coinPub);
+ const transferSecret = keyExchangeEcdheEddsa(
+ transferPriv,
+ oldCoinPub,
+ );
+ const fresh = setupRefreshPlanchet(transferSecret, coinNumber);
+ const coinPriv = fresh.coinPriv;
+ const coinPub = fresh.coinPub;
+ const blindingFactor = fresh.bks;
+ const pubHash = hash(coinPub);
+ const denomPub = decodeCrock(denomSel.denom.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(decodeCrock(meltCoin.denomPubHash))
+ .put(amountToBuffer(valueWithFee))
+ .put(amountToBuffer(meltFee))
+ .put(decodeCrock(meltCoin.coinPub))
+ .build();
+
+ const confirmSig = eddsaSign(confirmData, decodeCrock(meltCoin.coinPriv));
+
+ let valueOutput = Amounts.getZero(currency);
+ for (const denomSel of newCoinDenoms.selectedDenoms) {
+ const denom = denomSel.denom;
+ for (let i = 0; i < denomSel.count; i++) {
+ valueOutput = Amounts.add(valueOutput, denom.value).amount;
+ }
+ }
+
+ const newDenoms: string[] = [];
+ const newDenomHashes: string[] = [];
+
+ for (const denomSel of newCoinDenoms.selectedDenoms) {
+ const denom = denomSel.denom;
+ for (let i = 0; i < denomSel.count; i++) {
+ newDenoms.push(denom.denomPub);
+ newDenomHashes.push(denom.denomPubHash);
+ }
+ }
+
+ const refreshSession: RefreshSessionRecord = {
+ confirmSig: encodeCrock(confirmSig),
+ exchangeBaseUrl,
+ hash: encodeCrock(sessionHash),
+ meltCoinPub: meltCoin.coinPub,
+ newDenomHashes,
+ newDenoms,
+ norevealIndex: undefined,
+ planchetsForGammas: planchetsForGammas,
+ transferPrivs,
+ transferPubs,
+ amountRefreshOutput: valueOutput,
+ amountRefreshInput: valueWithFee,
+ timestampCreated: getTimestampNow(),
+ finishedTimestamp: undefined,
+ lastError: undefined,
+ };
+
+ return refreshSession;
+ }
+
+ /**
+ * Hash a string including the zero terminator.
+ */
+ hashString(str: string): string {
+ const b = stringToBytes(str + "\0");
+ return encodeCrock(hash(b));
+ }
+
+ /**
+ * Hash a crockford encoded value.
+ */
+ hashEncoded(encodedBytes: string): string {
+ return encodeCrock(hash(decodeCrock(encodedBytes)));
+ }
+
+ 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();
+ 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/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.d.ts.map b/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.d.ts.map
new file mode 100644
index 000000000..cfdedef09
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"cryptoWorker.d.ts","sourceRoot":"","sources":["cryptoWorker.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI,CAAC;IAEhC,SAAS,IAAI,IAAI,CAAC;IAElB,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IAC1C,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CACzC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts
new file mode 100644
index 000000000..9f3ee6f50
--- /dev/null
+++ b/packages/taler-wallet-core/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;
+}
diff --git a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.d.ts.map b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.d.ts.map
new file mode 100644
index 000000000..d89774cd5
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"nodeThreadWorker.d.ts","sourceRoot":"","sources":["nodeThreadWorker.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAiC9C;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CA6ClD;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,CAEhD;AAED,qBAAa,6BAA8B,YAAW,mBAAmB;IACvE,WAAW,IAAI,YAAY;IAO3B,cAAc,IAAI,MAAM;CAGzB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts
new file mode 100644
index 000000000..6c9dfc569
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts
@@ -0,0 +1,183 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports
+ */
+import { CryptoWorkerFactory } from "./cryptoApi";
+import { CryptoWorker } from "./cryptoWorker";
+import os from "os";
+import { CryptoImplementation } from "./cryptoImplementation";
+
+const f = __filename;
+
+const workerCode = `
+ // Try loading the glue library for Android
+ try {
+ require("akono");
+ } catch (e) {
+ // Probably we're not on Android ...
+ }
+ 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): void {
+ 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 (): Promise<void> => {
+ const impl = new CryptoImplementation();
+
+ if (!(operation in impl)) {
+ console.error(`crypto operation '${operation}' not found`);
+ return;
+ }
+
+ try {
+ const result = (impl as any)[operation](...args);
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const worker_threads = require("worker_threads");
+ 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): void {
+ 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: import("worker_threads").Worker;
+
+ constructor() {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const worker_threads = require("worker_threads");
+ 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): void {
+ this.nodeWorker.postMessage(msg);
+ }
+
+ /**
+ * Forcibly terminate the worker thread.
+ */
+ terminate(): void {
+ this.nodeWorker.terminate();
+ }
+}
diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts
new file mode 100644
index 000000000..5327670bd
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts
@@ -0,0 +1,136 @@
+/*
+ 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");
+ }
+ return new SynchronousCryptoWorker();
+ }
+
+ 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): void {
+ if (this.onmessage) {
+ this.onmessage({ data: msg });
+ }
+ }
+
+ private async handleRequest(
+ operation: string,
+ id: number,
+ args: string[],
+ ): Promise<void> {
+ 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 {
+ setTimeout(() => this.dispatchMessage({ result, id }), 0);
+ } catch (e) {
+ console.log("got error during dispatch", e);
+ }
+ }
+
+ /**
+ * Send a message to the worker thread.
+ */
+ postMessage(msg: any): void {
+ 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(): void {
+ // This is a no-op.
+ }
+}