commit 61157df2531fed7db753bbce30e2a5faaef9fb77
parent 36233a934b60f007af34d7f80a3452c12a95a584
Author: Florian Dold <dold@taler.net>
Date: Tue, 21 Jul 2026 02:16:13 +0200
util: reject wrong-length Crockford input for fixed-size values
decodeCrock derives the output length from the input, so a truncated or
over-long string yields a buffer of the wrong size. Add decodeCrockFixed,
which rejects any input that does not encode the expected number of bytes,
and use it for the AML decision h_payto that is put into a signed blob.
Diffstat:
2 files changed, 25 insertions(+), 2 deletions(-)
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
@@ -335,6 +335,29 @@ export function decodeCrock(encoded: string): Uint8Array {
return out;
}
+/**
+ * Decode a Crockford base32 string that must encode exactly expectedLen bytes.
+ *
+ * decodeCrock derives the output length from the input, so a truncated or
+ * over-long string silently yields a buffer of the wrong size. GNUnet's
+ * string_to_data instead decodes into a fixed-size buffer and rejects any input
+ * whose length does not match; use this wherever the decoded size is known.
+ */
+export function decodeCrockFixed(
+ encoded: string,
+ expectedLen: number,
+): Uint8Array {
+ const expectedChars = Math.ceil((expectedLen * 8) / 5);
+ if (encoded.length !== expectedChars) {
+ throw new EncodingError();
+ }
+ const out = decodeCrock(encoded);
+ if (out.length !== expectedLen) {
+ throw new EncodingError();
+ }
+ return out;
+}
+
export async function hashArgon2id(
password: Uint8Array,
salt: Uint8Array,
diff --git a/packages/taler-util/src/taler-signatures.ts b/packages/taler-util/src/taler-signatures.ts
@@ -20,7 +20,7 @@ import {
bufferFromAmount,
bufferForUint64,
buildSigPS,
- decodeCrock,
+ decodeCrockFixed,
EddsaPrivP,
eddsaSign,
EddsaSigP,
@@ -56,7 +56,7 @@ export function signAmlDecision(
decision.attributes_expiration ?? TalerProtocolTimestamp.fromSeconds(0),
),
);
- builder.put(decodeCrock(decision.h_payto));
+ builder.put(decodeCrockFixed(decision.h_payto, 64));
builder.put(hash(stringToBytes(decision.justification)));
builder.put(hash(stringToBytes(canonicalJson(decision.properties) + "\0")));
builder.put(hash(stringToBytes(canonicalJson(decision.new_rules) + "\0")));