TestTotp.cpp 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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. #include "TestTotp.h"
  19. #include "core/Entry.h"
  20. #include "core/Totp.h"
  21. #include "crypto/Crypto.h"
  22. #include <QTest>
  23. QTEST_GUILESS_MAIN(TestTotp)
  24. void TestTotp::initTestCase()
  25. {
  26. QVERIFY(Crypto::init());
  27. }
  28. void TestTotp::testParseSecret()
  29. {
  30. // OTP URL Parsing
  31. QString secret = "otpauth://totp/"
  32. "ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm="
  33. "SHA1&digits=6&period=30";
  34. auto settings = Totp::parseSettings(secret);
  35. QVERIFY(!settings.isNull());
  36. QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"));
  37. QCOMPARE(settings->format, Totp::StorageFormat::OTPURL);
  38. QCOMPARE(settings->digits, 6u);
  39. QCOMPARE(settings->step, 30u);
  40. QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
  41. QCOMPARE(Totp::hasCustomSettings(settings), false);
  42. // OTP URL with non-default hash type
  43. secret = "otpauth://totp/"
  44. "ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm="
  45. "SHA512&digits=6&period=30";
  46. settings = Totp::parseSettings(secret);
  47. QVERIFY(!settings.isNull());
  48. QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"));
  49. QCOMPARE(settings->format, Totp::StorageFormat::OTPURL);
  50. QCOMPARE(settings->digits, 6u);
  51. QCOMPARE(settings->step, 30u);
  52. QCOMPARE(settings->algorithm, Totp::Algorithm::Sha512);
  53. QCOMPARE(Totp::hasCustomSettings(settings), true);
  54. // Max TOTP step of 24-hours
  55. secret.replace("period=30", "period=90000");
  56. settings = Totp::parseSettings(secret);
  57. QVERIFY(!settings.isNull());
  58. QCOMPARE(settings->step, 86400u);
  59. // KeeOTP Parsing
  60. secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8&otpHashMode=Sha256";
  61. settings = Totp::parseSettings(secret);
  62. QVERIFY(!settings.isNull());
  63. QCOMPARE(settings->key, QString("HXDMVJECJJWSRBY="));
  64. QCOMPARE(settings->format, Totp::StorageFormat::KEEOTP);
  65. QCOMPARE(settings->digits, 8u);
  66. QCOMPARE(settings->step, 25u);
  67. QCOMPARE(settings->algorithm, Totp::Algorithm::Sha256);
  68. QCOMPARE(Totp::hasCustomSettings(settings), true);
  69. // Semi-colon delineated "TOTP Settings"
  70. secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
  71. settings = Totp::parseSettings("30;8", secret);
  72. QVERIFY(!settings.isNull());
  73. QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq"));
  74. QCOMPARE(settings->format, Totp::StorageFormat::LEGACY);
  75. QCOMPARE(settings->digits, 8u);
  76. QCOMPARE(settings->step, 30u);
  77. QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
  78. QCOMPARE(Totp::hasCustomSettings(settings), true);
  79. // Bare secret (no "TOTP Settings" attribute)
  80. secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
  81. settings = Totp::parseSettings("", secret);
  82. QVERIFY(!settings.isNull());
  83. QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq"));
  84. QCOMPARE(settings->format, Totp::StorageFormat::LEGACY);
  85. QCOMPARE(settings->digits, 6u);
  86. QCOMPARE(settings->step, 30u);
  87. QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
  88. QCOMPARE(Totp::hasCustomSettings(settings), false);
  89. // Blank settings (expected failure)
  90. settings = Totp::parseSettings("", "");
  91. QVERIFY(settings.isNull());
  92. // TOTP Settings with blank secret (expected failure)
  93. settings = Totp::parseSettings("30;8", "");
  94. QVERIFY(settings.isNull());
  95. }
  96. void TestTotp::testTotpCode()
  97. {
  98. // Test vectors from RFC 6238
  99. // https://tools.ietf.org/html/rfc6238#appendix-B
  100. auto settings = Totp::createSettings("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP);
  101. // Test 6 digit TOTP (default)
  102. quint64 time = 1234567890;
  103. QCOMPARE(Totp::generateTotp(settings, time), QString("005924"));
  104. time = 1111111109;
  105. QCOMPARE(Totp::generateTotp(settings, time), QString("081804"));
  106. // Test 8 digit TOTP (custom)
  107. settings->digits = 8;
  108. time = 1111111111;
  109. QCOMPARE(Totp::generateTotp(settings, time), QString("14050471"));
  110. time = 2000000000;
  111. QCOMPARE(Totp::generateTotp(settings, time), QString("69279037"));
  112. }
  113. void TestTotp::testSteamTotp()
  114. {
  115. // Legacy parsing
  116. auto settings = Totp::parseSettings("30;S", "63BEDWCQZKTQWPESARIERL5DTTQFCJTK");
  117. QCOMPARE(settings->key, QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK"));
  118. QCOMPARE(settings->encoder.shortName, Totp::STEAM_SHORTNAME);
  119. QCOMPARE(settings->format, Totp::StorageFormat::LEGACY);
  120. QCOMPARE(settings->digits, Totp::STEAM_DIGITS);
  121. QCOMPARE(settings->step, 30u);
  122. // OTP URL Parsing
  123. QString secret = "otpauth://totp/"
  124. "test:test@example.com?secret=63BEDWCQZKTQWPESARIERL5DTTQFCJTK&issuer=Valve&algorithm="
  125. "SHA1&digits=5&period=30&encoder=steam";
  126. settings = Totp::parseSettings(secret);
  127. QCOMPARE(settings->key, QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK"));
  128. QCOMPARE(settings->encoder.shortName, Totp::STEAM_SHORTNAME);
  129. QCOMPARE(settings->format, Totp::StorageFormat::OTPURL);
  130. QCOMPARE(settings->digits, Totp::STEAM_DIGITS);
  131. QCOMPARE(settings->step, 30u);
  132. // These time/value pairs were created by running the Steam Guard function of the
  133. // Steam mobile app with a throw-away steam account. The above secret was extracted
  134. // from the Steam app's data for use in testing here.
  135. quint64 time = 1511200518;
  136. QCOMPARE(Totp::generateTotp(settings, time), QString("FR8RV"));
  137. time = 1511200714;
  138. QCOMPARE(Totp::generateTotp(settings, time), QString("9P3VP"));
  139. }
  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. // Nullptr Settings (expected reset of TOTP)
  160. entry.setTotp(nullptr);
  161. QVERIFY(!entry.hasTotp());
  162. QCOMPARE(entry.historyItems().size(), 3);
  163. }
  164. void TestTotp::testKeePass2()
  165. {
  166. Entry entry;
  167. auto attr = entry.attributes();
  168. // Default settings
  169. attr->set("TimeOtp-Secret-Base32", "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");
  170. auto settings = entry.totpSettings();
  171. QVERIFY(settings);
  172. QCOMPARE(settings->key, QString("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"));
  173. QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
  174. QCOMPARE(settings->digits, 6u);
  175. QCOMPARE(settings->step, 30u);
  176. QCOMPARE(Totp::hasCustomSettings(settings), false);
  177. // Custom settings
  178. attr->set("TimeOtp-Algorithm", "HMAC-SHA-256");
  179. attr->set("TimeOtp-Length", "8");
  180. settings = entry.totpSettings();
  181. QVERIFY(settings);
  182. QCOMPARE(settings->key, QString("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"));
  183. QCOMPARE(settings->algorithm, Totp::Algorithm::Sha256);
  184. QCOMPARE(settings->digits, 8u);
  185. QCOMPARE(settings->step, 30u);
  186. QCOMPARE(Totp::hasCustomSettings(settings), true);
  187. // Base64 and other encodings are not supported
  188. attr->remove("TimeOtp-Secret-Base32");
  189. attr->set("TimeOtp-Secret-Base64", "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");
  190. settings = entry.totpSettings();
  191. QVERIFY(!settings);
  192. }