/* 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 */ /** * Entry-point for the wallet under qtart, the QuickJS-based GNU Taler runtime. */ /** * Imports. */ import { discoverPolicies, getBackupStartState, getRecoveryStartState, mergeDiscoveryAggregate, reduceAction, } from "@gnu-taler/anastasis-core"; import { userIdentifierDerive } from "@gnu-taler/anastasis-core/lib/crypto.js"; import { AmountString, CoreApiMessageEnvelope, CoreApiResponse, CoreApiResponseSuccess, Logger, WalletNotification, enableNativeLogging, getErrorDetailFromException, j2s, openPromise, performanceNow, setGlobalLogLevelFromString, } from "@gnu-taler/taler-util"; import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; import { qjsOs } from "@gnu-taler/taler-util/qtart"; import { DefaultNodeWalletArgs, Wallet, WalletApiOperation, createNativeWalletHost2, } from "@gnu-taler/taler-wallet-core"; setGlobalLogLevelFromString("trace"); const logger = new Logger("taler-wallet-embedded/index.ts"); /** * Sends JSON to the host application, i.e. the process that * runs the JavaScript interpreter (quickjs / qtart) to run * the embedded wallet. */ function sendNativeMessage(ev: CoreApiMessageEnvelope): void { const m = JSON.stringify(ev); qjsOs.postMessageToHost(m); } class NativeWalletMessageHandler { walletArgs: DefaultNodeWalletArgs | undefined; maybeWallet: Wallet | undefined; wp = openPromise(); httpLib = createPlatformHttpLib(); /** * Handle a request from the native wallet. */ async handleMessage( operation: string, id: string, args: any, ): Promise { const wrapSuccessResponse = (result: unknown): CoreApiResponseSuccess => { return { type: "response", id, operation, result, }; }; let initResponse: any = {}; const reinit = async () => { logger.info("in reinit"); const wR = await createNativeWalletHost2(this.walletArgs); const w = wR.wallet; this.maybeWallet = w; const resp = await w.handleCoreApiRequest( "initWallet", "native-init", {}, ); initResponse = resp.type == "response" ? resp.result : resp.error; w.runTaskLoop().catch((e) => { logger.error( `Error during wallet retry loop: ${e.stack ?? e.toString()}`, ); }); this.wp.resolve(w); }; switch (operation) { case "init": { this.walletArgs = { notifyHandler: async (notification: WalletNotification) => { sendNativeMessage({ type: "notification", payload: notification }); }, persistentStoragePath: args.persistentStoragePath, httpLib: this.httpLib, cryptoWorkerType: args.cryptoWorkerType, ...args, }; const logLevel = args.logLevel; if (logLevel) { setGlobalLogLevelFromString(logLevel); } const nativeLogging = args.useNativeLogging ?? false; if (nativeLogging) { enableNativeLogging(); } await reinit(); return wrapSuccessResponse({ ...initResponse, }); } case "startTunnel": { // this.httpLib.useNfcTunnel = true; throw Error("not implemented"); } case "stopTunnel": { // this.httpLib.useNfcTunnel = false; throw Error("not implemented"); } case "tunnelResponse": { // httpLib.handleTunnelResponse(msg.args); throw Error("not implemented"); } case "reset": { throw Error( "reset not supported anymore, please use the clearDb wallet-core request", ); } default: { const wallet = await this.wp.promise; return await wallet.handleCoreApiRequest(operation, id, args); } } } } /** * Handle an Anastasis request from the native app. */ async function handleAnastasisRequest( operation: string, id: string, args: any, ): Promise { const wrapSuccessResponse = (result: unknown): CoreApiResponseSuccess => { return { type: "response", id, operation, result, }; }; let req = args ?? {}; switch (operation) { case "anastasisReduce": // TODO: do some input validation here let reduceRes = await reduceAction(req.state, req.action, req.args ?? {}); // For now, this will return "success" even if the wrapped Anastasis // response is a ReducerStateError. return wrapSuccessResponse(reduceRes); case "anastasisStartBackup": return wrapSuccessResponse(await getBackupStartState()); case "anastasisStartRecovery": return wrapSuccessResponse(await getRecoveryStartState()); case "anastasisDiscoverPolicies": let discoverRes = await discoverPolicies(req.state, req.cursor); let aggregatedPolicies = mergeDiscoveryAggregate( discoverRes.policies ?? [], req.state.discoveryState?.aggregatedPolicies ?? [], ); return wrapSuccessResponse({ ...req.state, discoveryState: { state: "finished", aggregatedPolicies, cursor: discoverRes.cursor, }, }); default: throw Error("unsupported anastasis operation"); } } export function installNativeWalletListener(): void { setGlobalLogLevelFromString("trace"); const handler = new NativeWalletMessageHandler(); const onMessage = async (msgStr: any): Promise => { if (typeof msgStr !== "string") { logger.error("expected string as message"); return; } const msg = JSON.parse(msgStr); const operation = msg.operation; if (typeof operation !== "string") { logger.error( "message to native wallet helper must contain operation of type string", ); return; } const id = msg.id; logger.info(`native listener: got request for ${operation} (${id})`); const startTimeNs = performanceNow(); let respMsg: CoreApiResponse; try { if (msg.operation.startsWith("anastasis")) { respMsg = await handleAnastasisRequest(operation, id, msg.args ?? {}); } else if (msg.operation === "testing-dangerously-eval") { // Eval code, used only for testing. No client may rely on this. logger.info(`evaluating ${msg.args.jscode}`); const f = new Function(msg.args.jscode); f(); respMsg = { type: "response", result: {}, operation: "testing-dangerously-eval", id: msg.id, }; } { respMsg = await handler.handleMessage(operation, id, msg.args ?? {}); } } catch (e) { respMsg = { type: "error", id, operation, error: getErrorDetailFromException(e), }; } const endTimeNs = performanceNow(); const requestDurationMs = Math.round( Number((endTimeNs - startTimeNs) / 1000n / 1000n), ); logger.info( `native listener: sending back ${respMsg.type} message for operation ${operation} (${id}) after ${requestDurationMs} ms`, ); sendNativeMessage(respMsg); }; qjsOs.setMessageFromHostHandler((m) => onMessage(m)); logger.info("native wallet listener installed"); } // @ts-ignore globalThis.installNativeWalletListener = installNativeWalletListener; export async function testWithGv() { const w = await createNativeWalletHost2({}); await w.wallet.client.call(WalletApiOperation.InitWallet, { config: { features: { allowHttp: true, }, }, }); await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, { amountToSpend: "KUDOS:1" as AmountString, amountToWithdraw: "KUDOS:3" as AmountString, corebankApiBaseUrl: "https://bank.demo.taler.net/", exchangeBaseUrl: "https://exchange.demo.taler.net/", merchantBaseUrl: "https://backend.demo.taler.net/", merchantAuthToken: "secret-token:sandbox", }); await w.wallet.runTaskLoop({ stopWhenDone: true, }); } export async function testWithFdold() { const w = await createNativeWalletHost2({}); await w.wallet.client.call(WalletApiOperation.InitWallet, { config: { features: { allowHttp: true, }, }, }); await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, { amountToSpend: "TESTKUDOS:1" as AmountString, amountToWithdraw: "TESTKUDOS:3" as AmountString, corebankApiBaseUrl: "https://bank.taler.fdold.eu/", exchangeBaseUrl: "https://exchange.taler.fdold.eu/", merchantBaseUrl: "https://merchant.taler.fdold.eu/", }); await w.wallet.runTaskLoop({ stopWhenDone: true, }); } export async function testWithLocal(path: string) { console.log("running local test"); const w = await createNativeWalletHost2({ persistentStoragePath: path ?? "walletdb.json", }); console.log("created wallet"); await w.wallet.client.call(WalletApiOperation.InitWallet, { config: { features: { allowHttp: true, }, testing: { skipDefaults: true, }, }, }); console.log("initialized wallet"); await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, { amountToSpend: "TESTKUDOS:1" as AmountString, amountToWithdraw: "TESTKUDOS:3" as AmountString, corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/", exchangeBaseUrl: "http://localhost:8081/", merchantBaseUrl: "http://localhost:8083/", }); console.log("started integration test"); await w.wallet.runTaskLoop({ stopWhenDone: true, }); console.log("done with task loop"); w.wallet.stop(); console.log("DB stats:", j2s(w.getDbStats())); } export async function testArgon2id() { const userIdVector = { input_id_data: { name: "Fleabag", ssn: "AB123", }, input_server_salt: "FZ48EFS7WS3R2ZR4V53A3GFFY4", output_id: "YS45R6CGJV84K1NN7T14ZBCPVTZ6H15XJSM1FV0R748MHPV82SM0126EBZKBAAGCR34Q9AFKPEW1HRT2Q9GQ5JRA3642AB571DKZS18", }; if ( (await userIdentifierDerive( userIdVector.input_id_data, userIdVector.input_server_salt, )) != userIdVector.output_id ) { throw Error("argon2id is not working!"); } console.log("argon2id is working!"); } // @ts-ignore globalThis.testWithGv = testWithGv; // @ts-ignore globalThis.testWithLocal = testWithLocal; // @ts-ignore globalThis.testArgon2id = testArgon2id; // @ts-ignore globalThis.testReduceAction = reduceAction; // @ts-ignore globalThis.testDiscoverPolicies = discoverPolicies; // @ts-ignore globalThis.testWithFdold = testWithFdold;