wallet-core: improve crypto worker code duplication

Also add new testCrypto call for later testing
This commit is contained in:
Florian Dold 2022-10-05 12:52:49 +02:00
parent 4d232fd565
commit dd14e67c70
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
12 changed files with 182 additions and 180 deletions

View File

@ -152,6 +152,14 @@ export enum TalerErrorCode {
GENERIC_PARAMETER_MALFORMED = 26, GENERIC_PARAMETER_MALFORMED = 26,
/**
* The reserve public key given as part of a /reserves/ endpoint was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_RESERVE_PUB_MALFORMED = 27,
/** /**
* The currencies involved in the operation do not match. * The currencies involved in the operation do not match.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@ -456,14 +464,6 @@ export enum TalerErrorCode {
EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1018, EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1018,
/**
* The reserve public key was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_GENERIC_RESERVE_PUB_MALFORMED = 1019,
/** /**
* The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong. * The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@ -1288,6 +1288,38 @@ export enum TalerErrorCode {
EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID = 1778, EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID = 1778,
/**
* The signature by the reserve affirming the open operation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE = 1785,
/**
* The signature by the reserve affirming the close operation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE = 1786,
/**
* The signature by the reserve affirming the attestion request is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_RESERVES_ATTEST_BAD_SIGNATURE = 1787,
/**
* The exchange does not know an origin account to which the remaining reserve balance could be wired to, and the wallet failed to provide one.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT = 1788,
/** /**
* The auditor that was supposed to be disabled is unknown to this exchange. * The auditor that was supposed to be disabled is unknown to this exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@ -1768,14 +1800,6 @@ export enum TalerErrorCode {
MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE = 2001, MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE = 2001,
/**
* The reserve key of given to a /reserves/ handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_GENERIC_RESERVE_PUB_MALFORMED = 2002,
/** /**
* The proposal is not known to the backend. * The proposal is not known to the backend.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@ -3064,6 +3088,14 @@ export enum TalerErrorCode {
WALLET_CRYPTO_WORKER_ERROR = 7023, WALLET_CRYPTO_WORKER_ERROR = 7023,
/**
* The crypto worker received a bad request.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
WALLET_CRYPTO_WORKER_BAD_REQUEST = 7024,
/** /**
* We encountered a timeout with our payment backend. * We encountered a timeout with our payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504). * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).

View File

@ -29,7 +29,7 @@ import { timer, performanceNow, TimerHandle } from "../../util/timer.js";
import { nullCrypto, TalerCryptoInterface } from "../cryptoImplementation.js"; import { nullCrypto, TalerCryptoInterface } from "../cryptoImplementation.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js"; import { CryptoWorker } from "./cryptoWorkerInterface.js";
const logger = new Logger("cryptoApi.ts"); const logger = new Logger("cryptoDispatcher.ts");
/** /**
* State of a crypto worker. * State of a crypto worker.
@ -238,7 +238,7 @@ export class CryptoDispatcher {
} }
handleWorkerMessage(ws: WorkerInfo, msg: any): void { handleWorkerMessage(ws: WorkerInfo, msg: any): void {
const id = msg.data.id; const id = msg.id;
if (typeof id !== "number") { if (typeof id !== "number") {
logger.error("rpc id must be number"); logger.error("rpc id must be number");
return; return;
@ -256,12 +256,12 @@ export class CryptoDispatcher {
if (currentWorkItem.state === WorkItemState.Running) { if (currentWorkItem.state === WorkItemState.Running) {
this.numBusy--; this.numBusy--;
currentWorkItem.state = WorkItemState.Finished; currentWorkItem.state = WorkItemState.Finished;
if (msg.data.type === "success") { if (msg.type === "success") {
currentWorkItem.resolve(msg.data.result); currentWorkItem.resolve(msg.result);
} else if (msg.data.type === "error") { } else if (msg.type === "error") {
currentWorkItem.reject( currentWorkItem.reject(
TalerError.fromDetail(TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR, { TalerError.fromDetail(TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR, {
innerError: msg.data.error, innerError: msg.error,
}), }),
); );
} else { } else {

View File

@ -1,8 +1,65 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
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/>
*/
/**
* Imports.
*/
import { TalerErrorDetail } from "@gnu-taler/taler-util";
/**
* Common interface for all crypto workers.
*/
export interface CryptoWorker { export interface CryptoWorker {
postMessage(message: any): void; postMessage(message: any): void;
terminate(): void; terminate(): void;
onmessage: ((m: any) => void) | undefined; onmessage: ((m: any) => void) | undefined;
onerror: ((m: any) => void) | undefined; onerror: ((m: any) => void) | undefined;
} }
/**
* Type of requests sent to the crypto worker.
*/
export type CryptoWorkerRequestMessage = {
/**
* Operation ID to correlate request with the response.
*/
id: number;
/**
* Operation to execute.
*/
operation: string;
/**
* Operation-specific request payload.
*/
req: any;
};
/**
* Type of messages sent back by the crypto worker.
*/
export type CryptoWorkerResponseMessage =
| {
type: "success";
id: number;
result: any;
}
| {
type: "error";
id?: number;
error: TalerErrorDetail;
};

View File

@ -17,12 +17,12 @@
/** /**
* Imports * Imports
*/ */
import { Logger } from "@gnu-taler/taler-util";
import os from "os";
import { nativeCryptoR } from "../cryptoImplementation.js";
import { CryptoWorkerFactory } from "./cryptoDispatcher.js"; import { CryptoWorkerFactory } from "./cryptoDispatcher.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js"; import { CryptoWorker } from "./cryptoWorkerInterface.js";
import os from "os"; import { processRequestWithImpl } from "./worker-common.js";
import { Logger } from "@gnu-taler/taler-util";
import { nativeCryptoR } from "../cryptoImplementation.js";
import { getErrorDetailFromException } from "../../errors.js";
const logger = new Logger("nodeThreadWorker.ts"); const logger = new Logger("nodeThreadWorker.ts");
@ -71,44 +71,7 @@ const workerCode = `
*/ */
export function handleWorkerMessage(msg: any): void { export function handleWorkerMessage(msg: any): void {
const handleRequest = async (): Promise<void> => { const handleRequest = async (): Promise<void> => {
const req = msg.req; const responseMsg = await processRequestWithImpl(msg, nativeCryptoR);
if (typeof req !== "object") {
logger.error("request must be an object");
return;
}
const id = msg.id;
if (typeof id !== "number") {
logger.error("RPC id must be number");
return;
}
const operation = msg.operation;
if (typeof operation !== "string") {
logger.error("RPC operation must be string");
return;
}
const impl = nativeCryptoR;
if (!(operation in impl)) {
logger.error(`crypto operation '${operation}' not found`);
return;
}
let responseMsg: any;
try {
const result = await (impl as any)[operation](impl, req);
responseMsg = { data: { type: "success", result, id } };
} catch (e: any) {
logger.error(`error during operation: ${e.stack ?? e.toString()}`);
responseMsg = {
data: {
type: "error",
error: getErrorDetailFromException(e),
id,
},
};
}
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const _r = "require"; const _r = "require";
@ -122,16 +85,12 @@ export function handleWorkerMessage(msg: any): void {
logger.error("parent port not available (not running in thread?"); logger.error("parent port not available (not running in thread?");
} }
} catch (e: any) { } catch (e: any) {
logger.error( logger.error(`error in node worker: ${e.stack ?? e.toString()}`);
`error sending back operation result: ${e.stack ?? e.toString()}`,
);
return; return;
} }
}; };
handleRequest().catch((e) => { handleRequest();
logger.error("error in node worker", e);
});
} }
export function handleWorkerError(e: Error): void { export function handleWorkerError(e: Error): void {

View File

@ -24,6 +24,9 @@ import { OpenedPromise, openPromise } from "../../util/promiseUtils.js";
const logger = new Logger("synchronousWorkerFactory.ts"); const logger = new Logger("synchronousWorkerFactory.ts");
/**
* Client for the crypto helper process (taler-crypto-worker from exchange.git).
*/
export class CryptoRpcClient { export class CryptoRpcClient {
proc: child_process.ChildProcessByStdio< proc: child_process.ChildProcessByStdio<
internal.Writable, internal.Writable,

View File

@ -14,20 +14,24 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Logger } from "@gnu-taler/taler-util"; import { j2s, Logger } from "@gnu-taler/taler-util";
import { getErrorDetailFromException } from "../../errors.js";
import { import {
nativeCryptoR, nativeCryptoR,
TalerCryptoInterfaceR, TalerCryptoInterfaceR,
} from "../cryptoImplementation.js"; } from "../cryptoImplementation.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js";
import { CryptoRpcClient } from "./rpcClient.js"; import { CryptoRpcClient } from "./rpcClient.js";
import { processRequestWithImpl } from "./worker-common.js";
const logger = new Logger("synchronousWorker.ts"); const logger = new Logger("synchronousWorker.ts");
/** /**
* Worker implementation that uses node subprocesses. * Worker implementation that uses node subprocesses.
*
* The node cryto worker can also use IPC to offload cryptographic
* operations to a helper process (ususally written in C / part of taler-exchange).
*/ */
export class SynchronousCryptoWorker { export class SynchronousCryptoWorker implements CryptoWorker {
/** /**
* Function to be called when we receive a message from the worker thread. * Function to be called when we receive a message from the worker thread.
*/ */
@ -144,61 +148,19 @@ export class SynchronousCryptoWorker {
} }
} }
private async handleRequest(
operation: string,
id: number,
req: unknown,
): Promise<void> {
const impl = this.cryptoImplR;
if (!(operation in impl)) {
logger.error(`crypto operation '${operation}' not found`);
return;
}
let responseMsg: any;
try {
const result = await (impl as any)[operation](impl, req);
responseMsg = { data: { type: "success", result, id } };
} catch (e: any) {
logger.error(`error during operation: ${e.stack ?? e.toString()}`);
responseMsg = {
data: {
type: "error",
id,
error: getErrorDetailFromException(e),
},
};
}
try {
setTimeout(() => this.dispatchMessage(responseMsg), 0);
} catch (e) {
logger.error("got error during dispatch", e);
}
}
/** /**
* Send a message to the worker thread. * Send a message to the worker thread.
*/ */
postMessage(msg: any): void { postMessage(msg: any): void {
const req = msg.req; const handleRequest = async () => {
if (typeof req !== "object") { const responseMsg = await processRequestWithImpl(msg, this.cryptoImplR);
logger.error("request must be an object"); try {
return; setTimeout(() => this.dispatchMessage(responseMsg), 0);
} } catch (e) {
const id = msg.id; logger.error("got error during dispatch", e);
if (typeof id !== "number") { }
logger.error("RPC id must be number"); };
return; handleRequest().catch((e) => {
}
const operation = msg.operation;
if (typeof operation !== "string") {
logger.error("RPC operation must be string");
return;
}
this.handleRequest(operation, id, req).catch((e) => {
logger.error("Error while handling crypto request:", e); logger.error("Error while handling crypto request:", e);
}); });
} }

View File

@ -14,19 +14,26 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
/**
* Imports.
*/
import { Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { getErrorDetailFromException } from "../../errors.js";
import { import {
nativeCryptoR, nativeCryptoR,
TalerCryptoInterfaceR, TalerCryptoInterfaceR,
} from "../cryptoImplementation.js"; } from "../cryptoImplementation.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js";
import {
processRequestWithImpl,
} from "./worker-common.js";
const logger = new Logger("synchronousWorker.ts"); const logger = new Logger("synchronousWorker.ts");
/** /**
* Worker implementation that uses node subprocesses. * Worker implementation that synchronously executes cryptographic
* operations.
*/ */
export class SynchronousCryptoWorker { export class SynchronousCryptoWorker implements CryptoWorker {
/** /**
* Function to be called when we receive a message from the worker thread. * Function to be called when we receive a message from the worker thread.
*/ */
@ -65,61 +72,22 @@ export class SynchronousCryptoWorker {
} }
} }
private async handleRequest(
operation: string,
id: number,
req: unknown,
): Promise<void> {
const impl = this.cryptoImplR;
if (!(operation in impl)) {
logger.error(`crypto operation '${operation}' not found`);
return;
}
let responseMsg: any;
try {
const result = await (impl as any)[operation](impl, req);
responseMsg = { data: { type: "success", result, id } };
} catch (e: any) {
logger.error(`error during operation: ${e.stack ?? e.toString()}`);
responseMsg = {
data: {
type: "error",
id,
error: getErrorDetailFromException(e),
},
};
}
try {
setTimeout(() => this.dispatchMessage(responseMsg), 0);
} catch (e) {
logger.error("got error during dispatch", e);
}
}
/** /**
* Send a message to the worker thread. * Send a message to the worker thread.
*/ */
postMessage(msg: any): void { postMessage(msg: any): void {
const req = msg.req; const handleRequest = async () => {
if (typeof req !== "object") { const responseMsg = await processRequestWithImpl(
logger.error("request must be an object"); msg,
return; this.cryptoImplR,
} );
const id = msg.id; try {
if (typeof id !== "number") { setTimeout(() => this.dispatchMessage(responseMsg), 0);
logger.error("RPC id must be number"); } catch (e) {
return; logger.error("got error during dispatch", e);
} }
const operation = msg.operation; };
if (typeof operation !== "string") { handleRequest().catch((e) => {
logger.error("RPC operation must be string");
return;
}
this.handleRequest(operation, id, req).catch((e) => {
logger.error("Error while handling crypto request:", e); logger.error("Error while handling crypto request:", e);
}); });
} }

View File

@ -76,6 +76,9 @@ export interface DetailsMap {
[TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR]: { [TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR]: {
innerError: TalerErrorDetail; innerError: TalerErrorDetail;
}; };
[TalerErrorCode.WALLET_CRYPTO_WORKER_BAD_REQUEST]: {
detail: string;
};
} }
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never; type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never;

View File

@ -92,6 +92,7 @@ export enum WalletApiOperation {
WithdrawTestBalance = "withdrawTestBalance", WithdrawTestBalance = "withdrawTestBalance",
PreparePayForUri = "preparePayForUri", PreparePayForUri = "preparePayForUri",
RunIntegrationTest = "runIntegrationTest", RunIntegrationTest = "runIntegrationTest",
CryptoTest = "cryptoTest",
TestPay = "testPay", TestPay = "testPay",
AddExchange = "addExchange", AddExchange = "addExchange",
GetTransactions = "getTransactions", GetTransactions = "getTransactions",
@ -524,6 +525,15 @@ export type RunIntegrationTestOp = {
response: {}; response: {};
}; };
/**
* Test crypto worker.
*/
export type CryptoTestOp = {
op: WalletApiOperation.CryptoTest;
request: {};
response: any;
};
/** /**
* Make withdrawal on a test deployment of the exchange * Make withdrawal on a test deployment of the exchange
* and merchant. * and merchant.
@ -639,6 +649,7 @@ export type WalletOperations = {
[WalletApiOperation.AddBackupProvider]: AddBackupProviderOp; [WalletApiOperation.AddBackupProvider]: AddBackupProviderOp;
[WalletApiOperation.GetBackupInfo]: GetBackupInfoOp; [WalletApiOperation.GetBackupInfo]: GetBackupInfoOp;
[WalletApiOperation.RunIntegrationTest]: RunIntegrationTestOp; [WalletApiOperation.RunIntegrationTest]: RunIntegrationTestOp;
[WalletApiOperation.CryptoTest]: CryptoTestOp;
[WalletApiOperation.WithdrawTestBalance]: WithdrawTestBalanceOp; [WalletApiOperation.WithdrawTestBalance]: WithdrawTestBalanceOp;
[WalletApiOperation.TestPay]: TestPayOp; [WalletApiOperation.TestPay]: TestPayOp;
[WalletApiOperation.ExportDb]: ExportDbOp; [WalletApiOperation.ExportDb]: ExportDbOp;

View File

@ -1445,6 +1445,9 @@ async function dispatchRequestInternal(
logger.info(`started fakebank withdrawal: ${j2s(fbResp)}`); logger.info(`started fakebank withdrawal: ${j2s(fbResp)}`);
return {}; return {};
} }
case "testCrypto": {
return await ws.cryptoApi.hashString({ str: "hello world" });
}
case "clearDb": case "clearDb":
await clearDatabase(ws.db.idbHandle()); await clearDatabase(ws.db.idbHandle());
return {}; return {};

View File

@ -28,6 +28,8 @@ export class BrowserCryptoWorkerFactory implements CryptoWorkerFactory {
startWorker(): CryptoWorker { startWorker(): CryptoWorker {
const workerCtor = Worker; const workerCtor = Worker;
const workerPath = "/dist/browserWorkerEntry.js"; const workerPath = "/dist/browserWorkerEntry.js";
// FIXME: This is not really the same interface as the crypto worker!
// We need to wrap it.
return new workerCtor(workerPath) as CryptoWorker; return new workerCtor(workerPath) as CryptoWorker;
} }

View File

@ -217,6 +217,8 @@ async function reinitWallet(): Promise<void> {
timer = new SetTimeoutTimerAPI(); timer = new SetTimeoutTimerAPI();
} else { } else {
httpLib = new BrowserHttpLib(); httpLib = new BrowserHttpLib();
// We could (should?) use the BrowserCryptoWorkerFactory here,
// but right now we don't, to have less platform differences.
cryptoWorker = new SynchronousCryptoWorkerFactory(); cryptoWorker = new SynchronousCryptoWorkerFactory();
timer = new SetTimeoutTimerAPI(); timer = new SetTimeoutTimerAPI();
} }