init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
304
Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp
Normal file
304
Telegram/SourceFiles/payments/stripe/stripe_card_validator.cpp
Normal file
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
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
|
||||
Reference in New Issue
Block a user