taler-typescript-core

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

commit eb6c37a897206bedb244d7f205c22299a228f953
parent 89c2b5f4044b140695dcb75dd0567457308d4afb
Author: Florian Dold <florian@dold.me>
Date:   Fri, 13 Feb 2026 18:44:56 +0100

wallet-core: remove pre-v27 refresh

Diffstat:
Mpackages/taler-wallet-core/src/db.ts | 12++++--------
Mpackages/taler-wallet-core/src/refresh.ts | 490++++++++++++++++++++++++-------------------------------------------------------
2 files changed, 149 insertions(+), 353 deletions(-)

diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -1320,14 +1320,6 @@ export interface RefreshSessionRecord { coinIndex: number; /** - * 512-bit secret that can be used to derive - * the other cryptographic material for the refresh session. - * - * If this field is set, it's a legacy V1 refresh session. - */ - sessionSecretSeed?: string; - - /** * If this field is set, it's a V2 refresh session. */ sessionPublicSeed?: string; @@ -1352,6 +1344,10 @@ export interface RefreshSessionRecord { norevealIndex?: number; lastError?: TalerErrorDetail; + + // Reserved legacy fields: + // * sessionSecretSeed: string + // (legacy v1 refresh) } export enum RefundReason { diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts @@ -31,10 +31,10 @@ import { assertUnreachable, BlindedDenominationSignature, checkDbInvariant, + checkLogicInvariant, codecForCoinHistoryResponse, codecForExchangeMeltResponse, codecForExchangeRevealMeltResponseV2, - codecForExchangeRevealResponse, CoinPublicKeyString, CoinRefreshRequest, CoinStatus, @@ -42,9 +42,7 @@ import { DenomKeyType, Duration, encodeCrock, - ExchangeMeltRequest, ExchangeMeltRequestV2, - ExchangeProtocolVersion, ExchangeRefreshRevealRequest, ExchangeRefreshRevealRequestV2, ExchangeRefundRequest, @@ -706,15 +704,15 @@ async function refreshMelt( } // Make sure that we have a seed. - if ( - d.refreshSession.sessionPublicSeed == null && - d.refreshSession.sessionSecretSeed == null - ) { + if (d.refreshSession.sessionPublicSeed == null) { const exchange = await fetchFreshExchange(wex, d.oldCoin.exchangeBaseUrl); const exchangeVer = LibtoolVersion.parseVersion( exchange.protocolVersionRange, ); checkDbInvariant(!!exchangeVer, "bad exchange version string"); + if (exchangeVer.current < 27) { + throw Error("unsupported exchange version"); + } const seed = encodeCrock(getRandomBytes(64)); const updatedSession = await wex.db.runReadWriteTx( { @@ -728,17 +726,10 @@ async function refreshMelt( if (!refreshSession) { return undefined; } - if ( - refreshSession.sessionPublicSeed != null || - refreshSession.sessionSecretSeed != null - ) { + if (refreshSession.sessionPublicSeed != null) { return refreshSession; } - if (exchangeVer.current >= 27) { - refreshSession.sessionPublicSeed = seed; - } else { - refreshSession.sessionSecretSeed = seed; - } + refreshSession.sessionPublicSeed = seed; await tx.refreshSessions.put(refreshSession); return refreshSession; }, @@ -751,239 +742,124 @@ async function refreshMelt( const { newCoinDenoms, oldCoin, oldDenom, refreshGroup, refreshSession } = d; + checkLogicInvariant(refreshSession.sessionPublicSeed != null); + let maybeAch: HashCodeString | undefined; if (oldCoin.ageCommitmentProof) { maybeAch = AgeRestriction.hashCommitment( oldCoin.ageCommitmentProof.commitment, ); } + // New melt protocol. + const derived = await wex.cryptoApi.deriveRefreshSessionV2({ + kappa: 3, + meltCoinDenomPubHash: oldCoin.denomPubHash, + meltCoinPriv: oldCoin.coinPriv, + meltCoinPub: oldCoin.coinPub, + feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh), + meltCoinMaxAge: oldCoin.maxAge, + meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof, + newCoinDenoms, + sessionPublicSeed: refreshSession.sessionPublicSeed, + }); - if (refreshSession.sessionSecretSeed) { - // Old legacy melt protocol. - let exchangeProtocolVersion: ExchangeProtocolVersion; - switch (d.oldDenom.denomPub.cipher) { - case DenomKeyType.Rsa: { - exchangeProtocolVersion = ExchangeProtocolVersion.V12; - break; - } - default: - throw Error("unsupported key type"); + // Wallet stores new denoms run-length encoded, + // we need to expand the list of denominations + // for the exchange. + const newDenomsFlat: string[] = []; + for (let i = 0; i < newCoinDenoms.length; i++) { + const dsel = newCoinDenoms[i]; + for (let j = 0; j < dsel.count; j++) { + newDenomsFlat.push(dsel.denomPubHash); } + } - const derived = await wex.cryptoApi.deriveRefreshSession({ - exchangeProtocolVersion, - kappa: 3, - meltCoinDenomPubHash: oldCoin.denomPubHash, - meltCoinPriv: oldCoin.coinPriv, - meltCoinPub: oldCoin.coinPub, - feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh), - meltCoinMaxAge: oldCoin.maxAge, - meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof, - newCoinDenoms, - sessionSecretSeed: refreshSession.sessionSecretSeed, - }); - - const reqUrl = new URL( - `coins/${oldCoin.coinPub}/melt`, - oldCoin.exchangeBaseUrl, - ); - - const meltReqBody: ExchangeMeltRequest = { - coin_pub: oldCoin.coinPub, - confirm_sig: derived.confirmSig, - denom_pub_hash: oldCoin.denomPubHash, - denom_sig: oldCoin.denomSig, - rc: derived.hash, - value_with_fee: Amounts.stringify(derived.meltValueWithFee), - age_commitment_hash: maybeAch, - }; - - const resp = await wex.ws.runSequentialized( - [EXCHANGE_COINS_LOCK], - async () => { - return await cancelableFetch(wex, reqUrl, { - method: "POST", - body: meltReqBody, - timeout: getRefreshRequestTimeout(refreshGroup), - }); - }, - ); + const reqUrl = new URL(`melt`, oldCoin.exchangeBaseUrl); + const meltReqBody: ExchangeMeltRequestV2 = { + old_coin_pub: oldCoin.coinPub, + old_denom_pub_h: oldCoin.denomPubHash, + old_denom_sig: oldCoin.denomSig, + old_age_commitment_h: maybeAch, + refresh_seed: refreshSession.sessionPublicSeed, + confirm_sig: derived.confirmSig, + coin_evs: derived.planchets.map((x) => x.map((y) => y.coinEv)), + denoms_h: newDenomsFlat, + value_with_fee: Amounts.stringify(derived.meltValueWithFee), + }; + const resp = await wex.ws.runSequentialized( + [EXCHANGE_COINS_LOCK], + async () => { + return await cancelableFetch(wex, reqUrl, { + method: "POST", + body: meltReqBody, + timeout: getRefreshRequestTimeout(refreshGroup), + }); + }, + ); - switch (resp.status) { - case HttpStatusCode.NotFound: { - const errDetail = await readTalerErrorResponse(resp); - await handleRefreshMeltNotFound(ctx, coinIndex, resp, errDetail); - return; - } - case HttpStatusCode.Gone: { - const errDetail = await readTalerErrorResponse(resp); - await handleRefreshMeltGone(ctx, coinIndex, errDetail); - return; - } - case HttpStatusCode.Conflict: { - const errDetail = await readTalerErrorResponse(resp); - await handleRefreshMeltConflict( - ctx, - refreshGroup, - coinIndex, - errDetail, - derived.meltValueWithFee, - oldCoin, - ); - return; - } - case HttpStatusCode.Ok: - break; - default: { - const errDetail = await readTalerErrorResponse(resp); - throwUnexpectedRequestError(resp, errDetail); - } + switch (resp.status) { + case HttpStatusCode.NotFound: { + const errDetail = await readTalerErrorResponse(resp); + await handleRefreshMeltNotFound(ctx, coinIndex, resp, errDetail); + return; } + case HttpStatusCode.Gone: { + const errDetail = await readTalerErrorResponse(resp); + await handleRefreshMeltGone(ctx, coinIndex, errDetail); + return; + } + case HttpStatusCode.Conflict: { + const errDetail = await readTalerErrorResponse(resp); + await handleRefreshMeltConflict( + ctx, + refreshGroup, + coinIndex, + errDetail, + derived.meltValueWithFee, + oldCoin, + ); + return; + } + case HttpStatusCode.Ok: + break; + default: { + const errDetail = await readTalerErrorResponse(resp); + throwUnexpectedRequestError(resp, errDetail); + } + } - const meltResponse = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeMeltResponse(), - ); - - const norevealIndex = meltResponse.noreveal_index; - - refreshSession.norevealIndex = norevealIndex; + const meltResponse = await readSuccessResponseJsonOrThrow( + resp, + codecForExchangeMeltResponse(), + ); - await wex.db.runReadWriteTx( - { storeNames: ["refreshGroups", "refreshSessions"] }, - async (tx) => { - const rg = await tx.refreshGroups.get(refreshGroupId); - if (!rg) { - return; - } - if (rg.timestampFinished) { - return; - } - const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]); - if (!rs) { - return; - } - if (rs.norevealIndex !== undefined) { - return; - } - rs.norevealIndex = norevealIndex; - await tx.refreshSessions.put(rs); - }, - ); - } else if (refreshSession.sessionPublicSeed) { - // New melt protocol. - const derived = await wex.cryptoApi.deriveRefreshSessionV2({ - kappa: 3, - meltCoinDenomPubHash: oldCoin.denomPubHash, - meltCoinPriv: oldCoin.coinPriv, - meltCoinPub: oldCoin.coinPub, - feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh), - meltCoinMaxAge: oldCoin.maxAge, - meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof, - newCoinDenoms, - sessionPublicSeed: refreshSession.sessionPublicSeed, - }); + // FIXME: Check exchange's signature. - // Wallet stores new denoms run-length encoded, - // we need to expand the list of denominations - // for the exchange. - const newDenomsFlat: string[] = []; - for (let i = 0; i < newCoinDenoms.length; i++) { - const dsel = newCoinDenoms[i]; - for (let j = 0; j < dsel.count; j++) { - newDenomsFlat.push(dsel.denomPubHash); - } - } + const norevealIndex = meltResponse.noreveal_index; - const reqUrl = new URL(`melt`, oldCoin.exchangeBaseUrl); - const meltReqBody: ExchangeMeltRequestV2 = { - old_coin_pub: oldCoin.coinPub, - old_denom_pub_h: oldCoin.denomPubHash, - old_denom_sig: oldCoin.denomSig, - old_age_commitment_h: maybeAch, - refresh_seed: refreshSession.sessionPublicSeed, - confirm_sig: derived.confirmSig, - coin_evs: derived.planchets.map((x) => x.map((y) => y.coinEv)), - denoms_h: newDenomsFlat, - value_with_fee: Amounts.stringify(derived.meltValueWithFee), - }; - const resp = await wex.ws.runSequentialized( - [EXCHANGE_COINS_LOCK], - async () => { - return await cancelableFetch(wex, reqUrl, { - method: "POST", - body: meltReqBody, - timeout: getRefreshRequestTimeout(refreshGroup), - }); - }, - ); + refreshSession.norevealIndex = norevealIndex; - switch (resp.status) { - case HttpStatusCode.NotFound: { - const errDetail = await readTalerErrorResponse(resp); - await handleRefreshMeltNotFound(ctx, coinIndex, resp, errDetail); + await wex.db.runReadWriteTx( + { storeNames: ["refreshGroups", "refreshSessions"] }, + async (tx) => { + const rg = await tx.refreshGroups.get(refreshGroupId); + if (!rg) { return; } - case HttpStatusCode.Gone: { - const errDetail = await readTalerErrorResponse(resp); - await handleRefreshMeltGone(ctx, coinIndex, errDetail); + if (rg.timestampFinished) { return; } - case HttpStatusCode.Conflict: { - const errDetail = await readTalerErrorResponse(resp); - await handleRefreshMeltConflict( - ctx, - refreshGroup, - coinIndex, - errDetail, - derived.meltValueWithFee, - oldCoin, - ); + const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]); + if (!rs) { return; } - case HttpStatusCode.Ok: - break; - default: { - const errDetail = await readTalerErrorResponse(resp); - throwUnexpectedRequestError(resp, errDetail); + if (rs.norevealIndex !== undefined) { + return; } - } - - const meltResponse = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeMeltResponse(), - ); - - // FIXME: Check exchange's signature. - - const norevealIndex = meltResponse.noreveal_index; - - refreshSession.norevealIndex = norevealIndex; - - await wex.db.runReadWriteTx( - { storeNames: ["refreshGroups", "refreshSessions"] }, - async (tx) => { - const rg = await tx.refreshGroups.get(refreshGroupId); - if (!rg) { - return; - } - if (rg.timestampFinished) { - return; - } - const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]); - if (!rs) { - return; - } - if (rs.norevealIndex !== undefined) { - return; - } - rs.norevealIndex = norevealIndex; - await tx.refreshSessions.put(rs); - }, - ); - } else { - throw Error("unsupported refresh session (neither secret nor public seed)"); - } + rs.norevealIndex = norevealIndex; + await tx.refreshSessions.put(rs); + }, + ); } async function handleRefreshMeltGone( @@ -1381,131 +1257,55 @@ async function refreshReveal( let resEvSigs: BlindedDenominationSignature[]; let planchets: RefreshPlanchetInfo[][]; - if (refreshSession.sessionSecretSeed != null) { - // Legacy refresh session. - - let exchangeProtocolVersion: ExchangeProtocolVersion; - switch (d.oldDenom.denomPub.cipher) { - case DenomKeyType.Rsa: { - exchangeProtocolVersion = ExchangeProtocolVersion.V12; - break; - } - default: - throw Error("unsupported key type"); - } - - const derived = await wex.cryptoApi.deriveRefreshSession({ - exchangeProtocolVersion, - kappa: 3, - meltCoinDenomPubHash: oldCoin.denomPubHash, - meltCoinPriv: oldCoin.coinPriv, - meltCoinPub: oldCoin.coinPub, - feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh), - newCoinDenoms, - meltCoinMaxAge: oldCoin.maxAge, - meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof, - sessionSecretSeed: refreshSession.sessionSecretSeed, - }); - - const reqUrl = new URL( - `refreshes/${derived.hash}/reveal`, - oldCoin.exchangeBaseUrl, - ); + checkLogicInvariant(refreshSession.sessionPublicSeed != null); - const req = await assembleRefreshRevealRequest({ - cryptoApi: wex.cryptoApi, - derived, - newDenoms: newCoinDenoms, - norevealIndex: norevealIndex, - oldCoinPriv: oldCoin.coinPriv, - oldCoinPub: oldCoin.coinPub, - oldAgeCommitment: oldCoin.ageCommitmentProof?.commitment, - }); - - const resp = await wex.ws.runSequentialized( - [EXCHANGE_COINS_LOCK], - async () => - cancelableFetch(wex, reqUrl, { - body: req, - method: "POST", - timeout: getRefreshRequestTimeout(refreshGroup), - }), - ); + const derived = await wex.cryptoApi.deriveRefreshSessionV2({ + kappa: 3, + meltCoinDenomPubHash: oldCoin.denomPubHash, + meltCoinPriv: oldCoin.coinPriv, + meltCoinPub: oldCoin.coinPub, + sessionPublicSeed: refreshSession.sessionPublicSeed, + feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh), + newCoinDenoms, + meltCoinMaxAge: oldCoin.maxAge, + meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof, + }); + const req: ExchangeRefreshRevealRequestV2 = { + rc: derived.hash, + signatures: derived.signatures.filter((v, i) => i != norevealIndex), + age_commitment: oldCoin.ageCommitmentProof?.commitment?.publicKeys, + }; + const reqUrl = new URL(`reveal-melt`, oldCoin.exchangeBaseUrl); + const resp = await wex.ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => + cancelableFetch(wex, reqUrl, { + body: req, + method: "POST", + timeout: getRefreshRequestTimeout(refreshGroup), + }), + ); - switch (resp.status) { - case HttpStatusCode.Ok: - break; - case HttpStatusCode.Conflict: - case HttpStatusCode.Gone: { - const errDetail = await readTalerErrorResponse(resp); - await handleRefreshRevealError(ctx, coinIndex, errDetail); - return; - } - default: { - const errDetail = await readTalerErrorResponse(resp); - throwUnexpectedRequestError(resp, errDetail); - } + switch (resp.status) { + case HttpStatusCode.Ok: + break; + case HttpStatusCode.Conflict: + case HttpStatusCode.Gone: { + const errDetail = await readTalerErrorResponse(resp); + await handleRefreshRevealError(ctx, coinIndex, errDetail); + return; } - - const reveal = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeRevealResponse(), - ); - planchets = derived.planchets; - resEvSigs = reveal.ev_sigs.map((x) => x.ev_sig); - } else if (refreshSession.sessionPublicSeed != null) { - const derived = await wex.cryptoApi.deriveRefreshSessionV2({ - kappa: 3, - meltCoinDenomPubHash: oldCoin.denomPubHash, - meltCoinPriv: oldCoin.coinPriv, - meltCoinPub: oldCoin.coinPub, - sessionPublicSeed: refreshSession.sessionPublicSeed, - feeRefresh: Amounts.parseOrThrow(oldDenom.feeRefresh), - newCoinDenoms, - meltCoinMaxAge: oldCoin.maxAge, - meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof, - }); - const req: ExchangeRefreshRevealRequestV2 = { - rc: derived.hash, - signatures: derived.signatures.filter((v, i) => i != norevealIndex), - age_commitment: oldCoin.ageCommitmentProof?.commitment?.publicKeys, - }; - const reqUrl = new URL(`reveal-melt`, oldCoin.exchangeBaseUrl); - const resp = await wex.ws.runSequentialized( - [EXCHANGE_COINS_LOCK], - async () => - cancelableFetch(wex, reqUrl, { - body: req, - method: "POST", - timeout: getRefreshRequestTimeout(refreshGroup), - }), - ); - - switch (resp.status) { - case HttpStatusCode.Ok: - break; - case HttpStatusCode.Conflict: - case HttpStatusCode.Gone: { - const errDetail = await readTalerErrorResponse(resp); - await handleRefreshRevealError(ctx, coinIndex, errDetail); - return; - } - default: { - const errDetail = await readTalerErrorResponse(resp); - throwUnexpectedRequestError(resp, errDetail); - } + default: { + const errDetail = await readTalerErrorResponse(resp); + throwUnexpectedRequestError(resp, errDetail); } - - const reveal = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeRevealMeltResponseV2(), - ); - resEvSigs = reveal.ev_sigs; - planchets = derived.planchets; - } else { - throw Error("refresh session not supported"); } + const reveal = await readSuccessResponseJsonOrThrow( + resp, + codecForExchangeRevealMeltResponseV2(), + ); + resEvSigs = reveal.ev_sigs; + planchets = derived.planchets; + const coins: CoinRecord[] = []; for (let i = 0; i < refreshSession.newDenoms.length; i++) {