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 "./base64.js";
|
||||||
export * from "./merchant-api-types.js";
|
export * from "./merchant-api-types.js";
|
||||||
export * from "./errors.js";
|
export * from "./errors.js";
|
||||||
|
export * from "./iban.js";
|
||||||
|
@ -1663,11 +1663,10 @@ export interface ResumeTransactionRequest {
|
|||||||
transactionId: string;
|
transactionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForResumeTransaction =
|
export const codecForResumeTransaction = (): Codec<ResumeTransactionRequest> =>
|
||||||
(): Codec<ResumeTransactionRequest> =>
|
buildCodecForObject<ResumeTransactionRequest>()
|
||||||
buildCodecForObject<ResumeTransactionRequest>()
|
.property("transactionId", codecForString())
|
||||||
.property("transactionId", codecForString())
|
.build("ResumeTransactionRequest");
|
||||||
.build("ResumeTransactionRequest");
|
|
||||||
|
|
||||||
export interface AbortTransactionRequest {
|
export interface AbortTransactionRequest {
|
||||||
transactionId: string;
|
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,
|
WithdrawFakebankRequest,
|
||||||
WithdrawTestBalanceRequest,
|
WithdrawTestBalanceRequest,
|
||||||
WithdrawUriInfoResponse,
|
WithdrawUriInfoResponse,
|
||||||
|
ValidateIbanRequest,
|
||||||
|
ValidateIbanResponse,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { WalletContractData } from "./db.js";
|
import { WalletContractData } from "./db.js";
|
||||||
import {
|
import {
|
||||||
@ -197,6 +199,7 @@ export enum WalletApiOperation {
|
|||||||
Recycle = "recycle",
|
Recycle = "recycle",
|
||||||
SetDevMode = "setDevMode",
|
SetDevMode = "setDevMode",
|
||||||
ApplyDevExperiment = "applyDevExperiment",
|
ApplyDevExperiment = "applyDevExperiment",
|
||||||
|
ValidateIban = "validateIban",
|
||||||
}
|
}
|
||||||
|
|
||||||
// group: Initialization
|
// group: Initialization
|
||||||
@ -683,6 +686,14 @@ export type ConfirmPeerPullDebitOp = {
|
|||||||
response: EmptyObject;
|
response: EmptyObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// group: Data Validation
|
||||||
|
|
||||||
|
export type ValidateIbanOp = {
|
||||||
|
op: WalletApiOperation.ValidateIban;
|
||||||
|
request: ValidateIbanRequest;
|
||||||
|
response: ValidateIbanResponse;
|
||||||
|
};
|
||||||
|
|
||||||
// group: Database Management
|
// group: Database Management
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -937,6 +948,7 @@ export type WalletOperations = {
|
|||||||
[WalletApiOperation.Recycle]: RecycleOp;
|
[WalletApiOperation.Recycle]: RecycleOp;
|
||||||
[WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp;
|
[WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp;
|
||||||
[WalletApiOperation.SetDevMode]: SetDevModeOp;
|
[WalletApiOperation.SetDevMode]: SetDevModeOp;
|
||||||
|
[WalletApiOperation.ValidateIban]: ValidateIbanOp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WalletCoreRequestType<
|
export type WalletCoreRequestType<
|
||||||
|
@ -109,6 +109,9 @@ import {
|
|||||||
WalletNotification,
|
WalletNotification,
|
||||||
codecForSuspendTransaction,
|
codecForSuspendTransaction,
|
||||||
codecForResumeTransaction,
|
codecForResumeTransaction,
|
||||||
|
validateIban,
|
||||||
|
codecForValidateIbanRequest,
|
||||||
|
ValidateIbanResponse,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
||||||
import {
|
import {
|
||||||
@ -1043,6 +1046,14 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
|||||||
await runIntegrationTest(ws, req);
|
await runIntegrationTest(ws, req);
|
||||||
return {};
|
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: {
|
case WalletApiOperation.TestPay: {
|
||||||
const req = codecForTestPayArgs().decode(payload);
|
const req = codecForTestPayArgs().decode(payload);
|
||||||
return await testPay(ws, req);
|
return await testPay(ws, req);
|
||||||
|
Loading…
Reference in New Issue
Block a user