taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit b58dc3d60f5f19cbcd487847dedc4c720fea7e3e
parent 09d66257f8dac79ee1e05fa85ce1c11585c55aee
Author: Florian Dold <florian@dold.me>
Date:   Tue, 22 Apr 2025 23:14:36 +0200

wallet-core: towards refreshV2 crypto implementation

Diffstat:
Mpackages/taler-harness/src/harness/harness.ts | 12++++++++----
Mpackages/taler-harness/src/integrationtests/test-kyc-two-forms.ts | 4++--
Mpackages/taler-util/src/taler-crypto.ts | 11+++--------
Mpackages/taler-util/src/taler-signatures.ts | 4++--
Mpackages/taler-wallet-core/src/crypto/cryptoImplementation.ts | 214++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mpackages/taler-wallet-core/src/refresh.ts | 5++---
6 files changed, 202 insertions(+), 48 deletions(-)

diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -425,16 +425,20 @@ export class GlobalTestState { logger.warn(`process ${logName} exited ${j2s({ code, signal })}`); } }); + const stdoutLogFileName = this.testDir + `/${logName}-stdout.log`; + const stdoutLog = fs.createWriteStream(stdoutLogFileName, { + flags: "a", + autoClose: true, + }); const stderrLogFileName = this.testDir + `/${logName}-stderr.log`; const stderrLog = fs.createWriteStream(stderrLogFileName, { flags: "a", + autoClose: true, }); proc.stderr.pipe(stderrLog); - const stdoutLogFileName = this.testDir + `/${logName}-stdout.log`; - const stdoutLog = fs.createWriteStream(stdoutLogFileName, { - flags: "a", + proc.stdout.pipe(stdoutLog).on("error", (err) => { + console.error(err); }); - proc.stdout.pipe(stdoutLog); const procWrap = new ProcessWrapper(proc); this.procs.push(procWrap); return procWrap; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-two-forms.ts b/packages/taler-harness/src/integrationtests/test-kyc-two-forms.ts @@ -19,7 +19,7 @@ */ import { AmountString, - amountToBuffer, + bufferFromAmount, buildSigPS, codecForAccountKycStatus, codecForKycProcessClientInformation, @@ -158,7 +158,7 @@ export async function runKycTwoFormsTest(t: GlobalTestState) { const balance: AmountString = "TESTKUDOS:20"; const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP) - .put(amountToBuffer(balance)) + .put(bufferFromAmount(balance)) .build(); const body: WalletKycRequest = { balance, diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts @@ -37,10 +37,7 @@ import { DenomKeyType, DenominationPubKey, } from "./types-taler-exchange.js"; -import { - TokenEnvelope, - TokenIssuePublicKey, -} from "./types-taler-merchant.js"; +import { TokenEnvelope, TokenIssuePublicKey } from "./types-taler-merchant.js"; import { PayWalletData } from "./types-taler-wallet.js"; const isEddsaPubP: unique symbol = Symbol("isEddsaPubP"); @@ -972,9 +969,7 @@ export function hashTokenEvInner( } } -export function hashPayWalletData( - walletData: PayWalletData -): Uint8Array { +export function hashPayWalletData(walletData: PayWalletData): Uint8Array { const canon = canonicalJson(walletData) + "\0"; const bytes = stringToBytes(canon); return hash(bytes); @@ -1690,7 +1685,7 @@ export async function decryptContractForDeposit( }; } -export function amountToBuffer(amount: AmountLike): Uint8Array { +export function bufferFromAmount(amount: AmountLike): Uint8Array { const amountJ = Amounts.jsonifyAmount(amount); const buffer = new ArrayBuffer(8 + 4 + 12); const dvbuf = new DataView(buffer); diff --git a/packages/taler-util/src/taler-signatures.ts b/packages/taler-util/src/taler-signatures.ts @@ -16,7 +16,7 @@ import { AmountLike, canonicalJson, TalerProtocolTimestamp } from "./index.js"; import { - amountToBuffer, + bufferFromAmount, bufferForUint64, buildSigPS, decodeCrock, @@ -86,7 +86,7 @@ export function signWalletAccountSetup( balance: AmountLike, ): EddsaSigP { const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP) - .put(amountToBuffer(balance)) + .put(bufferFromAmount(balance)) .build(); return eddsaSign(sigBlob, key); diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -30,10 +30,10 @@ import { AmountJson, Amounts, AmountString, - amountToBuffer, BlindedDenominationSignature, bufferForUint32, bufferForUint64, + bufferFromAmount, buildSigPS, canonicalJson, CoinDepositPermission, @@ -908,7 +908,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { const withdrawRequest = buildSigPS( TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW, ) - .put(amountToBuffer(amountWithFee)) + .put(bufferFromAmount(amountWithFee)) .put(denomPubHash) .put(evHash) .build(); @@ -1164,8 +1164,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { .put(hash(stringToBytes(type + "\0"))) .put(timestampRoundedToBuffer(wf.startStamp)) .put(timestampRoundedToBuffer(wf.endStamp)) - .put(amountToBuffer(wf.wireFee)) - .put(amountToBuffer(wf.closingFee)) + .put(bufferFromAmount(wf.wireFee)) + .put(bufferFromAmount(wf.closingFee)) .build(); const sig = decodeCrock(wf.sig); const pub = decodeCrock(masterPub); @@ -1185,9 +1185,9 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { .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(bufferFromAmount(Amounts.parseOrThrow(gf.history_fee))) + .put(bufferFromAmount(Amounts.parseOrThrow(gf.account_fee))) + .put(bufferFromAmount(Amounts.parseOrThrow(gf.purse_fee))) .put(bufferForUint32(gf.purse_account_limit)) .build(); const sig = decodeCrock(gf.master_sig); @@ -1209,11 +1209,11 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { .put(timestampRoundedToBuffer(req.stampExpireWithdraw)) .put(timestampRoundedToBuffer(req.stampExpireDeposit)) .put(timestampRoundedToBuffer(req.stampExpireLegal)) - .put(amountToBuffer(value)) - .put(amountToBuffer(req.feeWithdraw)) - .put(amountToBuffer(req.feeDeposit)) - .put(amountToBuffer(req.feeRefresh)) - .put(amountToBuffer(req.feeRefund)) + .put(bufferFromAmount(value)) + .put(bufferFromAmount(req.feeWithdraw)) + .put(bufferFromAmount(req.feeDeposit)) + .put(bufferFromAmount(req.feeRefresh)) + .put(bufferFromAmount(req.feeRefund)) .put(decodeCrock(req.denomPubHash)) .build(); const sig = decodeCrock(req.masterSig); @@ -1404,8 +1404,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { .put(decodeCrock(depositInfo.denomPubHash)) .put(timestampRoundedToBuffer(depositInfo.timestamp)) .put(timestampRoundedToBuffer(depositInfo.refundDeadline)) - .put(amountToBuffer(depositInfo.spendAmount)) - .put(amountToBuffer(depositInfo.feeDeposit)) + .put(bufferFromAmount(depositInfo.spendAmount)) + .put(bufferFromAmount(depositInfo.feeDeposit)) .put(decodeCrock(depositInfo.merchantPub)) .put(walletDataHash) .build(); @@ -1451,7 +1451,163 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { tci: TalerCryptoInterfaceR, req: DeriveRefreshSessionRequestV2, ): Promise<DerivedRefreshSessionV2> { - throw new Error("Function not implemented."); + const { + newCoinDenoms, + feeRefresh, + kappa, + meltCoinDenomPubHash, + meltCoinPriv, + meltCoinPub, + sessionPublicSeed, + } = 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; + } + + const planchetsForGammas: RefreshPlanchetInfo[][] = []; + + const noncesBytes = kdfKw({ + outputLength: 64 * kappa, + salt: stringToBytes("refresh-kappa-nonces"), + ikm: decodeCrock(sessionPublicSeed), + }); + + const numDenoms = newCoinDenoms.length; + let numCoins = 0; + const coinDenomsHc = createHashContext(); + for (let i = 0; i < numDenoms; i++) { + for (let j = 0; j < newCoinDenoms[i].count; j++) { + coinDenomsHc.update(decodeCrock(newCoinDenoms[i].denomPubHash)); + numCoins++; + } + } + const coinDenomsHash = coinDenomsHc.finish(); + + const signatures: EddsaSignatureString[] = []; + + const sessionHc = createHashContext(); + + sessionHc.update(decodeCrock(sessionPublicSeed)); + sessionHc.update(decodeCrock(meltCoinPub)); + sessionHc.update(bufferFromAmount(valueWithFee)); + + for (let i = 0; i < kappa; i++) { + const planchets: RefreshPlanchetInfo[] = []; + const nonce = noncesBytes.slice(i * 64, i * 64 + 64); + + const coinLink = buildSigPS(TalerSignaturePurpose.WALLET_COIN_LINK) + .put(nonce) + .put(coinDenomsHash) + .put(bufferForUint32(i)) + .build(); + const sig = await tci.eddsaSign(tci, { + msg: encodeCrock(coinLink), + priv: meltCoinPriv, + }); + signatures.push(sig.sig); + + const planchetSecretBytes = kdfKw({ + outputLength: 32 * numCoins, + salt: stringToBytes("refresh-planchet-secret"), + ikm: decodeCrock(sig.sig), + }); + + for (let j = 0; j < numDenoms; j++) { + const denomSel = newCoinDenoms[j]; + for (let k = 0; k < newCoinDenoms[j].count; k++) { + const coinIndex = planchets.length; + const mySecret = planchetSecretBytes.slice( + 32 * coinIndex, + 32 * coinIndex + 32, + ); + let fresh: FreshCoinEncoded = await tci.setupRefreshPlanchet(tci, { + coinNumber: coinIndex, + transferSecret: encodeCrock(mySecret), + }); + let newAc: AgeCommitmentProof | undefined = undefined; + let newAch: HashCodeString | undefined = undefined; + if (req.meltCoinAgeCommitmentProof) { + newAc = await AgeRestriction.commitmentDerive( + req.meltCoinAgeCommitmentProof, + mySecret, + ); + newAch = AgeRestriction.hashCommitment(newAc.commitment); + } + 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: fresh.bks, + 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: fresh.bks, + coinEv, + coinPriv: fresh.coinPriv, + coinPub: fresh.coinPub, + coinEvHash: encodeCrock(coinEvHash), + maxAge: req.meltCoinMaxAge, + ageCommitmentProof: newAc, + }; + planchets.push(planchet); + sessionHc.update(coinEvHash); + } + } + + 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(bufferFromAmount(valueWithFee)) + .put(bufferFromAmount(feeRefresh)) + .build(); + + const confirmSigResp = await tci.eddsaSign(tci, { + msg: encodeCrock(confirmData), + priv: meltCoinPriv, + }); + + return { + signatures, + planchets: planchetsForGammas, + confirmSig: confirmSigResp.sig, + hash: encodeCrock(sessionHash), + meltCoinPub: meltCoinPub, + meltValueWithFee: valueWithFee, + }; }, async deriveRefreshSession( @@ -1511,7 +1667,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { } sessionHc.update(decodeCrock(meltCoinPub)); - sessionHc.update(amountToBuffer(valueWithFee)); + sessionHc.update(bufferFromAmount(valueWithFee)); for (let i = 0; i < kappa; i++) { const planchets: RefreshPlanchetInfo[] = []; @@ -1591,8 +1747,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { .put(sessionHash) .put(decodeCrock(meltCoinDenomPubHash)) .put(hAgeCommitment) - .put(amountToBuffer(valueWithFee)) - .put(amountToBuffer(meltFee)) + .put(bufferFromAmount(valueWithFee)) + .put(bufferFromAmount(meltFee)) .build(); const confirmSigResp = await tci.eddsaSign(tci, { @@ -1704,7 +1860,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { ): Promise<EddsaSigningResult> { const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE) .put(timestampRoundedToBuffer(req.purseExpiration)) - .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount))) + .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount))) .put(decodeCrock(req.hContractTerms)) .put(decodeCrock(req.mergePub)) .put(bufferForUint32(req.minAge)) @@ -1730,7 +1886,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { maybeAch = new Uint8Array(32); } const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT) - .put(amountToBuffer(Amounts.parseOrThrow(c.contribution))) + .put(bufferFromAmount(Amounts.parseOrThrow(c.contribution))) .put(decodeCrock(c.denomPubHash)) .put(maybeAch) .put(decodeCrock(req.pursePub)) @@ -1847,8 +2003,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { TalerSignaturePurpose.WALLET_ACCOUNT_MERGE, ) .put(timestampRoundedToBuffer(req.purseExpiration)) - .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount))) - .put(amountToBuffer(Amounts.parseOrThrow(req.purseFee))) + .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount))) + .put(bufferFromAmount(Amounts.parseOrThrow(req.purseFee))) .put(decodeCrock(req.contractTermsHash)) .put(decodeCrock(req.pursePub)) .put(timestampRoundedToBuffer(req.mergeTimestamp)) @@ -1892,8 +2048,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { TalerSignaturePurpose.WALLET_ACCOUNT_MERGE, ) .put(timestampRoundedToBuffer(req.purseExpiration)) - .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount))) - .put(amountToBuffer(Amounts.parseOrThrow(req.purseFee))) + .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount))) + .put(bufferFromAmount(Amounts.parseOrThrow(req.purseFee))) .put(decodeCrock(req.contractTermsHash)) .put(decodeCrock(req.pursePub)) .put(timestampRoundedToBuffer(req.mergeTimestamp)) @@ -1915,7 +2071,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { const purseSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE) .put(timestampRoundedToBuffer(req.purseExpiration)) - .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount))) + .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount))) .put(decodeCrock(req.contractTermsHash)) .put(decodeCrock(mergePub)) // FIXME: add age! @@ -1941,7 +2097,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { .put(decodeCrock(req.contractTermsHash)) .put(decodeCrock(req.coinPub)) .put(bufferForUint64(req.rtransactionId)) - .put(amountToBuffer(req.refundAmount)) + .put(bufferFromAmount(req.refundAmount)) .build(); const refundSigResp = await tci.eddsaSign(tci, { msg: encodeCrock(refundSigBlob), @@ -2005,7 +2161,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { req: SignWalletAccountSetupRequest, ): Promise<SignWalletAccountSetupResponse> { const sigData = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP) - .put(amountToBuffer(req.threshold)) + .put(bufferFromAmount(req.threshold)) .build(); const sigResp = await tci.eddsaSign(tci, { msg: encodeCrock(sigData), @@ -2059,8 +2215,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { const withdrawRequest = buildSigPS( TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW, ) - .put(amountToBuffer(req.amount)) - .put(amountToBuffer(req.fee)) + .put(bufferFromAmount(req.amount)) + .put(bufferFromAmount(req.fee)) .put(hPlanchets) .put(bufferForUint32(0)) // max_age_group .put(bufferForUint32(0)) // age mask diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts @@ -770,9 +770,7 @@ async function refreshMelt( }, ); if (!updatedSession) { - throw Error( - "Could not update refresh session (concurrent deletion?).", - ); + throw Error("Could not update refresh session (concurrent deletion?)."); } d.refreshSession = updatedSession; } @@ -924,6 +922,7 @@ async function refreshMelt( denoms_h: newCoinDenoms.map((x) => x.denomPubHash), value_with_fee: Amounts.stringify(derived.meltValueWithFee), }; + logger.info(`melt request body: ${j2s(meltReqBody)}`); const resp = await wex.ws.runSequentialized( [EXCHANGE_COINS_LOCK], async () => {