stripe_card_validator.cpp 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "stripe/stripe_card_validator.h"
  8. #include "base/qt/qt_string_view.h"
  9. #include <QtCore/QDate>
  10. #include <QtCore/QRegularExpression>
  11. namespace Stripe {
  12. namespace {
  13. constexpr auto kMinCvcLength = 3;
  14. struct BinRange {
  15. QString low;
  16. QString high;
  17. int length = 0;
  18. CardBrand brand = CardBrand::Unknown;
  19. };
  20. [[nodiscard]] const std::vector<BinRange> &AllRanges() {
  21. static auto kResult = std::vector<BinRange>{
  22. // Unknown
  23. { "", "", 19, CardBrand::Unknown },
  24. // American Express
  25. { "34", "34", 15, CardBrand::Amex },
  26. { "37", "37", 15, CardBrand::Amex },
  27. // Diners Club
  28. { "30", "30", 16, CardBrand::DinersClub },
  29. { "36", "36", 14, CardBrand::DinersClub },
  30. { "38", "39", 16, CardBrand::DinersClub },
  31. // Discover
  32. { "60", "60", 16, CardBrand::Discover },
  33. { "64", "65", 16, CardBrand::Discover },
  34. // JCB
  35. { "35", "35", 16, CardBrand::JCB },
  36. // Mastercard
  37. { "50", "59", 16, CardBrand::MasterCard },
  38. { "22", "27", 16, CardBrand::MasterCard },
  39. { "67", "67", 16, CardBrand::MasterCard }, // Maestro
  40. // UnionPay
  41. { "62", "62", 16, CardBrand::UnionPay },
  42. { "81", "81", 16, CardBrand::UnionPay },
  43. // Visa
  44. { "40", "49", 16, CardBrand::Visa },
  45. { "413600", "413600", 13, CardBrand::Visa },
  46. { "444509", "444509", 13, CardBrand::Visa },
  47. { "444509", "444509", 13, CardBrand::Visa },
  48. { "444550", "444550", 13, CardBrand::Visa },
  49. { "450603", "450603", 13, CardBrand::Visa },
  50. { "450617", "450617", 13, CardBrand::Visa },
  51. { "450628", "450629", 13, CardBrand::Visa },
  52. { "450636", "450636", 13, CardBrand::Visa },
  53. { "450640", "450641", 13, CardBrand::Visa },
  54. { "450662", "450662", 13, CardBrand::Visa },
  55. { "463100", "463100", 13, CardBrand::Visa },
  56. { "476142", "476142", 13, CardBrand::Visa },
  57. { "476143", "476143", 13, CardBrand::Visa },
  58. { "492901", "492902", 13, CardBrand::Visa },
  59. { "492920", "492920", 13, CardBrand::Visa },
  60. { "492923", "492923", 13, CardBrand::Visa },
  61. { "492928", "492930", 13, CardBrand::Visa },
  62. { "492937", "492937", 13, CardBrand::Visa },
  63. { "492939", "492939", 13, CardBrand::Visa },
  64. { "492960", "492960", 13, CardBrand::Visa },
  65. };
  66. return kResult;
  67. }
  68. [[nodiscard]] bool BinRangeMatchesNumber(
  69. const BinRange &range,
  70. const QString &sanitized) {
  71. const auto minWithLow = std::min(sanitized.size(), range.low.size());
  72. if (base::StringViewMid(sanitized, 0, minWithLow).toInt()
  73. < base::StringViewMid(range.low, 0, minWithLow).toInt()) {
  74. return false;
  75. }
  76. const auto minWithHigh = std::min(sanitized.size(), range.high.size());
  77. if (base::StringViewMid(sanitized, 0, minWithHigh).toInt()
  78. > base::StringViewMid(range.high, 0, minWithHigh).toInt()) {
  79. return false;
  80. }
  81. return true;
  82. }
  83. [[nodiscard]] bool IsNumeric(const QString &value) {
  84. static const auto RegExp = QRegularExpression("^[0-9]*$");
  85. return RegExp.match(value).hasMatch();
  86. }
  87. [[nodiscard]] QString RemoveWhitespaces(QString value) {
  88. static const auto RegExp = QRegularExpression("\\s");
  89. return value.replace(RegExp, QString());
  90. }
  91. [[nodiscard]] std::vector<BinRange> BinRangesForNumber(
  92. const QString &sanitized) {
  93. const auto &all = AllRanges();
  94. auto result = std::vector<BinRange>();
  95. result.reserve(all.size());
  96. for (const auto &range : all) {
  97. if (BinRangeMatchesNumber(range, sanitized)) {
  98. result.push_back(range);
  99. }
  100. }
  101. return result;
  102. }
  103. [[nodiscard]] BinRange MostSpecificBinRangeForNumber(
  104. const QString &sanitized) {
  105. auto possible = BinRangesForNumber(sanitized);
  106. const auto compare = [&](const BinRange &a, const BinRange &b) {
  107. if (sanitized.isEmpty()) {
  108. const auto aUnknown = (a.brand == CardBrand::Unknown);
  109. const auto bUnknown = (b.brand == CardBrand::Unknown);
  110. if (aUnknown && !bUnknown) {
  111. return true;
  112. } else if (!aUnknown && bUnknown) {
  113. return false;
  114. }
  115. }
  116. return a.low.size() < b.low.size();
  117. };
  118. std::sort(begin(possible), end(possible), compare);
  119. return possible.back();
  120. }
  121. [[nodiscard]] int MaxCvcLengthForBranch(CardBrand brand) {
  122. switch (brand) {
  123. case CardBrand::Amex:
  124. case CardBrand::Unknown:
  125. return 4;
  126. default:
  127. return 3;
  128. }
  129. }
  130. [[nodiscard]] std::vector<CardBrand> PossibleBrandsForNumber(
  131. const QString &sanitized) {
  132. const auto ranges = BinRangesForNumber(sanitized);
  133. auto result = std::vector<CardBrand>();
  134. for (const auto &range : ranges) {
  135. const auto brand = range.brand;
  136. if (brand == CardBrand::Unknown
  137. || (std::find(begin(result), end(result), brand)
  138. != end(result))) {
  139. continue;
  140. }
  141. result.push_back(brand);
  142. }
  143. return result;
  144. }
  145. [[nodiscard]] CardBrand BrandForNumber(const QString &number) {
  146. const auto sanitized = RemoveWhitespaces(number);
  147. if (!IsNumeric(sanitized)) {
  148. return CardBrand::Unknown;
  149. }
  150. const auto possible = PossibleBrandsForNumber(sanitized);
  151. return (possible.size() == 1) ? possible.front() : CardBrand::Unknown;
  152. }
  153. [[nodiscard]] bool IsValidLuhn(const QString &sanitized) {
  154. auto odd = true;
  155. auto sum = 0;
  156. for (auto i = sanitized.end(); i != sanitized.begin();) {
  157. --i;
  158. auto digit = int(i->unicode() - '0');
  159. odd = !odd;
  160. if (odd) {
  161. digit *= 2;
  162. }
  163. if (digit > 9) {
  164. digit -= 9;
  165. }
  166. sum += digit;
  167. }
  168. return (sum % 10) == 0;
  169. }
  170. } // namespace
  171. CardValidationResult ValidateCard(const QString &number) {
  172. const auto sanitized = RemoveWhitespaces(number);
  173. if (!IsNumeric(sanitized)) {
  174. return { .state = ValidationState::Invalid };
  175. } else if (sanitized.isEmpty()) {
  176. return { .state = ValidationState::Incomplete };
  177. }
  178. const auto range = MostSpecificBinRangeForNumber(sanitized);
  179. const auto brand = range.brand;
  180. static const auto &all = AllRanges();
  181. static const auto compare = [](const BinRange &a, const BinRange &b) {
  182. return a.length < b.length;
  183. };
  184. static const auto kMinLength = std::min_element(
  185. begin(all),
  186. end(all),
  187. compare)->length;
  188. static const auto kMaxLength = std::max_element(
  189. begin(all),
  190. end(all),
  191. compare)->length;
  192. if (sanitized.size() > kMaxLength) {
  193. return { .state = ValidationState::Invalid, .brand = brand };
  194. } else if (sanitized.size() < kMinLength) {
  195. return { .state = ValidationState::Incomplete, .brand = brand };
  196. } else if (!IsValidLuhn(sanitized)) {
  197. return { .state = ValidationState::Invalid, .brand = brand };
  198. } else if (sanitized.size() < kMaxLength) {
  199. return { .state = ValidationState::Valid, .brand = brand };
  200. }
  201. return {
  202. .state = ValidationState::Valid,
  203. .brand = brand,
  204. .finished = true,
  205. };
  206. }
  207. ExpireDateValidationResult ValidateExpireDate(
  208. const QString &date,
  209. const std::optional<QDate> &overrideExpireDateThreshold) {
  210. const auto sanitized = RemoveWhitespaces(date).replace('/', QString());
  211. if (!IsNumeric(sanitized)) {
  212. return { ValidationState::Invalid };
  213. } else if (sanitized.size() < 2) {
  214. return { ValidationState::Incomplete };
  215. }
  216. const auto normalized = (sanitized[0] > '1' ? "0" : "") + sanitized;
  217. const auto month = base::StringViewMid(normalized, 0, 2).toInt();
  218. if (month < 1 || month > 12) {
  219. return { ValidationState::Invalid };
  220. } else if (normalized.size() < 4) {
  221. return { ValidationState::Incomplete };
  222. } else if (normalized.size() > 4) {
  223. return { ValidationState::Invalid };
  224. }
  225. const auto year = 2000 + base::StringViewMid(normalized, 2).toInt();
  226. const auto thresholdDate = overrideExpireDateThreshold.value_or(
  227. QDate::currentDate());
  228. const auto thresholdMonth = thresholdDate.month();
  229. const auto thresholdYear = thresholdDate.year();
  230. if (year < thresholdYear) {
  231. return { ValidationState::Invalid };
  232. } else if (year == thresholdYear && month < thresholdMonth) {
  233. return { ValidationState::Invalid };
  234. }
  235. return { ValidationState::Valid, true };
  236. }
  237. ValidationState ValidateParsedExpireDate(
  238. quint32 month,
  239. quint32 year,
  240. const std::optional<QDate> &overrideExpireDateThreshold) {
  241. if ((year / 100) != 20) {
  242. return ValidationState::Invalid;
  243. }
  244. const auto date = QString("%1%2"
  245. ).arg(month, 2, 10, QChar('0')
  246. ).arg(year % 100, 2, 10, QChar('0'));
  247. return ValidateExpireDate(date, overrideExpireDateThreshold).state;
  248. }
  249. CvcValidationResult ValidateCvc(
  250. const QString &number,
  251. const QString &cvc) {
  252. if (!IsNumeric(cvc)) {
  253. return { ValidationState::Invalid };
  254. } else if (cvc.size() < kMinCvcLength) {
  255. return { ValidationState::Incomplete };
  256. }
  257. const auto maxLength = MaxCvcLengthForBranch(BrandForNumber(number));
  258. if (cvc.size() > maxLength) {
  259. return { ValidationState::Invalid };
  260. }
  261. return { ValidationState::Valid, (cvc.size() == maxLength) };
  262. }
  263. std::vector<int> CardNumberFormat(const QString &number) {
  264. static const auto kDefault = std::vector{ 4, 4, 4, 4 };
  265. const auto sanitized = RemoveWhitespaces(number);
  266. if (!IsNumeric(sanitized)) {
  267. return kDefault;
  268. }
  269. const auto range = MostSpecificBinRangeForNumber(sanitized);
  270. if (range.brand == CardBrand::DinersClub && range.length == 14) {
  271. return { 4, 6, 4 };
  272. } else if (range.brand == CardBrand::Amex) {
  273. return { 4, 6, 5 };
  274. }
  275. return kDefault;
  276. }
  277. } // namespace Stripe