|
| 1 | +// Copyright 2026 Google LLC |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +// Package iban implements IBAN detection logic. |
| 16 | +package iban |
| 17 | + |
| 18 | +import ( |
| 19 | + "bytes" |
| 20 | + "regexp" |
| 21 | + "strings" |
| 22 | + |
| 23 | + "github.com/google/osv-scalibr/veles" |
| 24 | + "github.com/google/osv-scalibr/veles/sensitiveinformation" |
| 25 | + "github.com/google/osv-scalibr/veles/sensitiveinformation/common/simpleregex" |
| 26 | +) |
| 27 | + |
| 28 | +const maxSecretLength = 41 |
| 29 | + |
| 30 | +// The printed/spaced IBAN format normally groups characters into blocks of four. |
| 31 | +// A few countries are exceptions to this and use their own block sizes, so they |
| 32 | +// need dedicated alternatives. |
| 33 | +// |
| 34 | +// https://en.wikipedia.org/wiki/International_Bank_Account_Number#Structure |
| 35 | +var ibanRe = regexp.MustCompile(strings.Join([]string{ |
| 36 | + `(?i)\b(?:`, |
| 37 | + // Compact (no spaces, also covers Egypt) or the standard blocks of four. |
| 38 | + `[A-Z]{2}\d{2}(?:[A-Z0-9]{11,30}|(?: [A-Z0-9]{4}){2,7}(?: [A-Z0-9]{1,3})?)`, |
| 39 | + // Burundi: 4 5 5 11 2 |
| 40 | + `|BI\d{2} [A-Z0-9]{5} [A-Z0-9]{5} [A-Z0-9]{11} [A-Z0-9]{2}`, |
| 41 | + // Libya: 4 3 3 15 |
| 42 | + `|LY\d{2} [A-Z0-9]{3} [A-Z0-9]{3} [A-Z0-9]{15}`, |
| 43 | + // El Salvador: 2 2 4 20 (note the space between country code and check digits) |
| 44 | + `|SV \d{2} [A-Z0-9]{4} [A-Z0-9]{20}`, |
| 45 | + `)\b`, |
| 46 | +}, "")) |
| 47 | + |
| 48 | +// https://en.wikipedia.org/wiki/International_Bank_Account_Number#IBAN_formats_by_country |
| 49 | +var countryLengths = map[string]int{ |
| 50 | + "AD": 24, "AE": 23, "AL": 28, "AT": 20, "AZ": 28, |
| 51 | + "BA": 20, "BE": 16, "BG": 22, "BH": 22, "BI": 27, "BR": 29, "BY": 28, |
| 52 | + "CH": 21, "CR": 22, "CY": 28, "CZ": 24, |
| 53 | + "DE": 22, "DJ": 27, "DK": 18, "DO": 28, |
| 54 | + "EE": 20, "EG": 29, "ES": 24, |
| 55 | + "FI": 18, "FK": 18, "FO": 18, "FR": 27, |
| 56 | + "GB": 22, "GE": 22, "GI": 23, "GL": 18, "GR": 27, "GT": 28, |
| 57 | + "HN": 28, "HR": 21, "HU": 28, |
| 58 | + "IE": 22, "IL": 23, "IQ": 23, "IS": 26, "IT": 27, |
| 59 | + "JO": 30, |
| 60 | + "KW": 30, "KZ": 20, |
| 61 | + "LB": 28, "LC": 32, "LI": 21, "LT": 20, "LU": 20, "LV": 21, "LY": 25, |
| 62 | + "MC": 27, "MD": 24, "ME": 22, "MK": 19, "MN": 20, "MR": 27, "MT": 31, "MU": 30, |
| 63 | + "NI": 28, "NL": 18, "NO": 15, |
| 64 | + "OM": 23, |
| 65 | + "PK": 24, "PL": 28, "PS": 29, "PT": 25, |
| 66 | + "QA": 29, |
| 67 | + "RO": 24, "RS": 22, "RU": 33, |
| 68 | + "SA": 24, "SC": 31, "SD": 18, "SE": 24, "SI": 19, "SK": 24, "SM": 27, "SO": 23, "ST": 25, "SV": 28, |
| 69 | + "TL": 23, "TN": 24, "TR": 26, |
| 70 | + "UA": 29, |
| 71 | + "VA": 22, "VG": 24, |
| 72 | + "YE": 30, |
| 73 | + "XK": 20, |
| 74 | +} |
| 75 | + |
| 76 | +// Most examples taken from https://www.citibank.pl/poland/citidirect/polish/pdf/iban.pdf |
| 77 | +var commonExamples = map[string]struct{}{ |
| 78 | + "AL47212110090000000235698741": {}, |
| 79 | + "AD1200012030200359100100": {}, |
| 80 | + "SA0380000000608010167519": {}, |
| 81 | + "AT611904300234573201": {}, |
| 82 | + "BE68539007547034": {}, |
| 83 | + "BA391290079401028494": {}, |
| 84 | + "BG80BNBG96611020345678": {}, |
| 85 | + "HR1210010051863000160": {}, |
| 86 | + "CY17002001280000001200527600": {}, |
| 87 | + "ME25505000012345678951": {}, |
| 88 | + "DK5000400440116243": {}, |
| 89 | + "EE382200221020145685": {}, |
| 90 | + "FI2112345600000785": {}, |
| 91 | + "FR1420041010050500013M02606": {}, |
| 92 | + "GI75NWBK000000007099453": {}, |
| 93 | + "GR1601101250000000012300695": {}, |
| 94 | + "GE29NB0000000101904917": {}, |
| 95 | + "ES9121000418450200051332": {}, |
| 96 | + "NL91ABNA0417164300": {}, |
| 97 | + "IE29AIBK93115212345678": {}, |
| 98 | + "IS140159260076545510730339": {}, |
| 99 | + "IL620108000000099999999": {}, |
| 100 | + "KZ86125KZT5004100100": {}, |
| 101 | + "KW81CBKU0000000000001234560101": {}, |
| 102 | + "LB62099900000001001901229114": {}, |
| 103 | + "LI21088100002324013AA": {}, |
| 104 | + "LT121000011101001000": {}, |
| 105 | + "LU280019400644750000": {}, |
| 106 | + "LV80BANK0000435195001": {}, |
| 107 | + "MK07250120000058984": {}, |
| 108 | + "MT84MALT011000012345MTLCAST001S": {}, |
| 109 | + "MR1300020001010000123456753": {}, |
| 110 | + "MU17BOMM0101101030300200000MUR": {}, |
| 111 | + "MC1112739000700011111000H79": {}, |
| 112 | + "DE89370400440532013000": {}, |
| 113 | + "NO9386011117947": {}, |
| 114 | + "PL61109010140000071219812874": {}, |
| 115 | + "PT50000201231234567890154": {}, |
| 116 | + "CZ6508000000192000145399": {}, |
| 117 | + "SK3112000000198742637541": {}, |
| 118 | + "RO49AAAA1B31007593840000": {}, |
| 119 | + "SM86U0322509800000000270100": {}, |
| 120 | + "RS35260005601001611379": {}, |
| 121 | + "SI56191000000123438": {}, |
| 122 | + "CH9300762011623852957": {}, |
| 123 | + "SE4550000000058398257466": {}, |
| 124 | + "TN5910006035183598478831": {}, |
| 125 | + "TR330006100519786457841326": {}, |
| 126 | + "HU42117730161111101800000000": {}, |
| 127 | + "GB29NWBK60161331926819": {}, |
| 128 | + "IT60X0542811101000000123456": {}, |
| 129 | + |
| 130 | + // Additional very common one |
| 131 | + "GB82WEST12345698765432": {}, |
| 132 | +} |
| 133 | + |
| 134 | +// NewDetector returns a Detector that finds International Bank Account Numbers (IBANs). |
| 135 | +func NewDetector() veles.Detector { |
| 136 | + return simpleregex.Detector{ |
| 137 | + MaxLen: maxSecretLength, |
| 138 | + Re: ibanRe, |
| 139 | + FromMatch: func(b []byte, _ bool) (sensitiveinformation.SensitiveInformation, bool) { |
| 140 | + if !validIBAN(string(b)) { |
| 141 | + return sensitiveinformation.SensitiveInformation{}, false |
| 142 | + } |
| 143 | + |
| 144 | + finding := sensitiveinformation.SensitiveInformation{ |
| 145 | + InfoType: sensitiveinformation.InfoType{ |
| 146 | + Name: "INTERNATIONAL_BANK_ACCOUNT_NUMBER", |
| 147 | + Sensitivity: sensitiveinformation.SensitivityLevelHigh, |
| 148 | + }, |
| 149 | + Likelihood: sensitiveinformation.LikelihoodVeryLikely, |
| 150 | + Raw: bytes.Clone(b), |
| 151 | + } |
| 152 | + |
| 153 | + return finding, true |
| 154 | + }, |
| 155 | + } |
| 156 | +} |
| 157 | + |
| 158 | +func validIBAN(s string) bool { |
| 159 | + iban := strings.ToUpper(strings.ReplaceAll(s, " ", "")) |
| 160 | + if want, ok := countryLengths[iban[:2]]; !ok || len(iban) != want { |
| 161 | + return false |
| 162 | + } |
| 163 | + |
| 164 | + if _, ok := commonExamples[iban]; ok { |
| 165 | + return false |
| 166 | + } |
| 167 | + |
| 168 | + // https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN |
| 169 | + // |
| 170 | + // Opposed to the example described in the Wiki, the algorithm takes 1-digit chunks instead |
| 171 | + // of nine. In the case of letter parts of the IBAN, 2-digits chunks are taken, as letters |
| 172 | + // map to two decimal digits (A=10..Z=35). |
| 173 | + // |
| 174 | + // The length of chunks is arbitrary thanks to the properties of modal arithmetics: |
| 175 | + // (a⋅10+b) mod 97=((a mod 97)⋅10+b) mod 97 |
| 176 | + rearranged := iban[4:] + iban[:4] |
| 177 | + mod := 0 |
| 178 | + for _, r := range rearranged { |
| 179 | + switch { |
| 180 | + case r >= '0' && r <= '9': |
| 181 | + mod = (mod*10 + int(r-'0')) % 97 |
| 182 | + case r >= 'A' && r <= 'Z': |
| 183 | + v := int(r-'A') + 10 |
| 184 | + mod = (mod*100 + v) % 97 |
| 185 | + default: |
| 186 | + return false |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + return mod == 1 |
| 191 | +} |
0 commit comments