taler-typescript-core

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

commit 1fbb99d0a221c97a46078d836fb722140b1b7ce4
parent 17d1b9159a98b70c996f94cc37b0745c7ad69cf6
Author: Florian Dold <dold@taler.net>
Date:   Tue, 21 Jul 2026 11:10:04 +0200

util: test the contract blob header on decryption

Diffstat:
Mpackages/taler-util/src/taler-crypto.test.ts | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+), 0 deletions(-)

diff --git a/packages/taler-util/src/taler-crypto.test.ts b/packages/taler-util/src/taler-crypto.test.ts @@ -54,7 +54,16 @@ import { hpkeComputeNonce, hpkeOpenOneshot, AgeRestriction, + bufferForUint32, + createEddsaKeyPair, + decryptContractForDeposit, + encryptContractForDeposit, + encryptWithDerivedKey, + typedArrayConcat, } from "./taler-crypto.js"; +import * as fflate from "fflate"; +import { canonicalJson } from "./helpers.js"; +import { randomBytes } from "./nacl-fast.js"; import * as talerCrypto from "./taler-crypto.js"; import { signAmlDecision } from "./taler-signatures.js"; import { TalerProtocolTimestamp } from "./time.js"; @@ -702,3 +711,60 @@ test("signAmlDecision rejects a malformed h_payto", (t) => { signAmlDecision(priv, { ...decision, h_payto: hPayto.slice(0, -1) }), ); }); + +test("contract decryption honours the declared plaintext length", async (t) => { + initNodePrng(); + const purse = createEddsaKeyPair(); + const contract = createEddsaKeyPair(); + const nonce = randomBytes(24) as any; + const contractTerms = { amount: "KUDOS:1", summary: "hello" }; + + const good = await encryptContractForDeposit( + purse.eddsaPub as any, + contract.eddsaPriv as any, + contractTerms, + nonce, + ); + assert.deepStrictEqual( + ( + await decryptContractForDeposit( + good, + purse.eddsaPub as any, + contract.eddsaPriv as any, + ) + ).contractTerms, + contractTerms, + ); + + // A blob whose header lies about the plaintext length, or names another + // format, is remote input; decompressing it unbounded lets the peer pick + // how much memory the wallet allocates. + const key = keyExchangeEcdhEddsa( + contract.eddsaPriv as any, + purse.eddsaPub as any, + ); + const compressed = fflate.zlibSync( + stringToBytes(canonicalJson(contractTerms) + "\0"), + ); + const forge = (ctype: number, clen: number): Promise<Uint8Array> => + encryptWithDerivedKey( + nonce, + key, + typedArrayConcat([ + bufferForUint32(ctype), + bufferForUint32(clen), + compressed, + ]), + "p2p-deposit-contract", + ); + + for (const bad of [await forge(1, 1 << 28), await forge(99, 44)]) { + await assert.rejects(() => + decryptContractForDeposit( + bad, + purse.eddsaPub as any, + contract.eddsaPriv as any, + ), + ); + } +});