implement IBAN validation
This commit is contained in:
parent
e311dc4bef
commit
a17a08ae28
32
packages/taler-util/src/iban.test.ts
Normal file
32
packages/taler-util/src/iban.test.ts
Normal 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");
|
||||
});
|
296
packages/taler-util/src/iban.ts
Normal file
296
packages/taler-util/src/iban.ts
Normal 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;
|
||||
}
|
@ -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";
|
||||
|
@ -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");
|
||||
|
@ -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<
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user