From d881f4fd258a27cc765a25c24e5fef9f86b6226f Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 23 Mar 2022 21:24:23 +0100 Subject: wallet: simplify crypto workers --- .../src/crypto/cryptoImplementation.ts | 982 +++++++++++++++++++++ .../taler-wallet-core/src/crypto/cryptoTypes.ts | 1 - .../src/crypto/workers/cryptoApi.ts | 524 ----------- .../src/crypto/workers/cryptoDispatcher.ts | 344 ++++++++ .../src/crypto/workers/cryptoImplementation.ts | 783 ---------------- .../src/crypto/workers/nodeThreadWorker.ts | 19 +- .../src/crypto/workers/rpcClient.ts | 90 ++ .../src/crypto/workers/synchronousWorker.ts | 49 +- .../src/crypto/workers/synchronousWorkerFactory.ts | 123 +-- packages/taler-wallet-core/src/dbless.ts | 8 +- packages/taler-wallet-core/src/headless/helpers.ts | 4 +- packages/taler-wallet-core/src/index.ts | 11 +- .../taler-wallet-core/src/internal-wallet-state.ts | 5 +- .../src/operations/backup/index.ts | 23 +- .../src/operations/backup/state.ts | 2 +- .../taler-wallet-core/src/operations/common.ts | 2 +- .../taler-wallet-core/src/operations/deposits.ts | 14 +- .../taler-wallet-core/src/operations/exchanges.ts | 24 +- packages/taler-wallet-core/src/operations/pay.ts | 33 +- .../taler-wallet-core/src/operations/refresh.ts | 22 +- .../taler-wallet-core/src/operations/reserves.ts | 2 +- packages/taler-wallet-core/src/operations/tip.ts | 22 +- .../taler-wallet-core/src/operations/withdraw.ts | 29 +- packages/taler-wallet-core/src/wallet.ts | 22 +- 24 files changed, 1592 insertions(+), 1546 deletions(-) create mode 100644 packages/taler-wallet-core/src/crypto/cryptoImplementation.ts delete mode 100644 packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts create mode 100644 packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts delete mode 100644 packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts create mode 100644 packages/taler-wallet-core/src/crypto/workers/rpcClient.ts (limited to 'packages/taler-wallet-core/src') diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts new file mode 100644 index 000000000..63b2687b6 --- /dev/null +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -0,0 +1,982 @@ +/* + 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 + */ + +/** + * Implementation of crypto-related high-level functions for the Taler wallet. + * + * @author Florian Dold + */ + +/** + * Imports. + */ + +// FIXME: Crypto should not use DB Types! +import { + AmountJson, + Amounts, + BenchmarkResult, + buildSigPS, + CoinDepositPermission, + CoinEnvelope, + createEddsaKeyPair, + createHashContext, + decodeCrock, + DenomKeyType, + DepositInfo, + eddsaGetPublic, + eddsaSign, + eddsaVerify, + encodeCrock, + ExchangeProtocolVersion, + FreshCoin, + hash, + hashCoinEv, + hashCoinEvInner, + hashDenomPub, + hashTruncate32, + keyExchangeEcdheEddsa, + Logger, + MakeSyncSignatureRequest, + PlanchetCreationRequest, + WithdrawalPlanchet, + randomBytes, + RecoupRefreshRequest, + RecoupRequest, + RefreshPlanchetInfo, + rsaBlind, + rsaUnblind, + rsaVerify, + setupRefreshPlanchet, + setupRefreshTransferPub, + setupTipPlanchet, + setupWithdrawPlanchet, + stringToBytes, + TalerSignaturePurpose, + BlindedDenominationSignature, + UnblindedSignature, + PlanchetUnblindInfo, + TalerProtocolTimestamp, +} from "@gnu-taler/taler-util"; +import bigint from "big-integer"; +import { DenominationRecord, WireFee } from "../db.js"; +import { + CreateRecoupRefreshReqRequest, + CreateRecoupReqRequest, + DerivedRefreshSession, + DerivedTipPlanchet, + DeriveRefreshSessionRequest, + DeriveTipRequest, + SignTrackTransactionRequest, +} from "./cryptoTypes.js"; + +//const logger = new Logger("cryptoImplementation.ts"); + +/** + * Interface for (asynchronous) cryptographic operations that + * Taler uses. + */ +export interface TalerCryptoInterface { + /** + * Create a pre-coin of the given denomination to be withdrawn from then given + * reserve. + */ + createPlanchet(req: PlanchetCreationRequest): Promise; + + eddsaSign(req: EddsaSignRequest): Promise; + + /** + * Create a planchet used for tipping, including the private keys. + */ + createTipPlanchet(req: DeriveTipRequest): Promise; + + signTrackTransaction( + req: SignTrackTransactionRequest, + ): Promise; + + createRecoupRequest(req: CreateRecoupReqRequest): Promise; + + createRecoupRefreshRequest( + req: CreateRecoupRefreshReqRequest, + ): Promise; + + isValidPaymentSignature( + req: PaymentSignatureValidationRequest, + ): Promise; + + isValidWireFee(req: WireFeeValidationRequest): Promise; + + isValidDenom(req: DenominationValidationRequest): Promise; + + isValidWireAccount( + req: WireAccountValidationRequest, + ): Promise; + + isValidContractTermsSignature( + req: ContractTermsValidationRequest, + ): Promise; + + createEddsaKeypair(req: {}): Promise; + + eddsaGetPublic(req: EddsaGetPublicRequest): Promise; + + unblindDenominationSignature( + req: UnblindDenominationSignatureRequest, + ): Promise; + + rsaUnblind(req: RsaUnblindRequest): Promise; + + rsaVerify(req: RsaVerificationRequest): Promise; + + signDepositPermission( + depositInfo: DepositInfo, + ): Promise; + + deriveRefreshSession( + req: DeriveRefreshSessionRequest, + ): Promise; + + hashString(req: HashStringRequest): Promise; + + signCoinLink(req: SignCoinLinkRequest): Promise; + + makeSyncSignature(req: MakeSyncSignatureRequest): Promise; +} + +/** + * Implementation of the Taler crypto interface where every function + * always throws. Only useful in practice as a way to iterate through + * all possible crypto functions. + * + * (This list can be easily auto-generated by your favorite IDE). + */ +export const nullCrypto: TalerCryptoInterface = { + createPlanchet: function ( + req: PlanchetCreationRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + eddsaSign: function (req: EddsaSignRequest): Promise { + throw new Error("Function not implemented."); + }, + createTipPlanchet: function ( + req: DeriveTipRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + signTrackTransaction: function ( + req: SignTrackTransactionRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + createRecoupRequest: function ( + req: CreateRecoupReqRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + createRecoupRefreshRequest: function ( + req: CreateRecoupRefreshReqRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + isValidPaymentSignature: function ( + req: PaymentSignatureValidationRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + isValidWireFee: function ( + req: WireFeeValidationRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + isValidDenom: function ( + req: DenominationValidationRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + isValidWireAccount: function ( + req: WireAccountValidationRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + isValidContractTermsSignature: function ( + req: ContractTermsValidationRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + createEddsaKeypair: function (req: {}): Promise { + throw new Error("Function not implemented."); + }, + eddsaGetPublic: function (req: EddsaGetPublicRequest): Promise { + throw new Error("Function not implemented."); + }, + unblindDenominationSignature: function ( + req: UnblindDenominationSignatureRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + rsaUnblind: function (req: RsaUnblindRequest): Promise { + throw new Error("Function not implemented."); + }, + rsaVerify: function (req: RsaVerificationRequest): Promise { + throw new Error("Function not implemented."); + }, + signDepositPermission: function ( + depositInfo: DepositInfo, + ): Promise { + throw new Error("Function not implemented."); + }, + deriveRefreshSession: function ( + req: DeriveRefreshSessionRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + hashString: function (req: HashStringRequest): Promise { + throw new Error("Function not implemented."); + }, + signCoinLink: function ( + req: SignCoinLinkRequest, + ): Promise { + throw new Error("Function not implemented."); + }, + makeSyncSignature: function ( + req: MakeSyncSignatureRequest, + ): Promise { + throw new Error("Function not implemented."); + }, +}; + +export type WithArg = X extends (req: infer T) => infer R + ? (tci: TalerCryptoInterfaceR, req: T) => R + : never; + +export type TalerCryptoInterfaceR = { + [x in keyof TalerCryptoInterface]: WithArg; +}; + +export interface SignCoinLinkRequest { + oldCoinPriv: string; + newDenomHash: string; + oldCoinPub: string; + transferPub: string; + coinEv: CoinEnvelope; +} + +export interface RsaVerificationRequest { + hm: string; + sig: string; + pk: string; +} + +export interface EddsaSigningResult { + sig: string; +} + +export interface ValidationResult { + valid: boolean; +} + +export interface HashStringRequest { + str: string; +} + +export interface HashStringResult { + h: string; +} + +export interface WireFeeValidationRequest { + type: string; + wf: WireFee; + masterPub: string; +} + +export interface DenominationValidationRequest { + denom: DenominationRecord; + masterPub: string; +} + +export interface PaymentSignatureValidationRequest { + sig: string; + contractHash: string; + merchantPub: string; +} + +export interface ContractTermsValidationRequest { + contractTermsHash: string; + sig: string; + merchantPub: string; +} + +export interface WireAccountValidationRequest { + versionCurrent: ExchangeProtocolVersion; + paytoUri: string; + sig: string; + masterPub: string; +} + +export interface EddsaKeypair { + priv: string; + pub: string; +} + +export interface EddsaGetPublicRequest { + priv: string; +} + +export interface UnblindDenominationSignatureRequest { + planchet: PlanchetUnblindInfo; + evSig: BlindedDenominationSignature; +} + +export interface RsaUnblindRequest { + blindedSig: string; + bk: string; + pk: string; +} + +export interface RsaUnblindResponse { + sig: string; +} + +export const nativeCryptoR: TalerCryptoInterfaceR = { + async eddsaSign( + tci: TalerCryptoInterfaceR, + req: EddsaSignRequest, + ): Promise { + return { + sig: encodeCrock(eddsaSign(decodeCrock(req.msg), decodeCrock(req.priv))), + }; + }, + + async createPlanchet( + tci: TalerCryptoInterfaceR, + req: PlanchetCreationRequest, + ): Promise { + const denomPub = req.denomPub; + if (denomPub.cipher === DenomKeyType.Rsa) { + const reservePub = decodeCrock(req.reservePub); + const denomPubRsa = decodeCrock(denomPub.rsa_public_key); + const derivedPlanchet = setupWithdrawPlanchet( + decodeCrock(req.secretSeed), + req.coinIndex, + ); + const coinPubHash = hash(derivedPlanchet.coinPub); + const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPubRsa); + const coinEv: CoinEnvelope = { + cipher: DenomKeyType.Rsa, + rsa_blinded_planchet: encodeCrock(ev), + }; + const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount; + const denomPubHash = hashDenomPub(req.denomPub); + const evHash = hashCoinEv(coinEv, encodeCrock(denomPubHash)); + const withdrawRequest = buildSigPS( + TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW, + ) + .put(amountToBuffer(amountWithFee)) + .put(denomPubHash) + .put(evHash) + .build(); + + const sigResult = await tci.eddsaSign(tci, { + msg: encodeCrock(withdrawRequest), + priv: req.reservePriv, + }); + + const planchet: WithdrawalPlanchet = { + blindingKey: encodeCrock(derivedPlanchet.bks), + coinEv, + coinPriv: encodeCrock(derivedPlanchet.coinPriv), + coinPub: encodeCrock(derivedPlanchet.coinPub), + coinValue: req.value, + denomPub, + denomPubHash: encodeCrock(denomPubHash), + reservePub: encodeCrock(reservePub), + withdrawSig: sigResult.sig, + coinEvHash: encodeCrock(evHash), + }; + return planchet; + } else { + throw Error("unsupported cipher, unable to create planchet"); + } + }, + + async createTipPlanchet( + tci: TalerCryptoInterfaceR, + req: DeriveTipRequest, + ): Promise { + if (req.denomPub.cipher !== DenomKeyType.Rsa) { + throw Error(`unsupported cipher (${req.denomPub.cipher})`); + } + const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex); + const denomPub = decodeCrock(req.denomPub.rsa_public_key); + const coinPubHash = hash(fc.coinPub); + const ev = rsaBlind(coinPubHash, fc.bks, denomPub); + const coinEv = { + cipher: DenomKeyType.Rsa, + rsa_blinded_planchet: encodeCrock(ev), + }; + const tipPlanchet: DerivedTipPlanchet = { + blindingKey: encodeCrock(fc.bks), + coinEv, + coinEvHash: encodeCrock( + hashCoinEv(coinEv, encodeCrock(hashDenomPub(req.denomPub))), + ), + coinPriv: encodeCrock(fc.coinPriv), + coinPub: encodeCrock(fc.coinPub), + }; + return tipPlanchet; + }, + + async signTrackTransaction( + tci: TalerCryptoInterfaceR, + req: SignTrackTransactionRequest, + ): Promise { + const p = buildSigPS(TalerSignaturePurpose.MERCHANT_TRACK_TRANSACTION) + .put(decodeCrock(req.contractTermsHash)) + .put(decodeCrock(req.wireHash)) + .put(decodeCrock(req.merchantPub)) + .put(decodeCrock(req.coinPub)) + .build(); + return { sig: encodeCrock(eddsaSign(p, decodeCrock(req.merchantPriv))) }; + }, + + /** + * Create and sign a message to recoup a coin. + */ + async createRecoupRequest( + tci: TalerCryptoInterfaceR, + req: CreateRecoupReqRequest, + ): Promise { + const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP) + .put(decodeCrock(req.denomPubHash)) + .put(decodeCrock(req.blindingKey)) + .build(); + + const coinPriv = decodeCrock(req.coinPriv); + const coinSig = eddsaSign(p, coinPriv); + if (req.denomPub.cipher === DenomKeyType.Rsa) { + const paybackRequest: RecoupRequest = { + coin_blind_key_secret: req.blindingKey, + coin_sig: encodeCrock(coinSig), + denom_pub_hash: req.denomPubHash, + denom_sig: req.denomSig, + // FIXME! + ewv: { + cipher: "RSA", + }, + }; + return paybackRequest; + } else { + throw new Error(); + } + }, + + /** + * Create and sign a message to recoup a coin. + */ + async createRecoupRefreshRequest( + tci: TalerCryptoInterfaceR, + req: CreateRecoupRefreshReqRequest, + ): Promise { + const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP_REFRESH) + .put(decodeCrock(req.denomPubHash)) + .put(decodeCrock(req.blindingKey)) + .build(); + + const coinPriv = decodeCrock(req.coinPriv); + const coinSig = eddsaSign(p, coinPriv); + if (req.denomPub.cipher === DenomKeyType.Rsa) { + const recoupRequest: RecoupRefreshRequest = { + coin_blind_key_secret: req.blindingKey, + coin_sig: encodeCrock(coinSig), + denom_pub_hash: req.denomPubHash, + denom_sig: req.denomSig, + // FIXME! + ewv: { + cipher: "RSA", + }, + }; + return recoupRequest; + } else { + throw new Error(); + } + }, + + /** + * Check if a payment signature is valid. + */ + async isValidPaymentSignature( + tci: TalerCryptoInterfaceR, + req: PaymentSignatureValidationRequest, + ): Promise { + const { contractHash, sig, merchantPub } = req; + const p = buildSigPS(TalerSignaturePurpose.MERCHANT_PAYMENT_OK) + .put(decodeCrock(contractHash)) + .build(); + const sigBytes = decodeCrock(sig); + const pubBytes = decodeCrock(merchantPub); + return { valid: eddsaVerify(p, sigBytes, pubBytes) }; + }, + + /** + * Check if a wire fee is correctly signed. + */ + async isValidWireFee( + tci: TalerCryptoInterfaceR, + req: WireFeeValidationRequest, + ): Promise { + const { type, wf, masterPub } = req; + const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_FEES) + .put(hash(stringToBytes(type + "\0"))) + .put(timestampRoundedToBuffer(wf.startStamp)) + .put(timestampRoundedToBuffer(wf.endStamp)) + .put(amountToBuffer(wf.wireFee)) + .put(amountToBuffer(wf.closingFee)) + .put(amountToBuffer(wf.wadFee)) + .build(); + const sig = decodeCrock(wf.sig); + const pub = decodeCrock(masterPub); + return { valid: eddsaVerify(p, sig, pub) }; + }, + + /** + * Check if the signature of a denomination is valid. + */ + async isValidDenom( + tci: TalerCryptoInterfaceR, + req: DenominationValidationRequest, + ): Promise { + const { masterPub, denom } = req; + const p = buildSigPS(TalerSignaturePurpose.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); + const res = eddsaVerify(p, sig, pub); + return { valid: res }; + }, + + async isValidWireAccount( + tci: TalerCryptoInterfaceR, + req: WireAccountValidationRequest, + ): Promise { + const { sig, masterPub, paytoUri } = req; + const paytoHash = hashTruncate32(stringToBytes(paytoUri + "\0")); + const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS) + .put(paytoHash) + .build(); + return { valid: eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)) }; + }, + + async isValidContractTermsSignature( + tci: TalerCryptoInterfaceR, + req: ContractTermsValidationRequest, + ): Promise { + const cthDec = decodeCrock(req.contractTermsHash); + const p = buildSigPS(TalerSignaturePurpose.MERCHANT_CONTRACT) + .put(cthDec) + .build(); + return { + valid: eddsaVerify(p, decodeCrock(req.sig), decodeCrock(req.merchantPub)), + }; + }, + + /** + * Create a new EdDSA key pair. + */ + async createEddsaKeypair(tci: TalerCryptoInterfaceR): Promise { + const pair = createEddsaKeyPair(); + return { + priv: encodeCrock(pair.eddsaPriv), + pub: encodeCrock(pair.eddsaPub), + }; + }, + + async eddsaGetPublic( + tci: TalerCryptoInterfaceR, + req: EddsaGetPublicRequest, + ): Promise { + return { + priv: req.priv, + pub: encodeCrock(eddsaGetPublic(decodeCrock(req.priv))), + }; + }, + + async unblindDenominationSignature( + tci: TalerCryptoInterfaceR, + req: UnblindDenominationSignatureRequest, + ): Promise { + if (req.evSig.cipher === DenomKeyType.Rsa) { + if (req.planchet.denomPub.cipher !== DenomKeyType.Rsa) { + throw new Error( + "planchet cipher does not match blind signature cipher", + ); + } + const denomSig = rsaUnblind( + decodeCrock(req.evSig.blinded_rsa_signature), + decodeCrock(req.planchet.denomPub.rsa_public_key), + decodeCrock(req.planchet.blindingKey), + ); + return { + cipher: DenomKeyType.Rsa, + rsa_signature: encodeCrock(denomSig), + }; + } else { + throw Error(`unblinding for cipher ${req.evSig.cipher} not implemented`); + } + }, + + /** + * Unblind a blindly signed value. + */ + async rsaUnblind( + tci: TalerCryptoInterfaceR, + req: RsaUnblindRequest, + ): Promise { + const denomSig = rsaUnblind( + decodeCrock(req.blindedSig), + decodeCrock(req.pk), + decodeCrock(req.bk), + ); + return { sig: encodeCrock(denomSig) }; + }, + + /** + * Unblind a blindly signed value. + */ + async rsaVerify( + tci: TalerCryptoInterfaceR, + req: RsaVerificationRequest, + ): Promise { + return { + valid: rsaVerify( + hash(decodeCrock(req.hm)), + decodeCrock(req.sig), + decodeCrock(req.pk), + ), + }; + }, + + /** + * Generate updated coins (to store in the database) + * and deposit permissions for each given coin. + */ + async signDepositPermission( + tci: TalerCryptoInterfaceR, + depositInfo: DepositInfo, + ): Promise { + // FIXME: put extensions here if used + const hExt = new Uint8Array(64); + const hAgeCommitment = new Uint8Array(32); + let d: Uint8Array; + if (depositInfo.denomKeyType === DenomKeyType.Rsa) { + d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT) + .put(decodeCrock(depositInfo.contractTermsHash)) + .put(hAgeCommitment) + .put(hExt) + .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)) + .build(); + } else { + throw Error("unsupported exchange protocol version"); + } + const coinSigRes = await this.eddsaSign(tci, { + msg: encodeCrock(d), + priv: depositInfo.coinPriv, + }); + + if (depositInfo.denomKeyType === DenomKeyType.Rsa) { + const s: CoinDepositPermission = { + coin_pub: depositInfo.coinPub, + coin_sig: coinSigRes.sig, + contribution: Amounts.stringify(depositInfo.spendAmount), + h_denom: depositInfo.denomPubHash, + exchange_url: depositInfo.exchangeBaseUrl, + ub_sig: { + cipher: DenomKeyType.Rsa, + rsa_signature: depositInfo.denomSig.rsa_signature, + }, + }; + return s; + } else { + throw Error( + `unsupported denomination cipher (${depositInfo.denomKeyType})`, + ); + } + }, + + async deriveRefreshSession( + tci: TalerCryptoInterfaceR, + req: DeriveRefreshSessionRequest, + ): Promise { + const { + newCoinDenoms, + feeRefresh: meltFee, + kappa, + meltCoinDenomPubHash, + meltCoinPriv, + meltCoinPub, + sessionSecretSeed: refreshSessionSecretSeed, + } = req; + + const currency = newCoinDenoms[0].value.currency; + let valueWithFee = Amounts.getZero(currency); + + for (const ncd of newCoinDenoms) { + const t = Amounts.add(ncd.value, ncd.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: RefreshPlanchetInfo[][] = []; + + for (let i = 0; i < kappa; i++) { + const transferKeyPair = setupRefreshTransferPub( + decodeCrock(refreshSessionSecretSeed), + i, + ); + sessionHc.update(transferKeyPair.ecdhePub); + transferPrivs.push(encodeCrock(transferKeyPair.ecdhePriv)); + transferPubs.push(encodeCrock(transferKeyPair.ecdhePub)); + } + + for (const denomSel of newCoinDenoms) { + for (let i = 0; i < denomSel.count; i++) { + if (denomSel.denomPub.cipher === DenomKeyType.Rsa) { + const denomPubHash = hashDenomPub(denomSel.denomPub); + sessionHc.update(denomPubHash); + } else { + throw new Error(); + } + } + } + + sessionHc.update(decodeCrock(meltCoinPub)); + sessionHc.update(amountToBuffer(valueWithFee)); + + for (let i = 0; i < kappa; i++) { + const planchets: RefreshPlanchetInfo[] = []; + for (let j = 0; j < newCoinDenoms.length; j++) { + const denomSel = newCoinDenoms[j]; + for (let k = 0; k < denomSel.count; k++) { + const coinIndex = planchets.length; + const transferPriv = decodeCrock(transferPrivs[i]); + const oldCoinPub = decodeCrock(meltCoinPub); + const transferSecret = keyExchangeEcdheEddsa( + transferPriv, + oldCoinPub, + ); + let coinPub: Uint8Array; + let coinPriv: Uint8Array; + let blindingFactor: Uint8Array; + // FIXME: make setupRefreshPlanchet a crypto api fn + let fresh: FreshCoin = setupRefreshPlanchet( + transferSecret, + coinIndex, + ); + coinPriv = fresh.coinPriv; + coinPub = fresh.coinPub; + blindingFactor = fresh.bks; + const coinPubHash = hash(coinPub); + if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) { + throw Error("unsupported cipher, can't create refresh session"); + } + const rsaDenomPub = decodeCrock(denomSel.denomPub.rsa_public_key); + const ev = rsaBlind(coinPubHash, blindingFactor, rsaDenomPub); + const coinEv: CoinEnvelope = { + cipher: DenomKeyType.Rsa, + rsa_blinded_planchet: encodeCrock(ev), + }; + const coinEvHash = hashCoinEv( + coinEv, + encodeCrock(hashDenomPub(denomSel.denomPub)), + ); + const planchet: RefreshPlanchetInfo = { + blindingKey: encodeCrock(blindingFactor), + coinEv, + coinPriv: encodeCrock(coinPriv), + coinPub: encodeCrock(coinPub), + coinEvHash: encodeCrock(coinEvHash), + }; + planchets.push(planchet); + hashCoinEvInner(coinEv, sessionHc); + } + } + planchetsForGammas.push(planchets); + } + + const sessionHash = sessionHc.finish(); + let confirmData: Uint8Array; + // FIXME: fill in age commitment + const hAgeCommitment = new Uint8Array(32); + confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT) + .put(sessionHash) + .put(decodeCrock(meltCoinDenomPubHash)) + .put(hAgeCommitment) + .put(amountToBuffer(valueWithFee)) + .put(amountToBuffer(meltFee)) + .build(); + + const confirmSigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(confirmData), + priv: meltCoinPriv, + }); + + const refreshSession: DerivedRefreshSession = { + confirmSig: confirmSigResp.sig, + hash: encodeCrock(sessionHash), + meltCoinPub: meltCoinPub, + planchetsForGammas: planchetsForGammas, + transferPrivs, + transferPubs, + meltValueWithFee: valueWithFee, + }; + + return refreshSession; + }, + + /** + * Hash a string including the zero terminator. + */ + async hashString( + tci: TalerCryptoInterfaceR, + req: HashStringRequest, + ): Promise { + const b = stringToBytes(req.str + "\0"); + return { h: encodeCrock(hash(b)) }; + }, + + async signCoinLink( + tci: TalerCryptoInterfaceR, + req: SignCoinLinkRequest, + ): Promise { + const coinEvHash = hashCoinEv(req.coinEv, req.newDenomHash); + // FIXME: fill in + const hAgeCommitment = new Uint8Array(32); + const coinLink = buildSigPS(TalerSignaturePurpose.WALLET_COIN_LINK) + .put(decodeCrock(req.newDenomHash)) + .put(decodeCrock(req.transferPub)) + .put(hAgeCommitment) + .put(coinEvHash) + .build(); + return tci.eddsaSign(tci, { + msg: encodeCrock(coinLink), + priv: req.oldCoinPriv, + }); + }, + + async makeSyncSignature( + tci: TalerCryptoInterfaceR, + req: MakeSyncSignatureRequest, + ): Promise { + const hNew = decodeCrock(req.newHash); + let hOld: Uint8Array; + if (req.oldHash) { + hOld = decodeCrock(req.oldHash); + } else { + hOld = new Uint8Array(64); + } + const sigBlob = buildSigPS(TalerSignaturePurpose.SYNC_BACKUP_UPLOAD) + .put(hOld) + .put(hNew) + .build(); + const uploadSig = eddsaSign(sigBlob, decodeCrock(req.accountPriv)); + return { sig: encodeCrock(uploadSig) }; + }, +}; + +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); + if (typeof dvbuf.setBigUint64 !== "undefined") { + dvbuf.setBigUint64(0, BigInt(amount.value)); + } else { + const arr = bigint(amount.value).toArray(2 ** 8).value; + let offset = 8 - arr.length; + for (let i = 0; i < arr.length; i++) { + dvbuf.setUint8(offset++, arr[i]); + } + } + dvbuf.setUint32(8, amount.fraction); + u8buf.set(curr, 8 + 4); + + return u8buf; +} + +function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array { + const b = new ArrayBuffer(8); + const v = new DataView(b); + // The buffer we sign over represents the timestamp in microseconds. + if (typeof v.setBigUint64 !== "undefined") { + const s = BigInt(ts.t_s) * BigInt(1000 * 1000); + v.setBigUint64(0, s); + } else { + const s = + ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000); + const arr = s.toArray(2 ** 8).value; + let offset = 8 - arr.length; + for (let i = 0; i < arr.length; i++) { + v.setUint8(offset++, arr[i]); + } + } + return new Uint8Array(b); +} + +export interface EddsaSignRequest { + msg: string; + priv: string; +} + +export interface EddsaSignResponse { + sig: string; +} + +export const nativeCrypto: TalerCryptoInterface = Object.fromEntries( + Object.keys(nativeCryptoR).map((name) => { + return [ + name, + (req: any) => + nativeCryptoR[name as keyof TalerCryptoInterfaceR](nativeCryptoR, req), + ]; + }), +) as any; diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts index 3b3396046..deff15071 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts @@ -29,7 +29,6 @@ */ import { AmountJson, - AmountString, CoinEnvelope, DenominationPubKey, ExchangeProtocolVersion, diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts deleted file mode 100644 index ca498bff1..000000000 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts +++ /dev/null @@ -1,524 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2016 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 - */ - -/** - * API to access the Taler crypto worker thread. - * @author Florian Dold - */ - -/** - * Imports. - */ -import { DenominationRecord, WireFee } from "../../db.js"; - -import { CryptoWorker } from "./cryptoWorkerInterface.js"; - -import { - BlindedDenominationSignature, - CoinDepositPermission, - CoinEnvelope, - PlanchetUnblindInfo, - RecoupRefreshRequest, - RecoupRequest, - UnblindedSignature, -} from "@gnu-taler/taler-util"; - -import { - BenchmarkResult, - WithdrawalPlanchet, - PlanchetCreationRequest, - DepositInfo, - MakeSyncSignatureRequest, -} from "@gnu-taler/taler-util"; - -import * as timer from "../../util/timer.js"; -import { Logger } from "@gnu-taler/taler-util"; -import { - CreateRecoupRefreshReqRequest, - CreateRecoupReqRequest, - DerivedRefreshSession, - DerivedTipPlanchet, - DeriveRefreshSessionRequest, - DeriveTipRequest, - SignTrackTransactionRequest, -} from "../cryptoTypes.js"; - -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. - */ - idleTimeoutHandle: 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: BigInt; -} - -/** - * 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 CryptoApiStoppedError extends Error { - constructor() { - super("Crypto API stopped"); - Object.setPrototypeOf(this, CryptoApiStoppedError.prototype); - } -} - -/** - * 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.idleTimeoutHandle) { - worker.idleTimeoutHandle.clear(); - worker.idleTimeoutHandle = null; - } - if (worker.currentWorkItem) { - worker.currentWorkItem.reject(Error("explicitly terminated")); - worker.currentWorkItem = null; - } - if (worker.w) { - logger.trace("terminating worker"); - worker.w.terminate(); - worker.w = null; - } - } - } - - stop(): void { - this.stopped = true; - this.terminateWorkers(); - } - - /** - * Start a worker (if not started) and set as busy. - */ - wake(ws: WorkerState, work: WorkItem): void { - if (this.stopped) { - throw new CryptoApiStoppedError(); - } - 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(); - timer.after(0, () => worker.postMessage(msg)); - } - - resetWorkerTimeout(ws: WorkerState): void { - if (ws.idleTimeoutHandle !== null) { - ws.idleTimeoutHandle.clear(); - ws.idleTimeoutHandle = null; - } - const destroy = (): void => { - logger.trace("destroying crypto worker after idle timeout"); - // terminate worker if it's idle - if (ws.w && ws.currentWorkItem === null) { - ws.w.terminate(); - ws.w = null; - } - }; - ws.idleTimeoutHandle = timer.after(15 * 1000, destroy); - ws.idleTimeoutHandle.unref(); - } - - handleWorkerError(ws: WorkerState, e: any): void { - if (ws.currentWorkItem) { - logger.error(`error in worker during ${ws.currentWorkItem.operation}`, e); - } else { - logger.error("error in worker", e); - } - logger.error(e.message); - try { - if (ws.w) { - ws.w.terminate(); - ws.w = null; - } - } catch (e) { - logger.error(e as string); - } - 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(workerFactory.getConcurrency()); - - for (let i = 0; i < this.workers.length; i++) { - this.workers[i] = { - currentWorkItem: null, - idleTimeoutHandle: null, - w: null, - }; - } - - this.workQueues = []; - for (let i = 0; i < NUM_PRIO; i++) { - this.workQueues.push([]); - } - } - - private doRpc( - operation: string, - priority: number, - ...args: any[] - ): Promise { - if (this.stopped) { - throw new CryptoApiStoppedError(); - } - const p: Promise = new Promise((resolve, reject) => { - const rpcId = this.nextRpcId++; - const workItem: WorkItem = { - operation, - args, - resolve, - reject, - rpcId, - startTime: BigInt(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"); - }); - - // Make sure that we wait for the result while a timer is active - // to prevent the event loop from dying, as just waiting for a promise - // does not keep the process alive in Node. - // (The worker child process won't keep us alive either, because we un-ref - // it to make sure it doesn't keep us alive if there is no work.) - return new Promise((resolve, reject) => { - let timedOut = false; - const timeout = timer.after(5000, () => { - logger.warn("crypto RPC call timed out"); - timedOut = true; - reject(new Error("crypto RPC call timed out")); - }); - p.then((x) => { - if (timedOut) { - return; - } - timeout.clear(); - resolve(x); - }); - p.catch((x) => { - if (timedOut) { - return; - } - timeout.clear(); - reject(x); - }); - }); - } - - createPlanchet(req: PlanchetCreationRequest): Promise { - return this.doRpc("createPlanchet", 1, req); - } - - unblindDenominationSignature(req: { - planchet: PlanchetUnblindInfo; - evSig: BlindedDenominationSignature; - }): Promise { - return this.doRpc( - "unblindDenominationSignature", - 1, - req, - ); - } - - createTipPlanchet(req: DeriveTipRequest): Promise { - return this.doRpc("createTipPlanchet", 1, req); - } - - signTrackTransaction(req: SignTrackTransactionRequest): Promise { - return this.doRpc("signTrackTransaction", 1, req); - } - - hashString(str: string): Promise { - return this.doRpc("hashString", 1, str); - } - - hashEncoded(encodedBytes: string): Promise { - return this.doRpc("hashEncoded", 1, encodedBytes); - } - - isValidDenom(denom: DenominationRecord, masterPub: string): Promise { - return this.doRpc("isValidDenom", 2, denom, masterPub); - } - - isValidWireFee( - type: string, - wf: WireFee, - masterPub: string, - ): Promise { - return this.doRpc("isValidWireFee", 2, type, wf, masterPub); - } - - isValidPaymentSignature( - sig: string, - contractHash: string, - merchantPub: string, - ): Promise { - return this.doRpc( - "isValidPaymentSignature", - 1, - sig, - contractHash, - merchantPub, - ); - } - - signDepositPermission( - depositInfo: DepositInfo, - ): Promise { - return this.doRpc( - "signDepositPermission", - 3, - depositInfo, - ); - } - - createEddsaKeypair(): Promise<{ priv: string; pub: string }> { - return this.doRpc<{ priv: string; pub: string }>("createEddsaKeypair", 1); - } - - eddsaGetPublic(key: string): Promise<{ priv: string; pub: string }> { - return this.doRpc<{ priv: string; pub: string }>("eddsaGetPublic", 1, key); - } - - rsaUnblind(sig: string, bk: string, pk: string): Promise { - return this.doRpc("rsaUnblind", 4, sig, bk, pk); - } - - rsaVerify(hm: string, sig: string, pk: string): Promise { - return this.doRpc("rsaVerify", 4, hm, sig, pk); - } - - isValidWireAccount( - versionCurrent: number, - paytoUri: string, - sig: string, - masterPub: string, - ): Promise { - return this.doRpc( - "isValidWireAccount", - 4, - versionCurrent, - paytoUri, - sig, - masterPub, - ); - } - - isValidContractTermsSignature( - contractTermsHash: string, - sig: string, - merchantPub: string, - ): Promise { - return this.doRpc( - "isValidContractTermsSignature", - 4, - contractTermsHash, - sig, - merchantPub, - ); - } - - createRecoupRequest(req: CreateRecoupReqRequest): Promise { - return this.doRpc("createRecoupRequest", 1, req); - } - - createRecoupRefreshRequest( - req: CreateRecoupRefreshReqRequest, - ): Promise { - return this.doRpc( - "createRecoupRefreshRequest", - 1, - req, - ); - } - - deriveRefreshSession( - req: DeriveRefreshSessionRequest, - ): Promise { - return this.doRpc("deriveRefreshSession", 4, req); - } - - signCoinLink( - oldCoinPriv: string, - newDenomHash: string, - oldCoinPub: string, - transferPub: string, - coinEv: CoinEnvelope, - ): Promise { - return this.doRpc( - "signCoinLink", - 4, - oldCoinPriv, - newDenomHash, - oldCoinPub, - transferPub, - coinEv, - ); - } - - benchmark(repetitions: number): Promise { - return this.doRpc("benchmark", 1, repetitions); - } - - makeSyncSignature(req: MakeSyncSignatureRequest): Promise { - return this.doRpc("makeSyncSignature", 3, req); - } -} diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts new file mode 100644 index 000000000..810273cca --- /dev/null +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts @@ -0,0 +1,344 @@ +/* + This file is part of GNU Taler + (C) 2016 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 + */ + +/** + * API to access the Taler crypto worker thread. + * @author Florian Dold + */ + +/** + * Imports. + */ +import { Logger } from "@gnu-taler/taler-util"; +import * as timer from "../../util/timer.js"; +import { nullCrypto, TalerCryptoInterface } from "../cryptoImplementation.js"; +import { CryptoWorker } from "./cryptoWorkerInterface.js"; + +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. + */ + idleTimeoutHandle: timer.TimerHandle | null; +} + +interface WorkItem { + operation: string; + req: unknown; + 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: BigInt; +} + +/** + * 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 CryptoApiStoppedError extends Error { + constructor() { + super("Crypto API stopped"); + Object.setPrototypeOf(this, CryptoApiStoppedError.prototype); + } +} + +/** + * Dispatcher for cryptographic operations to underlying crypto workers. + */ +export class CryptoDispatcher { + 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.idleTimeoutHandle) { + worker.idleTimeoutHandle.clear(); + worker.idleTimeoutHandle = null; + } + if (worker.currentWorkItem) { + worker.currentWorkItem.reject(Error("explicitly terminated")); + worker.currentWorkItem = null; + } + if (worker.w) { + logger.trace("terminating worker"); + worker.w.terminate(); + worker.w = null; + } + } + } + + stop(): void { + this.stopped = true; + this.terminateWorkers(); + } + + /** + * Start a worker (if not started) and set as busy. + */ + wake(ws: WorkerState, work: WorkItem): void { + if (this.stopped) { + throw new CryptoApiStoppedError(); + } + 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 = { + req: work.req, + id: work.rpcId, + operation: work.operation, + }; + this.resetWorkerTimeout(ws); + work.startTime = timer.performanceNow(); + timer.after(0, () => worker.postMessage(msg)); + } + + resetWorkerTimeout(ws: WorkerState): void { + if (ws.idleTimeoutHandle !== null) { + ws.idleTimeoutHandle.clear(); + ws.idleTimeoutHandle = null; + } + const destroy = (): void => { + logger.trace("destroying crypto worker after idle timeout"); + // terminate worker if it's idle + if (ws.w && ws.currentWorkItem === null) { + ws.w.terminate(); + ws.w = null; + } + }; + ws.idleTimeoutHandle = timer.after(15 * 1000, destroy); + ws.idleTimeoutHandle.unref(); + } + + handleWorkerError(ws: WorkerState, e: any): void { + if (ws.currentWorkItem) { + logger.error(`error in worker during ${ws.currentWorkItem.operation}`, e); + } else { + logger.error("error in worker", e); + } + logger.error(e.message); + try { + if (ws.w) { + ws.w.terminate(); + ws.w = null; + } + } catch (e) { + logger.error(e as string); + } + 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); + } + + cryptoApi: TalerCryptoInterface; + + constructor(workerFactory: CryptoWorkerFactory) { + const fns: any = {}; + for (const name of Object.keys(nullCrypto)) { + fns[name] = (x: any) => this.doRpc(name, 0, x); + } + + this.cryptoApi = fns; + + this.workerFactory = workerFactory; + this.workers = new Array(workerFactory.getConcurrency()); + + for (let i = 0; i < this.workers.length; i++) { + this.workers[i] = { + currentWorkItem: null, + idleTimeoutHandle: null, + w: null, + }; + } + + this.workQueues = []; + for (let i = 0; i < NUM_PRIO; i++) { + this.workQueues.push([]); + } + } + + private doRpc( + operation: string, + priority: number, + req: unknown, + ): Promise { + if (this.stopped) { + throw new CryptoApiStoppedError(); + } + const p: Promise = new Promise((resolve, reject) => { + const rpcId = this.nextRpcId++; + const workItem: WorkItem = { + operation, + req, + resolve, + reject, + rpcId, + startTime: BigInt(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"); + }); + + // Make sure that we wait for the result while a timer is active + // to prevent the event loop from dying, as just waiting for a promise + // does not keep the process alive in Node. + // (The worker child process won't keep us alive either, because we un-ref + // it to make sure it doesn't keep us alive if there is no work.) + return new Promise((resolve, reject) => { + let timedOut = false; + const timeout = timer.after(5000, () => { + logger.warn("crypto RPC call timed out"); + timedOut = true; + reject(new Error("crypto RPC call timed out")); + }); + p.then((x) => { + if (timedOut) { + return; + } + timeout.clear(); + resolve(x); + }); + p.catch((x) => { + if (timedOut) { + return; + } + timeout.clear(); + reject(x); + }); + }); + } +} diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts deleted file mode 100644 index b27067885..000000000 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts +++ /dev/null @@ -1,783 +0,0 @@ -/* - 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 - */ - -/** - * Implementation of crypto-related high-level functions for the Taler wallet. - * - * @author Florian Dold - */ - -/** - * Imports. - */ - -// FIXME: Crypto should not use DB Types! -import { - AmountJson, - Amounts, - BenchmarkResult, - buildSigPS, - CoinDepositPermission, - CoinEnvelope, - createEddsaKeyPair, - createHashContext, - decodeCrock, - DenomKeyType, - DepositInfo, - eddsaGetPublic, - eddsaSign, - eddsaVerify, - encodeCrock, - ExchangeProtocolVersion, - FreshCoin, - hash, - HashCodeString, - hashCoinEv, - hashCoinEvInner, - hashDenomPub, - hashTruncate32, - keyExchangeEcdheEddsa, - Logger, - MakeSyncSignatureRequest, - PlanchetCreationRequest, - WithdrawalPlanchet, - randomBytes, - RecoupRefreshRequest, - RecoupRequest, - RefreshPlanchetInfo, - rsaBlind, - rsaUnblind, - rsaVerify, - setupRefreshPlanchet, - setupRefreshTransferPub, - setupTipPlanchet, - setupWithdrawPlanchet, - stringToBytes, - TalerSignaturePurpose, - AbsoluteTime, - BlindedDenominationSignature, - UnblindedSignature, - PlanchetUnblindInfo, - TalerProtocolTimestamp, -} from "@gnu-taler/taler-util"; -import bigint from "big-integer"; -import { DenominationRecord, WireFee } from "../../db.js"; -import * as timer from "../../util/timer.js"; -import { - CreateRecoupRefreshReqRequest, - CreateRecoupReqRequest, - DerivedRefreshSession, - DerivedTipPlanchet, - DeriveRefreshSessionRequest, - DeriveTipRequest, - SignTrackTransactionRequest, -} from "../cryptoTypes.js"; - -const logger = new Logger("cryptoImplementation.ts"); - -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); - if (typeof dvbuf.setBigUint64 !== "undefined") { - dvbuf.setBigUint64(0, BigInt(amount.value)); - } else { - const arr = bigint(amount.value).toArray(2 ** 8).value; - let offset = 8 - arr.length; - for (let i = 0; i < arr.length; i++) { - dvbuf.setUint8(offset++, arr[i]); - } - } - dvbuf.setUint32(8, amount.fraction); - u8buf.set(curr, 8 + 4); - - return u8buf; -} - -function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array { - const b = new ArrayBuffer(8); - const v = new DataView(b); - // The buffer we sign over represents the timestamp in microseconds. - if (typeof v.setBigUint64 !== "undefined") { - const s = BigInt(ts.t_s) * BigInt(1000 * 1000); - v.setBigUint64(0, s); - } else { - const s = - ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000); - const arr = s.toArray(2 ** 8).value; - let offset = 8 - arr.length; - for (let i = 0; i < arr.length; i++) { - v.setUint8(offset++, arr[i]); - } - } - return new Uint8Array(b); -} - -export interface PrimitiveWorker { - setupRefreshPlanchet(arg0: { - transfer_secret: string; - coin_index: number; - }): Promise<{ - coin_pub: string; - coin_priv: string; - blinding_key: string; - }>; - eddsaVerify(req: { - msg: string; - sig: string; - pub: string; - }): Promise<{ valid: boolean }>; - - eddsaSign(req: { msg: string; priv: string }): Promise<{ sig: string }>; -} - -async function myEddsaSign( - primitiveWorker: PrimitiveWorker | undefined, - req: { msg: string; priv: string }, -): Promise<{ sig: string }> { - if (primitiveWorker) { - return primitiveWorker.eddsaSign(req); - } - const sig = eddsaSign(decodeCrock(req.msg), decodeCrock(req.priv)); - return { - sig: encodeCrock(sig), - }; -} - -export class CryptoImplementation { - static enableTracing = false; - - constructor(private primitiveWorker?: PrimitiveWorker) {} - - /** - * Create a pre-coin of the given denomination to be withdrawn from then given - * reserve. - */ - async createPlanchet( - req: PlanchetCreationRequest, - ): Promise { - const denomPub = req.denomPub; - if (denomPub.cipher === DenomKeyType.Rsa) { - const reservePub = decodeCrock(req.reservePub); - const denomPubRsa = decodeCrock(denomPub.rsa_public_key); - const derivedPlanchet = setupWithdrawPlanchet( - decodeCrock(req.secretSeed), - req.coinIndex, - ); - const coinPubHash = hash(derivedPlanchet.coinPub); - const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPubRsa); - const coinEv: CoinEnvelope = { - cipher: DenomKeyType.Rsa, - rsa_blinded_planchet: encodeCrock(ev), - }; - const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount; - const denomPubHash = hashDenomPub(req.denomPub); - const evHash = hashCoinEv(coinEv, encodeCrock(denomPubHash)); - const withdrawRequest = buildSigPS( - TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW, - ) - .put(amountToBuffer(amountWithFee)) - .put(denomPubHash) - .put(evHash) - .build(); - - const sigResult = await myEddsaSign(this.primitiveWorker, { - msg: encodeCrock(withdrawRequest), - priv: req.reservePriv, - }); - - const planchet: WithdrawalPlanchet = { - blindingKey: encodeCrock(derivedPlanchet.bks), - coinEv, - coinPriv: encodeCrock(derivedPlanchet.coinPriv), - coinPub: encodeCrock(derivedPlanchet.coinPub), - coinValue: req.value, - denomPub, - denomPubHash: encodeCrock(denomPubHash), - reservePub: encodeCrock(reservePub), - withdrawSig: sigResult.sig, - coinEvHash: encodeCrock(evHash), - }; - return planchet; - } else { - throw Error("unsupported cipher, unable to create planchet"); - } - } - - /** - * Create a planchet used for tipping, including the private keys. - */ - createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet { - if (req.denomPub.cipher !== DenomKeyType.Rsa) { - throw Error(`unsupported cipher (${req.denomPub.cipher})`); - } - const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex); - const denomPub = decodeCrock(req.denomPub.rsa_public_key); - const coinPubHash = hash(fc.coinPub); - const ev = rsaBlind(coinPubHash, fc.bks, denomPub); - const coinEv = { - cipher: DenomKeyType.Rsa, - rsa_blinded_planchet: encodeCrock(ev), - }; - const tipPlanchet: DerivedTipPlanchet = { - blindingKey: encodeCrock(fc.bks), - coinEv, - coinEvHash: encodeCrock( - hashCoinEv(coinEv, encodeCrock(hashDenomPub(req.denomPub))), - ), - coinPriv: encodeCrock(fc.coinPriv), - coinPub: encodeCrock(fc.coinPub), - }; - return tipPlanchet; - } - - signTrackTransaction(req: SignTrackTransactionRequest): string { - const p = buildSigPS(TalerSignaturePurpose.MERCHANT_TRACK_TRANSACTION) - .put(decodeCrock(req.contractTermsHash)) - .put(decodeCrock(req.wireHash)) - .put(decodeCrock(req.merchantPub)) - .put(decodeCrock(req.coinPub)) - .build(); - return encodeCrock(eddsaSign(p, decodeCrock(req.merchantPriv))); - } - - /** - * Create and sign a message to recoup a coin. - */ - createRecoupRequest(req: CreateRecoupReqRequest): RecoupRequest { - const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP) - .put(decodeCrock(req.denomPubHash)) - .put(decodeCrock(req.blindingKey)) - .build(); - - const coinPriv = decodeCrock(req.coinPriv); - const coinSig = eddsaSign(p, coinPriv); - if (req.denomPub.cipher === DenomKeyType.Rsa) { - const paybackRequest: RecoupRequest = { - coin_blind_key_secret: req.blindingKey, - coin_sig: encodeCrock(coinSig), - denom_pub_hash: req.denomPubHash, - denom_sig: req.denomSig, - // FIXME! - ewv: { - cipher: "RSA", - }, - }; - return paybackRequest; - } else { - throw new Error(); - } - } - - /** - * Create and sign a message to recoup a coin. - */ - createRecoupRefreshRequest( - req: CreateRecoupRefreshReqRequest, - ): RecoupRefreshRequest { - const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP_REFRESH) - .put(decodeCrock(req.denomPubHash)) - .put(decodeCrock(req.blindingKey)) - .build(); - - const coinPriv = decodeCrock(req.coinPriv); - const coinSig = eddsaSign(p, coinPriv); - if (req.denomPub.cipher === DenomKeyType.Rsa) { - const recoupRequest: RecoupRefreshRequest = { - coin_blind_key_secret: req.blindingKey, - coin_sig: encodeCrock(coinSig), - denom_pub_hash: req.denomPubHash, - denom_sig: req.denomSig, - // FIXME! - ewv: { - cipher: "RSA", - }, - }; - return recoupRequest; - } else { - throw new Error(); - } - } - - /** - * Check if a payment signature is valid. - */ - isValidPaymentSignature( - sig: string, - contractHash: string, - merchantPub: string, - ): boolean { - const p = buildSigPS(TalerSignaturePurpose.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. - */ - async isValidWireFee( - type: string, - wf: WireFee, - masterPub: string, - ): Promise { - const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_FEES) - .put(hash(stringToBytes(type + "\0"))) - .put(timestampRoundedToBuffer(wf.startStamp)) - .put(timestampRoundedToBuffer(wf.endStamp)) - .put(amountToBuffer(wf.wireFee)) - .put(amountToBuffer(wf.closingFee)) - .put(amountToBuffer(wf.wadFee)) - .build(); - const sig = decodeCrock(wf.sig); - const pub = decodeCrock(masterPub); - if (this.primitiveWorker) { - return ( - await this.primitiveWorker.eddsaVerify({ - msg: encodeCrock(p), - pub: masterPub, - sig: encodeCrock(sig), - }) - ).valid; - } - return eddsaVerify(p, sig, pub); - } - - /** - * Check if the signature of a denomination is valid. - */ - async isValidDenom( - denom: DenominationRecord, - masterPub: string, - ): Promise { - const p = buildSigPS(TalerSignaturePurpose.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); - const res = eddsaVerify(p, sig, pub); - return res; - } - - isValidWireAccount( - versionCurrent: ExchangeProtocolVersion, - paytoUri: string, - sig: string, - masterPub: string, - ): boolean { - const paytoHash = hashTruncate32(stringToBytes(paytoUri + "\0")); - const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS) - .put(paytoHash) - .build(); - return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)); - } - - isValidContractTermsSignature( - contractTermsHash: string, - sig: string, - merchantPub: string, - ): boolean { - const cthDec = decodeCrock(contractTermsHash); - const p = buildSigPS(TalerSignaturePurpose.MERCHANT_CONTRACT) - .put(cthDec) - .build(); - return eddsaVerify(p, decodeCrock(sig), decodeCrock(merchantPub)); - } - - /** - * Create a new EdDSA key pair. - */ - createEddsaKeypair(): { priv: string; pub: string } { - const pair = createEddsaKeyPair(); - return { - priv: encodeCrock(pair.eddsaPriv), - pub: encodeCrock(pair.eddsaPub), - }; - } - - eddsaGetPublic(key: string): { priv: string; pub: string } { - return { - priv: key, - pub: encodeCrock(eddsaGetPublic(decodeCrock(key))), - }; - } - - unblindDenominationSignature(req: { - planchet: PlanchetUnblindInfo; - evSig: BlindedDenominationSignature; - }): UnblindedSignature { - if (req.evSig.cipher === DenomKeyType.Rsa) { - if (req.planchet.denomPub.cipher !== DenomKeyType.Rsa) { - throw new Error( - "planchet cipher does not match blind signature cipher", - ); - } - const denomSig = rsaUnblind( - decodeCrock(req.evSig.blinded_rsa_signature), - decodeCrock(req.planchet.denomPub.rsa_public_key), - decodeCrock(req.planchet.blindingKey), - ); - return { - cipher: DenomKeyType.Rsa, - rsa_signature: encodeCrock(denomSig), - }; - } else { - throw Error(`unblinding for cipher ${req.evSig.cipher} not implemented`); - } - } - - /** - * 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. - */ - async signDepositPermission( - depositInfo: DepositInfo, - ): Promise { - // FIXME: put extensions here if used - const hExt = new Uint8Array(64); - const hAgeCommitment = new Uint8Array(32); - let d: Uint8Array; - if (depositInfo.denomKeyType === DenomKeyType.Rsa) { - d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT) - .put(decodeCrock(depositInfo.contractTermsHash)) - .put(hAgeCommitment) - .put(hExt) - .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)) - .build(); - } else { - throw Error("unsupported exchange protocol version"); - } - const coinSigRes = await myEddsaSign(this.primitiveWorker, { - msg: encodeCrock(d), - priv: depositInfo.coinPriv, - }); - - if (depositInfo.denomKeyType === DenomKeyType.Rsa) { - const s: CoinDepositPermission = { - coin_pub: depositInfo.coinPub, - coin_sig: coinSigRes.sig, - contribution: Amounts.stringify(depositInfo.spendAmount), - h_denom: depositInfo.denomPubHash, - exchange_url: depositInfo.exchangeBaseUrl, - ub_sig: { - cipher: DenomKeyType.Rsa, - rsa_signature: depositInfo.denomSig.rsa_signature, - }, - }; - return s; - } else { - throw Error( - `unsupported denomination cipher (${depositInfo.denomKeyType})`, - ); - } - } - - async deriveRefreshSession( - req: DeriveRefreshSessionRequest, - ): Promise { - const { - newCoinDenoms, - feeRefresh: meltFee, - kappa, - meltCoinDenomPubHash, - meltCoinPriv, - meltCoinPub, - sessionSecretSeed: refreshSessionSecretSeed, - } = req; - - const currency = newCoinDenoms[0].value.currency; - let valueWithFee = Amounts.getZero(currency); - - for (const ncd of newCoinDenoms) { - const t = Amounts.add(ncd.value, ncd.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: RefreshPlanchetInfo[][] = []; - - for (let i = 0; i < kappa; i++) { - const transferKeyPair = setupRefreshTransferPub( - decodeCrock(refreshSessionSecretSeed), - i, - ); - sessionHc.update(transferKeyPair.ecdhePub); - transferPrivs.push(encodeCrock(transferKeyPair.ecdhePriv)); - transferPubs.push(encodeCrock(transferKeyPair.ecdhePub)); - } - - for (const denomSel of newCoinDenoms) { - for (let i = 0; i < denomSel.count; i++) { - if (denomSel.denomPub.cipher === DenomKeyType.Rsa) { - const denomPubHash = hashDenomPub(denomSel.denomPub); - sessionHc.update(denomPubHash); - } else { - throw new Error(); - } - } - } - - sessionHc.update(decodeCrock(meltCoinPub)); - sessionHc.update(amountToBuffer(valueWithFee)); - - for (let i = 0; i < kappa; i++) { - const planchets: RefreshPlanchetInfo[] = []; - for (let j = 0; j < newCoinDenoms.length; j++) { - const denomSel = newCoinDenoms[j]; - for (let k = 0; k < denomSel.count; k++) { - const coinIndex = planchets.length; - const transferPriv = decodeCrock(transferPrivs[i]); - const oldCoinPub = decodeCrock(meltCoinPub); - const transferSecret = keyExchangeEcdheEddsa( - transferPriv, - oldCoinPub, - ); - let coinPub: Uint8Array; - let coinPriv: Uint8Array; - let blindingFactor: Uint8Array; - // disabled while not implemented in the C code - if (0 && this.primitiveWorker) { - const r = await this.primitiveWorker.setupRefreshPlanchet({ - transfer_secret: encodeCrock(transferSecret), - coin_index: coinIndex, - }); - coinPub = decodeCrock(r.coin_pub); - coinPriv = decodeCrock(r.coin_priv); - blindingFactor = decodeCrock(r.blinding_key); - } else { - let fresh: FreshCoin = setupRefreshPlanchet( - transferSecret, - coinIndex, - ); - coinPriv = fresh.coinPriv; - coinPub = fresh.coinPub; - blindingFactor = fresh.bks; - } - const coinPubHash = hash(coinPub); - if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) { - throw Error("unsupported cipher, can't create refresh session"); - } - const rsaDenomPub = decodeCrock(denomSel.denomPub.rsa_public_key); - const ev = rsaBlind(coinPubHash, blindingFactor, rsaDenomPub); - const coinEv: CoinEnvelope = { - cipher: DenomKeyType.Rsa, - rsa_blinded_planchet: encodeCrock(ev), - }; - const coinEvHash = hashCoinEv( - coinEv, - encodeCrock(hashDenomPub(denomSel.denomPub)), - ); - const planchet: RefreshPlanchetInfo = { - blindingKey: encodeCrock(blindingFactor), - coinEv, - coinPriv: encodeCrock(coinPriv), - coinPub: encodeCrock(coinPub), - coinEvHash: encodeCrock(coinEvHash), - }; - planchets.push(planchet); - hashCoinEvInner(coinEv, sessionHc); - } - } - planchetsForGammas.push(planchets); - } - - const sessionHash = sessionHc.finish(); - let confirmData: Uint8Array; - // FIXME: fill in age commitment - const hAgeCommitment = new Uint8Array(32); - confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT) - .put(sessionHash) - .put(decodeCrock(meltCoinDenomPubHash)) - .put(hAgeCommitment) - .put(amountToBuffer(valueWithFee)) - .put(amountToBuffer(meltFee)) - .build(); - - const confirmSigResp = await myEddsaSign(this.primitiveWorker, { - msg: encodeCrock(confirmData), - priv: meltCoinPriv, - }); - - const refreshSession: DerivedRefreshSession = { - confirmSig: confirmSigResp.sig, - hash: encodeCrock(sessionHash), - meltCoinPub: meltCoinPub, - planchetsForGammas: planchetsForGammas, - transferPrivs, - transferPubs, - meltValueWithFee: valueWithFee, - }; - - 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))); - } - - async signCoinLink( - oldCoinPriv: string, - newDenomHash: string, - oldCoinPub: string, - transferPub: string, - coinEv: CoinEnvelope, - ): Promise { - const coinEvHash = hashCoinEv(coinEv, newDenomHash); - // FIXME: fill in - const hAgeCommitment = new Uint8Array(32); - const coinLink = buildSigPS(TalerSignaturePurpose.WALLET_COIN_LINK) - .put(decodeCrock(newDenomHash)) - .put(decodeCrock(transferPub)) - .put(hAgeCommitment) - .put(coinEvHash) - .build(); - const sig = await myEddsaSign(this.primitiveWorker, { - msg: encodeCrock(coinLink), - priv: oldCoinPriv, - }); - return sig.sig; - } - - benchmark(repetitions: number): BenchmarkResult { - let time_hash = BigInt(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 = BigInt(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 = BigInt(0); - for (let i = 0; i < repetitions; i++) { - const start = timer.performanceNow(); - createEddsaKeyPair(); - time_eddsa_create += timer.performanceNow() - start; - } - - let time_eddsa_sign = BigInt(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 = BigInt(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: Number(time_hash), - hash_big: Number(time_hash_big), - eddsa_create: Number(time_eddsa_create), - eddsa_sign: Number(time_eddsa_sign), - eddsa_verify: Number(time_eddsa_verify), - }, - }; - } - - makeSyncSignature(req: MakeSyncSignatureRequest): string { - const hNew = decodeCrock(req.newHash); - let hOld: Uint8Array; - if (req.oldHash) { - hOld = decodeCrock(req.oldHash); - } else { - hOld = new Uint8Array(64); - } - const sigBlob = buildSigPS(TalerSignaturePurpose.SYNC_BACKUP_UPLOAD) - .put(hOld) - .put(hNew) - .build(); - const uploadSig = eddsaSign(sigBlob, decodeCrock(req.accountPriv)); - return encodeCrock(uploadSig); - } -} diff --git a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts index df57635d1..42370fc1b 100644 --- a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts +++ b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts @@ -17,11 +17,11 @@ /** * Imports */ -import { CryptoWorkerFactory } from "./cryptoApi.js"; +import { CryptoWorkerFactory } from "./cryptoDispatcher.js"; import { CryptoWorker } from "./cryptoWorkerInterface.js"; import os from "os"; -import { CryptoImplementation } from "./cryptoImplementation.js"; import { Logger } from "@gnu-taler/taler-util"; +import { nativeCryptoR } from "../cryptoImplementation.js"; const logger = new Logger("nodeThreadWorker.ts"); @@ -69,9 +69,9 @@ const workerCode = ` * a message. */ export function handleWorkerMessage(msg: any): void { - const args = msg.args; - if (!Array.isArray(args)) { - console.error("args must be array"); + const req = msg.req; + if (typeof req !== "object") { + console.error("request must be an object"); return; } const id = msg.id; @@ -86,7 +86,7 @@ export function handleWorkerMessage(msg: any): void { } const handleRequest = async (): Promise => { - const impl = new CryptoImplementation(); + const impl = nativeCryptoR; if (!(operation in impl)) { console.error(`crypto operation '${operation}' not found`); @@ -94,12 +94,11 @@ export function handleWorkerMessage(msg: any): void { } try { - const result = await (impl as any)[operation](...args); + const result = await (impl as any)[operation](impl, req); // eslint-disable-next-line @typescript-eslint/no-var-requires const _r = "require"; - const worker_threads: typeof import("worker_threads") = module[_r]( - "worker_threads", - ); + const worker_threads: typeof import("worker_threads") = + module[_r]("worker_threads"); // const worker_threads = require("worker_threads"); const p = worker_threads.parentPort; diff --git a/packages/taler-wallet-core/src/crypto/workers/rpcClient.ts b/packages/taler-wallet-core/src/crypto/workers/rpcClient.ts new file mode 100644 index 000000000..a8df8b4c6 --- /dev/null +++ b/packages/taler-wallet-core/src/crypto/workers/rpcClient.ts @@ -0,0 +1,90 @@ +/* + 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 + */ + +/** + * Imports. + */ +import { Logger } from "@gnu-taler/taler-util"; +import child_process from "child_process"; +import type internal from "stream"; +import { OpenedPromise, openPromise } from "../../util/promiseUtils.js"; + +const logger = new Logger("synchronousWorkerFactory.ts"); + +export class CryptoRpcClient { + proc: child_process.ChildProcessByStdio< + internal.Writable, + internal.Readable, + null + >; + requests: Array<{ + p: OpenedPromise; + req: any; + }> = []; + + constructor() { + const stdoutChunks: Buffer[] = []; + this.proc = child_process.spawn("taler-crypto-worker", { + //stdio: ["pipe", "pipe", "inherit"], + stdio: ["pipe", "pipe", "inherit"], + detached: true, + }); + this.proc.on("close", (): void => { + logger.error("child process exited"); + }); + (this.proc.stdout as any).unref(); + (this.proc.stdin as any).unref(); + this.proc.unref(); + + this.proc.stdout.on("data", (x) => { + // console.log("got chunk", x.toString("utf-8")); + if (x instanceof Buffer) { + const nlIndex = x.indexOf("\n"); + if (nlIndex >= 0) { + const before = x.slice(0, nlIndex); + const after = x.slice(nlIndex + 1); + stdoutChunks.push(after); + const str = Buffer.concat([...stdoutChunks, before]).toString( + "utf-8", + ); + const req = this.requests.shift(); + if (!req) { + throw Error("request was undefined"); + } + if (this.requests.length === 0) { + this.proc.unref(); + } + //logger.info(`got response: ${str}`); + req.p.resolve(JSON.parse(str)); + } else { + stdoutChunks.push(x); + } + } else { + throw Error(`unexpected data chunk type (${typeof x})`); + } + }); + } + + async queueRequest(req: any): Promise { + const p = openPromise(); + if (this.requests.length === 0) { + this.proc.ref(); + } + this.requests.push({ req, p }); + this.proc.stdin.write(`${JSON.stringify(req)}\n`); + return p.promise; + } +} diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts index 4d341718e..1d7539ed6 100644 --- a/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts +++ b/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts @@ -16,11 +16,10 @@ import { Logger } from "@gnu-taler/taler-util"; import { - CryptoImplementation, - PrimitiveWorker -} from "./cryptoImplementation.js"; - - + nativeCryptoR, + TalerCryptoInterfaceR, +} from "../cryptoImplementation.js"; +import { CryptoRpcClient } from "./rpcClient.js"; const logger = new Logger("synchronousWorker.ts"); @@ -38,9 +37,33 @@ export class SynchronousCryptoWorker { */ onerror: undefined | ((m: any) => void); - constructor(private primitiveWorker?: PrimitiveWorker) { + cryptoImplR: TalerCryptoInterfaceR; + + rpcClient: CryptoRpcClient | undefined; + + constructor() { this.onerror = undefined; this.onmessage = undefined; + + this.cryptoImplR = { ...nativeCryptoR }; + + if ( + process.env["TALER_WALLET_RPC_CRYPRO"] || + // Old name + process.env["TALER_WALLET_PRIMITIVE_WORKER"] + ) { + const rpc = (this.rpcClient = new CryptoRpcClient()); + this.cryptoImplR.eddsaSign = async (_, req) => { + logger.trace("making RPC request"); + return await rpc.queueRequest({ + op: "eddsa_sign", + args: { + msg: req.msg, + priv: req.priv, + }, + }); + }; + } } /** @@ -66,9 +89,9 @@ export class SynchronousCryptoWorker { private async handleRequest( operation: string, id: number, - args: string[], + req: unknown, ): Promise { - const impl = new CryptoImplementation(this.primitiveWorker); + const impl = this.cryptoImplR; if (!(operation in impl)) { console.error(`crypto operation '${operation}' not found`); @@ -77,7 +100,7 @@ export class SynchronousCryptoWorker { let result: any; try { - result = await (impl as any)[operation](...args); + result = await (impl as any)[operation](impl, req); } catch (e) { logger.error("error during operation", e); return; @@ -94,9 +117,9 @@ export class SynchronousCryptoWorker { * 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"); + const req = msg.req; + if (typeof req !== "object") { + console.error("request must be an object"); return; } const id = msg.id; @@ -110,7 +133,7 @@ export class SynchronousCryptoWorker { return; } - this.handleRequest(operation, id, args).catch((e) => { + this.handleRequest(operation, id, req).catch((e) => { console.error("Error while handling crypto request:", e); }); } diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactory.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactory.ts index ca63c7687..47f58be13 100644 --- a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactory.ts +++ b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactory.ts @@ -14,121 +14,13 @@ GNU Taler; see the file COPYING. If not, see */ -import { - PrimitiveWorker, -} from "./cryptoImplementation.js"; - -import { CryptoWorkerFactory } from "./cryptoApi.js"; +/** + * Imports. + */ +import { CryptoWorkerFactory } from "./cryptoDispatcher.js"; import { CryptoWorker } from "./cryptoWorkerInterface.js"; - -import child_process from "child_process"; -import type internal from "stream"; -import { OpenedPromise, openPromise } from "../../index.js"; -import { Logger } from "@gnu-taler/taler-util"; import { SynchronousCryptoWorker } from "./synchronousWorker.js"; -const logger = new Logger("synchronousWorkerFactory.ts"); - -class MyPrimitiveWorker implements PrimitiveWorker { - proc: child_process.ChildProcessByStdio< - internal.Writable, - internal.Readable, - null - >; - requests: Array<{ - p: OpenedPromise; - req: any; - }> = []; - - constructor() { - const stdoutChunks: Buffer[] = []; - this.proc = child_process.spawn("taler-crypto-worker", { - //stdio: ["pipe", "pipe", "inherit"], - stdio: ["pipe", "pipe", "inherit"], - detached: true, - }); - this.proc.on("close", (): void => { - logger.error("child process exited"); - }); - (this.proc.stdout as any).unref(); - (this.proc.stdin as any).unref(); - this.proc.unref(); - - this.proc.stdout.on("data", (x) => { - // console.log("got chunk", x.toString("utf-8")); - if (x instanceof Buffer) { - const nlIndex = x.indexOf("\n"); - if (nlIndex >= 0) { - const before = x.slice(0, nlIndex); - const after = x.slice(nlIndex + 1); - stdoutChunks.push(after); - const str = Buffer.concat([...stdoutChunks, before]).toString( - "utf-8", - ); - const req = this.requests.shift(); - if (!req) { - throw Error("request was undefined") - } - if (this.requests.length === 0) { - this.proc.unref(); - } - //logger.info(`got response: ${str}`); - req.p.resolve(JSON.parse(str)); - } else { - stdoutChunks.push(x); - } - } else { - throw Error(`unexpected data chunk type (${typeof x})`); - } - }); - } - - async setupRefreshPlanchet(req: { - transfer_secret: string; - coin_index: number; - }): Promise<{ - coin_pub: string; - coin_priv: string; - blinding_key: string; - }> { - return this.queueRequest({ - op: "setup_refresh_planchet", - args: req, - }); - } - - async queueRequest(req: any): Promise { - const p = openPromise(); - if (this.requests.length === 0) { - this.proc.ref(); - } - this.requests.push({ req, p }); - this.proc.stdin.write(`${JSON.stringify(req)}\n`); - return p.promise; - } - - async eddsaVerify(req: { - msg: string; - sig: string; - pub: string; - }): Promise<{ valid: boolean }> { - return this.queueRequest({ - op: "eddsa_verify", - args: req, - }); - } - - async eddsaSign(req: { - msg: string; - priv: string; - }): Promise<{ sig: string }> { - return this.queueRequest({ - op: "eddsa_sign", - args: req, - }); - } -} - /** * The synchronous crypto worker produced by this factory doesn't run in the * background, but actually blocks the caller until the operation is done. @@ -139,12 +31,7 @@ export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory { throw Error("cannot make worker, require(...) not defined"); } - let primitiveWorker; - if (process.env["TALER_WALLET_PRIMITIVE_WORKER"]) { - primitiveWorker = new MyPrimitiveWorker(); - } - - return new SynchronousCryptoWorker(primitiveWorker); + return new SynchronousCryptoWorker(); } getConcurrency(): number { diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts index 854a3ea09..ea5a42323 100644 --- a/packages/taler-wallet-core/src/dbless.ts +++ b/packages/taler-wallet-core/src/dbless.ts @@ -47,10 +47,10 @@ import { AbsoluteTime, UnblindedSignature, } from "@gnu-taler/taler-util"; +import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { DenominationRecord } from "./db.js"; import { assembleRefreshRevealRequest, - CryptoApi, ExchangeInfo, getBankWithdrawalInfo, HttpRequestLibrary, @@ -149,7 +149,7 @@ export async function topupReserveWithDemobank( export async function withdrawCoin(args: { http: HttpRequestLibrary; - cryptoApi: CryptoApi; + cryptoApi: TalerCryptoInterface; reserveKeyPair: ReserveKeypair; denom: DenominationRecord; exchangeBaseUrl: string; @@ -212,7 +212,7 @@ export function findDenomOrThrow( export async function depositCoin(args: { http: HttpRequestLibrary; - cryptoApi: CryptoApi; + cryptoApi: TalerCryptoInterface; exchangeBaseUrl: string; coin: CoinInfo; amount: AmountString; @@ -263,7 +263,7 @@ export async function depositCoin(args: { export async function refreshCoin(req: { http: HttpRequestLibrary; - cryptoApi: CryptoApi; + cryptoApi: TalerCryptoInterface; oldCoin: CoinInfo; newDenoms: DenominationRecord[]; }): Promise { diff --git a/packages/taler-wallet-core/src/headless/helpers.ts b/packages/taler-wallet-core/src/headless/helpers.ts index 120c4cd46..7bc8235fd 100644 --- a/packages/taler-wallet-core/src/headless/helpers.ts +++ b/packages/taler-wallet-core/src/headless/helpers.ts @@ -25,7 +25,9 @@ import type { IDBFactory } from "@gnu-taler/idb-bridge"; // eslint-disable-next-line no-duplicate-imports import { - BridgeIDBFactory, MemoryBackend, shimIndexedDB + BridgeIDBFactory, + MemoryBackend, + shimIndexedDB, } from "@gnu-taler/idb-bridge"; import { AccessStats } from "@gnu-taler/idb-bridge/src/MemoryBackend"; import { Logger, WalletNotification } from "@gnu-taler/taler-util"; diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts index ca701820e..979d631c0 100644 --- a/packages/taler-wallet-core/src/index.ts +++ b/packages/taler-wallet-core/src/index.ts @@ -33,9 +33,11 @@ export * from "./db-utils.js"; // Crypto and crypto workers // export * from "./crypto/workers/nodeThreadWorker.js"; -export { CryptoImplementation } from "./crypto/workers/cryptoImplementation.js"; export type { CryptoWorker } from "./crypto/workers/cryptoWorkerInterface.js"; -export { CryptoWorkerFactory, CryptoApi } from "./crypto/workers/cryptoApi.js"; +export { + CryptoWorkerFactory, + CryptoDispatcher, +} from "./crypto/workers/cryptoDispatcher.js"; export { SynchronousCryptoWorker } from "./crypto/workers/synchronousWorker.js"; export * from "./pending-types.js"; @@ -58,3 +60,8 @@ export * from "./operations/refresh.js"; export * from "./dbless.js"; +export { + nativeCryptoR, + nativeCrypto, + nullCrypto, +} from "./crypto/cryptoImplementation.js"; diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts index 904398722..5ecf796ed 100644 --- a/packages/taler-wallet-core/src/internal-wallet-state.ts +++ b/packages/taler-wallet-core/src/internal-wallet-state.ts @@ -36,7 +36,8 @@ import { DenominationPubKey, TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; -import { CryptoApi } from "./crypto/workers/cryptoApi.js"; +import { CryptoDispatcher } from "./crypto/workers/cryptoDispatcher.js"; +import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { ExchangeDetailsRecord, ExchangeRecord, WalletStoresV1 } from "./db.js"; import { PendingOperationsResponse } from "./pending-types.js"; import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js"; @@ -200,7 +201,7 @@ export interface InternalWalletState { memoProcessRefresh: AsyncOpMemoMap; memoProcessRecoup: AsyncOpMemoMap; memoProcessDeposit: AsyncOpMemoMap; - cryptoApi: CryptoApi; + cryptoApi: TalerCryptoInterface; timerGroup: TimerGroup; stopped: boolean; diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index 8ddc0c064..5013b9032 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -69,7 +69,7 @@ import { rsaBlind, stringToBytes, } from "@gnu-taler/taler-util"; -import { CryptoApi } from "../../crypto/workers/cryptoApi.js"; +import { CryptoDispatcher } from "../../crypto/workers/cryptoDispatcher.js"; import { BackupProviderRecord, BackupProviderState, @@ -99,6 +99,7 @@ import { exportBackup } from "./export.js"; import { BackupCryptoPrecomputedData, importBackup } from "./import.js"; import { getWalletBackupState, provideBackupState } from "./state.js"; import { guardOperationException } from "../common.js"; +import { TalerCryptoInterface } from "../../crypto/cryptoImplementation.js"; const logger = new Logger("operations/backup.ts"); @@ -154,7 +155,7 @@ export async function encryptBackup( * FIXME: Move computations into crypto worker. */ async function computeBackupCryptoData( - cryptoApi: CryptoApi, + cryptoApi: TalerCryptoInterface, backupContent: WalletBackupContentV1, ): Promise { const cryptoData: BackupCryptoPrecomputedData = { @@ -193,18 +194,18 @@ async function computeBackupCryptoData( } } for (const prop of backupContent.proposals) { - const contractTermsHash = await cryptoApi.hashString( - canonicalJson(prop.contract_terms_raw), - ); + const { h: contractTermsHash } = await cryptoApi.hashString({ + str: canonicalJson(prop.contract_terms_raw), + }); const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(prop.nonce_priv))); cryptoData.proposalNoncePrivToPub[prop.nonce_priv] = noncePub; cryptoData.proposalIdToContractTermsHash[prop.proposal_id] = contractTermsHash; } for (const purch of backupContent.purchases) { - const contractTermsHash = await cryptoApi.hashString( - canonicalJson(purch.contract_terms_raw), - ); + const { h: contractTermsHash } = await cryptoApi.hashString({ + str: canonicalJson(purch.contract_terms_raw), + }); const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(purch.nonce_priv))); cryptoData.proposalNoncePrivToPub[purch.nonce_priv] = noncePub; cryptoData.proposalIdToContractTermsHash[purch.proposal_id] = @@ -286,13 +287,13 @@ async function runBackupCycleForProvider( logger.trace(`trying to upload backup to ${provider.baseUrl}`); logger.trace(`old hash ${oldHash}, new hash ${newHash}`); - const syncSig = await ws.cryptoApi.makeSyncSignature({ + const syncSigResp = await ws.cryptoApi.makeSyncSignature({ newHash: encodeCrock(currentBackupHash), oldHash: provider.lastBackupHash, accountPriv: encodeCrock(accountKeyPair.eddsaPriv), }); - logger.trace(`sync signature is ${syncSig}`); + logger.trace(`sync signature is ${syncSigResp}`); const accountBackupUrl = new URL( `/backups/${encodeCrock(accountKeyPair.eddsaPub)}`, @@ -304,7 +305,7 @@ async function runBackupCycleForProvider( body: encBackup, headers: { "content-type": "application/octet-stream", - "sync-signature": syncSig, + "sync-signature": syncSigResp.sig, "if-none-match": newHash, ...(provider.lastBackupHash ? { diff --git a/packages/taler-wallet-core/src/operations/backup/state.ts b/packages/taler-wallet-core/src/operations/backup/state.ts index f25cc170a..293f56137 100644 --- a/packages/taler-wallet-core/src/operations/backup/state.ts +++ b/packages/taler-wallet-core/src/operations/backup/state.ts @@ -41,7 +41,7 @@ export async function provideBackupState( } // We need to generate the key outside of the transaction // due to how IndexedDB works. - const k = await ws.cryptoApi.createEddsaKeypair(); + const k = await ws.cryptoApi.createEddsaKeypair({}); const d = getRandomBytes(5); // FIXME: device ID should be configured when wallet is initialized // and be based on hostname diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index 5525b4deb..5261b114d 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -15,7 +15,7 @@ */ import { TalerErrorDetail, TalerErrorCode } from "@gnu-taler/taler-util"; -import { CryptoApiStoppedError } from "../crypto/workers/cryptoApi.js"; +import { CryptoApiStoppedError } from "../crypto/workers/cryptoDispatcher.js"; import { TalerError, getErrorDetailFromException } from "../errors.js"; /** diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index ad3f614f3..2e14afdf1 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -254,14 +254,14 @@ export async function trackDepositGroup( `deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${dp.coin_pub}`, dp.exchange_url, ); - const sig = await ws.cryptoApi.signTrackTransaction({ + const sigResp = await ws.cryptoApi.signTrackTransaction({ coinPub: dp.coin_pub, contractTermsHash: depositGroup.contractTermsHash, merchantPriv: depositGroup.merchantPriv, merchantPub: depositGroup.merchantPub, wireHash, }); - url.searchParams.set("merchant_sig", sig); + url.searchParams.set("merchant_sig", sigResp.sig); const httpResp = await ws.http.get(url.href); const body = await httpResp.json(); responses.push({ @@ -391,8 +391,8 @@ export async function createDepositGroup( const now = AbsoluteTime.now(); const nowRounded = AbsoluteTime.toTimestamp(now); - const noncePair = await ws.cryptoApi.createEddsaKeypair(); - const merchantPair = await ws.cryptoApi.createEddsaKeypair(); + const noncePair = await ws.cryptoApi.createEddsaKeypair({}); + const merchantPair = await ws.cryptoApi.createEddsaKeypair({}); const wireSalt = encodeCrock(getRandomBytes(16)); const wireHash = hashWire(req.depositPaytoUri, wireSalt); const contractTerms: ContractTerms = { @@ -421,9 +421,9 @@ export async function createDepositGroup( refund_deadline: TalerProtocolTimestamp.zero(), }; - const contractTermsHash = await ws.cryptoApi.hashString( - canonicalJson(contractTerms), - ); + const { h: contractTermsHash } = await ws.cryptoApi.hashString({ + str: canonicalJson(contractTerms), + }); const contractData = extractContractData( contractTerms, diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 94159369b..51b5c7806 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -46,7 +46,7 @@ import { TalerProtocolDuration, } from "@gnu-taler/taler-util"; import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util"; -import { CryptoApi } from "../crypto/workers/cryptoApi.js"; +import { CryptoDispatcher } from "../crypto/workers/cryptoDispatcher.js"; import { DenominationRecord, DenominationVerificationStatus, @@ -243,12 +243,13 @@ async function validateWireInfo( if (ws.insecureTrustExchange) { isValid = true; } else { - isValid = await ws.cryptoApi.isValidWireAccount( + const { valid: v } = await ws.cryptoApi.isValidWireAccount({ + masterPub: masterPublicKey, + paytoUri: a.payto_uri, + sig: a.master_sig, versionCurrent, - a.payto_uri, - a.master_sig, - masterPublicKey, - ); + }); + isValid = v; } if (!isValid) { throw Error("exchange acct signature invalid"); @@ -272,11 +273,12 @@ async function validateWireInfo( if (ws.insecureTrustExchange) { isValid = true; } else { - isValid = await ws.cryptoApi.isValidWireFee( - wireMethod, - fee, - masterPublicKey, - ); + const { valid: v } = await ws.cryptoApi.isValidWireFee({ + masterPub: masterPublicKey, + type: wireMethod, + wf: fee, + }); + isValid = v; } if (!isValid) { throw Error("exchange wire fee signature invalid"); diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 1c1a0f506..97f38bae6 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -55,7 +55,10 @@ import { TransactionType, URL, } from "@gnu-taler/taler-util"; -import { EXCHANGE_COINS_LOCK, InternalWalletState } from "../internal-wallet-state.js"; +import { + EXCHANGE_COINS_LOCK, + InternalWalletState, +} from "../internal-wallet-state.js"; import { AbortStatus, AllowedAuditorInfo, @@ -100,6 +103,7 @@ import { import { getExchangeDetails } from "./exchanges.js"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js"; import { guardOperationException } from "./common.js"; +import { EddsaKeypair } from "../crypto/cryptoImplementation.js"; /** * Logger. @@ -795,11 +799,11 @@ async function processDownloadProposalImpl( ); } - const sigValid = await ws.cryptoApi.isValidContractTermsSignature( + const sigValid = await ws.cryptoApi.isValidContractTermsSignature({ contractTermsHash, - proposalResp.sig, - parsedContractTerms.merchant_pub, - ); + merchantPub: parsedContractTerms.merchant_pub, + sig: proposalResp.sig, + }); if (!sigValid) { const err = makeErrorDetail( @@ -921,9 +925,14 @@ async function startDownloadProposal( return oldProposal.proposalId; } - const { priv, pub } = await (noncePriv - ? ws.cryptoApi.eddsaGetPublic(noncePriv) - : ws.cryptoApi.createEddsaKeypair()); + let noncePair: EddsaKeypair; + if (noncePriv) { + noncePair = await ws.cryptoApi.eddsaGetPublic({ priv: noncePriv }); + } else { + noncePair = await ws.cryptoApi.createEddsaKeypair({}); + } + + const { priv, pub } = noncePair; const proposalId = encodeCrock(getRandomBytes(32)); const proposalRecord: ProposalRecord = { @@ -1673,11 +1682,11 @@ async function processPurchasePayImpl( logger.trace("got success from pay URL", merchantResp); const merchantPub = purchase.download.contractData.merchantPub; - const valid: boolean = await ws.cryptoApi.isValidPaymentSignature( - merchantResp.sig, - purchase.download.contractData.contractTermsHash, + const { valid } = await ws.cryptoApi.isValidPaymentSignature({ + contractHash: purchase.download.contractData.contractTermsHash, merchantPub, - ); + sig: merchantResp.sig, + }); if (!valid) { logger.error("merchant payment signature invalid"); diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index c422674a9..a77738262 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -76,9 +76,9 @@ import { RefreshNewDenomInfo, } from "../crypto/cryptoTypes.js"; import { GetReadWriteAccess } from "../util/query.js"; -import { CryptoApi } from "../index.browser.js"; import { guardOperationException } from "./common.js"; -import { CryptoApiStoppedError } from "../crypto/workers/cryptoApi.js"; +import { CryptoApiStoppedError } from "../crypto/workers/cryptoDispatcher.js"; +import { TalerCryptoInterface } from "../crypto/cryptoImplementation.js"; const logger = new Logger("refresh.ts"); @@ -461,7 +461,7 @@ async function refreshMelt( } export async function assembleRefreshRevealRequest(args: { - cryptoApi: CryptoApi; + cryptoApi: TalerCryptoInterface; derived: DerivedRefreshSession; norevealIndex: number; oldCoinPub: CoinPublicKeyString; @@ -494,14 +494,14 @@ export async function assembleRefreshRevealRequest(args: { const dsel = newDenoms[i]; for (let j = 0; j < dsel.count; j++) { const newCoinIndex = linkSigs.length; - const linkSig = await cryptoApi.signCoinLink( - oldCoinPriv, - dsel.denomPubHash, - oldCoinPub, - derived.transferPubs[norevealIndex], - planchets[newCoinIndex].coinEv, - ); - linkSigs.push(linkSig); + const linkSig = await cryptoApi.signCoinLink({ + coinEv: planchets[newCoinIndex].coinEv, + newDenomHash: dsel.denomPubHash, + oldCoinPriv: oldCoinPriv, + oldCoinPub: oldCoinPub, + transferPub: derived.transferPubs[norevealIndex], + }); + linkSigs.push(linkSig.sig); newDenomsFlat.push(dsel.denomPubHash); } } diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index dd0fa5423..9cbd63c45 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -170,7 +170,7 @@ export async function createReserve( ws: InternalWalletState, req: CreateReserveRequest, ): Promise { - const keypair = await ws.cryptoApi.createEddsaKeypair(); + const keypair = await ws.cryptoApi.createEddsaKeypair({}); const now = AbsoluteTime.toTimestamp(AbsoluteTime.now()); const canonExchange = canonicalizeBaseUrl(req.exchange); diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 7bd81b825..cd29f8a86 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -336,17 +336,17 @@ async function processTipImpl( throw Error("unsupported cipher"); } - const denomSigRsa = await ws.cryptoApi.rsaUnblind( - blindedSig.blinded_rsa_signature, - planchet.blindingKey, - denom.denomPub.rsa_public_key, - ); + const denomSigRsa = await ws.cryptoApi.rsaUnblind({ + bk: planchet.blindingKey, + blindedSig: blindedSig.blinded_rsa_signature, + pk: denom.denomPub.rsa_public_key, + }); - const isValid = await ws.cryptoApi.rsaVerify( - planchet.coinPub, - denomSigRsa, - denom.denomPub.rsa_public_key, - ); + const isValid = await ws.cryptoApi.rsaVerify({ + hm: planchet.coinPub, + pk: denom.denomPub.rsa_public_key, + sig: denomSigRsa.sig, + }); if (!isValid) { await ws.db @@ -377,7 +377,7 @@ async function processTipImpl( }, currentAmount: denom.value, denomPubHash: denom.denomPubHash, - denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa }, + denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig }, exchangeBaseUrl: tipRecord.exchangeBaseUrl, status: CoinStatus.Fresh, suspended: false, diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index b7feae06a..7685ede73 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -603,17 +603,17 @@ async function processPlanchetVerifyAndStoreCoin( throw Error("unsupported cipher"); } - const denomSigRsa = await ws.cryptoApi.rsaUnblind( - evSig.blinded_rsa_signature, - planchet.blindingKey, - planchetDenomPub.rsa_public_key, - ); + const denomSigRsa = await ws.cryptoApi.rsaUnblind({ + bk: planchet.blindingKey, + blindedSig: evSig.blinded_rsa_signature, + pk: planchetDenomPub.rsa_public_key, + }); - const isValid = await ws.cryptoApi.rsaVerify( - planchet.coinPub, - denomSigRsa, - planchetDenomPub.rsa_public_key, - ); + const isValid = await ws.cryptoApi.rsaVerify({ + hm: planchet.coinPub, + pk: planchetDenomPub.rsa_public_key, + sig: denomSigRsa.sig, + }); if (!isValid) { await ws.db @@ -640,7 +640,7 @@ async function processPlanchetVerifyAndStoreCoin( if (planchetDenomPub.cipher === DenomKeyType.Rsa) { denomSig = { cipher: planchetDenomPub.cipher, - rsa_signature: denomSigRsa, + rsa_signature: denomSigRsa.sig, }; } else { throw Error("unsupported cipher"); @@ -759,10 +759,11 @@ export async function updateWithdrawalDenoms( if (ws.insecureTrustExchange) { valid = true; } else { - valid = await ws.cryptoApi.isValidDenom( + const res = await ws.cryptoApi.isValidDenom({ denom, - exchangeDetails.masterPublicKey, - ); + masterPub: exchangeDetails.masterPublicKey, + }); + valid = res.valid; } logger.trace(`Done validating ${denom.denomPubHash}`); if (!valid) { diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 5e71cfbc7..d1e9aa646 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -90,7 +90,10 @@ import { RecoupOperations, ReserveOperations, } from "./internal-wallet-state.js"; -import { CryptoApi, CryptoWorkerFactory } from "./crypto/workers/cryptoApi.js"; +import { + CryptoDispatcher, + CryptoWorkerFactory, +} from "./crypto/workers/cryptoDispatcher.js"; import { AuditorTrustRecord, CoinSourceType, @@ -99,10 +102,7 @@ import { ReserveRecordStatus, WalletStoresV1, } from "./db.js"; -import { - getErrorDetailFromException, - TalerError, -} from "./errors.js"; +import { getErrorDetailFromException, TalerError } from "./errors.js"; import { exportBackup } from "./operations/backup/export.js"; import { addBackupProvider, @@ -193,6 +193,10 @@ import { import { DbAccess, GetReadWriteAccess } from "./util/query.js"; import { TimerGroup } from "./util/timer.js"; import { WalletCoreApiClient } from "./wallet-api-types.js"; +import { + TalerCryptoInterface, + TalerCryptoInterfaceR, +} from "./crypto/cryptoImplementation.js"; const builtinAuditors: AuditorTrustRecord[] = [ { @@ -1158,7 +1162,8 @@ class InternalWalletStateImpl implements InternalWalletState { memoProcessRefresh: AsyncOpMemoMap = new AsyncOpMemoMap(); memoProcessRecoup: AsyncOpMemoMap = new AsyncOpMemoMap(); memoProcessDeposit: AsyncOpMemoMap = new AsyncOpMemoMap(); - cryptoApi: CryptoApi; + cryptoApi: TalerCryptoInterface; + cryptoDispatcher: CryptoDispatcher; merchantInfoCache: Record = {}; @@ -1213,7 +1218,8 @@ class InternalWalletStateImpl implements InternalWalletState { public http: HttpRequestLibrary, cryptoWorkerFactory: CryptoWorkerFactory, ) { - this.cryptoApi = new CryptoApi(cryptoWorkerFactory); + this.cryptoDispatcher = new CryptoDispatcher(cryptoWorkerFactory); + this.cryptoApi = this.cryptoDispatcher.cryptoApi; } async getDenomInfo( @@ -1258,7 +1264,7 @@ class InternalWalletStateImpl implements InternalWalletState { stop(): void { this.stopped = true; this.timerGroup.stopCurrentAndFutureTimers(); - this.cryptoApi.stop(); + this.cryptoDispatcher.stop(); } async runUntilDone( -- cgit v1.2.3