/* 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; }