2
* Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
3
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
5
* This program is free software: you can redistribute it and/or modify
6
* it under the terms of the GNU General Public License as published by
7
* the Free Software Foundation, either version 2 or (at your option)
8
* version 3 of the License.
10
* This program is distributed in the hope that it will be useful,
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
* GNU General Public License for more details.
15
* You should have received a copy of the GNU General Public License
16
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21
#include "core/Base32.h"
22
#include "core/Clock.h"
24
#include <QMessageAuthenticationCode>
25
#include <QSharedPointer>
32
static QList<Totp::Encoder> totpEncoders{
33
{"", "", "0123456789", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP, false},
34
{"steam", Totp::STEAM_SHORTNAME, "23456789BCDFGHJKMNPQRTVWXY", Totp::STEAM_DIGITS, Totp::DEFAULT_STEP, true},
37
static Totp::Algorithm getHashTypeByName(const QString& name)
39
if (name.compare(QString("SHA512"), Qt::CaseInsensitive) == 0) {
40
return Totp::Algorithm::Sha512;
42
if (name.compare(QString("SHA256"), Qt::CaseInsensitive) == 0) {
43
return Totp::Algorithm::Sha256;
45
return Totp::Algorithm::Sha1;
48
static QString getNameForHashType(const Totp::Algorithm hashType)
51
case Totp::Algorithm::Sha512:
53
case Totp::Algorithm::Sha256:
60
QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, const QString& key)
62
// Early out if both strings are empty
63
if (rawSettings.isEmpty() && key.isEmpty()) {
67
// Create default settings
68
auto settings = createSettings(key, DEFAULT_DIGITS, DEFAULT_STEP);
70
QUrl url(rawSettings);
71
if (url.isValid() && url.scheme() == "otpauth") {
72
// Default OTP url format
74
settings->format = StorageFormat::OTPURL;
75
settings->key = query.queryItemValue("secret");
76
if (query.hasQueryItem("digits")) {
77
settings->digits = query.queryItemValue("digits").toUInt();
79
if (query.hasQueryItem("period")) {
80
settings->step = query.queryItemValue("period").toUInt();
82
if (query.hasQueryItem("encoder")) {
83
settings->encoder = getEncoderByName(query.queryItemValue("encoder"));
85
if (query.hasQueryItem("algorithm")) {
86
settings->algorithm = getHashTypeByName(query.queryItemValue("algorithm"));
89
QUrlQuery query(rawSettings);
90
if (query.hasQueryItem("key")) {
91
// Compatibility with "KeeOtp" plugin
92
settings->format = StorageFormat::KEEOTP;
93
settings->key = query.queryItemValue("key");
94
if (query.hasQueryItem("size")) {
95
settings->digits = query.queryItemValue("size").toUInt();
97
if (query.hasQueryItem("step")) {
98
settings->step = query.queryItemValue("step").toUInt();
100
if (query.hasQueryItem("otpHashMode")) {
101
settings->algorithm = getHashTypeByName(query.queryItemValue("otpHashMode"));
104
if (settings->key.isEmpty()) {
105
// Legacy format cannot work with an empty key
109
// Parse semi-colon separated values ([step];[digits|S])
110
settings->format = StorageFormat::LEGACY;
111
auto vars = rawSettings.split(";");
112
if (vars.size() >= 2) {
113
if (vars[1] == STEAM_SHORTNAME) {
114
// Explicit steam encoder
115
settings->encoder = steamEncoder();
117
// Extract step and digits
118
settings->step = vars[0].toUInt();
119
settings->digits = vars[1].toUInt();
125
// Bound digits and step
126
settings->digits = qBound(1u, settings->digits, 10u);
127
settings->step = qBound(1u, settings->step, 86400u);
129
// Detect custom settings, used by setup GUI
130
if (settings->encoder.shortName.isEmpty()
131
&& (settings->digits != DEFAULT_DIGITS || settings->step != DEFAULT_STEP
132
|| settings->algorithm != DEFAULT_ALGORITHM)) {
133
settings->custom = true;
139
QSharedPointer<Totp::Settings> Totp::createSettings(const QString& key,
142
const Totp::StorageFormat format,
143
const QString& encoderShortName,
144
const Totp::Algorithm algorithm)
146
bool isCustom = digits != DEFAULT_DIGITS || step != DEFAULT_STEP || algorithm != DEFAULT_ALGORITHM;
147
return QSharedPointer<Totp::Settings>(
148
new Totp::Settings{format, getEncoderByShortName(encoderShortName), algorithm, key, isCustom, digits, step});
151
QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings,
152
const QString& title,
153
const QString& username,
156
if (settings.isNull()) {
161
if (settings->format == StorageFormat::OTPURL || forceOtp) {
162
auto urlstring = QString("otpauth://totp/%1:%2?secret=%3&period=%4&digits=%5&issuer=%1")
163
.arg(title.isEmpty() ? "KeePassXC" : QString(QUrl::toPercentEncoding(title)),
164
username.isEmpty() ? "none" : QString(QUrl::toPercentEncoding(username)),
165
QString(QUrl::toPercentEncoding(Base32::sanitizeInput(settings->key.toLatin1()))),
166
QString::number(settings->step),
167
QString::number(settings->digits));
169
if (!settings->encoder.name.isEmpty()) {
170
urlstring.append("&encoder=").append(settings->encoder.name);
172
if (settings->algorithm != Totp::DEFAULT_ALGORITHM) {
173
urlstring.append("&algorithm=").append(getNameForHashType(settings->algorithm));
176
} else if (settings->format == StorageFormat::KEEOTP) {
178
auto keyString = QString("key=%1&size=%2&step=%3")
179
.arg(QString(Base32::sanitizeInput(settings->key.toLatin1())))
180
.arg(settings->digits)
181
.arg(settings->step);
182
if (settings->algorithm != Totp::DEFAULT_ALGORITHM) {
183
keyString.append("&otpHashMode=").append(getNameForHashType(settings->algorithm));
186
} else if (!settings->encoder.shortName.isEmpty()) {
187
// Semicolon output [step];[encoder]
188
return QString("%1;%2").arg(settings->step).arg(settings->encoder.shortName);
190
// Semicolon output [step];[digits]
191
return QString("%1;%2").arg(settings->step).arg(settings->digits);
195
QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time)
197
Q_ASSERT(!settings.isNull());
198
if (settings.isNull()) {
199
return QObject::tr("Invalid Settings", "TOTP");
202
const Encoder& encoder = settings->encoder;
203
uint step = settings->custom ? settings->step : encoder.step;
204
uint digits = settings->custom ? settings->digits : encoder.digits;
208
current = qToBigEndian(static_cast<quint64>(Clock::currentSecondsSinceEpoch()) / step);
210
current = qToBigEndian(time / step);
213
QVariant secret = Base32::decode(Base32::sanitizeInput(settings->key.toLatin1()));
214
if (secret.isNull()) {
215
return QObject::tr("Invalid Key", "TOTP");
218
QCryptographicHash::Algorithm cryptoHash;
219
switch (settings->algorithm) {
220
case Totp::Algorithm::Sha512:
221
cryptoHash = QCryptographicHash::Sha512;
223
case Totp::Algorithm::Sha256:
224
cryptoHash = QCryptographicHash::Sha256;
227
cryptoHash = QCryptographicHash::Sha1;
230
QMessageAuthenticationCode code(cryptoHash);
231
code.setKey(secret.toByteArray());
232
code.addData(QByteArray(reinterpret_cast<char*>(¤t), sizeof(current)));
233
QByteArray hmac = code.result();
235
int offset = (hmac[hmac.length() - 1] & 0xf);
239
((hmac[offset] & 0x7f) << 24)
240
| ((hmac[offset + 1] & 0xff) << 16)
241
| ((hmac[offset + 2] & 0xff) << 8)
242
| (hmac[offset + 3] & 0xff);
246
int startpos = digits - 1;
247
if (encoder.reverse) {
251
quint32 digitsPower = pow(encoder.alphabet.size(), digits);
253
quint64 password = binary % digitsPower;
254
QString retval(int(digits), encoder.alphabet[0]);
255
for (quint8 pos = startpos; password > 0; pos += direction) {
256
retval[pos] = encoder.alphabet[int(password % encoder.alphabet.size())];
257
password /= encoder.alphabet.size();
262
QList<QPair<QString, QString>> Totp::supportedEncoders()
264
QList<QPair<QString, QString>> encoders;
265
for (auto& encoder : totpEncoders) {
266
encoders << QPair<QString, QString>(encoder.name, encoder.shortName);
271
QList<QPair<QString, Totp::Algorithm>> Totp::supportedAlgorithms()
273
QList<QPair<QString, Algorithm>> algorithms;
274
algorithms << QPair<QString, Algorithm>(QStringLiteral("SHA-1"), Algorithm::Sha1);
275
algorithms << QPair<QString, Algorithm>(QStringLiteral("SHA-256"), Algorithm::Sha256);
276
algorithms << QPair<QString, Algorithm>(QStringLiteral("SHA-512"), Algorithm::Sha512);
280
Totp::Encoder& Totp::defaultEncoder()
282
// The first encoder is always the default
283
Q_ASSERT(!totpEncoders.empty());
284
return totpEncoders[0];
287
Totp::Encoder& Totp::steamEncoder()
289
return getEncoderByShortName("S");
292
Totp::Encoder& Totp::getEncoderByShortName(const QString& shortName)
294
for (auto& encoder : totpEncoders) {
295
if (encoder.shortName == shortName) {
299
return defaultEncoder();
302
Totp::Encoder& Totp::getEncoderByName(const QString& name)
304
for (auto& encoder : totpEncoders) {
305
if (encoder.name == name) {
309
return defaultEncoder();