taler-typescript-core

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

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:
Apackages/taler-util/src/rfc3548.test.ts | 45+++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/rfc3548.ts | 14++++++++++++++
Mpackages/taler-util/src/types-taler-merchant.ts | 2+-
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;