| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- // Unless explicitly stated otherwise all files in this repository are licensed
- // under the Apache License Version 2.0.
- // This product includes software developed at Datadog (https://www.datadoghq.com/).
- // Copyright 2016-present Datadog, Inc.
- package obfuscate
- // IsCardNumber checks if b could be a credit card number by checking the digit count and IIN prefix.
- // If validateLuhn is true, the Luhn checksum is also applied to potential candidates.
- func IsCardNumber(b string, validateLuhn bool) (ok bool) {
- //
- // Just credit card numbers for now, based on:
- // • https://baymard.com/checkout-usability/credit-card-patterns
- // • https://www.regular-expressions.info/creditcard.html
- //
- if len(b) == 0 {
- return false
- }
- if len(b) < 12 {
- // fast path: can not be a credit card
- return false
- }
- if b[0] != ' ' && b[0] != '-' && (b[0] < '0' || b[0] > '9') {
- // fast path: only valid characters are 0-9, space (" ") and dash("-")
- return false
- }
- prefix := 0 // holds up to b[:6] digits as a numeric value (for example []byte{"523"} becomes int(523)) for checking prefixes
- count := 0 // counts digits encountered
- foundPrefix := false // reports whether we've detected a valid prefix
- recdigit := func(_ byte) {} // callback on each found digit; no-op by default (we only need this for Luhn)
- if validateLuhn {
- // we need Luhn checksum validation, so we have to take additional action
- // and record all digits found
- buf := make([]byte, 0, len(b))
- recdigit = func(b byte) { buf = append(buf, b) }
- defer func() {
- if !ok {
- // if isCardNumber returned false, it means that b can not be
- // a credit card number
- return
- }
- // potentially a credit card number, run the Luhn checksum
- ok = luhnValid(buf)
- }()
- }
- loop:
- for i := range b {
- // We traverse and search b for a valid IIN credit card prefix based
- // on the digits found, ignoring spaces and dashes.
- // Source: https://www.regular-expressions.info/creditcard.html
- switch b[i] {
- case ' ', '-':
- // ignore space (' ') and dash ('-')
- continue loop
- }
- if b[i] < '0' || b[i] > '9' {
- // not a 0 to 9 digit; can not be a credit card number; abort
- return false
- }
- count++
- recdigit(b[i])
- if !foundPrefix {
- // we have not yet found a valid prefix so we convert the digits
- // that we have so far into a numeric value:
- prefix = prefix*10 + (int(b[i]) - '0')
- maybe, yes := validCardPrefix(prefix)
- if yes {
- // we've found a valid prefix; continue counting
- foundPrefix = true
- } else if !maybe {
- // this is not a valid prefix and we should not continue looking
- return false
- }
- }
- if count > 16 {
- // too many digits
- return false
- }
- }
- if count < 12 {
- // too few digits
- return false
- }
- return foundPrefix
- }
- // luhnValid checks that the number represented in the given string validates the Luhn Checksum algorithm.
- // str is expected to contain exclusively digits at all positions.
- //
- // See:
- // • https://en.wikipedia.org/wiki/Luhn_algorithm
- // • https://dev.to/shiraazm/goluhn-a-simple-library-for-generating-calculating-and-verifying-luhn-numbers-588j
- //
- func luhnValid(str []byte) bool {
- var (
- sum int
- alt bool
- )
- n := len(str)
- for i := n - 1; i > -1; i-- {
- if str[i] < '0' || str[i] > '9' {
- return false // not a number!
- }
- mod := int(str[i] - 0x30) // convert byte to int
- if alt {
- mod *= 2
- if mod > 9 {
- mod = (mod % 10) + 1
- }
- }
- alt = !alt
- sum += mod
- }
- return sum%10 == 0
- }
- // validCardPrefix validates whether b is a valid card prefix. Maybe returns true if
- // the prefix could be an IIN once more digits are revealed and yes reports whether
- // b is a fully valid IIN.
- //
- // If yes is false and maybe is false, there is no reason to continue searching. The
- // prefix is invalid.
- //
- // IMPORTANT: If adding new prefixes to this algorithm, make sure that you update
- // the "maybe" clauses above, in the shorter prefixes than the one you are adding.
- // This refers to the cases which return true, false.
- //
- // TODO(x): this whole code could be code generated from a prettier data structure.
- // Ultimately, it could even be user-configurable.
- func validCardPrefix(n int) (maybe, yes bool) {
- // Validates IIN prefix possibilities
- // Source: https://www.regular-expressions.info/creditcard.html
- if n > 699999 {
- // too long for any known prefix; stop looking
- return false, false
- }
- if n < 10 {
- switch n {
- case 1, 4:
- // 1 & 4 are valid IIN
- return false, true
- case 2, 3, 5, 6:
- // 2, 3, 5, 6 could be the start of valid IIN
- return true, false
- default:
- // invalid IIN
- return false, false
- }
- }
- if n < 100 {
- if (n >= 34 && n <= 39) ||
- (n >= 51 && n <= 55) ||
- n == 62 ||
- n == 65 {
- // 34-39, 51-55, 62, 65 are valid IIN
- return false, true
- }
- if n == 30 || n == 63 || n == 64 || n == 35 || n == 50 || n == 60 ||
- (n >= 22 && n <= 27) || (n >= 56 && n <= 58) || (n >= 60 && n <= 69) {
- // 30, 63, 64, 35, 50, 60, 22-27, 56-58, 60-69 may end up as valid IIN
- return true, false
- }
- }
- if n < 1000 {
- if (n >= 300 && n <= 305) ||
- (n >= 644 && n <= 649) ||
- n == 309 ||
- n == 636 {
- // 300‑305, 309, 636, 644‑649 are valid IIN
- return false, true
- }
- if (n >= 352 && n <= 358) || n == 501 || n == 601 ||
- (n >= 222 && n <= 272) || (n >= 500 && n <= 509) ||
- (n >= 560 && n <= 589) || (n >= 600 && n <= 699) {
- // 352-358, 501, 601, 222-272, 500-509, 560-589, 600-699 may be a 4 or 6 digit IIN prefix
- return true, false
- }
- }
- if n < 10000 {
- if (n >= 3528 && n <= 3589) ||
- n == 5019 ||
- n == 6011 {
- // 3528‑3589, 5019, 6011 are valid IINs
- return false, true
- }
- if (n >= 2221 && n <= 2720) || (n >= 5000 && n <= 5099) ||
- (n >= 5600 && n <= 5899) || (n >= 6000 && n <= 6999) {
- // maybe a 6-digit IIN
- return true, false
- }
- }
- if n < 100000 {
- if (n >= 22210 && n <= 27209) ||
- (n >= 50000 && n <= 50999) ||
- (n >= 56000 && n <= 58999) ||
- (n >= 60000 && n <= 69999) {
- // maybe a 6-digit IIN
- return true, false
- }
- }
- if n < 1000000 {
- if (n >= 222100 && n <= 272099) ||
- (n >= 500000 && n <= 509999) ||
- (n >= 560000 && n <= 589999) ||
- (n >= 600000 && n <= 699999) {
- // 222100‑272099, 500000‑509999, 560000‑589999, 600000‑699999 are valid IIN
- return false, true
- }
- }
- // unknown IIN
- return false, false
- }
|