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