/* This file is part of GNU Taler (C) 2019-2020 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * Implementation of crypto-related high-level functions for the Taler wallet. * * @author Florian Dold */ /** * Imports. */ import { AgeCommitmentProof, AgeRestriction, AmountJson, AmountLike, Amounts, AmountString, BlindedDenominationSignature, bufferForUint32, bufferForUint64, buildSigPS, CoinDepositPermission, CoinEnvelope, createHashContext, decodeCrock, decryptContractForDeposit, decryptContractForMerge, DenomKeyType, DepositInfo, ecdhGetPublic, eddsaGetPublic, EddsaPublicKeyString, eddsaSign, eddsaVerify, encodeCrock, encryptContractForDeposit, encryptContractForMerge, ExchangeProtocolVersion, getRandomBytes, GlobalFees, hash, HashCodeString, hashCoinEv, hashCoinEvInner, hashCoinPub, hashDenomPub, hashTruncate32, j2s, kdf, kdfKw, keyExchangeEcdhEddsa, Logger, MakeSyncSignatureRequest, PlanchetCreationRequest, PlanchetUnblindInfo, PurseDeposit, RecoupRefreshRequest, RecoupRequest, RefreshPlanchetInfo, rsaBlind, rsaUnblind, rsaVerify, setupTipPlanchet, stringToBytes, TalerProtocolDuration, TalerProtocolTimestamp, TalerSignaturePurpose, UnblindedSignature, WireFee, WithdrawalPlanchet, } from "@gnu-taler/taler-util"; import bigint from "big-integer"; // FIXME: Crypto should not use DB Types! import { DenominationRecord } from "../db.js"; import { CreateRecoupRefreshReqRequest, CreateRecoupReqRequest, DecryptContractForDepositRequest, DecryptContractForDepositResponse, DecryptContractRequest, DecryptContractResponse, DerivedRefreshSession, DerivedTipPlanchet, DeriveRefreshSessionRequest, DeriveTipRequest, EncryptContractForDepositRequest, EncryptContractForDepositResponse, EncryptContractRequest, EncryptContractResponse, EncryptedContract, 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; eddsaSign(req: EddsaSignRequest): Promise; /** * Create a planchet used for tipping, including the private keys. */ createTipPlanchet(req: DeriveTipRequest): Promise; signTrackTransaction( req: SignTrackTransactionRequest, ): Promise; createRecoupRequest(req: CreateRecoupReqRequest): Promise; createRecoupRefreshRequest( req: CreateRecoupRefreshReqRequest, ): Promise; isValidPaymentSignature( req: PaymentSignatureValidationRequest, ): Promise; isValidWireFee(req: WireFeeValidationRequest): Promise; isValidGlobalFees( req: GlobalFeesValidationRequest, ): Promise; isValidDenom(req: DenominationValidationRequest): Promise; isValidWireAccount( req: WireAccountValidationRequest, ): Promise; isValidContractTermsSignature( req: ContractTermsValidationRequest, ): Promise; createEddsaKeypair(req: unknown): Promise; eddsaGetPublic(req: EddsaGetPublicRequest): Promise; unblindDenominationSignature( req: UnblindDenominationSignatureRequest, ): Promise; rsaUnblind(req: RsaUnblindRequest): Promise; rsaVerify(req: RsaVerificationRequest): Promise; rsaBlind(req: RsaBlindRequest): Promise; signDepositPermission( depositInfo: DepositInfo, ): Promise; deriveRefreshSession( req: DeriveRefreshSessionRequest, ): Promise; hashString(req: HashStringRequest): Promise; signCoinLink(req: SignCoinLinkRequest): Promise; makeSyncSignature(req: MakeSyncSignatureRequest): Promise; setupRefreshPlanchet( req: SetupRefreshPlanchetRequest, ): Promise; setupWithdrawalPlanchet( req: SetupWithdrawalPlanchetRequest, ): Promise; keyExchangeEcdheEddsa( req: KeyExchangeEcdheEddsaRequest, ): Promise; ecdheGetPublic(req: EddsaGetPublicRequest): Promise; setupRefreshTransferPub( req: SetupRefreshTransferPubRequest, ): Promise; signPurseCreation(req: SignPurseCreationRequest): Promise; signPurseDeposits( req: SignPurseDepositsRequest, ): Promise; encryptContractForMerge( req: EncryptContractRequest, ): Promise; decryptContractForMerge( req: DecryptContractRequest, ): Promise; encryptContractForDeposit( req: EncryptContractForDepositRequest, ): Promise; decryptContractForDeposit( req: DecryptContractForDepositRequest, ): Promise; signPurseMerge(req: SignPurseMergeRequest): Promise; signReservePurseCreate( req: SignReservePurseCreateRequest, ): Promise; signRefund(req: SignRefundRequest): Promise; } /** * Implementation of the Taler crypto interface where every function * always throws. Only useful in practice as a way to iterate through * all possible crypto functions. * * (This list can be easily auto-generated by your favorite IDE). */ export const nullCrypto: TalerCryptoInterface = { createPlanchet: function ( req: PlanchetCreationRequest, ): Promise { throw new Error("Function not implemented."); }, eddsaSign: function (req: EddsaSignRequest): Promise { throw new Error("Function not implemented."); }, createTipPlanchet: function ( req: DeriveTipRequest, ): Promise { throw new Error("Function not implemented."); }, signTrackTransaction: function ( req: SignTrackTransactionRequest, ): Promise { throw new Error("Function not implemented."); }, createRecoupRequest: function ( req: CreateRecoupReqRequest, ): Promise { throw new Error("Function not implemented."); }, createRecoupRefreshRequest: function ( req: CreateRecoupRefreshReqRequest, ): Promise { throw new Error("Function not implemented."); }, isValidPaymentSignature: function ( req: PaymentSignatureValidationRequest, ): Promise { throw new Error("Function not implemented."); }, isValidWireFee: function ( req: WireFeeValidationRequest, ): Promise { throw new Error("Function not implemented."); }, isValidDenom: function ( req: DenominationValidationRequest, ): Promise { throw new Error("Function not implemented."); }, isValidWireAccount: function ( req: WireAccountValidationRequest, ): Promise { throw new Error("Function not implemented."); }, isValidGlobalFees: function ( req: GlobalFeesValidationRequest, ): Promise { throw new Error("Function not implemented."); }, isValidContractTermsSignature: function ( req: ContractTermsValidationRequest, ): Promise { throw new Error("Function not implemented."); }, createEddsaKeypair: function (req: unknown): Promise { throw new Error("Function not implemented."); }, eddsaGetPublic: function (req: EddsaGetPublicRequest): Promise { throw new Error("Function not implemented."); }, unblindDenominationSignature: function ( req: UnblindDenominationSignatureRequest, ): Promise { throw new Error("Function not implemented."); }, rsaUnblind: function (req: RsaUnblindRequest): Promise { throw new Error("Function not implemented."); }, rsaVerify: function (req: RsaVerificationRequest): Promise { throw new Error("Function not implemented."); }, signDepositPermission: function ( depositInfo: DepositInfo, ): Promise { throw new Error("Function not implemented."); }, deriveRefreshSession: function ( req: DeriveRefreshSessionRequest, ): Promise { throw new Error("Function not implemented."); }, hashString: function (req: HashStringRequest): Promise { throw new Error("Function not implemented."); }, signCoinLink: function ( req: SignCoinLinkRequest, ): Promise { throw new Error("Function not implemented."); }, makeSyncSignature: function ( req: MakeSyncSignatureRequest, ): Promise { throw new Error("Function not implemented."); }, setupRefreshPlanchet: function ( req: SetupRefreshPlanchetRequest, ): Promise { throw new Error("Function not implemented."); }, rsaBlind: function (req: RsaBlindRequest): Promise { throw new Error("Function not implemented."); }, keyExchangeEcdheEddsa: function ( req: KeyExchangeEcdheEddsaRequest, ): Promise { throw new Error("Function not implemented."); }, setupWithdrawalPlanchet: function ( req: SetupWithdrawalPlanchetRequest, ): Promise { throw new Error("Function not implemented."); }, ecdheGetPublic: function ( req: EddsaGetPublicRequest, ): Promise { throw new Error("Function not implemented."); }, setupRefreshTransferPub: function ( req: SetupRefreshTransferPubRequest, ): Promise { throw new Error("Function not implemented."); }, signPurseCreation: function ( req: SignPurseCreationRequest, ): Promise { throw new Error("Function not implemented."); }, signPurseDeposits: function ( req: SignPurseDepositsRequest, ): Promise { throw new Error("Function not implemented."); }, encryptContractForMerge: function ( req: EncryptContractRequest, ): Promise { throw new Error("Function not implemented."); }, decryptContractForMerge: function ( req: DecryptContractRequest, ): Promise { throw new Error("Function not implemented."); }, signPurseMerge: function ( req: SignPurseMergeRequest, ): Promise { throw new Error("Function not implemented."); }, encryptContractForDeposit: function ( req: EncryptContractForDepositRequest, ): Promise { throw new Error("Function not implemented."); }, decryptContractForDeposit: function ( req: DecryptContractForDepositRequest, ): Promise { throw new Error("Function not implemented."); }, signReservePurseCreate: function ( req: SignReservePurseCreateRequest, ): Promise { throw new Error("Function not implemented."); }, signRefund: function (req: SignRefundRequest): Promise { throw new Error("Function not implemented."); }, }; export type WithArg = X extends (req: infer T) => infer R ? (tci: TalerCryptoInterfaceR, req: T) => R : never; export type TalerCryptoInterfaceR = { [x in keyof TalerCryptoInterface]: WithArg; }; export interface SignCoinLinkRequest { oldCoinPriv: string; newDenomHash: string; oldCoinPub: string; transferPub: string; coinEv: CoinEnvelope; } export interface 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 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; } 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 { return { sig: encodeCrock(eddsaSign(decodeCrock(req.msg), decodeCrock(req.priv))), }; }, async rsaBlind( tci: TalerCryptoInterfaceR, req: RsaBlindRequest, ): Promise { const res = rsaBlind( decodeCrock(req.hm), decodeCrock(req.bks), decodeCrock(req.pub), ); return { blinded: encodeCrock(res), }; }, async setupRefreshPlanchet( tci: TalerCryptoInterfaceR, req: SetupRefreshPlanchetRequest, ): Promise { 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 { 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 { 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.restrictionCommit( denomPub.age_mask, age, ); 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 { 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 { 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 { const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP) .put(decodeCrock(req.denomPubHash)) .put(decodeCrock(req.blindingKey)) .build(); const coinPriv = decodeCrock(req.coinPriv); const coinSig = eddsaSign(p, coinPriv); if (req.denomPub.cipher === DenomKeyType.Rsa) { const paybackRequest: RecoupRequest = { coin_blind_key_secret: req.blindingKey, coin_sig: encodeCrock(coinSig), denom_pub_hash: req.denomPubHash, denom_sig: req.denomSig, // FIXME! ewv: { cipher: "RSA", }, }; return paybackRequest; } else { throw new Error(); } }, /** * Create and sign a message to recoup a coin. */ async createRecoupRefreshRequest( tci: TalerCryptoInterfaceR, req: CreateRecoupRefreshReqRequest, ): Promise { const p = buildSigPS(TalerSignaturePurpose.WALLET_COIN_RECOUP_REFRESH) .put(decodeCrock(req.denomPubHash)) .put(decodeCrock(req.blindingKey)) .build(); const coinPriv = decodeCrock(req.coinPriv); const coinSig = eddsaSign(p, coinPriv); if (req.denomPub.cipher === DenomKeyType.Rsa) { const recoupRequest: RecoupRefreshRequest = { coin_blind_key_secret: req.blindingKey, coin_sig: encodeCrock(coinSig), denom_pub_hash: req.denomPubHash, denom_sig: req.denomSig, // FIXME! ewv: { cipher: "RSA", }, }; return recoupRequest; } else { throw new Error(); } }, /** * Check if a payment signature is valid. */ async isValidPaymentSignature( tci: TalerCryptoInterfaceR, req: PaymentSignatureValidationRequest, ): Promise { const { contractHash, sig, merchantPub } = req; const p = buildSigPS(TalerSignaturePurpose.MERCHANT_PAYMENT_OK) .put(decodeCrock(contractHash)) .build(); const sigBytes = decodeCrock(sig); const pubBytes = decodeCrock(merchantPub); return { valid: eddsaVerify(p, sigBytes, pubBytes) }; }, /** * Check if a wire fee is correctly signed. */ async isValidWireFee( tci: TalerCryptoInterfaceR, req: WireFeeValidationRequest, ): Promise { const { type, wf, masterPub } = req; const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_FEES) .put(hash(stringToBytes(type + "\0"))) .put(timestampRoundedToBuffer(wf.startStamp)) .put(timestampRoundedToBuffer(wf.endStamp)) .put(amountToBuffer(wf.wireFee)) .put(amountToBuffer(wf.closingFee)) .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 { 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 { const { masterPub, denom } = req; const value: AmountJson = { currency: denom.currency, fraction: denom.amountFrac, value: denom.amountVal, }; 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(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 { const { sig, masterPub, paytoUri } = req; const paytoHash = hashTruncate32(stringToBytes(paytoUri + "\0")); const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS) .put(paytoHash) .build(); return { valid: eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)) }; }, async isValidContractTermsSignature( tci: TalerCryptoInterfaceR, req: ContractTermsValidationRequest, ): Promise { const cthDec = decodeCrock(req.contractTermsHash); const p = buildSigPS(TalerSignaturePurpose.MERCHANT_CONTRACT) .put(cthDec) .build(); return { valid: eddsaVerify(p, decodeCrock(req.sig), decodeCrock(req.merchantPub)), }; }, /** * Create a new EdDSA key pair. */ async createEddsaKeypair(tci: TalerCryptoInterfaceR): Promise { const 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 { return { priv: req.priv, pub: encodeCrock(eddsaGetPublic(decodeCrock(req.priv))), }; }, async unblindDenominationSignature( tci: TalerCryptoInterfaceR, req: UnblindDenominationSignatureRequest, ): Promise { if (req.evSig.cipher === DenomKeyType.Rsa) { if (req.planchet.denomPub.cipher !== DenomKeyType.Rsa) { throw new Error( "planchet cipher does not match blind signature cipher", ); } const denomSig = rsaUnblind( decodeCrock(req.evSig.blinded_rsa_signature), decodeCrock(req.planchet.denomPub.rsa_public_key), decodeCrock(req.planchet.blindingKey), ); return { cipher: DenomKeyType.Rsa, rsa_signature: encodeCrock(denomSig), }; } else { throw Error(`unblinding for cipher ${req.evSig.cipher} not implemented`); } }, /** * Unblind a blindly signed value. */ async rsaUnblind( tci: TalerCryptoInterfaceR, req: RsaUnblindRequest, ): Promise { const denomSig = rsaUnblind( decodeCrock(req.blindedSig), decodeCrock(req.pk), decodeCrock(req.bk), ); return { sig: encodeCrock(denomSig) }; }, /** * Unblind a blindly signed value. */ async rsaVerify( tci: TalerCryptoInterfaceR, req: RsaVerificationRequest, ): Promise { return { valid: rsaVerify( hash(decodeCrock(req.hm)), decodeCrock(req.sig), decodeCrock(req.pk), ), }; }, /** * Generate updated coins (to store in the database) * and deposit permissions for each given coin. */ async signDepositPermission( tci: TalerCryptoInterfaceR, depositInfo: DepositInfo, ): Promise { // FIXME: put extensions here if used const hExt = new Uint8Array(64); let hAgeCommitment: Uint8Array; let minimumAgeSig: string | undefined = undefined; if (depositInfo.ageCommitmentProof) { const ach = AgeRestriction.hashCommitment( depositInfo.ageCommitmentProof.commitment, ); hAgeCommitment = decodeCrock(ach); if (depositInfo.requiredMinimumAge != null) { minimumAgeSig = encodeCrock( AgeRestriction.commitmentAttest( depositInfo.ageCommitmentProof, depositInfo.requiredMinimumAge, ), ); } } else { // All zeros. hAgeCommitment = new Uint8Array(32); } let d: Uint8Array; if (depositInfo.denomKeyType === DenomKeyType.Rsa) { d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT) .put(decodeCrock(depositInfo.contractTermsHash)) .put(hAgeCommitment) .put(hExt) .put(decodeCrock(depositInfo.wireInfoHash)) .put(decodeCrock(depositInfo.denomPubHash)) .put(timestampRoundedToBuffer(depositInfo.timestamp)) .put(timestampRoundedToBuffer(depositInfo.refundDeadline)) .put(amountToBuffer(depositInfo.spendAmount)) .put(amountToBuffer(depositInfo.feeDeposit)) .put(decodeCrock(depositInfo.merchantPub)) .build(); } else { throw Error("unsupported exchange protocol version"); } const coinSigRes = await this.eddsaSign(tci, { msg: encodeCrock(d), priv: depositInfo.coinPriv, }); if (depositInfo.denomKeyType === DenomKeyType.Rsa) { const s: CoinDepositPermission = { coin_pub: depositInfo.coinPub, coin_sig: coinSigRes.sig, contribution: Amounts.stringify(depositInfo.spendAmount), h_denom: depositInfo.denomPubHash, exchange_url: depositInfo.exchangeBaseUrl, ub_sig: { cipher: DenomKeyType.Rsa, rsa_signature: depositInfo.denomSig.rsa_signature, }, }; if (depositInfo.requiredMinimumAge != null) { // 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 { 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 { const b = stringToBytes(req.str + "\0"); return { h: encodeCrock(hash(b)) }; }, async signCoinLink( tci: TalerCryptoInterfaceR, req: SignCoinLinkRequest, ): Promise { const coinEvHash = hashCoinEv(req.coinEv, req.newDenomHash); // FIXME: fill in const hAgeCommitment = new Uint8Array(32); const coinLink = buildSigPS(TalerSignaturePurpose.WALLET_COIN_LINK) .put(decodeCrock(req.newDenomHash)) .put(decodeCrock(req.transferPub)) .put(hAgeCommitment) .put(coinEvHash) .build(); return tci.eddsaSign(tci, { msg: encodeCrock(coinLink), priv: req.oldCoinPriv, }); }, async makeSyncSignature( tci: TalerCryptoInterfaceR, req: MakeSyncSignatureRequest, ): Promise { const hNew = decodeCrock(req.newHash); let hOld: Uint8Array; if (req.oldHash) { hOld = decodeCrock(req.oldHash); } else { hOld = new Uint8Array(64); } const sigBlob = buildSigPS(TalerSignaturePurpose.SYNC_BACKUP_UPLOAD) .put(hOld) .put(hNew) .build(); const uploadSig = eddsaSign(sigBlob, decodeCrock(req.accountPriv)); return { sig: encodeCrock(uploadSig) }; }, async keyExchangeEcdheEddsa( tci: TalerCryptoInterfaceR, req: KeyExchangeEcdheEddsaRequest, ): Promise { return { h: encodeCrock( keyExchangeEcdhEddsa( decodeCrock(req.ecdhePriv), decodeCrock(req.eddsaPub), ), ), }; }, async ecdheGetPublic( tci: TalerCryptoInterfaceR, req: EcdheGetPublicRequest, ): Promise { return { pub: encodeCrock(ecdhGetPublic(decodeCrock(req.priv))), }; }, async setupRefreshTransferPub( tci: TalerCryptoInterfaceR, req: SetupRefreshTransferPubRequest, ): Promise { 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 { 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 { const hExchangeBaseUrl = hash(stringToBytes(req.exchangeBaseUrl + "\0")); const deposits: PurseDeposit[] = []; for (const c of req.coins) { let haveAch: boolean; let maybeAch: Uint8Array; if (c.ageCommitmentProof) { haveAch = true; maybeAch = decodeCrock( AgeRestriction.hashCommitment(c.ageCommitmentProof.commitment), ); } else { haveAch = false; 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 { const enc = await encryptContractForMerge( decodeCrock(req.pursePub), decodeCrock(req.contractPriv), decodeCrock(req.mergePriv), req.contractTerms, ); 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 { 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 { const enc = await encryptContractForDeposit( decodeCrock(req.pursePub), decodeCrock(req.contractPriv), req.contractTerms, ); 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 { const res = await decryptContractForDeposit( decodeCrock(req.ciphertext), decodeCrock(req.pursePub), decodeCrock(req.contractPriv), ); return { contractTerms: res.contractTerms, }; }, async signPurseMerge( tci: TalerCryptoInterfaceR, req: SignPurseMergeRequest, ): Promise { 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 { 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 { 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, }; }, }; function amountToBuffer(amount: AmountLike): Uint8Array { const amountJ = Amounts.jsonifyAmount(amount); const buffer = new ArrayBuffer(8 + 4 + 12); const dvbuf = new DataView(buffer); const u8buf = new Uint8Array(buffer); const curr = stringToBytes(amountJ.currency); if (typeof dvbuf.setBigUint64 !== "undefined") { dvbuf.setBigUint64(0, BigInt(amountJ.value)); } else { const arr = bigint(amountJ.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, amountJ.fraction); u8buf.set(curr, 8 + 4); return u8buf; } function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array { const b = new ArrayBuffer(8); const v = new DataView(b); // The buffer we sign over represents the timestamp in microseconds. if (typeof v.setBigUint64 !== "undefined") { const s = BigInt(ts.t_s) * BigInt(1000 * 1000); v.setBigUint64(0, s); } else { const s = ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000); const arr = s.toArray(2 ** 8).value; let offset = 8 - arr.length; for (let i = 0; i < arr.length; i++) { v.setUint8(offset++, arr[i]); } } return new Uint8Array(b); } function durationRoundedToBuffer(ts: TalerProtocolDuration): Uint8Array { const b = new ArrayBuffer(8); const v = new DataView(b); // The buffer we sign over represents the timestamp in microseconds. if (typeof v.setBigUint64 !== "undefined") { const s = BigInt(ts.d_us); v.setBigUint64(0, s); } else { const s = ts.d_us === "forever" ? bigint.zero : bigint(ts.d_us); const arr = s.toArray(2 ** 8).value; let offset = 8 - arr.length; for (let i = 0; i < arr.length; i++) { v.setUint8(offset++, arr[i]); } } return new Uint8Array(b); } export interface EddsaSignRequest { msg: string; priv: string; } export interface EddsaSignResponse { sig: string; } export const nativeCrypto: TalerCryptoInterface = Object.fromEntries( Object.keys(nativeCryptoR).map((name) => { return [ name, (req: any) => nativeCryptoR[name as keyof TalerCryptoInterfaceR](nativeCryptoR, req), ]; }), ) as any;