diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/taler-util/src/iban.test.ts | 32 | ||||
| -rw-r--r-- | packages/taler-util/src/iban.ts | 296 | ||||
| -rw-r--r-- | packages/taler-util/src/index.ts | 1 | ||||
| -rw-r--r-- | packages/taler-util/src/wallet-types.ts | 27 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/wallet-api-types.ts | 12 | ||||
| -rw-r--r-- | packages/taler-wallet-core/src/wallet.ts | 11 | 
6 files changed, 374 insertions, 5 deletions
| diff --git a/packages/taler-util/src/iban.test.ts b/packages/taler-util/src/iban.test.ts new file mode 100644 index 000000000..69fb2dd75 --- /dev/null +++ b/packages/taler-util/src/iban.test.ts @@ -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"); +}); diff --git a/packages/taler-util/src/iban.ts b/packages/taler-util/src/iban.ts new file mode 100644 index 000000000..3a40e45a4 --- /dev/null +++ b/packages/taler-util/src/iban.ts @@ -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; +} diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts index cf4f545a4..1c882a82b 100644 --- a/packages/taler-util/src/index.ts +++ b/packages/taler-util/src/index.ts @@ -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"; diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 338124d08..b08b02ca3 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -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"); diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 84bad09fe..0b1968857 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -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< diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 6197f000e..363214e7b 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -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); | 
