summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/crypto
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/crypto')
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts1787
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoTypes.ts254
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.test.ts128
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts386
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts457
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts593
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts8
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoWorkerInterface.ts65
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts64
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts136
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactoryPlain.ts38
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/synchronousWorkerPlain.ts98
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/worker-common.ts107
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;
+}