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 "TestTotp.h"
20 #include "TestGlobal.h"
21
22 #include "crypto/Crypto.h"
23 #include "totp/totp.h"
24
QTEST_GUILESS_MAIN(TestTotp)25 QTEST_GUILESS_MAIN(TestTotp)
26
27 void TestTotp::initTestCase()
28 {
29 QVERIFY(Crypto::init());
30 }
31
testParseSecret()32 void TestTotp::testParseSecret()
33 {
34 // OTP URL Parsing
35 QString secret = "otpauth://totp/"
36 "ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm="
37 "SHA1&digits=6&period=30";
38 auto settings = Totp::parseSettings(secret);
39 QVERIFY(!settings.isNull());
40 QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"));
41 QCOMPARE(settings->custom, false);
42 QCOMPARE(settings->format, Totp::StorageFormat::OTPURL);
43 QCOMPARE(settings->digits, 6u);
44 QCOMPARE(settings->step, 30u);
45 QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
46
47 // OTP URL with non-default hash type
48 secret = "otpauth://totp/"
49 "ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm="
50 "SHA512&digits=6&period=30";
51 settings = Totp::parseSettings(secret);
52 QVERIFY(!settings.isNull());
53 QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"));
54 QCOMPARE(settings->custom, true);
55 QCOMPARE(settings->format, Totp::StorageFormat::OTPURL);
56 QCOMPARE(settings->digits, 6u);
57 QCOMPARE(settings->step, 30u);
58 QCOMPARE(settings->algorithm, Totp::Algorithm::Sha512);
59
60 // KeeOTP Parsing
61 secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8&otpHashMode=Sha256";
62 settings = Totp::parseSettings(secret);
63 QVERIFY(!settings.isNull());
64 QCOMPARE(settings->key, QString("HXDMVJECJJWSRBY="));
65 QCOMPARE(settings->custom, true);
66 QCOMPARE(settings->format, Totp::StorageFormat::KEEOTP);
67 QCOMPARE(settings->digits, 8u);
68 QCOMPARE(settings->step, 25u);
69 QCOMPARE(settings->algorithm, Totp::Algorithm::Sha256);
70
71 // Semi-colon delineated "TOTP Settings"
72 secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
73 settings = Totp::parseSettings("30;8", secret);
74 QVERIFY(!settings.isNull());
75 QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq"));
76 QCOMPARE(settings->custom, true);
77 QCOMPARE(settings->format, Totp::StorageFormat::LEGACY);
78 QCOMPARE(settings->digits, 8u);
79 QCOMPARE(settings->step, 30u);
80 QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
81
82 // Bare secret (no "TOTP Settings" attribute)
83 secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
84 settings = Totp::parseSettings("", secret);
85 QVERIFY(!settings.isNull());
86 QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq"));
87 QCOMPARE(settings->custom, false);
88 QCOMPARE(settings->format, Totp::StorageFormat::LEGACY);
89 QCOMPARE(settings->digits, 6u);
90 QCOMPARE(settings->step, 30u);
91 QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
92 }
93
testTotpCode()94 void TestTotp::testTotpCode()
95 {
96 // Test vectors from RFC 6238
97 // https://tools.ietf.org/html/rfc6238#appendix-B
98 auto settings = Totp::createSettings("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP);
99
100 // Test 6 digit TOTP (default)
101 quint64 time = 1234567890;
102 QCOMPARE(Totp::generateTotp(settings, time), QString("005924"));
103
104 time = 1111111109;
105 QCOMPARE(Totp::generateTotp(settings, time), QString("081804"));
106
107 // Test 8 digit TOTP (custom)
108 settings->digits = 8;
109 settings->custom = true;
110 time = 1111111111;
111 QCOMPARE(Totp::generateTotp(settings, time), QString("14050471"));
112
113 time = 2000000000;
114 QCOMPARE(Totp::generateTotp(settings, time), QString("69279037"));
115 }
116
testSteamTotp()117 void TestTotp::testSteamTotp()
118 {
119 // OTP URL Parsing
120 QString secret = "otpauth://totp/"
121 "test:test@example.com?secret=63BEDWCQZKTQWPESARIERL5DTTQFCJTK&issuer=Valve&algorithm="
122 "SHA1&digits=5&period=30&encoder=steam";
123 auto settings = Totp::parseSettings(secret);
124
125 QCOMPARE(settings->key, QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK"));
126 QCOMPARE(settings->encoder.shortName, Totp::STEAM_SHORTNAME);
127 QCOMPARE(settings->format, Totp::StorageFormat::OTPURL);
128 QCOMPARE(settings->digits, Totp::STEAM_DIGITS);
129 QCOMPARE(settings->step, 30u);
130
131 // These time/value pairs were created by running the Steam Guard function of the
132 // Steam mobile app with a throw-away steam account. The above secret was extracted
133 // from the Steam app's data for use in testing here.
134 quint64 time = 1511200518;
135 QCOMPARE(Totp::generateTotp(settings, time), QString("FR8RV"));
136 time = 1511200714;
137 QCOMPARE(Totp::generateTotp(settings, time), QString("9P3VP"));
138 }
139
testEntryHistory()140 void TestTotp::testEntryHistory()
141 {
142 Entry entry;
143 uint step = 16;
144 uint digits = 6;
145 auto settings = Totp::createSettings("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", digits, step);
146 // Test that entry starts without TOTP
147 QCOMPARE(entry.historyItems().size(), 0);
148 QVERIFY(!entry.hasTotp());
149 // Add TOTP to entry
150 entry.setTotp(settings);
151 QCOMPARE(entry.historyItems().size(), 1);
152 QVERIFY(entry.hasTotp());
153 QCOMPARE(entry.totpSettings()->key, QString("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"));
154 // Change key and verify settings changed
155 settings->key = "foo";
156 entry.setTotp(settings);
157 QCOMPARE(entry.historyItems().size(), 2);
158 QCOMPARE(entry.totpSettings()->key, QString("foo"));
159 }
160