commit af64bee36a35e6460ab3c390752e786311eee0d4
parent 817c567b71dd7f68d0e6525178b225160930b3ab
Author: Florian Dold <dold@taler.net>
Date: Wed, 19 Aug 2026 18:16:21 +0200
taler-util: normalize merchant OTP keys
Diffstat:
3 files changed, 60 insertions(+), 1 deletion(-)
diff --git a/packages/taler-util/src/rfc3548.test.ts b/packages/taler-util/src/rfc3548.test.ts
@@ -0,0 +1,45 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import assert from "node:assert";
+import test from "node:test";
+import {
+ normalizeRfc3548Base32Key,
+ randomRfc3548Base32Key,
+} from "./rfc3548.js";
+import { initNodePrng } from "./prng-node.js";
+
+initNodePrng();
+
+test("OTP keys use 32 RFC 3548 Base32 characters", () => {
+ const first = randomRfc3548Base32Key();
+ const second = randomRfc3548Base32Key();
+ assert.match(first, /^[A-Z2-7]{32}$/);
+ assert.match(second, /^[A-Z2-7]{32}$/);
+ assert.notStrictEqual(first, second);
+});
+
+test("manual OTP keys normalize case and whitespace but reject invalid input", () => {
+ assert.strictEqual(
+ normalizeRfc3548Base32Key("jbsw y3dp ehpk 3pxp jbsw y3dp ehpk 3pxp"),
+ "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP",
+ );
+ assert.strictEqual(normalizeRfc3548Base32Key("JBSWY3DPEHPK3PXP"), undefined);
+ assert.strictEqual(
+ normalizeRfc3548Base32Key("JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PX!"),
+ undefined,
+ );
+});
diff --git a/packages/taler-util/src/rfc3548.ts b/packages/taler-util/src/rfc3548.ts
@@ -59,6 +59,20 @@ export function isRfc3548Base32Charset(s: string): boolean {
return true;
}
+/**
+ * Normalize a manually entered 160-bit OTP key.
+ *
+ * Whitespace is ignored so that a key copied in groups remains usable. Other
+ * punctuation is rejected instead of being silently discarded.
+ */
+export function normalizeRfc3548Base32Key(s: string): string | undefined {
+ const normalized = s.toUpperCase().replace(/\s/g, "");
+ if (normalized.length !== 32 || !isRfc3548Base32Charset(normalized)) {
+ return undefined;
+ }
+ return normalized;
+}
+
export function randomRfc3548Base32Key(): string {
const buf = getRandomBytes(20);
return encodeRfc3548Base32(buf);
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts
@@ -3335,7 +3335,7 @@ export interface OtpDevicePatchDetails {
// Taler base32-crockford encoding.
// Instead it uses the RFC 3548 encoding to
// be compatible with the TOTP standard.
- otp_key: string;
+ otp_key?: string;
// Algorithm for computing the POS confirmation.
otp_algorithm: Integer;