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:
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,
+ ),
+ );
+ }
+});