credit_cards.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. // Unless explicitly stated otherwise all files in this repository are licensed
  2. // under the Apache License Version 2.0.
  3. // This product includes software developed at Datadog (https://www.datadoghq.com/).
  4. // Copyright 2016-present Datadog, Inc.
  5. package obfuscate
  6. // IsCardNumber checks if b could be a credit card number by checking the digit count and IIN prefix.
  7. // If validateLuhn is true, the Luhn checksum is also applied to potential candidates.
  8. func IsCardNumber(b string, validateLuhn bool) (ok bool) {
  9. //
  10. // Just credit card numbers for now, based on:
  11. // • https://baymard.com/checkout-usability/credit-card-patterns
  12. // • https://www.regular-expressions.info/creditcard.html
  13. //
  14. if len(b) == 0 {
  15. return false
  16. }
  17. if len(b) < 12 {
  18. // fast path: can not be a credit card
  19. return false
  20. }
  21. if b[0] != ' ' && b[0] != '-' && (b[0] < '0' || b[0] > '9') {
  22. // fast path: only valid characters are 0-9, space (" ") and dash("-")
  23. return false
  24. }
  25. prefix := 0 // holds up to b[:6] digits as a numeric value (for example []byte{"523"} becomes int(523)) for checking prefixes
  26. count := 0 // counts digits encountered
  27. foundPrefix := false // reports whether we've detected a valid prefix
  28. recdigit := func(_ byte) {} // callback on each found digit; no-op by default (we only need this for Luhn)
  29. if validateLuhn {
  30. // we need Luhn checksum validation, so we have to take additional action
  31. // and record all digits found
  32. buf := make([]byte, 0, len(b))
  33. recdigit = func(b byte) { buf = append(buf, b) }
  34. defer func() {
  35. if !ok {
  36. // if isCardNumber returned false, it means that b can not be
  37. // a credit card number
  38. return
  39. }
  40. // potentially a credit card number, run the Luhn checksum
  41. ok = luhnValid(buf)
  42. }()
  43. }
  44. loop:
  45. for i := range b {
  46. // We traverse and search b for a valid IIN credit card prefix based
  47. // on the digits found, ignoring spaces and dashes.
  48. // Source: https://www.regular-expressions.info/creditcard.html
  49. switch b[i] {
  50. case ' ', '-':
  51. // ignore space (' ') and dash ('-')
  52. continue loop
  53. }
  54. if b[i] < '0' || b[i] > '9' {
  55. // not a 0 to 9 digit; can not be a credit card number; abort
  56. return false
  57. }
  58. count++
  59. recdigit(b[i])
  60. if !foundPrefix {
  61. // we have not yet found a valid prefix so we convert the digits
  62. // that we have so far into a numeric value:
  63. prefix = prefix*10 + (int(b[i]) - '0')
  64. maybe, yes := validCardPrefix(prefix)
  65. if yes {
  66. // we've found a valid prefix; continue counting
  67. foundPrefix = true
  68. } else if !maybe {
  69. // this is not a valid prefix and we should not continue looking
  70. return false
  71. }
  72. }
  73. if count > 16 {
  74. // too many digits
  75. return false
  76. }
  77. }
  78. if count < 12 {
  79. // too few digits
  80. return false
  81. }
  82. return foundPrefix
  83. }
  84. // luhnValid checks that the number represented in the given string validates the Luhn Checksum algorithm.
  85. // str is expected to contain exclusively digits at all positions.
  86. //
  87. // See:
  88. // • https://en.wikipedia.org/wiki/Luhn_algorithm
  89. // • https://dev.to/shiraazm/goluhn-a-simple-library-for-generating-calculating-and-verifying-luhn-numbers-588j
  90. //
  91. func luhnValid(str []byte) bool {
  92. var (
  93. sum int
  94. alt bool
  95. )
  96. n := len(str)
  97. for i := n - 1; i > -1; i-- {
  98. if str[i] < '0' || str[i] > '9' {
  99. return false // not a number!
  100. }
  101. mod := int(str[i] - 0x30) // convert byte to int
  102. if alt {
  103. mod *= 2
  104. if mod > 9 {
  105. mod = (mod % 10) + 1
  106. }
  107. }
  108. alt = !alt
  109. sum += mod
  110. }
  111. return sum%10 == 0
  112. }
  113. // validCardPrefix validates whether b is a valid card prefix. Maybe returns true if
  114. // the prefix could be an IIN once more digits are revealed and yes reports whether
  115. // b is a fully valid IIN.
  116. //
  117. // If yes is false and maybe is false, there is no reason to continue searching. The
  118. // prefix is invalid.
  119. //
  120. // IMPORTANT: If adding new prefixes to this algorithm, make sure that you update
  121. // the "maybe" clauses above, in the shorter prefixes than the one you are adding.
  122. // This refers to the cases which return true, false.
  123. //
  124. // TODO(x): this whole code could be code generated from a prettier data structure.
  125. // Ultimately, it could even be user-configurable.
  126. func validCardPrefix(n int) (maybe, yes bool) {
  127. // Validates IIN prefix possibilities
  128. // Source: https://www.regular-expressions.info/creditcard.html
  129. if n > 699999 {
  130. // too long for any known prefix; stop looking
  131. return false, false
  132. }
  133. if n < 10 {
  134. switch n {
  135. case 1, 4:
  136. // 1 & 4 are valid IIN
  137. return false, true
  138. case 2, 3, 5, 6:
  139. // 2, 3, 5, 6 could be the start of valid IIN
  140. return true, false
  141. default:
  142. // invalid IIN
  143. return false, false
  144. }
  145. }
  146. if n < 100 {
  147. if (n >= 34 && n <= 39) ||
  148. (n >= 51 && n <= 55) ||
  149. n == 62 ||
  150. n == 65 {
  151. // 34-39, 51-55, 62, 65 are valid IIN
  152. return false, true
  153. }
  154. if n == 30 || n == 63 || n == 64 || n == 35 || n == 50 || n == 60 ||
  155. (n >= 22 && n <= 27) || (n >= 56 && n <= 58) || (n >= 60 && n <= 69) {
  156. // 30, 63, 64, 35, 50, 60, 22-27, 56-58, 60-69 may end up as valid IIN
  157. return true, false
  158. }
  159. }
  160. if n < 1000 {
  161. if (n >= 300 && n <= 305) ||
  162. (n >= 644 && n <= 649) ||
  163. n == 309 ||
  164. n == 636 {
  165. // 300‑305, 309, 636, 644‑649 are valid IIN
  166. return false, true
  167. }
  168. if (n >= 352 && n <= 358) || n == 501 || n == 601 ||
  169. (n >= 222 && n <= 272) || (n >= 500 && n <= 509) ||
  170. (n >= 560 && n <= 589) || (n >= 600 && n <= 699) {
  171. // 352-358, 501, 601, 222-272, 500-509, 560-589, 600-699 may be a 4 or 6 digit IIN prefix
  172. return true, false
  173. }
  174. }
  175. if n < 10000 {
  176. if (n >= 3528 && n <= 3589) ||
  177. n == 5019 ||
  178. n == 6011 {
  179. // 3528‑3589, 5019, 6011 are valid IINs
  180. return false, true
  181. }
  182. if (n >= 2221 && n <= 2720) || (n >= 5000 && n <= 5099) ||
  183. (n >= 5600 && n <= 5899) || (n >= 6000 && n <= 6999) {
  184. // maybe a 6-digit IIN
  185. return true, false
  186. }
  187. }
  188. if n < 100000 {
  189. if (n >= 22210 && n <= 27209) ||
  190. (n >= 50000 && n <= 50999) ||
  191. (n >= 56000 && n <= 58999) ||
  192. (n >= 60000 && n <= 69999) {
  193. // maybe a 6-digit IIN
  194. return true, false
  195. }
  196. }
  197. if n < 1000000 {
  198. if (n >= 222100 && n <= 272099) ||
  199. (n >= 500000 && n <= 509999) ||
  200. (n >= 560000 && n <= 589999) ||
  201. (n >= 600000 && n <= 699999) {
  202. // 222100‑272099, 500000‑509999, 560000‑589999, 600000‑699999 are valid IIN
  203. return false, true
  204. }
  205. }
  206. // unknown IIN
  207. return false, false
  208. }