diff options
Diffstat (limited to 'packages/taler-wallet-embedded/src/wallet-qjs.ts')
-rw-r--r-- | packages/taler-wallet-embedded/src/wallet-qjs.ts | 379 |
1 files changed, 379 insertions, 0 deletions
diff --git a/packages/taler-wallet-embedded/src/wallet-qjs.ts b/packages/taler-wallet-embedded/src/wallet-qjs.ts new file mode 100644 index 000000000..98b73fc44 --- /dev/null +++ b/packages/taler-wallet-embedded/src/wallet-qjs.ts @@ -0,0 +1,379 @@ +/* + 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/> + */ + +/** + * 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, + PartialWalletRunConfig, + 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; + walletConfig: PartialWalletRunConfig | undefined; + maybeWallet: Wallet | undefined; + wp = openPromise<Wallet>(); + httpLib = createPlatformHttpLib(); + + /** + * Handle a request from the native wallet. + */ + async handleMessage( + operation: string, + id: string, + args: any, + ): Promise<CoreApiResponse> { + 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", { + config: this.walletConfig, + }); + initResponse = resp.type == "response" ? resp.result : resp.error; + 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, + }; + this.walletConfig = args.config ?? {}; + 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<CoreApiResponse> { + 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<void> => { + 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.client.call(WalletApiOperation.TestingWaitTasksDone, {}); + await w.wallet.client.call(WalletApiOperation.Shutdown, {}); +} + +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.client.call(WalletApiOperation.TestingWaitTasksDone, {}); + await w.wallet.client.call(WalletApiOperation.Shutdown, {}); +} + +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.client.call(WalletApiOperation.TestingWaitTasksDone, {}); + console.log("done with task loop"); + await w.wallet.client.call(WalletApiOperation.Shutdown, {}); + 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; |