diff options
Diffstat (limited to 'packages/taler-wallet-core/src/crypto')
13 files changed, 2851 insertions, 1270 deletions
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..2a2958a71 --- /dev/null +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -0,0 +1,1787 @@ +/* + This file is part of GNU Taler + (C) 2019-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Implementation of crypto-related high-level functions for the Taler wallet. + * + * @author Florian Dold <dold@taler.net> + */ + +/** + * Imports. + */ + +import { + AgeCommitmentProof, + AgeRestriction, + AmountJson, + Amounts, + AmountString, + amountToBuffer, + BlindedDenominationSignature, + bufferForUint32, + bufferForUint64, + buildSigPS, + canonicalJson, + CoinDepositPermission, + CoinEnvelope, + createHashContext, + decodeCrock, + decryptContractForDeposit, + decryptContractForMerge, + DenomKeyType, + DepositInfo, + durationRoundedToBuffer, + ecdhGetPublic, + eddsaGetPublic, + EddsaPublicKeyString, + eddsaSign, + eddsaVerify, + encodeCrock, + encryptContractForDeposit, + encryptContractForMerge, + ExchangeProtocolVersion, + getRandomBytes, + GlobalFees, + hash, + HashCodeString, + hashCoinEv, + hashCoinEvInner, + hashCoinPub, + hashDenomPub, + hashTruncate32, + kdf, + kdfKw, + keyExchangeEcdhEddsa, + Logger, + MakeSyncSignatureRequest, + PlanchetCreationRequest, + PlanchetUnblindInfo, + PurseDeposit, + RecoupRefreshRequest, + RecoupRequest, + RefreshPlanchetInfo, + rsaBlind, + rsaUnblind, + rsaVerify, + setupTipPlanchet, + stringToBytes, + TalerProtocolTimestamp, + TalerSignaturePurpose, + timestampRoundedToBuffer, + UnblindedSignature, + WireFee, + WithdrawalPlanchet, +} from "@gnu-taler/taler-util"; +// FIXME: Crypto should not use DB Types! +import { DenominationRecord, timestampProtocolFromDb } from "../db.js"; +import { + CreateRecoupRefreshReqRequest, + CreateRecoupReqRequest, + DecryptContractForDepositRequest, + DecryptContractForDepositResponse, + DecryptContractRequest, + DecryptContractResponse, + DerivedRefreshSession, + DerivedTipPlanchet, + DeriveRefreshSessionRequest, + DeriveTipRequest, + EncryptContractForDepositRequest, + EncryptContractForDepositResponse, + EncryptContractRequest, + EncryptContractResponse, + SignCoinHistoryRequest, + SignCoinHistoryResponse, + SignDeletePurseRequest, + SignDeletePurseResponse, + SignPurseMergeRequest, + SignPurseMergeResponse, + SignRefundRequest, + SignRefundResponse, + SignReservePurseCreateRequest, + SignReservePurseCreateResponse, + 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<WithdrawalPlanchet>; + + eddsaSign(req: EddsaSignRequest): Promise<EddsaSignResponse>; + + /** + * Create a planchet used for tipping, including the private keys. + */ + createTipPlanchet(req: DeriveTipRequest): Promise<DerivedTipPlanchet>; + + signTrackTransaction( + req: SignTrackTransactionRequest, + ): Promise<EddsaSigningResult>; + + createRecoupRequest(req: CreateRecoupReqRequest): Promise<RecoupRequest>; + + createRecoupRefreshRequest( + req: CreateRecoupRefreshReqRequest, + ): Promise<RecoupRefreshRequest>; + + isValidPaymentSignature( + req: PaymentSignatureValidationRequest, + ): Promise<ValidationResult>; + + isValidWireFee(req: WireFeeValidationRequest): Promise<ValidationResult>; + + isValidGlobalFees( + req: GlobalFeesValidationRequest, + ): Promise<ValidationResult>; + + isValidDenom(req: DenominationValidationRequest): Promise<ValidationResult>; + + isValidWireAccount( + req: WireAccountValidationRequest, + ): Promise<ValidationResult>; + + isValidContractTermsSignature( + req: ContractTermsValidationRequest, + ): Promise<ValidationResult>; + + createEddsaKeypair(req: {}): Promise<EddsaKeypair>; + + eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>; + + unblindDenominationSignature( + req: UnblindDenominationSignatureRequest, + ): Promise<UnblindedSignature>; + + rsaUnblind(req: RsaUnblindRequest): Promise<RsaUnblindResponse>; + + rsaVerify(req: RsaVerificationRequest): Promise<ValidationResult>; + + rsaBlind(req: RsaBlindRequest): Promise<RsaBlindResponse>; + + signDepositPermission( + depositInfo: DepositInfo, + ): Promise<CoinDepositPermission>; + + deriveRefreshSession( + req: DeriveRefreshSessionRequest, + ): Promise<DerivedRefreshSession>; + + hashString(req: HashStringRequest): Promise<HashStringResult>; + + signCoinLink(req: SignCoinLinkRequest): Promise<EddsaSigningResult>; + + makeSyncSignature(req: MakeSyncSignatureRequest): Promise<EddsaSigningResult>; + + setupRefreshPlanchet( + req: SetupRefreshPlanchetRequest, + ): Promise<FreshCoinEncoded>; + + setupWithdrawalPlanchet( + req: SetupWithdrawalPlanchetRequest, + ): Promise<FreshCoinEncoded>; + + keyExchangeEcdheEddsa( + req: KeyExchangeEcdheEddsaRequest, + ): Promise<KeyExchangeResult>; + + ecdheGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>; + + setupRefreshTransferPub( + req: SetupRefreshTransferPubRequest, + ): Promise<TransferPubResponse>; + + signPurseCreation(req: SignPurseCreationRequest): Promise<EddsaSigningResult>; + + signReserveHistoryReq( + req: SignReserveHistoryReqRequest, + ): Promise<SignReserveHistoryReqResponse>; + + signPurseDeposits( + req: SignPurseDepositsRequest, + ): Promise<SignPurseDepositsResponse>; + + encryptContractForMerge( + req: EncryptContractRequest, + ): Promise<EncryptContractResponse>; + + decryptContractForMerge( + req: DecryptContractRequest, + ): Promise<DecryptContractResponse>; + + encryptContractForDeposit( + req: EncryptContractForDepositRequest, + ): Promise<EncryptContractForDepositResponse>; + + decryptContractForDeposit( + req: DecryptContractForDepositRequest, + ): Promise<DecryptContractForDepositResponse>; + + signPurseMerge(req: SignPurseMergeRequest): Promise<SignPurseMergeResponse>; + + signReservePurseCreate( + req: SignReservePurseCreateRequest, + ): Promise<SignReservePurseCreateResponse>; + + signRefund(req: SignRefundRequest): Promise<SignRefundResponse>; + + signDeletePurse( + req: SignDeletePurseRequest, + ): Promise<SignDeletePurseResponse>; + + signCoinHistoryRequest( + req: SignCoinHistoryRequest, + ): Promise<SignCoinHistoryResponse>; +} + +/** + * 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<WithdrawalPlanchet> { + throw new Error("Function not implemented."); + }, + eddsaSign: function (req: EddsaSignRequest): Promise<EddsaSignResponse> { + throw new Error("Function not implemented."); + }, + createTipPlanchet: function ( + req: DeriveTipRequest, + ): Promise<DerivedTipPlanchet> { + throw new Error("Function not implemented."); + }, + signTrackTransaction: function ( + req: SignTrackTransactionRequest, + ): Promise<EddsaSigningResult> { + throw new Error("Function not implemented."); + }, + createRecoupRequest: function ( + req: CreateRecoupReqRequest, + ): Promise<RecoupRequest> { + throw new Error("Function not implemented."); + }, + createRecoupRefreshRequest: function ( + req: CreateRecoupRefreshReqRequest, + ): Promise<RecoupRefreshRequest> { + throw new Error("Function not implemented."); + }, + isValidPaymentSignature: function ( + req: PaymentSignatureValidationRequest, + ): Promise<ValidationResult> { + throw new Error("Function not implemented."); + }, + isValidWireFee: function ( + req: WireFeeValidationRequest, + ): Promise<ValidationResult> { + throw new Error("Function not implemented."); + }, + isValidDenom: function ( + req: DenominationValidationRequest, + ): Promise<ValidationResult> { + throw new Error("Function not implemented."); + }, + isValidWireAccount: function ( + req: WireAccountValidationRequest, + ): Promise<ValidationResult> { + throw new Error("Function not implemented."); + }, + isValidGlobalFees: function ( + req: GlobalFeesValidationRequest, + ): Promise<ValidationResult> { + throw new Error("Function not implemented."); + }, + isValidContractTermsSignature: function ( + req: ContractTermsValidationRequest, + ): Promise<ValidationResult> { + throw new Error("Function not implemented."); + }, + createEddsaKeypair: function (req: unknown): Promise<EddsaKeypair> { + throw new Error("Function not implemented."); + }, + eddsaGetPublic: function (req: EddsaGetPublicRequest): Promise<EddsaKeypair> { + throw new Error("Function not implemented."); + }, + unblindDenominationSignature: function ( + req: UnblindDenominationSignatureRequest, + ): Promise<UnblindedSignature> { + throw new Error("Function not implemented."); + }, + rsaUnblind: function (req: RsaUnblindRequest): Promise<RsaUnblindResponse> { + throw new Error("Function not implemented."); + }, + rsaVerify: function (req: RsaVerificationRequest): Promise<ValidationResult> { + throw new Error("Function not implemented."); + }, + signDepositPermission: function ( + depositInfo: DepositInfo, + ): Promise<CoinDepositPermission> { + throw new Error("Function not implemented."); + }, + deriveRefreshSession: function ( + req: DeriveRefreshSessionRequest, + ): Promise<DerivedRefreshSession> { + throw new Error("Function not implemented."); + }, + hashString: function (req: HashStringRequest): Promise<HashStringResult> { + throw new Error("Function not implemented."); + }, + signCoinLink: function ( + req: SignCoinLinkRequest, + ): Promise<EddsaSigningResult> { + throw new Error("Function not implemented."); + }, + makeSyncSignature: function ( + req: MakeSyncSignatureRequest, + ): Promise<EddsaSigningResult> { + throw new Error("Function not implemented."); + }, + setupRefreshPlanchet: function ( + req: SetupRefreshPlanchetRequest, + ): Promise<FreshCoinEncoded> { + throw new Error("Function not implemented."); + }, + rsaBlind: function (req: RsaBlindRequest): Promise<RsaBlindResponse> { + throw new Error("Function not implemented."); + }, + keyExchangeEcdheEddsa: function ( + req: KeyExchangeEcdheEddsaRequest, + ): Promise<KeyExchangeResult> { + throw new Error("Function not implemented."); + }, + setupWithdrawalPlanchet: function ( + req: SetupWithdrawalPlanchetRequest, + ): Promise<FreshCoinEncoded> { + throw new Error("Function not implemented."); + }, + ecdheGetPublic: function ( + req: EddsaGetPublicRequest, + ): Promise<EddsaGetPublicResponse> { + throw new Error("Function not implemented."); + }, + setupRefreshTransferPub: function ( + req: SetupRefreshTransferPubRequest, + ): Promise<TransferPubResponse> { + throw new Error("Function not implemented."); + }, + signPurseCreation: function ( + req: SignPurseCreationRequest, + ): Promise<EddsaSigningResult> { + throw new Error("Function not implemented."); + }, + signPurseDeposits: function ( + req: SignPurseDepositsRequest, + ): Promise<SignPurseDepositsResponse> { + throw new Error("Function not implemented."); + }, + encryptContractForMerge: function ( + req: EncryptContractRequest, + ): Promise<EncryptContractResponse> { + throw new Error("Function not implemented."); + }, + decryptContractForMerge: function ( + req: DecryptContractRequest, + ): Promise<DecryptContractResponse> { + throw new Error("Function not implemented."); + }, + signPurseMerge: function ( + req: SignPurseMergeRequest, + ): Promise<SignPurseMergeResponse> { + throw new Error("Function not implemented."); + }, + encryptContractForDeposit: function ( + req: EncryptContractForDepositRequest, + ): Promise<EncryptContractForDepositResponse> { + throw new Error("Function not implemented."); + }, + decryptContractForDeposit: function ( + req: DecryptContractForDepositRequest, + ): Promise<DecryptContractForDepositResponse> { + throw new Error("Function not implemented."); + }, + signReservePurseCreate: function ( + req: SignReservePurseCreateRequest, + ): Promise<SignReservePurseCreateResponse> { + throw new Error("Function not implemented."); + }, + signRefund: function (req: SignRefundRequest): Promise<SignRefundResponse> { + throw new Error("Function not implemented."); + }, + signDeletePurse: function ( + req: SignDeletePurseRequest, + ): Promise<SignDeletePurseResponse> { + throw new Error("Function not implemented."); + }, + signCoinHistoryRequest: function ( + req: SignCoinHistoryRequest, + ): Promise<SignCoinHistoryResponse> { + throw new Error("Function not implemented."); + }, + signReserveHistoryReq: function ( + req: SignReserveHistoryReqRequest, + ): Promise<SignReserveHistoryReqResponse> { + throw new Error("Function not implemented."); + }, +}; + +export type WithArg<X> = X extends (req: infer T) => infer R + ? (tci: TalerCryptoInterfaceR, req: T) => R + : never; + +export type TalerCryptoInterfaceR = { + [x in keyof TalerCryptoInterface]: WithArg<TalerCryptoInterface[x]>; +}; + +export interface SignCoinLinkRequest { + oldCoinPriv: string; + newDenomHash: string; + oldCoinPub: string; + transferPub: string; + coinEv: CoinEnvelope; +} + +export interface SetupRefreshPlanchetRequest { + transferSecret: string; + coinNumber: number; +} + +export interface SetupWithdrawalPlanchetRequest { + secretSeed: string; + coinNumber: number; +} + +export interface SignPurseCreationRequest { + pursePriv: string; + purseExpiration: TalerProtocolTimestamp; + purseAmount: AmountString; + hContractTerms: HashCodeString; + mergePub: EddsaPublicKeyString; + minAge: number; +} + +export interface SignReserveHistoryReqRequest { + reservePriv: string; + startOffset: number; +} + +export interface SignReserveHistoryReqResponse { + sig: string; +} + +export interface SpendCoinDetails { + coinPub: string; + coinPriv: string; + contribution: AmountString; + denomPubHash: string; + denomSig: UnblindedSignature; + ageCommitmentProof: AgeCommitmentProof | undefined; +} + +export interface SignPurseDepositsRequest { + pursePub: string; + exchangeBaseUrl: string; + coins: SpendCoinDetails[]; +} + +export interface SignPurseDepositsResponse { + deposits: PurseDeposit[]; +} + +export interface RsaVerificationRequest { + hm: string; + sig: string; + pk: string; +} + +export interface RsaBlindRequest { + hm: string; + bks: string; + pub: 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 GlobalFeesValidationRequest { + gf: GlobalFees; + 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; + conversionUrl?: string; + debitRestrictions?: any[]; + creditRestrictions?: any[]; +} + +export interface EddsaKeypair { + priv: string; + pub: string; +} + +export interface EddsaGetPublicRequest { + priv: string; +} + +export interface EddsaGetPublicResponse { + pub: string; +} + +export interface EcdheGetPublicRequest { + priv: string; +} + +export interface EcdheGetPublicResponse { + pub: string; +} + +export interface UnblindDenominationSignatureRequest { + planchet: PlanchetUnblindInfo; + evSig: BlindedDenominationSignature; +} + +export interface FreshCoinEncoded { + coinPub: string; + coinPriv: string; + bks: string; +} + +export interface RsaUnblindRequest { + blindedSig: string; + bk: string; + pk: string; +} + +export interface RsaBlindResponse { + blinded: string; +} + +export interface RsaUnblindResponse { + sig: string; +} + +export interface KeyExchangeEcdheEddsaRequest { + ecdhePriv: string; + eddsaPub: string; +} + +export interface KeyExchangeResult { + h: string; +} + +export interface SetupRefreshTransferPubRequest { + secretSeed: string; + transferPubIndex: number; +} + +export interface TransferPubResponse { + transferPub: string; + transferPriv: string; +} + +/** + * JS-native implementation of the Taler crypto worker operations. + */ +export const nativeCryptoR: TalerCryptoInterfaceR = { + async eddsaSign( + tci: TalerCryptoInterfaceR, + req: EddsaSignRequest, + ): Promise<EddsaSignResponse> { + return { + sig: encodeCrock(eddsaSign(decodeCrock(req.msg), decodeCrock(req.priv))), + }; + }, + + async rsaBlind( + tci: TalerCryptoInterfaceR, + req: RsaBlindRequest, + ): Promise<RsaBlindResponse> { + const res = rsaBlind( + decodeCrock(req.hm), + decodeCrock(req.bks), + decodeCrock(req.pub), + ); + return { + blinded: encodeCrock(res), + }; + }, + + async setupRefreshPlanchet( + tci: TalerCryptoInterfaceR, + req: SetupRefreshPlanchetRequest, + ): Promise<FreshCoinEncoded> { + const transferSecret = decodeCrock(req.transferSecret); + const coinNumber = req.coinNumber; + // See TALER_transfer_secret_to_planchet_secret in C impl + const planchetMasterSecret = kdfKw({ + ikm: transferSecret, + outputLength: 32, + salt: bufferForUint32(coinNumber), + info: stringToBytes("taler-coin-derivation"), + }); + + const coinPriv = kdfKw({ + ikm: planchetMasterSecret, + outputLength: 32, + salt: stringToBytes("coin"), + }); + + const bks = kdfKw({ + ikm: planchetMasterSecret, + outputLength: 32, + salt: stringToBytes("bks"), + }); + + const coinPrivEnc = encodeCrock(coinPriv); + const coinPubRes = await tci.eddsaGetPublic(tci, { + priv: coinPrivEnc, + }); + + return { + bks: encodeCrock(bks), + coinPriv: coinPrivEnc, + coinPub: coinPubRes.pub, + }; + }, + + async setupWithdrawalPlanchet( + tci: TalerCryptoInterfaceR, + req: SetupWithdrawalPlanchetRequest, + ): Promise<FreshCoinEncoded> { + const info = stringToBytes("taler-withdrawal-coin-derivation"); + const saltArrBuf = new ArrayBuffer(4); + const salt = new Uint8Array(saltArrBuf); + const saltDataView = new DataView(saltArrBuf); + saltDataView.setUint32(0, req.coinNumber); + const secretSeedDec = decodeCrock(req.secretSeed); + const out = kdf(64, secretSeedDec, salt, info); + const coinPriv = out.slice(0, 32); + const bks = out.slice(32, 64); + const coinPrivEnc = encodeCrock(coinPriv); + const coinPubRes = await tci.eddsaGetPublic(tci, { + priv: coinPrivEnc, + }); + return { + bks: encodeCrock(bks), + coinPriv: coinPrivEnc, + coinPub: coinPubRes.pub, + }; + }, + + async createPlanchet( + tci: TalerCryptoInterfaceR, + req: PlanchetCreationRequest, + ): Promise<WithdrawalPlanchet> { + const denomPub = req.denomPub; + if (denomPub.cipher === DenomKeyType.Rsa) { + const reservePub = decodeCrock(req.reservePub); + const derivedPlanchet = await tci.setupWithdrawalPlanchet(tci, { + coinNumber: req.coinIndex, + secretSeed: req.secretSeed, + }); + + let maybeAcp: AgeCommitmentProof | undefined = undefined; + let maybeAgeCommitmentHash: string | undefined = undefined; + if (denomPub.age_mask) { + const age = req.restrictAge || AgeRestriction.AGE_UNRESTRICTED; + logger.info(`creating age-restricted planchet (age ${age})`); + maybeAcp = await AgeRestriction.restrictionCommitSeeded( + denomPub.age_mask, + age, + stringToBytes(req.secretSeed), + ); + maybeAgeCommitmentHash = AgeRestriction.hashCommitment( + maybeAcp.commitment, + ); + } + + const coinPubHash = hashCoinPub( + derivedPlanchet.coinPub, + maybeAgeCommitmentHash, + ); + + const blindResp = await tci.rsaBlind(tci, { + bks: derivedPlanchet.bks, + hm: encodeCrock(coinPubHash), + pub: denomPub.rsa_public_key, + }); + const coinEv: CoinEnvelope = { + cipher: DenomKeyType.Rsa, + rsa_blinded_planchet: blindResp.blinded, + }; + 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: derivedPlanchet.bks, + coinEv, + coinPriv: derivedPlanchet.coinPriv, + coinPub: derivedPlanchet.coinPub, + coinValue: req.value, + denomPub, + denomPubHash: encodeCrock(denomPubHash), + reservePub: encodeCrock(reservePub), + withdrawSig: sigResult.sig, + coinEvHash: encodeCrock(evHash), + ageCommitmentProof: maybeAcp, + }; + return planchet; + } else { + throw Error("unsupported cipher, unable to create planchet"); + } + }, + + async createTipPlanchet( + tci: TalerCryptoInterfaceR, + req: DeriveTipRequest, + ): Promise<DerivedTipPlanchet> { + if (req.denomPub.cipher !== DenomKeyType.Rsa) { + throw Error(`unsupported cipher (${req.denomPub.cipher})`); + } + const fc = await setupTipPlanchet( + decodeCrock(req.secretSeed), + req.denomPub, + req.planchetIndex, + ); + const maybeAch = fc.ageCommitmentProof + ? AgeRestriction.hashCommitment(fc.ageCommitmentProof.commitment) + : undefined; + const denomPub = decodeCrock(req.denomPub.rsa_public_key); + const coinPubHash = hashCoinPub(encodeCrock(fc.coinPub), maybeAch); + const blindResp = await tci.rsaBlind(tci, { + bks: encodeCrock(fc.bks), + hm: encodeCrock(coinPubHash), + pub: encodeCrock(denomPub), + }); + const coinEv = { + cipher: DenomKeyType.Rsa, + rsa_blinded_planchet: blindResp.blinded, + }; + const tipPlanchet: DerivedTipPlanchet = { + blindingKey: encodeCrock(fc.bks), + coinEv, + coinEvHash: encodeCrock( + hashCoinEv(coinEv, encodeCrock(hashDenomPub(req.denomPub))), + ), + coinPriv: encodeCrock(fc.coinPriv), + coinPub: encodeCrock(fc.coinPub), + ageCommitmentProof: fc.ageCommitmentProof, + }; + return tipPlanchet; + }, + + async signTrackTransaction( + tci: TalerCryptoInterfaceR, + req: SignTrackTransactionRequest, + ): Promise<EddsaSigningResult> { + const p = buildSigPS(TalerSignaturePurpose.MERCHANT_TRACK_TRANSACTION) + .put(decodeCrock(req.contractTermsHash)) + .put(decodeCrock(req.wireHash)) + .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<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. + */ + async createRecoupRefreshRequest( + tci: TalerCryptoInterfaceR, + req: CreateRecoupRefreshReqRequest, + ): Promise<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. + */ + async isValidPaymentSignature( + tci: TalerCryptoInterfaceR, + req: PaymentSignatureValidationRequest, + ): Promise<ValidationResult> { + 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<ValidationResult> { + 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)) + .build(); + const sig = decodeCrock(wf.sig); + const pub = decodeCrock(masterPub); + return { valid: eddsaVerify(p, sig, pub) }; + }, + + /** + * Check if a global fee is correctly signed. + */ + async isValidGlobalFees( + tci: TalerCryptoInterfaceR, + req: GlobalFeesValidationRequest, + ): Promise<ValidationResult> { + const { gf, masterPub } = req; + const p = buildSigPS(TalerSignaturePurpose.GLOBAL_FEES) + .put(timestampRoundedToBuffer(gf.start_date)) + .put(timestampRoundedToBuffer(gf.end_date)) + .put(durationRoundedToBuffer(gf.purse_timeout)) + .put(durationRoundedToBuffer(gf.history_expiration)) + .put(amountToBuffer(Amounts.parseOrThrow(gf.history_fee))) + .put(amountToBuffer(Amounts.parseOrThrow(gf.account_fee))) + .put(amountToBuffer(Amounts.parseOrThrow(gf.purse_fee))) + .put(bufferForUint32(gf.purse_account_limit)) + .build(); + const sig = decodeCrock(gf.master_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<ValidationResult> { + const { masterPub, denom } = req; + const value: AmountJson = Amounts.parseOrThrow(denom.value); + const p = buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY) + .put(decodeCrock(masterPub)) + .put(timestampRoundedToBuffer(timestampProtocolFromDb(denom.stampStart))) + .put( + timestampRoundedToBuffer( + timestampProtocolFromDb(denom.stampExpireWithdraw), + ), + ) + .put( + timestampRoundedToBuffer( + timestampProtocolFromDb(denom.stampExpireDeposit), + ), + ) + .put( + timestampRoundedToBuffer( + timestampProtocolFromDb(denom.stampExpireLegal), + ), + ) + .put(amountToBuffer(value)) + .put(amountToBuffer(denom.fees.feeWithdraw)) + .put(amountToBuffer(denom.fees.feeDeposit)) + .put(amountToBuffer(denom.fees.feeRefresh)) + .put(amountToBuffer(denom.fees.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<ValidationResult> { + const { sig, masterPub, paytoUri } = req; + const paytoHash = hashTruncate32(stringToBytes(paytoUri + "\0")); + const pb = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS); + pb.put(paytoHash); + if (req.versionCurrent >= 15) { + let conversionUrlHash; + if (!req.conversionUrl) { + conversionUrlHash = new Uint8Array(64); + } else { + conversionUrlHash = hash(stringToBytes(req.conversionUrl + "\0")); + } + pb.put(conversionUrlHash); + pb.put(hash(stringToBytes(canonicalJson(req.debitRestrictions) + "\0"))); + pb.put(hash(stringToBytes(canonicalJson(req.creditRestrictions) + "\0"))); + } + const p = pb.build(); + return { valid: eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)) }; + }, + + async isValidContractTermsSignature( + tci: TalerCryptoInterfaceR, + req: ContractTermsValidationRequest, + ): Promise<ValidationResult> { + 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<EddsaKeypair> { + const eddsaPriv = encodeCrock(getRandomBytes(32)); + const eddsaPubRes = await tci.eddsaGetPublic(tci, { + priv: eddsaPriv, + }); + return { + priv: eddsaPriv, + pub: eddsaPubRes.pub, + }; + }, + + async eddsaGetPublic( + tci: TalerCryptoInterfaceR, + req: EddsaGetPublicRequest, + ): Promise<EddsaKeypair> { + return { + priv: req.priv, + pub: encodeCrock(eddsaGetPublic(decodeCrock(req.priv))), + }; + }, + + async unblindDenominationSignature( + tci: TalerCryptoInterfaceR, + req: UnblindDenominationSignatureRequest, + ): Promise<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. + */ + async rsaUnblind( + tci: TalerCryptoInterfaceR, + req: RsaUnblindRequest, + ): Promise<RsaUnblindResponse> { + 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<ValidationResult> { + 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<CoinDepositPermission> { + // FIXME: put extensions here if used + const hExt = new Uint8Array(64); + let hAgeCommitment: Uint8Array; + let minimumAgeSig: string | undefined = undefined; + if (depositInfo.ageCommitmentProof) { + const ach = AgeRestriction.hashCommitment( + depositInfo.ageCommitmentProof.commitment, + ); + hAgeCommitment = decodeCrock(ach); + if (depositInfo.requiredMinimumAge) { + minimumAgeSig = encodeCrock( + AgeRestriction.commitmentAttest( + depositInfo.ageCommitmentProof, + depositInfo.requiredMinimumAge, + ), + ); + } + } else { + // All zeros. + hAgeCommitment = new Uint8Array(32); + } + // FIXME: Actually allow passing user data here! + const walletDataHash = new Uint8Array(64); + 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)) + .put(walletDataHash) + .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, + }, + }; + + if (depositInfo.requiredMinimumAge) { + // These are only required by the merchant + s.minimum_age_sig = minimumAgeSig; + s.age_commitment = + depositInfo.ageCommitmentProof?.commitment.publicKeys; + } else if (depositInfo.ageCommitmentProof) { + s.h_age_commitment = encodeCrock(hAgeCommitment); + } + + return s; + } else { + throw Error( + `unsupported denomination cipher (${depositInfo.denomKeyType})`, + ); + } + }, + + async deriveRefreshSession( + tci: TalerCryptoInterfaceR, + req: DeriveRefreshSessionRequest, + ): Promise<DerivedRefreshSession> { + const { + newCoinDenoms, + feeRefresh: meltFee, + kappa, + meltCoinDenomPubHash, + meltCoinPriv, + meltCoinPub, + sessionSecretSeed: refreshSessionSecretSeed, + } = req; + + const currency = Amounts.currencyOf(newCoinDenoms[0].value); + let valueWithFee = Amounts.zeroOfCurrency(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 = await tci.setupRefreshTransferPub(tci, { + secretSeed: refreshSessionSecretSeed, + transferPubIndex: i, + }); + sessionHc.update(decodeCrock(transferKeyPair.transferPub)); + transferPrivs.push(transferKeyPair.transferPriv); + transferPubs.push(transferKeyPair.transferPub); + } + + 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 transferSecretRes = await tci.keyExchangeEcdheEddsa(tci, { + ecdhePriv: transferPrivs[i], + eddsaPub: meltCoinPub, + }); + let coinPub: Uint8Array; + let coinPriv: Uint8Array; + let blindingFactor: Uint8Array; + let fresh: FreshCoinEncoded = await tci.setupRefreshPlanchet(tci, { + coinNumber: coinIndex, + transferSecret: transferSecretRes.h, + }); + let newAc: AgeCommitmentProof | undefined = undefined; + let newAch: HashCodeString | undefined = undefined; + if (req.meltCoinAgeCommitmentProof) { + newAc = await AgeRestriction.commitmentDerive( + req.meltCoinAgeCommitmentProof, + decodeCrock(transferSecretRes.h), + ); + newAch = AgeRestriction.hashCommitment(newAc.commitment); + } + coinPriv = decodeCrock(fresh.coinPriv); + coinPub = decodeCrock(fresh.coinPub); + blindingFactor = decodeCrock(fresh.bks); + const coinPubHash = hashCoinPub(fresh.coinPub, newAch); + if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) { + throw Error("unsupported cipher, can't create refresh session"); + } + const blindResult = await tci.rsaBlind(tci, { + bks: encodeCrock(blindingFactor), + hm: encodeCrock(coinPubHash), + pub: denomSel.denomPub.rsa_public_key, + }); + const coinEv: CoinEnvelope = { + cipher: DenomKeyType.Rsa, + rsa_blinded_planchet: blindResult.blinded, + }; + const coinEvHash = hashCoinEv( + coinEv, + encodeCrock(hashDenomPub(denomSel.denomPub)), + ); + const planchet: RefreshPlanchetInfo = { + blindingKey: encodeCrock(blindingFactor), + coinEv, + coinPriv: encodeCrock(coinPriv), + coinPub: encodeCrock(coinPub), + coinEvHash: encodeCrock(coinEvHash), + maxAge: req.meltCoinMaxAge, + ageCommitmentProof: newAc, + }; + planchets.push(planchet); + hashCoinEvInner(coinEv, sessionHc); + } + } + planchetsForGammas.push(planchets); + } + + const sessionHash = sessionHc.finish(); + let confirmData: Uint8Array; + let hAgeCommitment: Uint8Array; + if (req.meltCoinAgeCommitmentProof) { + hAgeCommitment = decodeCrock( + AgeRestriction.hashCommitment( + req.meltCoinAgeCommitmentProof.commitment, + ), + ); + } else { + 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<HashStringResult> { + const b = stringToBytes(req.str + "\0"); + return { h: encodeCrock(hash(b)) }; + }, + + async signCoinLink( + tci: TalerCryptoInterfaceR, + req: SignCoinLinkRequest, + ): Promise<EddsaSigningResult> { + 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<EddsaSigningResult> { + 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) }; + }, + async keyExchangeEcdheEddsa( + tci: TalerCryptoInterfaceR, + req: KeyExchangeEcdheEddsaRequest, + ): Promise<KeyExchangeResult> { + return { + h: encodeCrock( + keyExchangeEcdhEddsa( + decodeCrock(req.ecdhePriv), + decodeCrock(req.eddsaPub), + ), + ), + }; + }, + async ecdheGetPublic( + tci: TalerCryptoInterfaceR, + req: EcdheGetPublicRequest, + ): Promise<EcdheGetPublicResponse> { + return { + pub: encodeCrock(ecdhGetPublic(decodeCrock(req.priv))), + }; + }, + async setupRefreshTransferPub( + tci: TalerCryptoInterfaceR, + req: SetupRefreshTransferPubRequest, + ): Promise<TransferPubResponse> { + const info = stringToBytes("taler-transfer-pub-derivation"); + const saltArrBuf = new ArrayBuffer(4); + const salt = new Uint8Array(saltArrBuf); + const saltDataView = new DataView(saltArrBuf); + saltDataView.setUint32(0, req.transferPubIndex); + const out = kdf(32, decodeCrock(req.secretSeed), salt, info); + const transferPriv = encodeCrock(out); + return { + transferPriv, + transferPub: (await tci.ecdheGetPublic(tci, { priv: transferPriv })).pub, + }; + }, + async signPurseCreation( + tci: TalerCryptoInterfaceR, + req: SignPurseCreationRequest, + ): Promise<EddsaSigningResult> { + const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE) + .put(timestampRoundedToBuffer(req.purseExpiration)) + .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount))) + .put(decodeCrock(req.hContractTerms)) + .put(decodeCrock(req.mergePub)) + .put(bufferForUint32(req.minAge)) + .build(); + return await tci.eddsaSign(tci, { + msg: encodeCrock(sigBlob), + priv: req.pursePriv, + }); + }, + async signPurseDeposits( + tci: TalerCryptoInterfaceR, + req: SignPurseDepositsRequest, + ): Promise<SignPurseDepositsResponse> { + const hExchangeBaseUrl = hash(stringToBytes(req.exchangeBaseUrl + "\0")); + const deposits: PurseDeposit[] = []; + for (const c of req.coins) { + let maybeAch: Uint8Array; + if (c.ageCommitmentProof) { + maybeAch = decodeCrock( + AgeRestriction.hashCommitment(c.ageCommitmentProof.commitment), + ); + } else { + maybeAch = new Uint8Array(32); + } + const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT) + .put(amountToBuffer(Amounts.parseOrThrow(c.contribution))) + .put(decodeCrock(c.denomPubHash)) + .put(maybeAch) + .put(decodeCrock(req.pursePub)) + .put(hExchangeBaseUrl) + .build(); + const sigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(sigBlob), + priv: c.coinPriv, + }); + deposits.push({ + amount: c.contribution, + coin_pub: c.coinPub, + coin_sig: sigResp.sig, + denom_pub_hash: c.denomPubHash, + ub_sig: c.denomSig, + age_commitment: c.ageCommitmentProof + ? c.ageCommitmentProof.commitment.publicKeys + : undefined, + }); + } + return { + deposits, + }; + }, + async encryptContractForMerge( + tci: TalerCryptoInterfaceR, + req: EncryptContractRequest, + ): Promise<EncryptContractResponse> { + const enc = await encryptContractForMerge( + decodeCrock(req.pursePub), + decodeCrock(req.contractPriv), + decodeCrock(req.mergePriv), + req.contractTerms, + decodeCrock(req.nonce), + ); + const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT) + .put(hash(enc)) + .put(decodeCrock(req.contractPub)) + .build(); + const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv)); + return { + econtract: { + contract_pub: req.contractPub, + econtract: encodeCrock(enc), + econtract_sig: encodeCrock(sig), + }, + }; + }, + async decryptContractForMerge( + tci: TalerCryptoInterfaceR, + req: DecryptContractRequest, + ): Promise<DecryptContractResponse> { + const res = await decryptContractForMerge( + decodeCrock(req.ciphertext), + decodeCrock(req.pursePub), + decodeCrock(req.contractPriv), + ); + return { + contractTerms: res.contractTerms, + mergePriv: encodeCrock(res.mergePriv), + }; + }, + async encryptContractForDeposit( + tci: TalerCryptoInterfaceR, + req: EncryptContractForDepositRequest, + ): Promise<EncryptContractForDepositResponse> { + const enc = await encryptContractForDeposit( + decodeCrock(req.pursePub), + decodeCrock(req.contractPriv), + req.contractTerms, + decodeCrock(req.nonce), + ); + const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_ECONTRACT) + .put(hash(enc)) + .put(decodeCrock(req.contractPub)) + .build(); + const sig = eddsaSign(sigBlob, decodeCrock(req.pursePriv)); + return { + econtract: { + contract_pub: req.contractPub, + econtract: encodeCrock(enc), + econtract_sig: encodeCrock(sig), + }, + }; + }, + async decryptContractForDeposit( + tci: TalerCryptoInterfaceR, + req: DecryptContractForDepositRequest, + ): Promise<DecryptContractForDepositResponse> { + const res = await decryptContractForDeposit( + decodeCrock(req.ciphertext), + decodeCrock(req.pursePub), + decodeCrock(req.contractPriv), + ); + return { + contractTerms: res.contractTerms, + }; + }, + async signPurseMerge( + tci: TalerCryptoInterfaceR, + req: SignPurseMergeRequest, + ): Promise<SignPurseMergeResponse> { + const mergeSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_MERGE) + .put(timestampRoundedToBuffer(req.mergeTimestamp)) + .put(decodeCrock(req.pursePub)) + .put(hashTruncate32(stringToBytes(req.reservePayto + "\0"))) + .build(); + const mergeSigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(mergeSigBlob), + priv: req.mergePriv, + }); + + const reserveSigBlob = buildSigPS( + TalerSignaturePurpose.WALLET_ACCOUNT_MERGE, + ) + .put(timestampRoundedToBuffer(req.purseExpiration)) + .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount))) + .put(amountToBuffer(Amounts.parseOrThrow(req.purseFee))) + .put(decodeCrock(req.contractTermsHash)) + .put(decodeCrock(req.pursePub)) + .put(timestampRoundedToBuffer(req.mergeTimestamp)) + // FIXME: put in min_age + .put(bufferForUint32(0)) + .put(bufferForUint32(req.flags)) + .build(); + + logger.info( + `signing WALLET_ACCOUNT_MERGE over ${encodeCrock(reserveSigBlob)}`, + ); + + const reserveSigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(reserveSigBlob), + priv: req.reservePriv, + }); + + return { + mergeSig: mergeSigResp.sig, + accountSig: reserveSigResp.sig, + }; + }, + async signReservePurseCreate( + tci: TalerCryptoInterfaceR, + req: SignReservePurseCreateRequest, + ): Promise<SignReservePurseCreateResponse> { + const mergeSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_MERGE) + .put(timestampRoundedToBuffer(req.mergeTimestamp)) + .put(decodeCrock(req.pursePub)) + .put(hashTruncate32(stringToBytes(req.reservePayto + "\0"))) + .build(); + const mergeSigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(mergeSigBlob), + priv: req.mergePriv, + }); + + logger.info(`payto URI: ${req.reservePayto}`); + logger.info(`signing WALLET_PURSE_MERGE over ${encodeCrock(mergeSigBlob)}`); + + const reserveSigBlob = buildSigPS( + TalerSignaturePurpose.WALLET_ACCOUNT_MERGE, + ) + .put(timestampRoundedToBuffer(req.purseExpiration)) + .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount))) + .put(amountToBuffer(Amounts.parseOrThrow(req.purseFee))) + .put(decodeCrock(req.contractTermsHash)) + .put(decodeCrock(req.pursePub)) + .put(timestampRoundedToBuffer(req.mergeTimestamp)) + // FIXME: put in min_age + .put(bufferForUint32(0)) + .put(bufferForUint32(req.flags)) + .build(); + + logger.info( + `signing WALLET_ACCOUNT_MERGE over ${encodeCrock(reserveSigBlob)}`, + ); + + const reserveSigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(reserveSigBlob), + priv: req.reservePriv, + }); + + const mergePub = encodeCrock(eddsaGetPublic(decodeCrock(req.mergePriv))); + + const purseSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE) + .put(timestampRoundedToBuffer(req.purseExpiration)) + .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount))) + .put(decodeCrock(req.contractTermsHash)) + .put(decodeCrock(mergePub)) + // FIXME: add age! + .put(bufferForUint32(0)) + .build(); + + const purseSigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(purseSigBlob), + priv: req.pursePriv, + }); + + return { + mergeSig: mergeSigResp.sig, + accountSig: reserveSigResp.sig, + purseSig: purseSigResp.sig, + }; + }, + async signRefund( + tci: TalerCryptoInterfaceR, + req: SignRefundRequest, + ): Promise<SignRefundResponse> { + const refundSigBlob = buildSigPS(TalerSignaturePurpose.MERCHANT_REFUND) + .put(decodeCrock(req.contractTermsHash)) + .put(decodeCrock(req.coinPub)) + .put(bufferForUint64(req.rtransactionId)) + .put(amountToBuffer(req.refundAmount)) + .build(); + const refundSigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(refundSigBlob), + priv: req.merchantPriv, + }); + return { + sig: refundSigResp.sig, + }; + }, + async signDeletePurse( + tci: TalerCryptoInterfaceR, + req: SignDeletePurseRequest, + ): Promise<SignDeletePurseResponse> { + const deleteSigBlob = buildSigPS( + TalerSignaturePurpose.WALLET_PURSE_DELETE, + ).build(); + const sigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(deleteSigBlob), + priv: req.pursePriv, + }); + return { + sig: sigResp.sig, + }; + }, + async signCoinHistoryRequest( + tci: TalerCryptoInterfaceR, + req: SignCoinHistoryRequest, + ): Promise<SignCoinHistoryResponse> { + const coinHistorySigBlob = buildSigPS( + TalerSignaturePurpose.WALLET_COIN_HISTORY, + ) + .put(bufferForUint64(req.startOffset)) + .build(); + const sigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(coinHistorySigBlob), + priv: req.coinPriv, + }); + return { + sig: sigResp.sig, + }; + }, + async signReserveHistoryReq( + tci: TalerCryptoInterfaceR, + req: SignReserveHistoryReqRequest, + ): Promise<SignReserveHistoryReqResponse> { + const reserveHistoryBlob = buildSigPS( + TalerSignaturePurpose.WALLET_RESERVE_HISTORY, + ) + .put(bufferForUint64(req.startOffset)) + .build(); + const sigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(reserveHistoryBlob), + priv: req.reservePriv, + }); + return { + sig: sigResp.sig, + }; + }, +}; + +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 922fbbfac..df25b87e4 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts @@ -27,13 +27,27 @@ /** * Imports. */ -import { AmountJson } from "@gnu-taler/taler-util"; +import { + AgeCommitmentProof, + AmountJson, + AmountString, + CoinEnvelope, + DenominationPubKey, + EddsaPublicKeyString, + EddsaSignatureString, + ExchangeProtocolVersion, + RefreshPlanchetInfo, + TalerProtocolTimestamp, + UnblindedSignature, + WalletAccountMergeFlags, +} from "@gnu-taler/taler-util"; export interface RefreshNewDenomInfo { count: number; - value: AmountJson; - feeWithdraw: AmountJson; - denomPub: string; + value: AmountString; + feeWithdraw: AmountString; + denomPub: DenominationPubKey; + denomPubHash: string; } /** @@ -41,11 +55,14 @@ export interface RefreshNewDenomInfo { * secret seed. */ export interface DeriveRefreshSessionRequest { + exchangeProtocolVersion: ExchangeProtocolVersion; sessionSecretSeed: string; kappa: number; meltCoinPub: string; meltCoinPriv: string; meltCoinDenomPubHash: string; + meltCoinMaxAge: number; + meltCoinAgeCommitmentProof?: AgeCommitmentProof; newCoinDenoms: RefreshNewDenomInfo[]; feeRefresh: AmountJson; } @@ -67,32 +84,7 @@ export interface DerivedRefreshSession { /** * Planchets for each cut-and-choose instance. */ - planchetsForGammas: { - /** - * Public key for the coin. - */ - publicKey: string; - - /** - * Private key for the coin. - */ - privateKey: string; - - /** - * Blinded public key. - */ - coinEv: string; - - /** - * Hash of the blinded public key. - */ - coinEvHash: string; - - /** - * Blinding key used. - */ - blindingKey: string; - }[][]; + planchetsForGammas: RefreshPlanchetInfo[][]; /** * The transfer keys, kappa of them. @@ -117,7 +109,7 @@ export interface DerivedRefreshSession { export interface DeriveTipRequest { secretSeed: string; - denomPub: string; + denomPub: DenominationPubKey; planchetIndex: number; } @@ -126,10 +118,11 @@ export interface DeriveTipRequest { */ export interface DerivedTipPlanchet { blindingKey: string; - coinEv: string; + coinEv: CoinEnvelope; coinEvHash: string; coinPriv: string; coinPub: string; + ageCommitmentProof: AgeCommitmentProof | undefined; } export interface SignTrackTransactionRequest { @@ -139,3 +132,200 @@ export interface SignTrackTransactionRequest { merchantPriv: string; merchantPub: string; } + +/** + * Request to create a recoup request payload. + */ +export interface CreateRecoupReqRequest { + coinPub: string; + coinPriv: string; + blindingKey: string; + denomPub: DenominationPubKey; + denomPubHash: string; + denomSig: UnblindedSignature; +} + +/** + * Request to create a recoup-refresh request payload. + */ +export interface CreateRecoupRefreshReqRequest { + coinPub: string; + coinPriv: string; + blindingKey: string; + denomPub: DenominationPubKey; + denomPubHash: string; + denomSig: UnblindedSignature; +} + +export interface EncryptedContract { + /** + * Encrypted contract. + */ + econtract: string; + + /** + * Signature over the (encrypted) contract. + */ + econtract_sig: EddsaSignatureString; + + /** + * Ephemeral public key for the DH operation to decrypt the encrypted contract. + */ + contract_pub: EddsaPublicKeyString; +} + +export interface EncryptContractRequest { + contractTerms: any; + contractPriv: string; + contractPub: string; + pursePub: string; + pursePriv: string; + mergePriv: string; + nonce: string; +} + +export interface EncryptContractResponse { + econtract: EncryptedContract; +} + +export interface EncryptContractForDepositRequest { + contractTerms: any; + + contractPriv: string; + contractPub: string; + + pursePub: string; + pursePriv: string; + + nonce: string; +} + +export interface EncryptContractForDepositResponse { + econtract: EncryptedContract; +} + +export interface DecryptContractRequest { + ciphertext: string; + pursePub: string; + contractPriv: string; +} + +export interface DecryptContractResponse { + contractTerms: any; + mergePriv: string; +} + +export interface DecryptContractForDepositRequest { + ciphertext: string; + pursePub: string; + contractPriv: string; +} + +export interface DecryptContractForDepositResponse { + contractTerms: any; +} + +export interface SignPurseMergeRequest { + mergeTimestamp: TalerProtocolTimestamp; + + pursePub: string; + + reservePayto: string; + + reservePriv: string; + + mergePriv: string; + + purseExpiration: TalerProtocolTimestamp; + + purseAmount: AmountString; + purseFee: AmountString; + + contractTermsHash: string; + + /** + * Flags. + */ + flags: WalletAccountMergeFlags; +} + +export interface SignPurseMergeResponse { + /** + * Signature made by the purse's merge private key. + */ + mergeSig: string; + + accountSig: string; +} + +export interface SignRefundRequest { + merchantPriv: string; + merchantPub: string; + contractTermsHash: string; + coinPub: string; + rtransactionId: number; + refundAmount: AmountString; +} + +export interface SignRefundResponse { + sig: string; +} + +export interface SignDeletePurseRequest { + pursePriv: string; +} + +export interface SignDeletePurseResponse { + sig: EddsaSignatureString; +} + +export interface SignCoinHistoryRequest { + coinPub: string; + coinPriv: string; + startOffset: number; +} + +export interface SignCoinHistoryResponse { + sig: EddsaSignatureString; +} + +export interface SignReservePurseCreateRequest { + mergeTimestamp: TalerProtocolTimestamp; + + pursePub: string; + + pursePriv: string; + + reservePayto: string; + + reservePriv: string; + + mergePriv: string; + + purseExpiration: TalerProtocolTimestamp; + + purseAmount: AmountString; + purseFee: AmountString; + + contractTermsHash: string; + + /** + * Flags. + */ + flags: WalletAccountMergeFlags; +} + +/** + * Response with signatures needed for creation of a purse + * from a reserve for a PULL payment. + */ +export interface SignReservePurseCreateResponse { + /** + * Signature made by the purse's merge private key. + */ + mergeSig: string; + + accountSig: string; + + purseSig: string; +} diff --git a/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.test.ts b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.test.ts new file mode 100644 index 000000000..96e2ee735 --- /dev/null +++ b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.test.ts @@ -0,0 +1,128 @@ +/* + This file is part of GNU Taler + (C) 2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { AbsoluteTime, TalerErrorCode } from "@gnu-taler/taler-util"; +import test from "ava"; +import { CryptoDispatcher, CryptoWorkerFactory } from "./crypto-dispatcher.js"; +import { + CryptoWorker, + CryptoWorkerResponseMessage, +} from "./cryptoWorkerInterface.js"; + +export class MyCryptoWorker implements CryptoWorker { + /** + * Function to be called when we receive a message from the worker thread. + */ + onmessage: undefined | ((m: any) => void) = undefined; + + /** + * Function to be called when we receive an error from the worker thread. + */ + onerror: undefined | ((m: any) => void) = undefined; + + /** + * Add an event listener for either an "error" or "message" event. + */ + addEventListener(event: "message" | "error", fn: (x: any) => void): void { + switch (event) { + case "message": + this.onmessage = fn; + break; + case "error": + this.onerror = fn; + break; + } + } + + private dispatchMessage(msg: any): void { + if (this.onmessage) { + this.onmessage(msg); + } + } + + /** + * Send a message to the worker thread. + */ + postMessage(msg: any): void { + const handleRequest = async () => { + let responseMsg: CryptoWorkerResponseMessage; + if (msg.operation === "testSuccess") { + responseMsg = { + id: msg.id, + type: "success", + result: { + testResult: 42, + }, + }; + } else if (msg.operation === "testError") { + responseMsg = { + id: msg.id, + type: "error", + error: { + code: TalerErrorCode.ANASTASIS_EMAIL_INVALID, + when: AbsoluteTime.now(), + hint: "bla", + }, + }; + } else if (msg.operation === "testTimeout") { + // Don't respond + return; + } + try { + setTimeout(() => this.dispatchMessage(responseMsg), 0); + } catch (e) { + console.error("got error during dispatch", e); + } + }; + handleRequest().catch((e) => { + console.error("Error while handling crypto request:", e); + }); + } + + /** + * Forcibly terminate the worker thread. + */ + terminate(): void { + // This is a no-op. + } +} + +export class MyCryptoWorkerFactory implements CryptoWorkerFactory { + startWorker(): CryptoWorker { + return new MyCryptoWorker(); + } + + getConcurrency(): number { + return 1; + } +} + +test("continues after error", async (t) => { + const cryptoDisp = new CryptoDispatcher(new MyCryptoWorkerFactory()); + const resp1 = await cryptoDisp.doRpc("testSuccess", 0, {}); + t.assert((resp1 as any).testResult === 42); + const exc = await t.throwsAsync(async () => { + const resp2 = await cryptoDisp.doRpc("testError", 0, {}); + }); + + // Check that it still works after one error. + const resp2 = await cryptoDisp.doRpc("testSuccess", 0, {}); + t.assert((resp2 as any).testResult === 42); + + // Check that it still works after timeout. + const resp3 = await cryptoDisp.doRpc("testSuccess", 0, {}); + t.assert((resp3 as any).testResult === 42); +}); diff --git a/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts new file mode 100644 index 000000000..f86163723 --- /dev/null +++ b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts @@ -0,0 +1,386 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * API to access the Taler crypto worker. + * + * @author Florian Dold + */ + +/** + * Imports. + */ +import { + j2s, + Logger, + openPromise, + performanceNow, + TalerError, + TalerErrorCode, + timer, + TimerHandle, +} from "@gnu-taler/taler-util"; +import { nullCrypto, TalerCryptoInterface } from "../cryptoImplementation.js"; +import { CryptoWorker } from "./cryptoWorkerInterface.js"; + +const logger = new Logger("cryptoDispatcher.ts"); + +/** + * State of a crypto worker. + */ +interface WorkerInfo { + /** + * 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: 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; + + state: WorkItemState; +} + +/** + * Number of different priorities. Each priority p + * must be 0 <= p < NUM_PRIO. + */ +const NUM_PRIO = 5; + +/** + * A crypto worker factory is responsible for creating new + * crypto workers on-demand. + */ +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); + } +} + +export enum WorkItemState { + Pending = 1, + Running = 2, + Finished = 3, +} + +/** + * Dispatcher for cryptographic operations to underlying crypto workers. + */ +export class CryptoDispatcher { + private nextRpcId = 1; + private workers: WorkerInfo[]; + 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(new CryptoApiStoppedError()); + 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: WorkerInfo, work: WorkItem): void { + if (this.stopped) { + return; + } + if (ws.currentWorkItem !== null) { + throw Error("assertion failed"); + } + ws.currentWorkItem = work; + this.numBusy++; + let worker: CryptoWorker; + if (!ws.w) { + worker = this.workerFactory.startWorker(); + worker.onmessage = (m: any) => this.handleWorkerMessage(ws, m); + worker.onerror = (e: any) => this.handleWorkerError(ws, e); + ws.w = worker; + } else { + worker = ws.w; + } + + const msg: any = { + req: work.req, + id: work.rpcId, + operation: work.operation, + }; + this.resetWorkerTimeout(ws); + work.startTime = performanceNow(); + work.state = WorkItemState.Running; + timer.after(0, () => worker.postMessage(msg)); + } + + resetWorkerTimeout(ws: WorkerInfo): 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(); + } + + private resetWorker(ws: WorkerInfo, e: any): void { + try { + if (ws.w) { + ws.w.terminate(); + ws.w = null; + } + } catch (e) { + logger.error(e as string); + } + if (ws.currentWorkItem !== null) { + ws.currentWorkItem.state = WorkItemState.Finished; + ws.currentWorkItem.reject(e); + ws.currentWorkItem = null; + this.numBusy--; + } + this.findWork(ws); + } + + handleWorkerError(ws: WorkerInfo, 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); + this.resetWorker(ws, e); + } + + private findWork(ws: WorkerInfo): 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: WorkerInfo, msg: any): void { + const id = msg.id; + if (typeof id !== "number") { + logger.error("rpc id must be number"); + return; + } + const currentWorkItem = ws.currentWorkItem; + ws.currentWorkItem = null; + if (!currentWorkItem) { + logger.error("unsolicited response from worker"); + return; + } + if (id !== currentWorkItem.rpcId) { + logger.error(`RPC with id ${id} has no registry entry`); + return; + } + if (currentWorkItem.state === WorkItemState.Running) { + this.numBusy--; + currentWorkItem.state = WorkItemState.Finished; + if (msg.type === "success") { + currentWorkItem.resolve(msg.result); + } else if (msg.type === "error") { + currentWorkItem.reject( + TalerError.fromDetail(TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR, { + innerError: msg.error, + }), + ); + } else { + logger.warn(`bad message: ${j2s(msg)}`); + currentWorkItem.reject(new Error("bad message from crypto worker")); + } + } + this.findWork(ws); + } + + 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<WorkerInfo>(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([]); + } + } + + doRpc<T>(operation: string, priority: number, req: unknown): Promise<T> { + if (this.stopped) { + throw new CryptoApiStoppedError(); + } + const rpcId = this.nextRpcId++; + const myProm = openPromise<T>(); + const workItem: WorkItem = { + operation, + req, + resolve: myProm.resolve, + reject: myProm.reject, + rpcId, + startTime: BigInt(0), + state: WorkItemState.Pending, + }; + let scheduled = false; + if (this.numBusy === this.workers.length) { + // All workers are busy, queue work item + const q = this.workQueues[priority]; + if (!q) { + throw Error("assertion failed"); + } + this.workQueues[priority].push(workItem); + scheduled = true; + } + if (!scheduled) { + for (const ws of this.workers) { + if (ws.currentWorkItem !== null) { + continue; + } + this.wake(ws, workItem); + scheduled = true; + break; + } + } + + if (!scheduled) { + // Could not schedule work. + 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<T>((resolve, reject) => { + let timeoutHandle: TimerHandle | undefined = undefined; + const timeoutMs = 5000; + const onTimeout = () => { + // FIXME: Maybe destroy and re-init worker if request is in processing + // state and really taking too long? + logger.warn( + `crypto RPC call ('${operation}') has been queued for a long time`, + ); + timeoutHandle = timer.after(timeoutMs, onTimeout); + }; + myProm.promise + .then((x) => { + timeoutHandle?.clear(); + resolve(x); + }) + .catch((x) => { + logger.info(`crypto RPC call ${operation} threw`); + timeoutHandle?.clear(); + reject(x); + }); + }); + } +} 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 6bace01a3..000000000 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts +++ /dev/null @@ -1,457 +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 <http://www.gnu.org/licenses/> - */ - -/** - * API to access the Taler crypto worker thread. - * @author Florian Dold - */ - -/** - * Imports. - */ -import { CoinRecord, DenominationRecord, WireFee } from "../../db.js"; - -import { CryptoWorker } from "./cryptoWorker.js"; - -import { RecoupRequest, CoinDepositPermission } from "@gnu-taler/taler-util"; - -import { - BenchmarkResult, - PlanchetCreationResult, - PlanchetCreationRequest, - DepositInfo, - MakeSyncSignatureRequest, -} from "@gnu-taler/taler-util"; - -import * as timer from "../../util/timer.js"; -import { Logger } from "@gnu-taler/taler-util"; -import { - 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. - */ - terminationTimerHandle: timer.TimerHandle | null; -} - -interface WorkItem { - operation: string; - args: any[]; - resolve: any; - reject: any; - - /** - * Serial id to identify a matching response. - */ - rpcId: number; - - /** - * Time when the work was submitted to a (non-busy) worker thread. - */ - startTime: 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; -} - -/** - * Crypto API that interfaces manages a background crypto thread - * for the execution of expensive operations. - */ -export class CryptoApi { - private nextRpcId = 1; - private workers: WorkerState[]; - private workQueues: WorkItem[][]; - - private workerFactory: CryptoWorkerFactory; - - /** - * Number of busy workers. - */ - private numBusy = 0; - - /** - * Did we stop accepting new requests? - */ - private stopped = false; - - /** - * Terminate all worker threads. - */ - terminateWorkers(): void { - for (const worker of this.workers) { - if (worker.w) { - logger.trace("terminating worker"); - worker.w.terminate(); - if (worker.terminationTimerHandle) { - worker.terminationTimerHandle.clear(); - worker.terminationTimerHandle = null; - } - if (worker.currentWorkItem) { - worker.currentWorkItem.reject(Error("explicitly terminated")); - worker.currentWorkItem = null; - } - worker.w = null; - } - } - } - - stop(): void { - this.terminateWorkers(); - this.stopped = true; - } - - /** - * Start a worker (if not started) and set as busy. - */ - wake(ws: WorkerState, work: WorkItem): void { - if (this.stopped) { - logger.trace("cryptoApi is stopped"); - return; - } - if (ws.currentWorkItem !== null) { - throw Error("assertion failed"); - } - ws.currentWorkItem = work; - this.numBusy++; - let worker: CryptoWorker; - if (!ws.w) { - worker = this.workerFactory.startWorker(); - worker.onmessage = (m: any) => this.handleWorkerMessage(ws, m); - worker.onerror = (e: any) => this.handleWorkerError(ws, e); - ws.w = worker; - } else { - worker = ws.w; - } - - const msg: any = { - args: work.args, - id: work.rpcId, - operation: work.operation, - }; - this.resetWorkerTimeout(ws); - work.startTime = timer.performanceNow(); - timer.after(0, () => worker.postMessage(msg)); - } - - resetWorkerTimeout(ws: WorkerState): void { - if (ws.terminationTimerHandle !== null) { - ws.terminationTimerHandle.clear(); - ws.terminationTimerHandle = null; - } - const destroy = (): void => { - // terminate worker if it's idle - if (ws.w && ws.currentWorkItem === null) { - ws.w.terminate(); - ws.w = null; - } - }; - ws.terminationTimerHandle = timer.after(15 * 1000, destroy); - //ws.terminationTimerHandle.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<WorkerState>(workerFactory.getConcurrency()); - - for (let i = 0; i < this.workers.length; i++) { - this.workers[i] = { - currentWorkItem: null, - terminationTimerHandle: null, - w: null, - }; - } - - this.workQueues = []; - for (let i = 0; i < NUM_PRIO; i++) { - this.workQueues.push([]); - } - } - - private doRpc<T>( - operation: string, - priority: number, - ...args: any[] - ): Promise<T> { - const p: Promise<T> = new Promise<T>((resolve, reject) => { - const rpcId = this.nextRpcId++; - const workItem: WorkItem = { - operation, - args, - resolve, - reject, - rpcId, - startTime: 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"); - }); - - return p; - } - - createPlanchet( - req: PlanchetCreationRequest, - ): Promise<PlanchetCreationResult> { - return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, req); - } - - createTipPlanchet(req: DeriveTipRequest): Promise<DerivedTipPlanchet> { - return this.doRpc<DerivedTipPlanchet>("createTipPlanchet", 1, req); - } - - signTrackTransaction(req: SignTrackTransactionRequest): Promise<string> { - return this.doRpc<string>("signTrackTransaction", 1, req); - } - - hashString(str: string): Promise<string> { - return this.doRpc<string>("hashString", 1, str); - } - - hashEncoded(encodedBytes: string): Promise<string> { - return this.doRpc<string>("hashEncoded", 1, encodedBytes); - } - - isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> { - return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub); - } - - isValidWireFee( - type: string, - wf: WireFee, - masterPub: string, - ): Promise<boolean> { - return this.doRpc<boolean>("isValidWireFee", 2, type, wf, masterPub); - } - - isValidPaymentSignature( - sig: string, - contractHash: string, - merchantPub: string, - ): Promise<boolean> { - return this.doRpc<boolean>( - "isValidPaymentSignature", - 1, - sig, - contractHash, - merchantPub, - ); - } - - signDepositPermission( - depositInfo: DepositInfo, - ): Promise<CoinDepositPermission> { - return this.doRpc<CoinDepositPermission>( - "signDepositPermission", - 3, - depositInfo, - ); - } - - createEddsaKeypair(): Promise<{ priv: string; pub: string }> { - return this.doRpc<{ priv: string; pub: string }>("createEddsaKeypair", 1); - } - - 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<string> { - return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk); - } - - rsaVerify(hm: string, sig: string, pk: string): Promise<boolean> { - return this.doRpc<boolean>("rsaVerify", 4, hm, sig, pk); - } - - isValidWireAccount( - paytoUri: string, - sig: string, - masterPub: string, - ): Promise<boolean> { - return this.doRpc<boolean>( - "isValidWireAccount", - 4, - paytoUri, - sig, - masterPub, - ); - } - - isValidContractTermsSignature( - contractTermsHash: string, - sig: string, - merchantPub: string, - ): Promise<boolean> { - return this.doRpc<boolean>( - "isValidContractTermsSignature", - 4, - contractTermsHash, - sig, - merchantPub, - ); - } - - createRecoupRequest(coin: CoinRecord): Promise<RecoupRequest> { - return this.doRpc<RecoupRequest>("createRecoupRequest", 1, coin); - } - - deriveRefreshSession( - req: DeriveRefreshSessionRequest, - ): Promise<DerivedRefreshSession> { - return this.doRpc<DerivedRefreshSession>("deriveRefreshSession", 4, req); - } - - signCoinLink( - oldCoinPriv: string, - newDenomHash: string, - oldCoinPub: string, - transferPub: string, - coinEv: string, - ): Promise<string> { - return this.doRpc<string>( - "signCoinLink", - 4, - oldCoinPriv, - newDenomHash, - oldCoinPub, - transferPub, - coinEv, - ); - } - - benchmark(repetitions: number): Promise<BenchmarkResult> { - return this.doRpc<BenchmarkResult>("benchmark", 1, repetitions); - } - - makeSyncSignature(req: MakeSyncSignatureRequest): Promise<string> { - return this.doRpc<string>("makeSyncSignature", 3, req); - } -} 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 c42ece778..000000000 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts +++ /dev/null @@ -1,593 +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 <http://www.gnu.org/licenses/> - */ - -/** - * Synchronous implementation of crypto-related functions for the wallet. - * - * The functionality is parameterized over an Emscripten environment. - * - * @author Florian Dold <dold@taler.net> - */ - -/** - * Imports. - */ - -// FIXME: Crypto should not use DB Types! -import { - CoinRecord, - DenominationRecord, - WireFee, - CoinSourceType, -} from "../../db.js"; - -import { - buildSigPS, - CoinDepositPermission, - RecoupRequest, - RefreshPlanchetInfo, - SignaturePurposeBuilder, - TalerSignaturePurpose, -} from "@gnu-taler/taler-util"; -// FIXME: These types should be internal to the wallet! -import { - BenchmarkResult, - PlanchetCreationResult, - PlanchetCreationRequest, - DepositInfo, - MakeSyncSignatureRequest, -} from "@gnu-taler/taler-util"; -import { AmountJson, Amounts } from "@gnu-taler/taler-util"; -import * as timer from "../../util/timer.js"; -import { - encodeCrock, - decodeCrock, - createEddsaKeyPair, - hash, - rsaBlind, - eddsaVerify, - eddsaSign, - rsaUnblind, - stringToBytes, - createHashContext, - keyExchangeEcdheEddsa, - setupRefreshPlanchet, - rsaVerify, - setupRefreshTransferPub, - setupTipPlanchet, - setupWithdrawPlanchet, - eddsaGetPublic, -} from "@gnu-taler/taler-util"; -import { randomBytes } from "@gnu-taler/taler-util"; -import { kdf } from "@gnu-taler/taler-util"; -import { Timestamp, timestampTruncateToSecond } from "@gnu-taler/taler-util"; - -import { Logger } from "@gnu-taler/taler-util"; -import { - DerivedRefreshSession, - DerivedTipPlanchet, - DeriveRefreshSessionRequest, - DeriveTipRequest, - SignTrackTransactionRequest, -} from "../cryptoTypes.js"; -import bigint from "big-integer"; - -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: Timestamp): Uint8Array { - const b = new ArrayBuffer(8); - const v = new DataView(b); - const tsRounded = timestampTruncateToSecond(ts); - if (typeof v.setBigUint64 !== "undefined") { - const s = BigInt(tsRounded.t_ms) * BigInt(1000); - v.setBigUint64(0, s); - } else { - const s = - tsRounded.t_ms === "never" - ? bigint.zero - : bigint(tsRounded.t_ms).times(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 class CryptoImplementation { - static enableTracing = false; - - /** - * Create a pre-coin of the given denomination to be withdrawn from then given - * reserve. - */ - createPlanchet(req: PlanchetCreationRequest): PlanchetCreationResult { - const reservePub = decodeCrock(req.reservePub); - const reservePriv = decodeCrock(req.reservePriv); - const denomPub = decodeCrock(req.denomPub); - const derivedPlanchet = setupWithdrawPlanchet( - decodeCrock(req.secretSeed), - req.coinIndex, - ); - const coinPubHash = hash(derivedPlanchet.coinPub); - const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPub); - const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount; - const denomPubHash = hash(denomPub); - const evHash = hash(ev); - - const withdrawRequest = buildSigPS( - TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW, - ) - .put(reservePub) - .put(amountToBuffer(amountWithFee)) - .put(denomPubHash) - .put(evHash) - .build(); - - const sig = eddsaSign(withdrawRequest, reservePriv); - - const planchet: PlanchetCreationResult = { - blindingKey: encodeCrock(derivedPlanchet.bks), - coinEv: encodeCrock(ev), - coinPriv: encodeCrock(derivedPlanchet.coinPriv), - coinPub: encodeCrock(derivedPlanchet.coinPub), - coinValue: req.value, - denomPub: encodeCrock(denomPub), - denomPubHash: encodeCrock(denomPubHash), - reservePub: encodeCrock(reservePub), - withdrawSig: encodeCrock(sig), - coinEvHash: encodeCrock(evHash), - }; - return planchet; - } - - /** - * Create a planchet used for tipping, including the private keys. - */ - createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet { - const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex); - const denomPub = decodeCrock(req.denomPub); - const coinPubHash = hash(fc.coinPub); - const ev = rsaBlind(coinPubHash, fc.bks, denomPub); - - const tipPlanchet: DerivedTipPlanchet = { - blindingKey: encodeCrock(fc.bks), - coinEv: encodeCrock(ev), - coinEvHash: encodeCrock(hash(ev)), - 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(coin: CoinRecord): RecoupRequest { - const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP) - .put(decodeCrock(coin.coinPub)) - .put(decodeCrock(coin.denomPubHash)) - .put(decodeCrock(coin.blindingKey)) - .build(); - - const coinPriv = decodeCrock(coin.coinPriv); - const coinSig = eddsaSign(p, coinPriv); - const paybackRequest: RecoupRequest = { - coin_blind_key_secret: coin.blindingKey, - coin_pub: coin.coinPub, - coin_sig: encodeCrock(coinSig), - denom_pub_hash: coin.denomPubHash, - denom_sig: coin.denomSig, - refreshed: coin.coinSource.type === CoinSourceType.Refresh, - }; - return paybackRequest; - } - - /** - * Check if a payment signature is valid. - */ - isValidPaymentSignature( - sig: string, - contractHash: string, - merchantPub: string, - ): boolean { - const p = buildSigPS(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. - */ - isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean { - 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)) - .build(); - const sig = decodeCrock(wf.sig); - const pub = decodeCrock(masterPub); - return eddsaVerify(p, sig, pub); - } - - /** - * Check if the signature of a denomination is valid. - */ - isValidDenom(denom: DenominationRecord, masterPub: string): boolean { - const p = buildSigPS(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( - paytoUri: string, - sig: string, - masterPub: string, - ): boolean { - const h = kdf( - 64, - stringToBytes("exchange-wire-signature"), - stringToBytes(paytoUri + "\0"), - new Uint8Array(0), - ); - const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS) - .put(h) - .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))), - }; - } - - /** - * Unblind a blindly signed value. - */ - rsaUnblind(blindedSig: string, bk: string, pk: string): string { - const denomSig = rsaUnblind( - decodeCrock(blindedSig), - decodeCrock(pk), - decodeCrock(bk), - ); - return encodeCrock(denomSig); - } - - /** - * Unblind a blindly signed value. - */ - rsaVerify(hm: string, sig: string, pk: string): boolean { - return rsaVerify(hash(decodeCrock(hm)), decodeCrock(sig), decodeCrock(pk)); - } - - /** - * Generate updated coins (to store in the database) - * and deposit permissions for each given coin. - */ - signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission { - const d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT) - .put(decodeCrock(depositInfo.contractTermsHash)) - .put(decodeCrock(depositInfo.wireInfoHash)) - .put(decodeCrock(depositInfo.denomPubHash)) - .put(timestampRoundedToBuffer(depositInfo.timestamp)) - .put(timestampRoundedToBuffer(depositInfo.refundDeadline)) - .put(amountToBuffer(depositInfo.spendAmount)) - .put(amountToBuffer(depositInfo.feeDeposit)) - .put(decodeCrock(depositInfo.merchantPub)) - .put(decodeCrock(depositInfo.coinPub)) - .build(); - const coinSig = eddsaSign(d, decodeCrock(depositInfo.coinPriv)); - - const s: CoinDepositPermission = { - coin_pub: depositInfo.coinPub, - coin_sig: encodeCrock(coinSig), - contribution: Amounts.stringify(depositInfo.spendAmount), - h_denom: depositInfo.denomPubHash, - exchange_url: depositInfo.exchangeBaseUrl, - ub_sig: depositInfo.denomSig, - }; - return s; - } - - deriveRefreshSession( - req: DeriveRefreshSessionRequest, - ): DerivedRefreshSession { - 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++) { - const r = decodeCrock(denomSel.denomPub); - sessionHc.update(r); - } - } - - 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 coinNumber = planchets.length; - const transferPriv = decodeCrock(transferPrivs[i]); - const oldCoinPub = decodeCrock(meltCoinPub); - const transferSecret = keyExchangeEcdheEddsa( - transferPriv, - oldCoinPub, - ); - const fresh = setupRefreshPlanchet(transferSecret, coinNumber); - const coinPriv = fresh.coinPriv; - const coinPub = fresh.coinPub; - const blindingFactor = fresh.bks; - const pubHash = hash(coinPub); - const denomPub = decodeCrock(denomSel.denomPub); - const ev = rsaBlind(pubHash, blindingFactor, denomPub); - const planchet: RefreshPlanchetInfo = { - blindingKey: encodeCrock(blindingFactor), - coinEv: encodeCrock(ev), - privateKey: encodeCrock(coinPriv), - publicKey: encodeCrock(coinPub), - coinEvHash: encodeCrock(hash(ev)), - }; - planchets.push(planchet); - sessionHc.update(ev); - } - } - planchetsForGammas.push(planchets); - } - - const sessionHash = sessionHc.finish(); - const confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT) - .put(sessionHash) - .put(decodeCrock(meltCoinDenomPubHash)) - .put(amountToBuffer(valueWithFee)) - .put(amountToBuffer(meltFee)) - .put(decodeCrock(meltCoinPub)) - .build(); - - const confirmSig = eddsaSign(confirmData, decodeCrock(meltCoinPriv)); - - const refreshSession: DerivedRefreshSession = { - confirmSig: encodeCrock(confirmSig), - 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))); - } - - signCoinLink( - oldCoinPriv: string, - newDenomHash: string, - oldCoinPub: string, - transferPub: string, - coinEv: string, - ): string { - const coinEvHash = hash(decodeCrock(coinEv)); - const coinLink = buildSigPS(TalerSignaturePurpose.WALLET_COIN_LINK) - .put(decodeCrock(newDenomHash)) - .put(decodeCrock(transferPub)) - .put(coinEvHash) - .build(); - const coinPriv = decodeCrock(oldCoinPriv); - const sig = eddsaSign(coinLink, coinPriv); - return encodeCrock(sig); - } - - benchmark(repetitions: number): BenchmarkResult { - let time_hash = 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/cryptoWorker.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts deleted file mode 100644 index 9f3ee6f50..000000000 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface CryptoWorker { - postMessage(message: any): void; - - terminate(): void; - - onmessage: ((m: any) => void) | undefined; - onerror: ((m: any) => void) | undefined; -} diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoWorkerInterface.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoWorkerInterface.ts new file mode 100644 index 000000000..b3620e950 --- /dev/null +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoWorkerInterface.ts @@ -0,0 +1,65 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { TalerErrorDetail } from "@gnu-taler/taler-util"; + +/** + * Common interface for all crypto workers. + */ +export interface CryptoWorker { + postMessage(message: any): void; + terminate(): void; + onmessage: ((m: any) => void) | undefined; + onerror: ((m: any) => void) | undefined; +} + +/** + * Type of requests sent to the crypto worker. + */ +export type CryptoWorkerRequestMessage = { + /** + * Operation ID to correlate request with the response. + */ + id: number; + + /** + * Operation to execute. + */ + operation: string; + + /** + * Operation-specific request payload. + */ + req: any; +}; + +/** + * Type of messages sent back by the crypto worker. + */ +export type CryptoWorkerResponseMessage = + | { + type: "success"; + id: number; + result: any; + } + | { + type: "error"; + id?: number; + error: TalerErrorDetail; + }; diff --git a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts index 3f7f9e170..eaa0108bb 100644 --- a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts +++ b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts @@ -17,15 +17,19 @@ /** * Imports */ -import { CryptoWorkerFactory } from "./cryptoApi.js"; -import { CryptoWorker } from "./cryptoWorker.js"; -import os from "os"; -import { CryptoImplementation } from "./cryptoImplementation.js"; import { Logger } from "@gnu-taler/taler-util"; +import os from "os"; +import url from "url"; +import { nativeCryptoR } from "../cryptoImplementation.js"; +import { CryptoWorkerFactory } from "./crypto-dispatcher.js"; +import { CryptoWorker } from "./cryptoWorkerInterface.js"; +import { processRequestWithImpl } from "./worker-common.js"; const logger = new Logger("nodeThreadWorker.ts"); -const f = __filename; +const f = import.meta.url + ? url.fileURLToPath(import.meta.url) + : "__not_available__"; const workerCode = ` // Try loading the glue library for embedded @@ -69,59 +73,31 @@ const workerCode = ` * a message. */ export function handleWorkerMessage(msg: any): void { - const args = msg.args; - if (!Array.isArray(args)) { - console.error("args must be array"); - return; - } - const id = msg.id; - if (typeof id !== "number") { - console.error("RPC id must be number"); - return; - } - const operation = msg.operation; - if (typeof operation !== "string") { - console.error("RPC operation must be string"); - return; - } - const handleRequest = async (): Promise<void> => { - const impl = new CryptoImplementation(); - - if (!(operation in impl)) { - console.error(`crypto operation '${operation}' not found`); - return; - } - + const responseMsg = await processRequestWithImpl(msg, nativeCryptoR); try { - const result = (impl as any)[operation](...args); // 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; - worker_threads.parentPort?.postMessage; if (p) { - p.postMessage({ data: { result, id } }); + p.postMessage(responseMsg); } else { - console.error("parent port not available (not running in thread?"); + logger.error("parent port not available (not running in thread?"); } - } catch (e) { - console.error("error during operation", e); + } catch (e: any) { + logger.error(`error in node worker: ${e.stack ?? e.toString()}`); return; } }; - handleRequest().catch((e) => { - console.error("error in node worker", e); - }); + handleRequest(); } export function handleWorkerError(e: Error): void { - console.log("got error from worker", e); + logger.error(`got error from worker: ${e.stack ?? e.toString()}`); } export class NodeThreadCryptoWorkerFactory implements CryptoWorkerFactory { @@ -162,7 +138,7 @@ class NodeThreadCryptoWorker implements CryptoWorker { this.nodeWorker = new worker_threads.Worker(workerCode, { eval: true }); this.nodeWorker.on("error", (err: Error) => { - console.error("error in node worker:", err); + logger.error("error in node worker:", err); if (this.onerror) { this.onerror(err); } @@ -175,7 +151,7 @@ class NodeThreadCryptoWorker implements CryptoWorker { this.onmessage(v); } }); - this.nodeWorker.unref(); + //this.nodeWorker.unref(); } /** diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts deleted file mode 100644 index f6b8ac5d7..000000000 --- a/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { CryptoImplementation } from "./cryptoImplementation.js"; - -import { CryptoWorkerFactory } from "./cryptoApi.js"; -import { CryptoWorker } from "./cryptoWorker.js"; - -/** - * The synchronous crypto worker produced by this factory doesn't run in the - * background, but actually blocks the caller until the operation is done. - */ -export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory { - startWorker(): CryptoWorker { - if (typeof require === "undefined") { - throw Error("cannot make worker, require(...) not defined"); - } - return new SynchronousCryptoWorker(); - } - - getConcurrency(): number { - return 1; - } -} - -/** - * Worker implementation that uses node subprocesses. - */ -export class SynchronousCryptoWorker { - /** - * Function to be called when we receive a message from the worker thread. - */ - onmessage: undefined | ((m: any) => void); - - /** - * Function to be called when we receive an error from the worker thread. - */ - onerror: undefined | ((m: any) => void); - - constructor() { - this.onerror = undefined; - this.onmessage = undefined; - } - - /** - * Add an event listener for either an "error" or "message" event. - */ - addEventListener(event: "message" | "error", fn: (x: any) => void): void { - switch (event) { - case "message": - this.onmessage = fn; - break; - case "error": - this.onerror = fn; - break; - } - } - - private dispatchMessage(msg: any): void { - if (this.onmessage) { - this.onmessage({ data: msg }); - } - } - - private async handleRequest( - operation: string, - id: number, - args: string[], - ): Promise<void> { - const impl = new CryptoImplementation(); - - if (!(operation in impl)) { - console.error(`crypto operation '${operation}' not found`); - return; - } - - let result: any; - try { - result = (impl as any)[operation](...args); - } catch (e) { - console.log("error during operation", e); - return; - } - - try { - setTimeout(() => this.dispatchMessage({ result, id }), 0); - } catch (e) { - console.log("got error during dispatch", e); - } - } - - /** - * Send a message to the worker thread. - */ - postMessage(msg: any): void { - const args = msg.args; - if (!Array.isArray(args)) { - console.error("args must be array"); - return; - } - const id = msg.id; - if (typeof id !== "number") { - console.error("RPC id must be number"); - return; - } - const operation = msg.operation; - if (typeof operation !== "string") { - console.error("RPC operation must be string"); - return; - } - - this.handleRequest(operation, id, args).catch((e) => { - console.error("Error while handling crypto request:", e); - }); - } - - /** - * Forcibly terminate the worker thread. - */ - terminate(): void { - // This is a no-op. - } -} diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryPlain.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryPlain.ts new file mode 100644 index 000000000..66381bc0e --- /dev/null +++ b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryPlain.ts @@ -0,0 +1,38 @@ +/* + This file is part of GNU Taler + (C) 2019 GNUnet e.V. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { CryptoWorkerFactory } from "./crypto-dispatcher.js"; +import { CryptoWorker } from "./cryptoWorkerInterface.js"; +import { SynchronousCryptoWorkerPlain } from "./synchronousWorkerPlain.js"; + +/** + * The synchronous crypto worker produced by this factory doesn't run in the + * background, but actually blocks the caller until the operation is done. + */ +export class SynchronousCryptoWorkerFactoryPlain + implements CryptoWorkerFactory +{ + startWorker(): CryptoWorker { + return new SynchronousCryptoWorkerPlain(); + } + + getConcurrency(): number { + return 1; + } +} diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerPlain.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerPlain.ts new file mode 100644 index 000000000..c80f2f58f --- /dev/null +++ b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerPlain.ts @@ -0,0 +1,98 @@ +/* + This file is part of GNU Taler + (C) 2019 GNUnet e.V. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { j2s, Logger } from "@gnu-taler/taler-util"; +import { + nativeCryptoR, + TalerCryptoInterfaceR, +} from "../cryptoImplementation.js"; +import { CryptoWorker } from "./cryptoWorkerInterface.js"; +import { processRequestWithImpl } from "./worker-common.js"; + +const logger = new Logger("synchronousWorker.ts"); + +/** + * Worker implementation that synchronously executes cryptographic + * operations. + */ +export class SynchronousCryptoWorkerPlain implements CryptoWorker { + /** + * Function to be called when we receive a message from the worker thread. + */ + onmessage: undefined | ((m: any) => void); + + /** + * Function to be called when we receive an error from the worker thread. + */ + onerror: undefined | ((m: any) => void); + + cryptoImplR: TalerCryptoInterfaceR; + + constructor() { + this.onerror = undefined; + this.onmessage = undefined; + this.cryptoImplR = { ...nativeCryptoR }; + } + + /** + * Add an event listener for either an "error" or "message" event. + */ + addEventListener(event: "message" | "error", fn: (x: any) => void): void { + switch (event) { + case "message": + this.onmessage = fn; + break; + case "error": + this.onerror = fn; + break; + } + } + + private dispatchMessage(msg: any): void { + if (this.onmessage) { + this.onmessage(msg); + } + } + + /** + * Send a message to the worker thread. + */ + postMessage(msg: any): void { + const handleRequest = async () => { + const responseMsg = await processRequestWithImpl(msg, this.cryptoImplR); + try { + setTimeout(() => this.dispatchMessage(responseMsg), 0); + } catch (e) { + logger.error("got error during dispatch", e); + } + }; + handleRequest().catch((e) => { + logger.error("Error while handling crypto request:", e); + logger.error("Stack:", e.stack); + logger.error(`request was ${j2s(msg)}`); + }); + } + + /** + * Forcibly terminate the worker thread. + */ + terminate(): void { + // This is a no-op. + } +} diff --git a/packages/taler-wallet-core/src/crypto/workers/worker-common.ts b/packages/taler-wallet-core/src/crypto/workers/worker-common.ts new file mode 100644 index 000000000..63147ce92 --- /dev/null +++ b/packages/taler-wallet-core/src/crypto/workers/worker-common.ts @@ -0,0 +1,107 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + j2s, + Logger, + stringifyError as safeStringifyError, + TalerErrorCode, +} from "@gnu-taler/taler-util"; +import { + getErrorDetailFromException, + makeErrorDetail, +} from "@gnu-taler/taler-util"; +import { TalerCryptoInterfaceR } from "../cryptoImplementation.js"; +import { + CryptoWorkerRequestMessage, + CryptoWorkerResponseMessage, +} from "./cryptoWorkerInterface.js"; + +const logger = new Logger("worker-common.ts"); + +/** + * Process a crypto worker request by calling into the table + * of supported operations. + * + * Does not throw, but returns an error response instead. + */ +export async function processRequestWithImpl( + reqMsg: CryptoWorkerRequestMessage, + impl: TalerCryptoInterfaceR, +): Promise<CryptoWorkerResponseMessage> { + if (typeof reqMsg !== "object") { + logger.error("request must be an object"); + return { + type: "error", + error: makeErrorDetail(TalerErrorCode.WALLET_CRYPTO_WORKER_BAD_REQUEST, { + detail: "", + }), + }; + } + const id = reqMsg.id; + if (typeof id !== "number") { + const msg = "RPC id must be number"; + logger.error(msg); + return { + type: "error", + error: makeErrorDetail(TalerErrorCode.WALLET_CRYPTO_WORKER_BAD_REQUEST, { + detail: msg, + }), + }; + } + const operation = reqMsg.operation; + if (typeof operation !== "string") { + const msg = "RPC operation must be string"; + logger.error(msg); + return { + type: "error", + id, + error: makeErrorDetail(TalerErrorCode.WALLET_CRYPTO_WORKER_BAD_REQUEST, { + detail: msg, + }), + }; + } + + if (!(operation in impl)) { + const msg = `crypto operation '${operation}' not found`; + logger.error(msg); + return { + type: "error", + id, + error: makeErrorDetail(TalerErrorCode.WALLET_CRYPTO_WORKER_BAD_REQUEST, { + detail: msg, + }), + }; + } + + let responseMsg: CryptoWorkerResponseMessage; + + try { + const result = await (impl as any)[operation](impl, reqMsg.req); + responseMsg = { type: "success", result, id }; + } catch (e: any) { + logger.error(`error during operation: ${safeStringifyError(e)}`); + responseMsg = { + type: "error", + error: getErrorDetailFromException(e), + id, + }; + } + return responseMsg; +} |