| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- /*
- This file is part of Telegram Desktop,
- the official desktop application for the Telegram messaging service.
- For license and copyright information please follow this link:
- https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
- */
- #include "stripe/stripe_card_validator.h"
- #include "base/qt/qt_string_view.h"
- #include <QtCore/QDate>
- #include <QtCore/QRegularExpression>
- namespace Stripe {
- namespace {
- constexpr auto kMinCvcLength = 3;
- struct BinRange {
- QString low;
- QString high;
- int length = 0;
- CardBrand brand = CardBrand::Unknown;
- };
- [[nodiscard]] const std::vector<BinRange> &AllRanges() {
- static auto kResult = std::vector<BinRange>{
- // Unknown
- { "", "", 19, CardBrand::Unknown },
- // American Express
- { "34", "34", 15, CardBrand::Amex },
- { "37", "37", 15, CardBrand::Amex },
- // Diners Club
- { "30", "30", 16, CardBrand::DinersClub },
- { "36", "36", 14, CardBrand::DinersClub },
- { "38", "39", 16, CardBrand::DinersClub },
- // Discover
- { "60", "60", 16, CardBrand::Discover },
- { "64", "65", 16, CardBrand::Discover },
- // JCB
- { "35", "35", 16, CardBrand::JCB },
- // Mastercard
- { "50", "59", 16, CardBrand::MasterCard },
- { "22", "27", 16, CardBrand::MasterCard },
- { "67", "67", 16, CardBrand::MasterCard }, // Maestro
- // UnionPay
- { "62", "62", 16, CardBrand::UnionPay },
- { "81", "81", 16, CardBrand::UnionPay },
- // Visa
- { "40", "49", 16, CardBrand::Visa },
- { "413600", "413600", 13, CardBrand::Visa },
- { "444509", "444509", 13, CardBrand::Visa },
- { "444509", "444509", 13, CardBrand::Visa },
- { "444550", "444550", 13, CardBrand::Visa },
- { "450603", "450603", 13, CardBrand::Visa },
- { "450617", "450617", 13, CardBrand::Visa },
- { "450628", "450629", 13, CardBrand::Visa },
- { "450636", "450636", 13, CardBrand::Visa },
- { "450640", "450641", 13, CardBrand::Visa },
- { "450662", "450662", 13, CardBrand::Visa },
- { "463100", "463100", 13, CardBrand::Visa },
- { "476142", "476142", 13, CardBrand::Visa },
- { "476143", "476143", 13, CardBrand::Visa },
- { "492901", "492902", 13, CardBrand::Visa },
- { "492920", "492920", 13, CardBrand::Visa },
- { "492923", "492923", 13, CardBrand::Visa },
- { "492928", "492930", 13, CardBrand::Visa },
- { "492937", "492937", 13, CardBrand::Visa },
- { "492939", "492939", 13, CardBrand::Visa },
- { "492960", "492960", 13, CardBrand::Visa },
- };
- return kResult;
- }
- [[nodiscard]] bool BinRangeMatchesNumber(
- const BinRange &range,
- const QString &sanitized) {
- const auto minWithLow = std::min(sanitized.size(), range.low.size());
- if (base::StringViewMid(sanitized, 0, minWithLow).toInt()
- < base::StringViewMid(range.low, 0, minWithLow).toInt()) {
- return false;
- }
- const auto minWithHigh = std::min(sanitized.size(), range.high.size());
- if (base::StringViewMid(sanitized, 0, minWithHigh).toInt()
- > base::StringViewMid(range.high, 0, minWithHigh).toInt()) {
- return false;
- }
- return true;
- }
- [[nodiscard]] bool IsNumeric(const QString &value) {
- static const auto RegExp = QRegularExpression("^[0-9]*$");
- return RegExp.match(value).hasMatch();
- }
- [[nodiscard]] QString RemoveWhitespaces(QString value) {
- static const auto RegExp = QRegularExpression("\\s");
- return value.replace(RegExp, QString());
- }
- [[nodiscard]] std::vector<BinRange> BinRangesForNumber(
- const QString &sanitized) {
- const auto &all = AllRanges();
- auto result = std::vector<BinRange>();
- result.reserve(all.size());
- for (const auto &range : all) {
- if (BinRangeMatchesNumber(range, sanitized)) {
- result.push_back(range);
- }
- }
- return result;
- }
- [[nodiscard]] BinRange MostSpecificBinRangeForNumber(
- const QString &sanitized) {
- auto possible = BinRangesForNumber(sanitized);
- const auto compare = [&](const BinRange &a, const BinRange &b) {
- if (sanitized.isEmpty()) {
- const auto aUnknown = (a.brand == CardBrand::Unknown);
- const auto bUnknown = (b.brand == CardBrand::Unknown);
- if (aUnknown && !bUnknown) {
- return true;
- } else if (!aUnknown && bUnknown) {
- return false;
- }
- }
- return a.low.size() < b.low.size();
- };
- std::sort(begin(possible), end(possible), compare);
- return possible.back();
- }
- [[nodiscard]] int MaxCvcLengthForBranch(CardBrand brand) {
- switch (brand) {
- case CardBrand::Amex:
- case CardBrand::Unknown:
- return 4;
- default:
- return 3;
- }
- }
- [[nodiscard]] std::vector<CardBrand> PossibleBrandsForNumber(
- const QString &sanitized) {
- const auto ranges = BinRangesForNumber(sanitized);
- auto result = std::vector<CardBrand>();
- for (const auto &range : ranges) {
- const auto brand = range.brand;
- if (brand == CardBrand::Unknown
- || (std::find(begin(result), end(result), brand)
- != end(result))) {
- continue;
- }
- result.push_back(brand);
- }
- return result;
- }
- [[nodiscard]] CardBrand BrandForNumber(const QString &number) {
- const auto sanitized = RemoveWhitespaces(number);
- if (!IsNumeric(sanitized)) {
- return CardBrand::Unknown;
- }
- const auto possible = PossibleBrandsForNumber(sanitized);
- return (possible.size() == 1) ? possible.front() : CardBrand::Unknown;
- }
- [[nodiscard]] bool IsValidLuhn(const QString &sanitized) {
- auto odd = true;
- auto sum = 0;
- for (auto i = sanitized.end(); i != sanitized.begin();) {
- --i;
- auto digit = int(i->unicode() - '0');
- odd = !odd;
- if (odd) {
- digit *= 2;
- }
- if (digit > 9) {
- digit -= 9;
- }
- sum += digit;
- }
- return (sum % 10) == 0;
- }
- } // namespace
- CardValidationResult ValidateCard(const QString &number) {
- const auto sanitized = RemoveWhitespaces(number);
- if (!IsNumeric(sanitized)) {
- return { .state = ValidationState::Invalid };
- } else if (sanitized.isEmpty()) {
- return { .state = ValidationState::Incomplete };
- }
- const auto range = MostSpecificBinRangeForNumber(sanitized);
- const auto brand = range.brand;
- static const auto &all = AllRanges();
- static const auto compare = [](const BinRange &a, const BinRange &b) {
- return a.length < b.length;
- };
- static const auto kMinLength = std::min_element(
- begin(all),
- end(all),
- compare)->length;
- static const auto kMaxLength = std::max_element(
- begin(all),
- end(all),
- compare)->length;
- if (sanitized.size() > kMaxLength) {
- return { .state = ValidationState::Invalid, .brand = brand };
- } else if (sanitized.size() < kMinLength) {
- return { .state = ValidationState::Incomplete, .brand = brand };
- } else if (!IsValidLuhn(sanitized)) {
- return { .state = ValidationState::Invalid, .brand = brand };
- } else if (sanitized.size() < kMaxLength) {
- return { .state = ValidationState::Valid, .brand = brand };
- }
- return {
- .state = ValidationState::Valid,
- .brand = brand,
- .finished = true,
- };
- }
- ExpireDateValidationResult ValidateExpireDate(
- const QString &date,
- const std::optional<QDate> &overrideExpireDateThreshold) {
- const auto sanitized = RemoveWhitespaces(date).replace('/', QString());
- if (!IsNumeric(sanitized)) {
- return { ValidationState::Invalid };
- } else if (sanitized.size() < 2) {
- return { ValidationState::Incomplete };
- }
- const auto normalized = (sanitized[0] > '1' ? "0" : "") + sanitized;
- const auto month = base::StringViewMid(normalized, 0, 2).toInt();
- if (month < 1 || month > 12) {
- return { ValidationState::Invalid };
- } else if (normalized.size() < 4) {
- return { ValidationState::Incomplete };
- } else if (normalized.size() > 4) {
- return { ValidationState::Invalid };
- }
- const auto year = 2000 + base::StringViewMid(normalized, 2).toInt();
- const auto thresholdDate = overrideExpireDateThreshold.value_or(
- QDate::currentDate());
- const auto thresholdMonth = thresholdDate.month();
- const auto thresholdYear = thresholdDate.year();
- if (year < thresholdYear) {
- return { ValidationState::Invalid };
- } else if (year == thresholdYear && month < thresholdMonth) {
- return { ValidationState::Invalid };
- }
- return { ValidationState::Valid, true };
- }
- ValidationState ValidateParsedExpireDate(
- quint32 month,
- quint32 year,
- const std::optional<QDate> &overrideExpireDateThreshold) {
- if ((year / 100) != 20) {
- return ValidationState::Invalid;
- }
- const auto date = QString("%1%2"
- ).arg(month, 2, 10, QChar('0')
- ).arg(year % 100, 2, 10, QChar('0'));
- return ValidateExpireDate(date, overrideExpireDateThreshold).state;
- }
- CvcValidationResult ValidateCvc(
- const QString &number,
- const QString &cvc) {
- if (!IsNumeric(cvc)) {
- return { ValidationState::Invalid };
- } else if (cvc.size() < kMinCvcLength) {
- return { ValidationState::Incomplete };
- }
- const auto maxLength = MaxCvcLengthForBranch(BrandForNumber(number));
- if (cvc.size() > maxLength) {
- return { ValidationState::Invalid };
- }
- return { ValidationState::Valid, (cvc.size() == maxLength) };
- }
- std::vector<int> CardNumberFormat(const QString &number) {
- static const auto kDefault = std::vector{ 4, 4, 4, 4 };
- const auto sanitized = RemoveWhitespaces(number);
- if (!IsNumeric(sanitized)) {
- return kDefault;
- }
- const auto range = MostSpecificBinRangeForNumber(sanitized);
- if (range.brand == CardBrand::DinersClub && range.length == 14) {
- return { 4, 6, 4 };
- } else if (range.brand == CardBrand::Amex) {
- return { 4, 6, 5 };
- }
- return kDefault;
- }
- } // namespace Stripe
|