implement IBAN validation

This commit is contained in:
Florian Dold 2023-03-29 18:49:06 +02:00
parent e311dc4bef
commit a17a08ae28
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 374 additions and 5 deletions

View File

@ -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 <http://www.gnu.org/licenses/>
*/
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");
});

View File

@ -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 <http://www.gnu.org/licenses/>
*/
/**
* 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 <dold@taler.net>
*/
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<string, IbanCountryInfo> = {
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;
}

View File

@ -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";

View File

@ -1663,11 +1663,10 @@ export interface ResumeTransactionRequest {
transactionId: string;
}
export const codecForResumeTransaction =
(): Codec<ResumeTransactionRequest> =>
buildCodecForObject<ResumeTransactionRequest>()
.property("transactionId", codecForString())
.build("ResumeTransactionRequest");
export const codecForResumeTransaction = (): Codec<ResumeTransactionRequest> =>
buildCodecForObject<ResumeTransactionRequest>()
.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<ValidateIbanRequest> =>
buildCodecForObject<ValidateIbanRequest>()
.property("iban", codecForString())
.build("ValidateIbanRequest");
export interface ValidateIbanResponse {
valid: boolean;
}
export const codecForValidateIbanResponse = (): Codec<ValidateIbanResponse> =>
buildCodecForObject<ValidateIbanResponse>()
.property("valid", codecForBoolean())
.build("ValidateIbanResponse");

View File

@ -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<

View File

@ -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<Op extends WalletApiOperation>(
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);