From 19eb6a8a60afed2da5733a2eef47de80009f824f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20E=2E=20Garc=C3=ADa?= Date: Sat, 14 Oct 2017 22:54:20 -0600 Subject: [PATCH] Add new Base32 implementation --- COPYING | 9 -- LICENSE.APACHE-2.0 | 201 ------------------------------ src/CMakeLists.txt | 5 +- src/core/Base32.cpp | 285 +++++++++++++++++++++++++++++++++++++++++++ src/core/Base32.h | 42 +++++++ src/core/Optional.h | 86 +++++++++++++ src/totp/base32.cpp | 68 ----------- src/totp/base32.h | 34 ------ src/totp/totp.cpp | 57 ++++++--- src/totp/totp.h | 11 +- tests/CMakeLists.txt | 3 + tests/TestBase32.cpp | 278 +++++++++++++++++++++++++++++++++++++++++ tests/TestBase32.h | 37 ++++++ tests/TestTotp.cpp | 31 +---- tests/TestTotp.h | 1 - 15 files changed, 789 insertions(+), 359 deletions(-) delete mode 100644 LICENSE.APACHE-2.0 create mode 100644 src/core/Base32.cpp create mode 100644 src/core/Base32.h create mode 100644 src/core/Optional.h delete mode 100644 src/totp/base32.cpp delete mode 100644 src/totp/base32.h create mode 100644 tests/TestBase32.cpp create mode 100644 tests/TestBase32.h diff --git a/COPYING b/COPYING index 481aaf726..403e4564b 100644 --- a/COPYING +++ b/COPYING @@ -214,10 +214,6 @@ Files: share/icons/database/C65_W.png Copyright: none License: public-domain -Files: src/crypto/salsa20/* -Copyright: none -License: public-domain - Files: src/streams/qtiocompressor.* src/streams/QtIOCompressor tests/modeltest.* @@ -241,8 +237,3 @@ Files: src/gui/KMessageWidget.h Copyright: 2011 Aurélien Gâteau 2014 Dominik Haumann License: LGPL-2.1 - -Files: src/totp/base32.cpp - src/totp/base32.h -Copyright: 2010 Google Inc. -License: Apache 2.0 \ No newline at end of file diff --git a/LICENSE.APACHE-2.0 b/LICENSE.APACHE-2.0 deleted file mode 100644 index 9c8f3ea08..000000000 --- a/LICENSE.APACHE-2.0 +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 791685576..28170b162 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -59,6 +59,9 @@ set(keepassx_SOURCES core/Tools.cpp core/Translator.cpp core/Uuid.cpp + core/Base32.h + core/Base32.cpp + core/Optional.h cli/PasswordInput.cpp cli/PasswordInput.h crypto/Crypto.cpp @@ -138,8 +141,6 @@ set(keepassx_SOURCES streams/qtiocompressor.cpp streams/StoreDataStream.cpp streams/SymmetricCipherStream.cpp - totp/base32.h - totp/base32.cpp totp/totp.h totp/totp.cpp ) diff --git a/src/core/Base32.cpp b/src/core/Base32.cpp new file mode 100644 index 000000000..8b2faf629 --- /dev/null +++ b/src/core/Base32.cpp @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* Conforms to RFC 4648. For details, see: https://tools.ietf.org/html/rfc4648 + * Use the functions Base32::addPadding/1, Base32::removePadding/1 or + * Base32::sanitizeInput/1 to fix input or output for a particular + * applications (e.g. to use with Google Authenticator). + */ + +#include "Base32.h" + +constexpr quint64 MASK_40BIT = quint64(0xF8) << 32; +constexpr quint64 MASK_35BIT = quint64(0x7C0000000); +constexpr quint64 MASK_25BIT = quint64(0x1F00000); +constexpr quint64 MASK_20BIT = quint64(0xF8000); +constexpr quint64 MASK_10BIT = quint64(0x3E0); + +constexpr char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +constexpr quint8 ALPH_POS_2 = 26; + +constexpr quint8 ASCII_2 = static_cast('2'); +constexpr quint8 ASCII_7 = static_cast('7'); +constexpr quint8 ASCII_A = static_cast('A'); +constexpr quint8 ASCII_Z = static_cast('Z'); +constexpr quint8 ASCII_a = static_cast('a'); +constexpr quint8 ASCII_z = static_cast('z'); +constexpr quint8 ASCII_EQ = static_cast('='); + +Optional Base32::decode(const QByteArray& encodedData) +{ + if (encodedData.size() <= 0) { + return Optional(""); + } + + if (encodedData.size() % 8 != 0) { + return Optional(); + } + + int nPads = 0; + for (int i = -1; i > -7; --i) { + if ('=' == encodedData[encodedData.size() + i]) + ++nPads; + } + + int specialOffset; + int nSpecialBytes; + + switch (nPads) { // in {0, 1, 3, 4, 6} + case 1: + nSpecialBytes = 4; + specialOffset = 3; + break; + case 3: + nSpecialBytes = 3; + specialOffset = 1; + break; + case 4: + nSpecialBytes = 2; + specialOffset = 4; + break; + case 6: + nSpecialBytes = 1; + specialOffset = 2; + break; + default: + nSpecialBytes = 0; + specialOffset = 0; + } + + Q_ASSERT(encodedData.size() > 0); + const int nQuanta = encodedData.size() / 8; + const int nBytes = (nQuanta - 1) * 5 + nSpecialBytes; + + QByteArray data(nBytes, Qt::Uninitialized); + + int i = 0; + int o = 0; + + while (i < encodedData.size()) { + quint64 quantum = 0; + int nQuantumBytes = 5; + + for (int n = 0; n < 8; n++) { + quint8 ch = static_cast(encodedData[i++]); + if ((ASCII_A <= ch && ch <= ASCII_Z) || (ASCII_a <= ch && ch <= ASCII_z)) { + ch -= ASCII_A; + if (ch >= ALPH_POS_2) + ch -= ASCII_a - ASCII_A; + } else { + if (ASCII_2 <= ch && ch <= ASCII_7) { + ch -= ASCII_2; + ch += ALPH_POS_2; + } else { + if (ASCII_EQ == ch) { + if (i == encodedData.size()) { + // finished with special quantum + quantum >>= specialOffset; + nQuantumBytes = nSpecialBytes; + } + continue; + } else { + // illegal character + return Optional(); + } + } + } + + quantum <<= 5; + quantum |= ch; + } + + const int offset = (nQuantumBytes - 1) * 8; + quint64 mask = quint64(0xFF) << offset; + for (int n = offset; n >= 0; n -= 8) { + char c = static_cast((quantum & mask) >> n); + data[o++] = c; + mask >>= 8; + } + } + + return Optional(data); +} + +QByteArray Base32::encode(const QByteArray& data) +{ + if (data.size() < 1) { + return QByteArray(); + } + + const int nBits = data.size() * 8; + const int rBits = nBits % 40; // in {0, 8, 16, 24, 32} + const int nQuanta = nBits / 40 + (rBits > 0 ? 1 : 0); + QByteArray encodedData(nQuanta * 8, Qt::Uninitialized); + + int i = 0; + int o = 0; + int n; + quint64 mask; + quint64 quantum; + + // 40-bits of input per input group + while (i + 5 <= data.size()) { + quantum = 0; + for (n = 32; n >= 0; n -= 8) { + quantum |= (static_cast(data[i++]) << n); + } + + mask = MASK_40BIT; + int index; + for (n = 35; n >= 0; n -= 5) { + index = (quantum & mask) >> n; + encodedData[o++] = alphabet[index]; + mask >>= 5; + } + } + + // < 40-bits of input at final input group + if (i < data.size()) { + Q_ASSERT(rBits > 0); + quantum = 0; + for (n = rBits - 8; n >= 0; n -= 8) + quantum |= static_cast(data[i++]) << n; + + switch (rBits) { + case 8: // expand to 10 bits + quantum <<= 2; + mask = MASK_10BIT; + n = 5; + break; + case 16: // expand to 20 bits + quantum <<= 4; + mask = MASK_20BIT; + n = 15; + break; + case 24: // expand to 25 bits + quantum <<= 1; + mask = MASK_25BIT; + n = 20; + break; + default: // expand to 35 bits + Q_ASSERT(rBits == 32); + quantum <<= 3; + mask = MASK_35BIT; + n = 30; + } + + while (n >= 0) { + int index = (quantum & mask) >> n; + encodedData[o++] = alphabet[index]; + mask >>= 5; + n -= 5; + } + + // add pad characters + while (o < encodedData.size()) + encodedData[o++] = '='; + } + + Q_ASSERT(encodedData.size() == o); + return encodedData; +} + +QByteArray Base32::addPadding(const QByteArray& encodedData) +{ + if (encodedData.size() <= 0 || encodedData.size() % 8 == 0) { + return encodedData; + } + + const int rBytes = encodedData.size() % 8; + // rBytes must be a member of {2, 4, 5, 7} + if (1 == rBytes || 3 == rBytes || 6 == rBytes) { + return encodedData; + } + + QByteArray newEncodedData(encodedData); + for (int nPads = 8 - rBytes; nPads > 0; --nPads) { + newEncodedData.append('='); + } + + return newEncodedData; +} + +QByteArray Base32::removePadding(const QByteArray& encodedData) +{ + if (encodedData.size() <= 0 || encodedData.size() % 8 != 0) { + return encodedData; // return same bad input + } + + int nPads = 0; + for (int i = -1; i > -7; --i) { + if ('=' == encodedData[encodedData.size() + i]) { + ++nPads; + } + } + + QByteArray newEncodedData(encodedData); + newEncodedData.remove(encodedData.size() - nPads, nPads); + newEncodedData.resize(encodedData.size() - nPads); + + return newEncodedData; +} + +QByteArray Base32::sanitizeInput(const QByteArray& encodedData) +{ + if (encodedData.size() <= 0) { + return encodedData; + } + + QByteArray newEncodedData(encodedData.size(), Qt::Uninitialized); + int i = 0; + for (auto ch : encodedData) { + switch (ch) { + case '0': + newEncodedData[i++] = 'O'; + break; + case '1': + newEncodedData[i++] = 'L'; + break; + case '8': + newEncodedData[i++] = 'B'; + break; + default: + if (('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') || ('2' <= ch && ch <= '7')) { + newEncodedData[i++] = ch; + } + } + } + newEncodedData.resize(i); + + return addPadding(newEncodedData); +} diff --git a/src/core/Base32.h b/src/core/Base32.h new file mode 100644 index 000000000..048828974 --- /dev/null +++ b/src/core/Base32.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* Conforms to RFC 4648. For details, see: https://tools.ietf.org/html/rfc4648 + * Use the functions Base32::addPadding/1, Base32::removePadding/1 or + * Base32::sanitizeInput/1 to fix input or output for a particular + * applications (e.g. to use with Google Authenticator). + */ + +#ifndef BASE32_H +#define BASE32_H + +#include "Optional.h" +#include +#include + +class Base32 +{ +public: + Base32() = default; + Q_REQUIRED_RESULT static Optional decode(const QByteArray&); + Q_REQUIRED_RESULT static QByteArray encode(const QByteArray&); + Q_REQUIRED_RESULT static QByteArray addPadding(const QByteArray&); + Q_REQUIRED_RESULT static QByteArray removePadding(const QByteArray&); + Q_REQUIRED_RESULT static QByteArray sanitizeInput(const QByteArray&); +}; + +#endif // BASE32_H diff --git a/src/core/Optional.h b/src/core/Optional.h new file mode 100644 index 000000000..0721daa3d --- /dev/null +++ b/src/core/Optional.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef OPTIONAL_H +#define OPTIONAL_H + +/* + * This utility class is for providing basic support for an option type. + * It can be replaced by std::optional (C++17) or + * std::experimental::optional (C++11) when they become fully supported + * by all the main compiler toolchains. + */ + +template class Optional +{ +public: + // None + Optional() + : m_hasValue(false) + , m_value(){}; + + // Some T + Optional(const T& value) + : m_hasValue(true) + , m_value(value){}; + + // Copy + Optional(const Optional& other) + : m_hasValue(other.m_hasValue) + , m_value(other.m_value){}; + + const Optional& operator=(const Optional& other) + { + m_hasValue = other.m_hasValue; + m_value = other.m_value; + return *this; + } + + bool operator==(const Optional& other) const + { + if (m_hasValue) + return other.m_hasValue && m_value == other.m_value; + else + return !other.m_hasValue; + } + + bool operator!=(const Optional& other) const + { + return !(*this == other); + } + + bool hasValue() const + { + return m_hasValue; + } + + T valueOr(const T& other) const + { + return m_hasValue ? m_value : other; + } + + Optional static makeOptional(const T& value) + { + return Optional(value); + } + +private: + bool m_hasValue; + T m_value; +}; + +#endif // OPTIONAL_H diff --git a/src/totp/base32.cpp b/src/totp/base32.cpp deleted file mode 100644 index 4c81cb491..000000000 --- a/src/totp/base32.cpp +++ /dev/null @@ -1,68 +0,0 @@ -// Base32 implementation -// Source: https://github.com/google/google-authenticator-libpam/blob/master/src/base32.c -// -// Copyright 2010 Google Inc. -// Author: Markus Gutschke -// Modifications Copyright 2017 KeePassXC team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "base32.h" - -Base32::Base32() -{ -} - -QByteArray Base32::base32_decode(const QByteArray encoded) -{ - QByteArray result; - - int buffer = 0; - int bitsLeft = 0; - - for (char ch : encoded) { - if (ch == 0 || ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '-' || ch == '=') { - continue; - } - - buffer <<= 5; - - // Deal with commonly mistyped characters - if (ch == '0') { - ch = 'O'; - } else if (ch == '1') { - ch = 'L'; - } else if (ch == '8') { - ch = 'B'; - } - - // Look up one base32 digit - if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) { - ch = (ch & 0x1F) - 1; - } else if (ch >= '2' && ch <= '7') { - ch -= '2' - 26; - } else { - return QByteArray(); - } - - buffer |= ch; - bitsLeft += 5; - - if (bitsLeft >= 8) { - result.append(static_cast (buffer >> (bitsLeft - 8))); - bitsLeft -= 8; - } - } - - return result; -} \ No newline at end of file diff --git a/src/totp/base32.h b/src/totp/base32.h deleted file mode 100644 index 75343fa43..000000000 --- a/src/totp/base32.h +++ /dev/null @@ -1,34 +0,0 @@ -// Base32 implementation -// Source: https://github.com/google/google-authenticator-libpam/blob/master/src/base32.h -// -// Copyright 2010 Google Inc. -// Author: Markus Gutschke -// Modifications Copyright 2017 KeePassXC team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef BASE32_H -#define BASE32_H - -#include -#include - -class Base32 -{ -public: - Base32(); - static QByteArray base32_decode(const QByteArray encoded); -}; - - -#endif //BASE32_H diff --git a/src/totp/totp.cpp b/src/totp/totp.cpp index 51af0e086..170ac2da7 100644 --- a/src/totp/totp.cpp +++ b/src/totp/totp.cpp @@ -17,16 +17,15 @@ */ #include "totp.h" -#include "base32.h" -#include -#include -#include -#include +#include "core/Base32.h" #include +#include #include +#include #include #include - +#include +#include const quint8 QTotp::defaultStep = 30; const quint8 QTotp::defaultDigits = 6; @@ -35,7 +34,7 @@ QTotp::QTotp() { } -QString QTotp::parseOtpString(QString key, quint8 &digits, quint8 &step) +QString QTotp::parseOtpString(QString key, quint8& digits, quint8& step) { QUrl url(key); @@ -58,7 +57,6 @@ QString QTotp::parseOtpString(QString key, quint8 &digits, quint8 &step) step = q_step; } - } else { // Compatibility with "KeeOtp" plugin string format QRegExp rx("key=(.+)", Qt::CaseInsensitive, QRegExp::RegExp); @@ -93,30 +91,53 @@ QString QTotp::parseOtpString(QString key, quint8 &digits, quint8 &step) return seed; } -QString QTotp::generateTotp(const QByteArray key, quint64 time, - const quint8 numDigits = defaultDigits, const quint8 step = defaultStep) +QString QTotp::generateTotp(const QByteArray key, + quint64 time, + const quint8 numDigits = defaultDigits, + const quint8 step = defaultStep) { quint64 current = qToBigEndian(time / step); - QByteArray secret = Base32::base32_decode(key); - if (secret.isEmpty()) { + Optional secret = Base32::decode(Base32::sanitizeInput(key)); + if (!secret.hasValue()) { return "Invalid TOTP secret key"; } QMessageAuthenticationCode code(QCryptographicHash::Sha1); - code.setKey(secret); + code.setKey(secret.valueOr("")); code.addData(QByteArray(reinterpret_cast(¤t), sizeof(current))); QByteArray hmac = code.result(); int offset = (hmac[hmac.length() - 1] & 0xf); - int binary = - ((hmac[offset] & 0x7f) << 24) - | ((hmac[offset + 1] & 0xff) << 16) - | ((hmac[offset + 2] & 0xff) << 8) - | (hmac[offset + 3] & 0xff); + int binary = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | + (hmac[offset + 3] & 0xff); quint32 digitsPower = pow(10, numDigits); quint64 password = binary % digitsPower; return QString("%1").arg(password, numDigits, 10, QChar('0')); } + +// See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format +QUrl QTotp::generateOtpString(const QString& secret, + const QString& type, + const QString& issuer, + const QString& username, + const QString& algorithm, + const quint8& digits, + const quint8& step) +{ + QUrl keyUri; + keyUri.setScheme("otpauth"); + keyUri.setHost(type); + keyUri.setPath(QString("/%1:%2").arg(issuer).arg(username)); + QUrlQuery parameters; + parameters.addQueryItem("secret", secret); + parameters.addQueryItem("issuer", issuer); + parameters.addQueryItem("algorithm", algorithm); + parameters.addQueryItem("digits", QString::number(digits)); + parameters.addQueryItem("period", QString::number(step)); + keyUri.setQuery(parameters); + + return keyUri; +} diff --git a/src/totp/totp.h b/src/totp/totp.h index 642b4f9a3..d5d8aa679 100644 --- a/src/totp/totp.h +++ b/src/totp/totp.h @@ -21,12 +21,21 @@ #include +class QUrl; + class QTotp { public: QTotp(); - static QString parseOtpString(QString rawSecret, quint8 &digits, quint8 &step); + static QString parseOtpString(QString rawSecret, quint8& digits, quint8& step); static QString generateTotp(const QByteArray key, quint64 time, const quint8 numDigits, const quint8 step); + static QUrl generateOtpString(const QString& secret, + const QString& type, + const QString& issuer, + const QString& username, + const QString& algorithm, + const quint8& digits, + const quint8& step); static const quint8 defaultStep; static const quint8 defaultDigits; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2a420270a..3f003f01c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -161,6 +161,9 @@ add_unit_test(NAME testentry SOURCES TestEntry.cpp add_unit_test(NAME testtotp SOURCES TestTotp.cpp LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testbase32 SOURCES TestBase32.cpp + LIBS ${TEST_LIBRARIES}) + add_unit_test(NAME testcsvparser SOURCES TestCsvParser.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/TestBase32.cpp b/tests/TestBase32.cpp new file mode 100644 index 000000000..3d829d96b --- /dev/null +++ b/tests/TestBase32.cpp @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "TestBase32.h" +#include "core/Base32.h" +#include + +QTEST_GUILESS_MAIN(TestBase32) + +void TestBase32::testDecode() +{ + // 3 quanta, all upper case + padding + QByteArray encodedData = "JBSWY3DPEB3W64TMMQXC4LQ="; + auto data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("Hello world...")); + + // 4 quanta, all upper case + encodedData = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("12345678901234567890")); + + // 4 quanta, all lower case + encodedData = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("12345678901234567890")); + + // 4 quanta, mixed upper and lower case + encodedData = "Gezdgnbvgy3tQojqgezdGnbvgy3tQojQ"; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("12345678901234567890")); + + // 1 pad characters + encodedData = "ORSXG5A="; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("test")); + + // 3 pad characters + encodedData = "L5PV6==="; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("___")); + + // 4 pad characters + encodedData = "MZXW6IDCMFZA===="; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("foo bar")); + + // six pad characters + encodedData = "MZXW6YTBOI======"; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("foobar")); + + encodedData = "IA======"; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("@")); + + // error: illegal character + encodedData = "1MZXW6YTBOI====="; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("ERROR")); + + // error: missing pad character + encodedData = "MZXW6YTBOI====="; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("ERROR")); + + // RFC 4648 test vectors + encodedData = ""; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("")); + + encodedData = "MY======"; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("f")); + + encodedData = "MZXQ===="; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("fo")); + + encodedData = "MZXW6==="; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("foo")); + + encodedData = "MZXW6YQ="; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("foob")); + + encodedData = "MZXW6YTB"; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("fooba")); + + encodedData = "MZXW6YTBOI======"; + data = Base32::decode(encodedData); + QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("foobar")); +} + +void TestBase32::testEncode() +{ + QByteArray data = "Hello world..."; + QByteArray encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("JBSWY3DPEB3W64TMMQXC4LQ=")); + + data = "12345678901234567890"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")); + + data = "012345678901234567890"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("GAYTEMZUGU3DOOBZGAYTEMZUGU3DOOBZGA======")); + + data = "test"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("ORSXG5A=")); + + data = "___"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("L5PV6===")); + + data = "foo bar"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("MZXW6IDCMFZA====")); + + data = "@"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("IA======")); + + // RFC 4648 test vectors + data = ""; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("")); + + data = "f"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("MY======")); + + data = "fo"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("MZXQ====")); + + data = "foo"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("MZXW6===")); + + data = "foob"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("MZXW6YQ=")); + + data = "fooba"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("MZXW6YTB")); + + data = "foobar"; + encodedData = Base32::encode(data); + QCOMPARE(encodedData, QByteArray("MZXW6YTBOI======")); +} + +void TestBase32::testAddPadding() +{ + // Empty. Invalid, returns input. + QByteArray data = ""; + QByteArray paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, data); + + // One byte of encoded data. Invalid, returns input. + data = "B"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, data); + + // Two bytes of encoded data. + data = "BB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, QByteArray("BB======")); + + // Three bytes of encoded data. Invalid, returns input. + data = "BBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, data); + + // Four bytes of encoded data. + data = "BBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, QByteArray("BBBB====")); + + // Five bytes of encoded data. + data = "BBBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, QByteArray("BBBBB===")); + + // Six bytes of encoded data. Invalid, returns input. + data = "BBBBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, data); + + // Seven bytes of encoded data. + data = "BBBBBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, QByteArray("BBBBBBB=")); + + // Eight bytes of encoded data. Valid, but returns same as input. + data = "BBBBBBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, data); + + // More than eight bytes (8+5). + data = "AAAAAAAABBBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, QByteArray("AAAAAAAABBBBB===")); +} + +void TestBase32::testRemovePadding() +{ + QByteArray data = ""; + QByteArray unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, data); + + data = "AAAAAAAABB======"; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, QByteArray("AAAAAAAABB")); + + data = "BBBB===="; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, QByteArray("BBBB")); + + data = "AAAAAAAABBBBB==="; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, QByteArray("AAAAAAAABBBBB")); + + data = "BBBBBBB="; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, QByteArray("BBBBBBB")); + + // Invalid: 7 bytes of data. Returns same as input. + data = "IIIIIII"; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, data); + + // Invalid: more padding than necessary. Returns same as input. + data = "AAAAAAAABBBB====="; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, data); +} + +void TestBase32::testSanitizeInput() +{ + // sanitize input (white space + missing padding) + QByteArray encodedData = "JBSW Y3DP EB3W 64TM MQXC 4LQA"; + auto data = Base32::decode(Base32::sanitizeInput(encodedData)); + QCOMPARE(QString::fromLatin1(data.valueOr("ERRROR")), QString("Hello world...")); + + // sanitize input (typo + missing padding) + encodedData = "J8SWY3DPE83W64TMMQXC4LQA"; + data = Base32::decode(Base32::sanitizeInput(encodedData)); + QCOMPARE(QString::fromLatin1(data.valueOr("ERRROR")), QString("Hello world...")); + + // sanitize input (other illegal characters) + encodedData = "J8SWY3D[PE83W64TMMQ]XC!4LQA"; + data = Base32::decode(Base32::sanitizeInput(encodedData)); + QCOMPARE(QString::fromLatin1(data.valueOr("ERRROR")), QString("Hello world...")); + + // sanitize input (NUL character) + encodedData = "J8SWY3DPE83W64TMMQXC4LQA"; + encodedData.insert(3, '\0'); + data = Base32::decode(Base32::sanitizeInput(encodedData)); + QCOMPARE(QString::fromLatin1(data.valueOr("ERRROR")), QString("Hello world...")); +} diff --git a/tests/TestBase32.h b/tests/TestBase32.h new file mode 100644 index 000000000..cf7cf092c --- /dev/null +++ b/tests/TestBase32.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSX_TESTBASE32_H +#define KEEPASSX_TESTBASE32_H + +#include + +class Base32; + +class TestBase32 : public QObject +{ + Q_OBJECT + +private slots: + void testEncode(); + void testDecode(); + void testAddPadding(); + void testRemovePadding(); + void testSanitizeInput(); +}; + +#endif // KEEPASSX_TESTBASE32_H diff --git a/tests/TestTotp.cpp b/tests/TestTotp.cpp index e22c2567e..48ff88144 100644 --- a/tests/TestTotp.cpp +++ b/tests/TestTotp.cpp @@ -18,15 +18,14 @@ #include "TestTotp.h" -#include -#include #include -#include +#include #include +#include +#include #include "crypto/Crypto.h" #include "totp/totp.h" -#include "totp/base32.h" QTEST_GUILESS_MAIN(TestTotp) @@ -35,12 +34,13 @@ void TestTotp::initTestCase() QVERIFY(Crypto::init()); } - void TestTotp::testParseSecret() { quint8 digits = 0; quint8 step = 0; - QString secret = "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"; + QString secret = "otpauth://totp/" + "ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=" + "SHA1&digits=6&period=30"; QCOMPARE(QTotp::parseOtpString(secret, digits, step), QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ")); QCOMPARE(digits, quint8(6)); QCOMPARE(step, quint8(30)); @@ -60,25 +60,6 @@ void TestTotp::testParseSecret() QCOMPARE(step, quint8(30)); } -void TestTotp::testBase32() -{ - QByteArray key = QString("JBSW Y3DP EB3W 64TM MQXC 4LQA").toLatin1(); - QByteArray secret = Base32::base32_decode(key); - QCOMPARE(QString::fromLatin1(secret), QString("Hello world...")); - - key = QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq").toLatin1(); - secret = Base32::base32_decode(key); - QCOMPARE(QString::fromLatin1(secret), QString("12345678901234567890")); - - key = QString("ORSXG5A=").toLatin1(); - secret = Base32::base32_decode(key); - QCOMPARE(QString::fromLatin1(secret), QString("test")); - - key = QString("MZXW6YTBOI======").toLatin1(); - secret = Base32::base32_decode(key); - QCOMPARE(QString::fromLatin1(secret), QString("foobar")); -} - void TestTotp::testTotpCode() { // Test vectors from RFC 6238 diff --git a/tests/TestTotp.h b/tests/TestTotp.h index d197294dd..785a9f522 100644 --- a/tests/TestTotp.h +++ b/tests/TestTotp.h @@ -30,7 +30,6 @@ class TestTotp : public QObject private slots: void initTestCase(); void testParseSecret(); - void testBase32(); void testTotpCode(); };