2023-03-29 18:49:06 +02:00
|
|
|
/*
|
|
|
|
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);
|
2023-05-22 18:12:38 +02:00
|
|
|
appendDigit(digits, cc);
|
2023-03-29 18:49:06 +02:00
|
|
|
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;
|
|
|
|
}
|