diff --git a/packages/taler-util/src/iban.test.ts b/packages/taler-util/src/iban.test.ts
new file mode 100644
index 000000000..69fb2dd75
--- /dev/null
+++ b/packages/taler-util/src/iban.test.ts
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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
+ */
+
+import test from "ava";
+import { generateIban, validateIban } from "./iban.js";
+
+test("iban validation", (t) => {
+ t.assert(validateIban("foo").type === "invalid");
+ t.assert(validateIban("NL71RABO9996666778").type === "valid");
+ t.assert(validateIban("NL71RABO9996666779").type === "invalid");
+});
+
+
+
+test("iban generation", (t) => {
+ let iban1 = generateIban("DE", 10);
+ console.log("generated IBAN", iban1);
+ t.assert(validateIban(iban1).type === "valid");
+});
diff --git a/packages/taler-util/src/iban.ts b/packages/taler-util/src/iban.ts
new file mode 100644
index 000000000..3a40e45a4
--- /dev/null
+++ b/packages/taler-util/src/iban.ts
@@ -0,0 +1,296 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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
+ */
+
+/**
+ * IBAN validation.
+ *
+ * Currently only validates the checksum.
+ *
+ * It does not validate:
+ * - Country-specific length
+ * - Country-specific checksums
+ *
+ * The country list is also not complete.
+ *
+ * @author Florian Dold
+ */
+
+export type IbanValidationResult =
+ | { type: "invalid" }
+ | {
+ type: "valid";
+ normalizedIban: string;
+ };
+
+export interface IbanCountryInfo {
+ name: string;
+ isSepa?: boolean;
+ length?: number;
+}
+
+/**
+ * Incomplete list, see https://www.swift.com/resource/iban-registry-pdf
+ */
+export const ibanCountryInfoTable: Record = {
+ AE: { name: "U.A.E." },
+ AF: { name: "Afghanistan" },
+ AL: { name: "Albania" },
+ AM: { name: "Armenia" },
+ AN: { name: "Netherlands Antilles" },
+ AR: { name: "Argentina" },
+ AT: { name: "Austria" },
+ AU: { name: "Australia" },
+ AZ: { name: "Azerbaijan" },
+ BA: { name: "Bosnia and Herzegovina" },
+ BD: { name: "Bangladesh" },
+ BE: { name: "Belgium" },
+ BG: { name: "Bulgaria" },
+ BH: { name: "Bahrain" },
+ BN: { name: "Brunei Darussalam" },
+ BO: { name: "Bolivia" },
+ BR: { name: "Brazil" },
+ BT: { name: "Bhutan" },
+ BY: { name: "Belarus" },
+ BZ: { name: "Belize" },
+ CA: { name: "Canada" },
+ CG: { name: "Congo" },
+ CH: { name: "Switzerland" },
+ CI: { name: "Cote d'Ivoire" },
+ CL: { name: "Chile" },
+ CM: { name: "Cameroon" },
+ CN: { name: "People's Republic of China" },
+ CO: { name: "Colombia" },
+ CR: { name: "Costa Rica" },
+ CS: { name: "Serbia and Montenegro" },
+ CZ: { name: "Czech Republic" },
+ DE: { name: "Germany" },
+ DK: { name: "Denmark" },
+ DO: { name: "Dominican Republic" },
+ DZ: { name: "Algeria" },
+ EC: { name: "Ecuador" },
+ EE: { name: "Estonia" },
+ EG: { name: "Egypt" },
+ ER: { name: "Eritrea" },
+ ES: { name: "Spain" },
+ ET: { name: "Ethiopia" },
+ FI: { name: "Finland" },
+ FO: { name: "Faroe Islands" },
+ FR: { name: "France" },
+ GB: { name: "United Kingdom" },
+ GD: { name: "Caribbean" },
+ GE: { name: "Georgia" },
+ GL: { name: "Greenland" },
+ GR: { name: "Greece" },
+ GT: { name: "Guatemala" },
+ HK: { name: "Hong Kong S.A.R." },
+ HN: { name: "Honduras" },
+ HR: { name: "Croatia" },
+ HT: { name: "Haiti" },
+ HU: { name: "Hungary" },
+ ID: { name: "Indonesia" },
+ IE: { name: "Ireland" },
+ IL: { name: "Israel" },
+ IN: { name: "India" },
+ IQ: { name: "Iraq" },
+ IR: { name: "Iran" },
+ IS: { name: "Iceland" },
+ IT: { name: "Italy" },
+ JM: { name: "Jamaica" },
+ JO: { name: "Jordan" },
+ JP: { name: "Japan" },
+ KE: { name: "Kenya" },
+ KG: { name: "Kyrgyzstan" },
+ KH: { name: "Cambodia" },
+ KR: { name: "South Korea" },
+ KW: { name: "Kuwait" },
+ KZ: { name: "Kazakhstan" },
+ LA: { name: "Laos" },
+ LB: { name: "Lebanon" },
+ LI: { name: "Liechtenstein" },
+ LK: { name: "Sri Lanka" },
+ LT: { name: "Lithuania" },
+ LU: { name: "Luxembourg" },
+ LV: { name: "Latvia" },
+ LY: { name: "Libya" },
+ MA: { name: "Morocco" },
+ MC: { name: "Principality of Monaco" },
+ MD: { name: "Moldava" },
+ ME: { name: "Montenegro" },
+ MK: { name: "Former Yugoslav Republic of Macedonia" },
+ ML: { name: "Mali" },
+ MM: { name: "Myanmar" },
+ MN: { name: "Mongolia" },
+ MO: { name: "Macau S.A.R." },
+ MT: { name: "Malta" },
+ MV: { name: "Maldives" },
+ MX: { name: "Mexico" },
+ MY: { name: "Malaysia" },
+ NG: { name: "Nigeria" },
+ NI: { name: "Nicaragua" },
+ NL: { name: "Netherlands" },
+ NO: { name: "Norway" },
+ NP: { name: "Nepal" },
+ NZ: { name: "New Zealand" },
+ OM: { name: "Oman" },
+ PA: { name: "Panama" },
+ PE: { name: "Peru" },
+ PH: { name: "Philippines" },
+ PK: { name: "Islamic Republic of Pakistan" },
+ PL: { name: "Poland" },
+ PR: { name: "Puerto Rico" },
+ PT: { name: "Portugal" },
+ PY: { name: "Paraguay" },
+ QA: { name: "Qatar" },
+ RE: { name: "Reunion" },
+ RO: { name: "Romania" },
+ RS: { name: "Serbia" },
+ RU: { name: "Russia" },
+ RW: { name: "Rwanda" },
+ SA: { name: "Saudi Arabia" },
+ SE: { name: "Sweden" },
+ SG: { name: "Singapore" },
+ SI: { name: "Slovenia" },
+ SK: { name: "Slovak" },
+ SN: { name: "Senegal" },
+ SO: { name: "Somalia" },
+ SR: { name: "Suriname" },
+ SV: { name: "El Salvador" },
+ SY: { name: "Syria" },
+ TH: { name: "Thailand" },
+ TJ: { name: "Tajikistan" },
+ TM: { name: "Turkmenistan" },
+ TN: { name: "Tunisia" },
+ TR: { name: "Turkey" },
+ TT: { name: "Trinidad and Tobago" },
+ TW: { name: "Taiwan" },
+ TZ: { name: "Tanzania" },
+ UA: { name: "Ukraine" },
+ US: { name: "United States" },
+ UY: { name: "Uruguay" },
+ VA: { name: "Vatican" },
+ VE: { name: "Venezuela" },
+ VN: { name: "Viet Nam" },
+ YE: { name: "Yemen" },
+ ZA: { name: "South Africa" },
+ ZW: { name: "Zimbabwe" },
+};
+
+let ccZero = "0".charCodeAt(0);
+let ccNine = "9".charCodeAt(0);
+let ccA = "A".charCodeAt(0);
+let ccZ = "Z".charCodeAt(0);
+
+/**
+ * Append a IBAN digit(s) based on a char code.
+ */
+function appendDigit(digits: number[], cc: number): boolean {
+ if (cc >= ccZero && cc <= ccNine) {
+ digits.push(cc - ccZero);
+ } else if (cc >= ccA && cc <= ccZ) {
+ const n = cc - ccA + 10;
+ digits.push(Math.floor(n / 10) % 10);
+ digits.push(n % 10);
+ } else {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Compute MOD-97-10 as per ISO/IEC 7064:2003.
+ */
+function mod97(digits: number[]): number {
+ let i = 0;
+ let modAccum = 0;
+ while (i < digits.length) {
+ let n = 0;
+ while (n < 9 && i < digits.length) {
+ modAccum = modAccum * 10 + digits[i];
+ i++;
+ n++;
+ }
+ modAccum = modAccum % 97;
+ }
+ return modAccum;
+}
+
+export function validateIban(ibanString: string): IbanValidationResult {
+ let myIban = ibanString.toLocaleUpperCase().replace(" ", "");
+ let countryCode = myIban.substring(0, 2);
+ let countryInfo = ibanCountryInfoTable[countryCode];
+
+ if (!countryInfo) {
+ return {
+ type: "invalid",
+ };
+ }
+
+ let digits: number[] = [];
+
+ for (let i = 4; i < myIban.length; i++) {
+ const cc = myIban.charCodeAt(i);
+ if (!appendDigit(digits, cc)) {
+ return {
+ type: "invalid",
+ };
+ }
+ }
+
+ for (let i = 0; i < 4; i++) {
+ if (!appendDigit(digits, ibanString.charCodeAt(i))) {
+ return {
+ type: "invalid",
+ };
+ }
+ }
+
+ const rem = mod97(digits);
+ if (rem === 1) {
+ return {
+ type: "valid",
+ normalizedIban: myIban,
+ };
+ } else {
+ return {
+ type: "invalid",
+ };
+ }
+}
+
+export function generateIban(countryCode: string, length: number): string {
+ let ibanSuffix = "";
+ let digits: number[] = [];
+
+ for (let i = 0; i < length; i++) {
+ const cc = ccZero + (Math.floor(Math.random() * 100) % 10);
+ appendDigit(digits, cc)
+ ibanSuffix += String.fromCharCode(cc);
+ }
+
+ appendDigit(digits, countryCode.charCodeAt(0));
+ appendDigit(digits, countryCode.charCodeAt(1));
+
+ // Try using "00" as check digits
+ appendDigit(digits, ccZero);
+ appendDigit(digits, ccZero);
+
+ const requiredChecksum = 98 - mod97(digits);
+
+ const checkDigit1 = Math.floor(requiredChecksum / 10) % 10;
+ const checkDigit2 = requiredChecksum % 10;
+
+ return countryCode + checkDigit1 + checkDigit2 + ibanSuffix;
+}
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index cf4f545a4..1c882a82b 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -37,3 +37,4 @@ export * from "./contract-terms.js";
export * from "./base64.js";
export * from "./merchant-api-types.js";
export * from "./errors.js";
+export * from "./iban.js";
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index 338124d08..b08b02ca3 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -1663,11 +1663,10 @@ export interface ResumeTransactionRequest {
transactionId: string;
}
-export const codecForResumeTransaction =
- (): Codec =>
- buildCodecForObject()
- .property("transactionId", codecForString())
- .build("ResumeTransactionRequest");
+export const codecForResumeTransaction = (): Codec =>
+ buildCodecForObject()
+ .property("transactionId", codecForString())
+ .build("ResumeTransactionRequest");
export interface AbortTransactionRequest {
transactionId: string;
@@ -2257,3 +2256,21 @@ export interface PayPeerInsufficientBalanceDetails {
};
};
}
+
+export interface ValidateIbanRequest {
+ iban: string;
+}
+
+export const codecForValidateIbanRequest = (): Codec =>
+ buildCodecForObject()
+ .property("iban", codecForString())
+ .build("ValidateIbanRequest");
+
+export interface ValidateIbanResponse {
+ valid: boolean;
+}
+
+export const codecForValidateIbanResponse = (): Codec =>
+ buildCodecForObject()
+ .property("valid", codecForBoolean())
+ .build("ValidateIbanResponse");
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 84bad09fe..0b1968857 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -110,6 +110,8 @@ import {
WithdrawFakebankRequest,
WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
+ ValidateIbanRequest,
+ ValidateIbanResponse,
} from "@gnu-taler/taler-util";
import { WalletContractData } from "./db.js";
import {
@@ -197,6 +199,7 @@ export enum WalletApiOperation {
Recycle = "recycle",
SetDevMode = "setDevMode",
ApplyDevExperiment = "applyDevExperiment",
+ ValidateIban = "validateIban",
}
// group: Initialization
@@ -683,6 +686,14 @@ export type ConfirmPeerPullDebitOp = {
response: EmptyObject;
};
+// group: Data Validation
+
+export type ValidateIbanOp = {
+ op: WalletApiOperation.ValidateIban;
+ request: ValidateIbanRequest;
+ response: ValidateIbanResponse;
+};
+
// group: Database Management
/**
@@ -937,6 +948,7 @@ export type WalletOperations = {
[WalletApiOperation.Recycle]: RecycleOp;
[WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp;
[WalletApiOperation.SetDevMode]: SetDevModeOp;
+ [WalletApiOperation.ValidateIban]: ValidateIbanOp;
};
export type WalletCoreRequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 6197f000e..363214e7b 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -109,6 +109,9 @@ import {
WalletNotification,
codecForSuspendTransaction,
codecForResumeTransaction,
+ validateIban,
+ codecForValidateIbanRequest,
+ ValidateIbanResponse,
} from "@gnu-taler/taler-util";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import {
@@ -1043,6 +1046,14 @@ async function dispatchRequestInternal(
await runIntegrationTest(ws, req);
return {};
}
+ case WalletApiOperation.ValidateIban: {
+ const req = codecForValidateIbanRequest().decode(payload);
+ const valRes = validateIban(req.iban);
+ const resp: ValidateIbanResponse = {
+ valid: valRes.type === "valid",
+ };
+ return resp;
+ }
case WalletApiOperation.TestPay: {
const req = codecForTestPayArgs().decode(payload);
return await testPay(ws, req);