diff options
Diffstat (limited to 'packages/taler-wallet-core/src/crypto/cryptoImplementation.ts')
-rw-r--r-- | packages/taler-wallet-core/src/crypto/cryptoImplementation.ts | 982 |
1 files changed, 982 insertions, 0 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..63b2687b6 --- /dev/null +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -0,0 +1,982 @@ +/* + This file is part of GNU Taler + (C) 2019-2020 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Implementation of crypto-related high-level functions for the Taler wallet. + * + * @author Florian Dold <dold@taler.net> + */ + +/** + * Imports. + */ + +// FIXME: Crypto should not use DB Types! +import { + AmountJson, + Amounts, + BenchmarkResult, + buildSigPS, + CoinDepositPermission, + CoinEnvelope, + createEddsaKeyPair, + createHashContext, + decodeCrock, + DenomKeyType, + DepositInfo, + eddsaGetPublic, + eddsaSign, + eddsaVerify, + encodeCrock, + ExchangeProtocolVersion, + FreshCoin, + hash, + hashCoinEv, + hashCoinEvInner, + hashDenomPub, + hashTruncate32, + keyExchangeEcdheEddsa, + Logger, + MakeSyncSignatureRequest, + PlanchetCreationRequest, + WithdrawalPlanchet, + randomBytes, + RecoupRefreshRequest, + RecoupRequest, + RefreshPlanchetInfo, + rsaBlind, + rsaUnblind, + rsaVerify, + setupRefreshPlanchet, + setupRefreshTransferPub, + setupTipPlanchet, + setupWithdrawPlanchet, + stringToBytes, + TalerSignaturePurpose, + BlindedDenominationSignature, + UnblindedSignature, + PlanchetUnblindInfo, + TalerProtocolTimestamp, +} from "@gnu-taler/taler-util"; +import bigint from "big-integer"; +import { DenominationRecord, WireFee } from "../db.js"; +import { + CreateRecoupRefreshReqRequest, + CreateRecoupReqRequest, + DerivedRefreshSession, + DerivedTipPlanchet, + DeriveRefreshSessionRequest, + DeriveTipRequest, + SignTrackTransactionRequest, +} from "./cryptoTypes.js"; + +//const logger = new Logger("cryptoImplementation.ts"); + +/** + * Interface for (asynchronous) cryptographic operations that + * Taler uses. + */ +export interface TalerCryptoInterface { + /** + * Create a pre-coin of the given denomination to be withdrawn from then given + * reserve. + */ + createPlanchet(req: PlanchetCreationRequest): Promise<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>; + + isValidDenom(req: DenominationValidationRequest): Promise<ValidationResult>; + + isValidWireAccount( + req: WireAccountValidationRequest, + ): Promise<ValidationResult>; + + isValidContractTermsSignature( + req: ContractTermsValidationRequest, + ): Promise<ValidationResult>; + + createEddsaKeypair(req: {}): Promise<EddsaKeypair>; + + eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaKeypair>; + + unblindDenominationSignature( + req: UnblindDenominationSignatureRequest, + ): Promise<UnblindedSignature>; + + rsaUnblind(req: RsaUnblindRequest): Promise<RsaUnblindResponse>; + + rsaVerify(req: RsaVerificationRequest): Promise<ValidationResult>; + + 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>; +} + +/** + * 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."); + }, + isValidContractTermsSignature: function ( + req: ContractTermsValidationRequest, + ): Promise<ValidationResult> { + throw new Error("Function not implemented."); + }, + createEddsaKeypair: function (req: {}): 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."); + }, +}; + +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 RsaVerificationRequest { + hm: string; + sig: string; + pk: string; +} + +export interface EddsaSigningResult { + sig: string; +} + +export interface ValidationResult { + valid: boolean; +} + +export interface HashStringRequest { + str: string; +} + +export interface HashStringResult { + h: string; +} + +export interface WireFeeValidationRequest { + type: string; + wf: WireFee; + masterPub: string; +} + +export interface DenominationValidationRequest { + denom: DenominationRecord; + masterPub: string; +} + +export interface PaymentSignatureValidationRequest { + sig: string; + contractHash: string; + merchantPub: string; +} + +export interface ContractTermsValidationRequest { + contractTermsHash: string; + sig: string; + merchantPub: string; +} + +export interface WireAccountValidationRequest { + versionCurrent: ExchangeProtocolVersion; + paytoUri: string; + sig: string; + masterPub: string; +} + +export interface EddsaKeypair { + priv: string; + pub: string; +} + +export interface EddsaGetPublicRequest { + priv: string; +} + +export interface UnblindDenominationSignatureRequest { + planchet: PlanchetUnblindInfo; + evSig: BlindedDenominationSignature; +} + +export interface RsaUnblindRequest { + blindedSig: string; + bk: string; + pk: string; +} + +export interface RsaUnblindResponse { + sig: string; +} + +export const nativeCryptoR: TalerCryptoInterfaceR = { + async eddsaSign( + tci: TalerCryptoInterfaceR, + req: EddsaSignRequest, + ): Promise<EddsaSignResponse> { + return { + sig: encodeCrock(eddsaSign(decodeCrock(req.msg), decodeCrock(req.priv))), + }; + }, + + async createPlanchet( + tci: TalerCryptoInterfaceR, + req: PlanchetCreationRequest, + ): Promise<WithdrawalPlanchet> { + const denomPub = req.denomPub; + if (denomPub.cipher === DenomKeyType.Rsa) { + const reservePub = decodeCrock(req.reservePub); + const denomPubRsa = decodeCrock(denomPub.rsa_public_key); + const derivedPlanchet = setupWithdrawPlanchet( + decodeCrock(req.secretSeed), + req.coinIndex, + ); + const coinPubHash = hash(derivedPlanchet.coinPub); + const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPubRsa); + const coinEv: CoinEnvelope = { + cipher: DenomKeyType.Rsa, + rsa_blinded_planchet: encodeCrock(ev), + }; + const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount; + const denomPubHash = hashDenomPub(req.denomPub); + const evHash = hashCoinEv(coinEv, encodeCrock(denomPubHash)); + const withdrawRequest = buildSigPS( + TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW, + ) + .put(amountToBuffer(amountWithFee)) + .put(denomPubHash) + .put(evHash) + .build(); + + const sigResult = await tci.eddsaSign(tci, { + msg: encodeCrock(withdrawRequest), + priv: req.reservePriv, + }); + + const planchet: WithdrawalPlanchet = { + blindingKey: encodeCrock(derivedPlanchet.bks), + coinEv, + coinPriv: encodeCrock(derivedPlanchet.coinPriv), + coinPub: encodeCrock(derivedPlanchet.coinPub), + coinValue: req.value, + denomPub, + denomPubHash: encodeCrock(denomPubHash), + reservePub: encodeCrock(reservePub), + withdrawSig: sigResult.sig, + coinEvHash: encodeCrock(evHash), + }; + return planchet; + } else { + throw Error("unsupported cipher, unable to create planchet"); + } + }, + + async createTipPlanchet( + tci: TalerCryptoInterfaceR, + req: DeriveTipRequest, + ): Promise<DerivedTipPlanchet> { + if (req.denomPub.cipher !== DenomKeyType.Rsa) { + throw Error(`unsupported cipher (${req.denomPub.cipher})`); + } + const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex); + const denomPub = decodeCrock(req.denomPub.rsa_public_key); + const coinPubHash = hash(fc.coinPub); + const ev = rsaBlind(coinPubHash, fc.bks, denomPub); + const coinEv = { + cipher: DenomKeyType.Rsa, + rsa_blinded_planchet: encodeCrock(ev), + }; + const tipPlanchet: DerivedTipPlanchet = { + blindingKey: encodeCrock(fc.bks), + coinEv, + coinEvHash: encodeCrock( + hashCoinEv(coinEv, encodeCrock(hashDenomPub(req.denomPub))), + ), + coinPriv: encodeCrock(fc.coinPriv), + coinPub: encodeCrock(fc.coinPub), + }; + return tipPlanchet; + }, + + 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.merchantPub)) + .put(decodeCrock(req.coinPub)) + .build(); + return { sig: encodeCrock(eddsaSign(p, decodeCrock(req.merchantPriv))) }; + }, + + /** + * Create and sign a message to recoup a coin. + */ + async createRecoupRequest( + tci: TalerCryptoInterfaceR, + req: CreateRecoupReqRequest, + ): Promise<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)) + .put(amountToBuffer(wf.wadFee)) + .build(); + const sig = decodeCrock(wf.sig); + const pub = decodeCrock(masterPub); + return { valid: eddsaVerify(p, sig, pub) }; + }, + + /** + * Check if the signature of a denomination is valid. + */ + async isValidDenom( + tci: TalerCryptoInterfaceR, + req: DenominationValidationRequest, + ): Promise<ValidationResult> { + const { masterPub, denom } = req; + const p = buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY) + .put(decodeCrock(masterPub)) + .put(timestampRoundedToBuffer(denom.stampStart)) + .put(timestampRoundedToBuffer(denom.stampExpireWithdraw)) + .put(timestampRoundedToBuffer(denom.stampExpireDeposit)) + .put(timestampRoundedToBuffer(denom.stampExpireLegal)) + .put(amountToBuffer(denom.value)) + .put(amountToBuffer(denom.feeWithdraw)) + .put(amountToBuffer(denom.feeDeposit)) + .put(amountToBuffer(denom.feeRefresh)) + .put(amountToBuffer(denom.feeRefund)) + .put(decodeCrock(denom.denomPubHash)) + .build(); + const sig = decodeCrock(denom.masterSig); + const pub = decodeCrock(masterPub); + const res = eddsaVerify(p, sig, pub); + return { valid: res }; + }, + + async isValidWireAccount( + tci: TalerCryptoInterfaceR, + req: WireAccountValidationRequest, + ): Promise<ValidationResult> { + 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<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 pair = createEddsaKeyPair(); + return { + priv: encodeCrock(pair.eddsaPriv), + pub: encodeCrock(pair.eddsaPub), + }; + }, + + 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); + const hAgeCommitment = new Uint8Array(32); + let d: Uint8Array; + if (depositInfo.denomKeyType === DenomKeyType.Rsa) { + d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT) + .put(decodeCrock(depositInfo.contractTermsHash)) + .put(hAgeCommitment) + .put(hExt) + .put(decodeCrock(depositInfo.wireInfoHash)) + .put(decodeCrock(depositInfo.denomPubHash)) + .put(timestampRoundedToBuffer(depositInfo.timestamp)) + .put(timestampRoundedToBuffer(depositInfo.refundDeadline)) + .put(amountToBuffer(depositInfo.spendAmount)) + .put(amountToBuffer(depositInfo.feeDeposit)) + .put(decodeCrock(depositInfo.merchantPub)) + .build(); + } else { + throw Error("unsupported exchange protocol version"); + } + const coinSigRes = await this.eddsaSign(tci, { + msg: encodeCrock(d), + priv: depositInfo.coinPriv, + }); + + if (depositInfo.denomKeyType === DenomKeyType.Rsa) { + const s: CoinDepositPermission = { + coin_pub: depositInfo.coinPub, + coin_sig: coinSigRes.sig, + contribution: Amounts.stringify(depositInfo.spendAmount), + h_denom: depositInfo.denomPubHash, + exchange_url: depositInfo.exchangeBaseUrl, + ub_sig: { + cipher: DenomKeyType.Rsa, + rsa_signature: depositInfo.denomSig.rsa_signature, + }, + }; + return s; + } else { + throw Error( + `unsupported denomination cipher (${depositInfo.denomKeyType})`, + ); + } + }, + + async deriveRefreshSession( + tci: TalerCryptoInterfaceR, + req: DeriveRefreshSessionRequest, + ): Promise<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++) { + if (denomSel.denomPub.cipher === DenomKeyType.Rsa) { + const denomPubHash = hashDenomPub(denomSel.denomPub); + sessionHc.update(denomPubHash); + } else { + throw new Error(); + } + } + } + + sessionHc.update(decodeCrock(meltCoinPub)); + sessionHc.update(amountToBuffer(valueWithFee)); + + for (let i = 0; i < kappa; i++) { + const planchets: RefreshPlanchetInfo[] = []; + for (let j = 0; j < newCoinDenoms.length; j++) { + const denomSel = newCoinDenoms[j]; + for (let k = 0; k < denomSel.count; k++) { + const coinIndex = planchets.length; + const transferPriv = decodeCrock(transferPrivs[i]); + const oldCoinPub = decodeCrock(meltCoinPub); + const transferSecret = keyExchangeEcdheEddsa( + transferPriv, + oldCoinPub, + ); + let coinPub: Uint8Array; + let coinPriv: Uint8Array; + let blindingFactor: Uint8Array; + // FIXME: make setupRefreshPlanchet a crypto api fn + let fresh: FreshCoin = setupRefreshPlanchet( + transferSecret, + coinIndex, + ); + coinPriv = fresh.coinPriv; + coinPub = fresh.coinPub; + blindingFactor = fresh.bks; + const coinPubHash = hash(coinPub); + if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) { + throw Error("unsupported cipher, can't create refresh session"); + } + const rsaDenomPub = decodeCrock(denomSel.denomPub.rsa_public_key); + const ev = rsaBlind(coinPubHash, blindingFactor, rsaDenomPub); + const coinEv: CoinEnvelope = { + cipher: DenomKeyType.Rsa, + rsa_blinded_planchet: encodeCrock(ev), + }; + const coinEvHash = hashCoinEv( + coinEv, + encodeCrock(hashDenomPub(denomSel.denomPub)), + ); + const planchet: RefreshPlanchetInfo = { + blindingKey: encodeCrock(blindingFactor), + coinEv, + coinPriv: encodeCrock(coinPriv), + coinPub: encodeCrock(coinPub), + coinEvHash: encodeCrock(coinEvHash), + }; + planchets.push(planchet); + hashCoinEvInner(coinEv, sessionHc); + } + } + planchetsForGammas.push(planchets); + } + + const sessionHash = sessionHc.finish(); + let confirmData: Uint8Array; + // FIXME: fill in age commitment + const hAgeCommitment = new Uint8Array(32); + confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT) + .put(sessionHash) + .put(decodeCrock(meltCoinDenomPubHash)) + .put(hAgeCommitment) + .put(amountToBuffer(valueWithFee)) + .put(amountToBuffer(meltFee)) + .build(); + + const confirmSigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(confirmData), + priv: meltCoinPriv, + }); + + const refreshSession: DerivedRefreshSession = { + confirmSig: confirmSigResp.sig, + hash: encodeCrock(sessionHash), + meltCoinPub: meltCoinPub, + planchetsForGammas: planchetsForGammas, + transferPrivs, + transferPubs, + meltValueWithFee: valueWithFee, + }; + + return refreshSession; + }, + + /** + * Hash a string including the zero terminator. + */ + async hashString( + tci: TalerCryptoInterfaceR, + req: HashStringRequest, + ): Promise<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) }; + }, +}; + +function amountToBuffer(amount: AmountJson): Uint8Array { + const buffer = new ArrayBuffer(8 + 4 + 12); + const dvbuf = new DataView(buffer); + const u8buf = new Uint8Array(buffer); + const curr = stringToBytes(amount.currency); + if (typeof dvbuf.setBigUint64 !== "undefined") { + dvbuf.setBigUint64(0, BigInt(amount.value)); + } else { + const arr = bigint(amount.value).toArray(2 ** 8).value; + let offset = 8 - arr.length; + for (let i = 0; i < arr.length; i++) { + dvbuf.setUint8(offset++, arr[i]); + } + } + dvbuf.setUint32(8, amount.fraction); + u8buf.set(curr, 8 + 4); + + return u8buf; +} + +function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array { + const b = new ArrayBuffer(8); + const v = new DataView(b); + // The buffer we sign over represents the timestamp in microseconds. + if (typeof v.setBigUint64 !== "undefined") { + const s = BigInt(ts.t_s) * BigInt(1000 * 1000); + v.setBigUint64(0, s); + } else { + const s = + ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000); + const arr = s.toArray(2 ** 8).value; + let offset = 8 - arr.length; + for (let i = 0; i < arr.length; i++) { + v.setUint8(offset++, arr[i]); + } + } + return new Uint8Array(b); +} + +export interface EddsaSignRequest { + msg: string; + priv: string; +} + +export interface EddsaSignResponse { + sig: string; +} + +export const nativeCrypto: TalerCryptoInterface = Object.fromEntries( + Object.keys(nativeCryptoR).map((name) => { + return [ + name, + (req: any) => + nativeCryptoR[name as keyof TalerCryptoInterfaceR](nativeCryptoR, req), + ]; + }), +) as any; |