keepassxc

Форк
0
/
Totp.cpp 
310 строк · 10.9 Кб
1
/*
2
 *  Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
3
 *  Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
4
 *
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.
9
 *
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.
14
 *
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/>.
17
 */
18

19
#include "Totp.h"
20

21
#include "core/Base32.h"
22
#include "core/Clock.h"
23

24
#include <QMessageAuthenticationCode>
25
#include <QSharedPointer>
26
#include <QUrlQuery>
27
#include <QVariant>
28
#include <QtEndian>
29

30
#include <cmath>
31

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},
35
};
36

37
static Totp::Algorithm getHashTypeByName(const QString& name)
38
{
39
    if (name.compare(QString("SHA512"), Qt::CaseInsensitive) == 0) {
40
        return Totp::Algorithm::Sha512;
41
    }
42
    if (name.compare(QString("SHA256"), Qt::CaseInsensitive) == 0) {
43
        return Totp::Algorithm::Sha256;
44
    }
45
    return Totp::Algorithm::Sha1;
46
}
47

48
static QString getNameForHashType(const Totp::Algorithm hashType)
49
{
50
    switch (hashType) {
51
    case Totp::Algorithm::Sha512:
52
        return "SHA512";
53
    case Totp::Algorithm::Sha256:
54
        return "SHA256";
55
    default:
56
        return "SHA1";
57
    }
58
}
59

60
QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, const QString& key)
61
{
62
    // Early out if both strings are empty
63
    if (rawSettings.isEmpty() && key.isEmpty()) {
64
        return {};
65
    }
66

67
    // Create default settings
68
    auto settings = createSettings(key, DEFAULT_DIGITS, DEFAULT_STEP);
69

70
    QUrl url(rawSettings);
71
    if (url.isValid() && url.scheme() == "otpauth") {
72
        // Default OTP url format
73
        QUrlQuery query(url);
74
        settings->format = StorageFormat::OTPURL;
75
        settings->key = query.queryItemValue("secret");
76
        if (query.hasQueryItem("digits")) {
77
            settings->digits = query.queryItemValue("digits").toUInt();
78
        }
79
        if (query.hasQueryItem("period")) {
80
            settings->step = query.queryItemValue("period").toUInt();
81
        }
82
        if (query.hasQueryItem("encoder")) {
83
            settings->encoder = getEncoderByName(query.queryItemValue("encoder"));
84
        }
85
        if (query.hasQueryItem("algorithm")) {
86
            settings->algorithm = getHashTypeByName(query.queryItemValue("algorithm"));
87
        }
88
    } else {
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();
96
            }
97
            if (query.hasQueryItem("step")) {
98
                settings->step = query.queryItemValue("step").toUInt();
99
            }
100
            if (query.hasQueryItem("otpHashMode")) {
101
                settings->algorithm = getHashTypeByName(query.queryItemValue("otpHashMode"));
102
            }
103
        } else {
104
            if (settings->key.isEmpty()) {
105
                // Legacy format cannot work with an empty key
106
                return {};
107
            }
108

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();
116
                } else {
117
                    // Extract step and digits
118
                    settings->step = vars[0].toUInt();
119
                    settings->digits = vars[1].toUInt();
120
                }
121
            }
122
        }
123
    }
124

125
    // Bound digits and step
126
    settings->digits = qBound(1u, settings->digits, 10u);
127
    settings->step = qBound(1u, settings->step, 86400u);
128

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;
134
    }
135

136
    return settings;
137
}
138

139
QSharedPointer<Totp::Settings> Totp::createSettings(const QString& key,
140
                                                    const uint digits,
141
                                                    const uint step,
142
                                                    const Totp::StorageFormat format,
143
                                                    const QString& encoderShortName,
144
                                                    const Totp::Algorithm algorithm)
145
{
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});
149
}
150

151
QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings,
152
                            const QString& title,
153
                            const QString& username,
154
                            bool forceOtp)
155
{
156
    if (settings.isNull()) {
157
        return {};
158
    }
159

160
    // OTP Url output
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));
168

169
        if (!settings->encoder.name.isEmpty()) {
170
            urlstring.append("&encoder=").append(settings->encoder.name);
171
        }
172
        if (settings->algorithm != Totp::DEFAULT_ALGORITHM) {
173
            urlstring.append("&algorithm=").append(getNameForHashType(settings->algorithm));
174
        }
175
        return urlstring;
176
    } else if (settings->format == StorageFormat::KEEOTP) {
177
        // KeeOtp output
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));
184
        }
185
        return keyString;
186
    } else if (!settings->encoder.shortName.isEmpty()) {
187
        // Semicolon output [step];[encoder]
188
        return QString("%1;%2").arg(settings->step).arg(settings->encoder.shortName);
189
    } else {
190
        // Semicolon output [step];[digits]
191
        return QString("%1;%2").arg(settings->step).arg(settings->digits);
192
    }
193
}
194

195
QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time)
196
{
197
    Q_ASSERT(!settings.isNull());
198
    if (settings.isNull()) {
199
        return QObject::tr("Invalid Settings", "TOTP");
200
    }
201

202
    const Encoder& encoder = settings->encoder;
203
    uint step = settings->custom ? settings->step : encoder.step;
204
    uint digits = settings->custom ? settings->digits : encoder.digits;
205

206
    quint64 current;
207
    if (time == 0) {
208
        current = qToBigEndian(static_cast<quint64>(Clock::currentSecondsSinceEpoch()) / step);
209
    } else {
210
        current = qToBigEndian(time / step);
211
    }
212

213
    QVariant secret = Base32::decode(Base32::sanitizeInput(settings->key.toLatin1()));
214
    if (secret.isNull()) {
215
        return QObject::tr("Invalid Key", "TOTP");
216
    }
217

218
    QCryptographicHash::Algorithm cryptoHash;
219
    switch (settings->algorithm) {
220
    case Totp::Algorithm::Sha512:
221
        cryptoHash = QCryptographicHash::Sha512;
222
        break;
223
    case Totp::Algorithm::Sha256:
224
        cryptoHash = QCryptographicHash::Sha256;
225
        break;
226
    default:
227
        cryptoHash = QCryptographicHash::Sha1;
228
        break;
229
    }
230
    QMessageAuthenticationCode code(cryptoHash);
231
    code.setKey(secret.toByteArray());
232
    code.addData(QByteArray(reinterpret_cast<char*>(&current), sizeof(current)));
233
    QByteArray hmac = code.result();
234

235
    int offset = (hmac[hmac.length() - 1] & 0xf);
236

237
    // clang-format off
238
    int binary =
239
            ((hmac[offset] & 0x7f) << 24)
240
            | ((hmac[offset + 1] & 0xff) << 16)
241
            | ((hmac[offset + 2] & 0xff) << 8)
242
            | (hmac[offset + 3] & 0xff);
243
    // clang-format on
244

245
    int direction = -1;
246
    int startpos = digits - 1;
247
    if (encoder.reverse) {
248
        direction = 1;
249
        startpos = 0;
250
    }
251
    quint32 digitsPower = pow(encoder.alphabet.size(), digits);
252

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();
258
    }
259
    return retval;
260
}
261

262
QList<QPair<QString, QString>> Totp::supportedEncoders()
263
{
264
    QList<QPair<QString, QString>> encoders;
265
    for (auto& encoder : totpEncoders) {
266
        encoders << QPair<QString, QString>(encoder.name, encoder.shortName);
267
    }
268
    return encoders;
269
}
270

271
QList<QPair<QString, Totp::Algorithm>> Totp::supportedAlgorithms()
272
{
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);
277
    return algorithms;
278
}
279

280
Totp::Encoder& Totp::defaultEncoder()
281
{
282
    // The first encoder is always the default
283
    Q_ASSERT(!totpEncoders.empty());
284
    return totpEncoders[0];
285
}
286

287
Totp::Encoder& Totp::steamEncoder()
288
{
289
    return getEncoderByShortName("S");
290
}
291

292
Totp::Encoder& Totp::getEncoderByShortName(const QString& shortName)
293
{
294
    for (auto& encoder : totpEncoders) {
295
        if (encoder.shortName == shortName) {
296
            return encoder;
297
        }
298
    }
299
    return defaultEncoder();
300
}
301

302
Totp::Encoder& Totp::getEncoderByName(const QString& name)
303
{
304
    for (auto& encoder : totpEncoders) {
305
        if (encoder.name == name) {
306
            return encoder;
307
        }
308
    }
309
    return defaultEncoder();
310
}
311

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.