mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-12-04 15:39:34 +01:00
301 lines
11 KiB
C++
301 lines
11 KiB
C++
/*
|
||
* Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
|
||
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU General Public License as published by
|
||
* the Free Software Foundation, either version 2 or (at your option)
|
||
* version 3 of the License.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU General Public License
|
||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#include "totp.h"
|
||
|
||
#include "core/Base32.h"
|
||
#include "core/Clock.h"
|
||
|
||
#include <QMessageAuthenticationCode>
|
||
#include <QSharedPointer>
|
||
#include <QUrlQuery>
|
||
#include <QVariant>
|
||
#include <QtEndian>
|
||
|
||
#include <cmath>
|
||
|
||
static QList<Totp::Encoder> totpEncoders{
|
||
{"", "", "0123456789", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP, false},
|
||
{"steam", Totp::STEAM_SHORTNAME, "23456789BCDFGHJKMNPQRTVWXY", Totp::STEAM_DIGITS, Totp::DEFAULT_STEP, true},
|
||
};
|
||
|
||
static Totp::Algorithm getHashTypeByName(const QString& name)
|
||
{
|
||
if (name.compare(QString("SHA512"), Qt::CaseInsensitive) == 0) {
|
||
return Totp::Algorithm::Sha512;
|
||
}
|
||
if (name.compare(QString("SHA256"), Qt::CaseInsensitive) == 0) {
|
||
return Totp::Algorithm::Sha256;
|
||
}
|
||
return Totp::Algorithm::Sha1;
|
||
}
|
||
|
||
static QString getNameForHashType(const Totp::Algorithm hashType)
|
||
{
|
||
switch (hashType) {
|
||
case Totp::Algorithm::Sha512:
|
||
return "SHA512";
|
||
case Totp::Algorithm::Sha256:
|
||
return "SHA256";
|
||
default:
|
||
return "SHA1";
|
||
}
|
||
}
|
||
|
||
QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, const QString& key)
|
||
{
|
||
// Create default settings
|
||
auto settings = createSettings(key, DEFAULT_DIGITS, DEFAULT_STEP);
|
||
|
||
QUrl url(rawSettings);
|
||
if (url.isValid() && url.scheme() == "otpauth") {
|
||
// Default OTP url format
|
||
QUrlQuery query(url);
|
||
settings->format = StorageFormat::OTPURL;
|
||
settings->key = query.queryItemValue("secret");
|
||
if (query.hasQueryItem("digits")) {
|
||
settings->digits = query.queryItemValue("digits").toUInt();
|
||
}
|
||
if (query.hasQueryItem("period")) {
|
||
settings->step = query.queryItemValue("period").toUInt();
|
||
}
|
||
if (query.hasQueryItem("encoder")) {
|
||
settings->encoder = getEncoderByName(query.queryItemValue("encoder"));
|
||
}
|
||
if (query.hasQueryItem("algorithm")) {
|
||
settings->algorithm = getHashTypeByName(query.queryItemValue("algorithm"));
|
||
}
|
||
} else {
|
||
QUrlQuery query(rawSettings);
|
||
if (query.hasQueryItem("key")) {
|
||
// Compatibility with "KeeOtp" plugin
|
||
settings->format = StorageFormat::KEEOTP;
|
||
settings->key = query.queryItemValue("key");
|
||
if (query.hasQueryItem("size")) {
|
||
settings->digits = query.queryItemValue("size").toUInt();
|
||
}
|
||
if (query.hasQueryItem("step")) {
|
||
settings->step = query.queryItemValue("step").toUInt();
|
||
}
|
||
if (query.hasQueryItem("otpHashMode")) {
|
||
settings->algorithm = getHashTypeByName(query.queryItemValue("otpHashMode"));
|
||
}
|
||
} else {
|
||
// Parse semi-colon separated values ([step];[digits|S])
|
||
settings->format = StorageFormat::LEGACY;
|
||
auto vars = rawSettings.split(";");
|
||
if (vars.size() >= 2) {
|
||
if (vars[1] == STEAM_SHORTNAME) {
|
||
// Explicit steam encoder
|
||
settings->encoder = steamEncoder();
|
||
} else {
|
||
// Extract step and digits
|
||
settings->step = vars[0].toUInt();
|
||
settings->digits = vars[1].toUInt();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Bound digits and step
|
||
settings->digits = qBound(1u, settings->digits, 10u);
|
||
settings->step = qBound(1u, settings->step, 86400u);
|
||
|
||
// Detect custom settings, used by setup GUI
|
||
if (settings->encoder.shortName.isEmpty()
|
||
&& (settings->digits != DEFAULT_DIGITS || settings->step != DEFAULT_STEP
|
||
|| settings->algorithm != DEFAULT_ALGORITHM)) {
|
||
settings->custom = true;
|
||
}
|
||
|
||
return settings;
|
||
}
|
||
|
||
QSharedPointer<Totp::Settings> Totp::createSettings(const QString& key,
|
||
const uint digits,
|
||
const uint step,
|
||
const Totp::StorageFormat format,
|
||
const QString& encoderShortName,
|
||
const Totp::Algorithm algorithm)
|
||
{
|
||
bool isCustom = digits != DEFAULT_DIGITS || step != DEFAULT_STEP || algorithm != DEFAULT_ALGORITHM;
|
||
return QSharedPointer<Totp::Settings>(
|
||
new Totp::Settings{format, getEncoderByShortName(encoderShortName), algorithm, key, isCustom, digits, step});
|
||
}
|
||
|
||
QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings,
|
||
const QString& title,
|
||
const QString& username,
|
||
bool forceOtp)
|
||
{
|
||
if (settings.isNull()) {
|
||
return {};
|
||
}
|
||
|
||
// OTP Url output
|
||
if (settings->format == StorageFormat::OTPURL || forceOtp) {
|
||
auto urlstring = QString("otpauth://totp/%1:%2?secret=%3&period=%4&digits=%5&issuer=%1")
|
||
.arg(title.isEmpty() ? "KeePassXC" : QString(QUrl::toPercentEncoding(title)),
|
||
username.isEmpty() ? "none" : QString(QUrl::toPercentEncoding(username)),
|
||
QString(QUrl::toPercentEncoding(Base32::sanitizeInput(settings->key.toLatin1()))),
|
||
QString::number(settings->step),
|
||
QString::number(settings->digits));
|
||
|
||
if (!settings->encoder.name.isEmpty()) {
|
||
urlstring.append("&encoder=").append(settings->encoder.name);
|
||
}
|
||
if (settings->algorithm != Totp::DEFAULT_ALGORITHM) {
|
||
urlstring.append("&algorithm=").append(getNameForHashType(settings->algorithm));
|
||
}
|
||
return urlstring;
|
||
} else if (settings->format == StorageFormat::KEEOTP) {
|
||
// KeeOtp output
|
||
auto keyString = QString("key=%1&size=%2&step=%3")
|
||
.arg(QString(Base32::sanitizeInput(settings->key.toLatin1())))
|
||
.arg(settings->digits)
|
||
.arg(settings->step);
|
||
if (settings->algorithm != Totp::DEFAULT_ALGORITHM) {
|
||
keyString.append("&otpHashMode=").append(getNameForHashType(settings->algorithm));
|
||
}
|
||
return keyString;
|
||
} else if (!settings->encoder.shortName.isEmpty()) {
|
||
// Semicolon output [step];[encoder]
|
||
return QString("%1;%2").arg(settings->step).arg(settings->encoder.shortName);
|
||
} else {
|
||
// Semicolon output [step];[digits]
|
||
return QString("%1;%2").arg(settings->step).arg(settings->digits);
|
||
}
|
||
}
|
||
|
||
QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time)
|
||
{
|
||
Q_ASSERT(!settings.isNull());
|
||
if (settings.isNull()) {
|
||
return QObject::tr("Invalid Settings", "TOTP");
|
||
}
|
||
|
||
const Encoder& encoder = settings->encoder;
|
||
uint step = settings->custom ? settings->step : encoder.step;
|
||
uint digits = settings->custom ? settings->digits : encoder.digits;
|
||
|
||
quint64 current;
|
||
if (time == 0) {
|
||
current = qToBigEndian(static_cast<quint64>(Clock::currentSecondsSinceEpoch()) / step);
|
||
} else {
|
||
current = qToBigEndian(time / step);
|
||
}
|
||
|
||
QVariant secret = Base32::decode(Base32::sanitizeInput(settings->key.toLatin1()));
|
||
if (secret.isNull()) {
|
||
return QObject::tr("Invalid Key", "TOTP");
|
||
}
|
||
|
||
QCryptographicHash::Algorithm cryptoHash;
|
||
switch (settings->algorithm) {
|
||
case Totp::Algorithm::Sha512:
|
||
cryptoHash = QCryptographicHash::Sha512;
|
||
break;
|
||
case Totp::Algorithm::Sha256:
|
||
cryptoHash = QCryptographicHash::Sha256;
|
||
break;
|
||
default:
|
||
cryptoHash = QCryptographicHash::Sha1;
|
||
break;
|
||
}
|
||
QMessageAuthenticationCode code(cryptoHash);
|
||
code.setKey(secret.toByteArray());
|
||
code.addData(QByteArray(reinterpret_cast<char*>(¤t), sizeof(current)));
|
||
QByteArray hmac = code.result();
|
||
|
||
int offset = (hmac[hmac.length() - 1] & 0xf);
|
||
|
||
// clang-format off
|
||
int binary =
|
||
((hmac[offset] & 0x7f) << 24)
|
||
| ((hmac[offset + 1] & 0xff) << 16)
|
||
| ((hmac[offset + 2] & 0xff) << 8)
|
||
| (hmac[offset + 3] & 0xff);
|
||
// clang-format on
|
||
|
||
int direction = -1;
|
||
int startpos = digits - 1;
|
||
if (encoder.reverse) {
|
||
direction = 1;
|
||
startpos = 0;
|
||
}
|
||
quint32 digitsPower = pow(encoder.alphabet.size(), digits);
|
||
|
||
quint64 password = binary % digitsPower;
|
||
QString retval(int(digits), encoder.alphabet[0]);
|
||
for (quint8 pos = startpos; password > 0; pos += direction) {
|
||
retval[pos] = encoder.alphabet[int(password % encoder.alphabet.size())];
|
||
password /= encoder.alphabet.size();
|
||
}
|
||
return retval;
|
||
}
|
||
|
||
QList<QPair<QString, QString>> Totp::supportedEncoders()
|
||
{
|
||
QList<QPair<QString, QString>> encoders;
|
||
for (auto& encoder : totpEncoders) {
|
||
encoders << QPair<QString, QString>(encoder.name, encoder.shortName);
|
||
}
|
||
return encoders;
|
||
}
|
||
|
||
QList<QPair<QString, Totp::Algorithm>> Totp::supportedAlgorithms()
|
||
{
|
||
QList<QPair<QString, Algorithm>> algorithms;
|
||
algorithms << QPair<QString, Algorithm>(QStringLiteral("SHA-1"), Algorithm::Sha1);
|
||
algorithms << QPair<QString, Algorithm>(QStringLiteral("SHA-256"), Algorithm::Sha256);
|
||
algorithms << QPair<QString, Algorithm>(QStringLiteral("SHA-512"), Algorithm::Sha512);
|
||
return algorithms;
|
||
}
|
||
|
||
Totp::Encoder& Totp::defaultEncoder()
|
||
{
|
||
// The first encoder is always the default
|
||
Q_ASSERT(!totpEncoders.empty());
|
||
return totpEncoders[0];
|
||
}
|
||
|
||
Totp::Encoder& Totp::steamEncoder()
|
||
{
|
||
return getEncoderByShortName("S");
|
||
}
|
||
|
||
Totp::Encoder& Totp::getEncoderByShortName(const QString& shortName)
|
||
{
|
||
for (auto& encoder : totpEncoders) {
|
||
if (encoder.shortName == shortName) {
|
||
return encoder;
|
||
}
|
||
}
|
||
return defaultEncoder();
|
||
}
|
||
|
||
Totp::Encoder& Totp::getEncoderByName(const QString& name)
|
||
{
|
||
for (auto& encoder : totpEncoders) {
|
||
if (encoder.name == name) {
|
||
return encoder;
|
||
}
|
||
}
|
||
return defaultEncoder();
|
||
}
|