From 5ee036dd0964d746951059dee7e2adc234aa4a9b Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 31 Aug 2022 21:19:25 -0400 Subject: [PATCH] Add option to generate OTP codes automatically * This uses the great libcotp library, I stripped it down to fit inside the repository. * This is a security-convenience trade-off, and it's made very clear with the tooltips on the settings page. * It's still secured by your system keychain, and it's up to the users whether that's good enough for them. Eventually down the line I would like to support more esoteric keychains such as Bitwarden or KeePass. * Right now it's only integrated into the auto-login desktop feature, but there will eventually be like an "auto-fill OTP" button in the main window. There's still a lot to clean up with these new features but they work a little at least :-) --- external/CMakeLists.txt | 3 + external/libbaseencode/.gitignore | 37 +++ external/libbaseencode/CMakeLists.txt | 24 ++ external/libbaseencode/LICENSE | 201 ++++++++++++++ external/libbaseencode/README.md | 41 +++ external/libbaseencode/SECURITY.md | 18 ++ external/libbaseencode/src/base32.c | 201 ++++++++++++++ external/libbaseencode/src/base64.c | 174 ++++++++++++ external/libbaseencode/src/baseencode.h | 28 ++ external/libbaseencode/src/common.h | 50 ++++ external/libcotp/.gitignore | 36 +++ external/libcotp/CMakeLists.txt | 34 +++ external/libcotp/LICENSE | 202 ++++++++++++++ external/libcotp/README.md | 61 +++++ external/libcotp/SECURITY.md | 20 ++ external/libcotp/cmake/FindGcrypt.cmake | 31 +++ external/libcotp/src/cotp.h | 65 +++++ external/libcotp/src/otp.c | 335 +++++++++++++++++++++++ launcher/desktop/CMakeLists.txt | 4 +- launcher/desktop/src/autologinwindow.cpp | 36 ++- launcher/desktop/src/settingswindow.cpp | 2 +- 21 files changed, 1596 insertions(+), 7 deletions(-) create mode 100644 external/libbaseencode/.gitignore create mode 100644 external/libbaseencode/CMakeLists.txt create mode 100644 external/libbaseencode/LICENSE create mode 100644 external/libbaseencode/README.md create mode 100644 external/libbaseencode/SECURITY.md create mode 100644 external/libbaseencode/src/base32.c create mode 100644 external/libbaseencode/src/base64.c create mode 100644 external/libbaseencode/src/baseencode.h create mode 100644 external/libbaseencode/src/common.h create mode 100644 external/libcotp/.gitignore create mode 100644 external/libcotp/CMakeLists.txt create mode 100644 external/libcotp/LICENSE create mode 100644 external/libcotp/README.md create mode 100644 external/libcotp/SECURITY.md create mode 100644 external/libcotp/cmake/FindGcrypt.cmake create mode 100644 external/libcotp/src/cotp.h create mode 100644 external/libcotp/src/otp.c diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 6643916..8afc659 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,3 +1,6 @@ +add_subdirectory(libbaseencode) +add_subdirectory(libcotp) + include(FetchContent) FetchContent_Declare( diff --git a/external/libbaseencode/.gitignore b/external/libbaseencode/.gitignore new file mode 100644 index 0000000..e599a98 --- /dev/null +++ b/external/libbaseencode/.gitignore @@ -0,0 +1,37 @@ +.idea/ +cmake-build-debug/ +build/ + +# Object files +*.o +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su diff --git a/external/libbaseencode/CMakeLists.txt b/external/libbaseencode/CMakeLists.txt new file mode 100644 index 0000000..cef52b1 --- /dev/null +++ b/external/libbaseencode/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.5) +project(baseencode) + +include(GNUInstallDirs) + +# set up versioning. +set(BUILD_MAJOR "1") +set(BUILD_MINOR "0") +set(BUILD_VERSION "14") +set(BUILD_VERSION ${BUILD_MAJOR}.${BUILD_MINOR}.${BUILD_VERSION}) + +set(CMAKE_C_STANDARD 11) + +set(BASEENCODE_HEADERS src/baseencode.h) +set(SOURCE_FILES src/base32.c src/base64.c) + +set(CMAKE_C_FLAGS "-Wall -Werror -fPIC") + +add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES}) + +target_link_libraries(${PROJECT_NAME} ${PROJECT_LIBS}) +target_include_directories(${PROJECT_NAME} PUBLIC src) + +set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${BUILD_VERSION} SOVERSION ${BUILD_MAJOR}) \ No newline at end of file diff --git a/external/libbaseencode/LICENSE b/external/libbaseencode/LICENSE new file mode 100644 index 0000000..0e580dd --- /dev/null +++ b/external/libbaseencode/LICENSE @@ -0,0 +1,201 @@ + 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 2018 Paolo Stivanin + + 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. diff --git a/external/libbaseencode/README.md b/external/libbaseencode/README.md new file mode 100644 index 0000000..aa65cb6 --- /dev/null +++ b/external/libbaseencode/README.md @@ -0,0 +1,41 @@ +# libbaseencode + + Coverity Scan Build Status + + +Library written in C for encoding and decoding data using base32 or base64 according to [RFC-4648](https://tools.ietf.org/html/rfc4648) + +# Requiremens +- GCC or Clang +- CMake + +# Build and Install +``` +$ git clone https://github.com/paolostivanin/libbaseencode.git +$ cd libbaseencode +$ mkdir build && cd $_ +$ cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr ../ +$ make +# make install +``` + +# How To Use It +``` +char *b32_encoded = base32_encode(unsigned char *input, size_t input_length, baseencode_error_t *err); + +unsigned char *b32_decoded = base32_decode(char *input, size_t input_length, baseencode_error_t *err); + +char *b64_encoded = base64_encode(unsigned char *input, size_t input_length, baseencode_error_t *err); + +unsigned char *b64_decoded = base64_decode(char *input, size_t input_length, baseencode_error_t *err); +``` +Please note that all the returned value **must be freed** once not needed any more. + +## Errors +In case of errors, `NULL` is returned and `err` is set to either one of: +``` +INVALID_INPUT, EMPTY_STRING, INPUT_TOO_BIG, INVALID_B32_DATA, INVALID_B64_DATA, MEMORY_ALLOCATION, +``` +otherwise, `err` is set to `SUCCESS` + diff --git a/external/libbaseencode/SECURITY.md b/external/libbaseencode/SECURITY.md new file mode 100644 index 0000000..6439793 --- /dev/null +++ b/external/libbaseencode/SECURITY.md @@ -0,0 +1,18 @@ +# Security Policy + +## Supported Versions + +The following list describes whether a version is eligible or not for security updates. + +| Version | Supported | EOL | +| ------- | ------------------ |-------------| +| 1.0.x | :heavy_check_mark: | - | + +## Reporting a Vulnerability + +Should you find a vulnerability, please report it privately to me via [e-mail](mailto:paolostivanin@users.noreply.github.com). +The following is the workflow: +- security issue is found, an e-mail is sent to me +- within 24 hours I will reply to your e-mail with some info like, for example, whether it actually is a security issue and how serious it is +- within 7 days I will develop and ship a fix +- once the update is out I will open a [security advisory](https://github.com/paolostivanin/OTPClient/security/advisories) diff --git a/external/libbaseencode/src/base32.c b/external/libbaseencode/src/base32.c new file mode 100644 index 0000000..096f043 --- /dev/null +++ b/external/libbaseencode/src/base32.c @@ -0,0 +1,201 @@ +#include +#include +#include +#include +#include "common.h" + + +static int is_valid_b32_input(const char *user_data, size_t data_len); + +static int get_char_index(unsigned char c); + +static const unsigned char b32_alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + + +// The encoding process represents 40-bit groups of input bits as output strings of 8 encoded characters. The input data must be null terminated. +char * +base32_encode(const unsigned char *user_data, size_t data_len, baseencode_error_t *err) +{ + baseencode_error_t error; + check_input(user_data, data_len, MAX_ENCODE_INPUT_LEN, &error); + if (error != SUCCESS) { + *err = error; + if (error == EMPTY_STRING) { + return strdup(""); + } else { + return NULL; + } + } + + size_t user_data_chars = 0, total_bits = 0; + int num_of_equals = 0; + for (int i = 0; i < data_len; i++) { + // As it's not known whether data_len is with or without the +1 for the null byte, a manual check is required. + // Check for null byte only at the end of the user given length, otherwise issue#23 may occur + if (user_data[i] == '\0' && i == data_len-1) { + break; + } else { + total_bits += 8; + user_data_chars += 1; + } + } + switch (total_bits % 40) { + case 8: + num_of_equals = 6; + break; + case 16: + num_of_equals = 4; + break; + case 24: + num_of_equals = 3; + break; + case 32: + num_of_equals = 1; + break; + default: + break; + } + + size_t output_length = (user_data_chars * 8 + 4) / 5; + char *encoded_data = calloc(output_length + num_of_equals + 1, 1); + if (encoded_data == NULL) { + *err = MEMORY_ALLOCATION; + return NULL; + } + + uint64_t first_octet, second_octet, third_octet, fourth_octet, fifth_octet; + uint64_t quintuple; + for (int i = 0, j = 0; i < user_data_chars;) { + first_octet = i < user_data_chars ? user_data[i++] : 0; + second_octet = i < user_data_chars ? user_data[i++] : 0; + third_octet = i < user_data_chars ? user_data[i++] : 0; + fourth_octet = i < user_data_chars ? user_data[i++] : 0; + fifth_octet = i < user_data_chars ? user_data[i++] : 0; + quintuple = + ((first_octet >> 3) << 35) + + ((((first_octet & 0x7) << 2) | (second_octet >> 6)) << 30) + + (((second_octet & 0x3F) >> 1) << 25) + + ((((second_octet & 0x01) << 4) | (third_octet >> 4)) << 20) + + ((((third_octet & 0xF) << 1) | (fourth_octet >> 7)) << 15) + + (((fourth_octet & 0x7F) >> 2) << 10) + + ((((fourth_octet & 0x3) << 3) | (fifth_octet >> 5)) << 5) + + (fifth_octet & 0x1F); + + encoded_data[j++] = b32_alphabet[(quintuple >> 35) & 0x1F]; + encoded_data[j++] = b32_alphabet[(quintuple >> 30) & 0x1F]; + encoded_data[j++] = b32_alphabet[(quintuple >> 25) & 0x1F]; + encoded_data[j++] = b32_alphabet[(quintuple >> 20) & 0x1F]; + encoded_data[j++] = b32_alphabet[(quintuple >> 15) & 0x1F]; + encoded_data[j++] = b32_alphabet[(quintuple >> 10) & 0x1F]; + encoded_data[j++] = b32_alphabet[(quintuple >> 5) & 0x1F]; + encoded_data[j++] = b32_alphabet[(quintuple >> 0) & 0x1F]; + } + + for (int i = 0; i < num_of_equals; i++) { + encoded_data[output_length + i] = '='; + } + encoded_data[output_length + num_of_equals] = '\0'; + + *err = SUCCESS; + return encoded_data; +} + + +unsigned char * +base32_decode(const char *user_data_untrimmed, size_t data_len, baseencode_error_t *err) +{ + baseencode_error_t error; + check_input((unsigned char *)user_data_untrimmed, data_len, MAX_DECODE_BASE32_INPUT_LEN, &error); + if (error != SUCCESS) { + *err = error; + if (error == EMPTY_STRING) { + return (unsigned char *) strdup(""); + } else { + return NULL; + } + } + + char *user_data = strdup(user_data_untrimmed); + data_len -= strip_char(user_data, ' '); + + if (!is_valid_b32_input(user_data, data_len)) { + *err = INVALID_B32_DATA; + free(user_data); + return NULL; + } + + size_t user_data_chars = 0; + for (int i = 0; i < data_len; i++) { + // As it's not known whether data_len is with or without the +1 for the null byte, a manual check is required. + if (user_data[i] != '=' && user_data[i] != '\0') { + user_data_chars += 1; + } + } + + size_t output_length = (size_t) ((user_data_chars + 1.6 + 1) / 1.6); // round up + unsigned char *decoded_data = calloc(output_length + 1, 1); + if (decoded_data == NULL) { + *err = MEMORY_ALLOCATION; + free(user_data); + return NULL; + } + + uint8_t mask = 0, current_byte = 0; + int bits_left = 8; + for (int i = 0, j = 0; i < user_data_chars; i++) { + int char_index = get_char_index((unsigned char)user_data[i]); + if (bits_left > BITS_PER_B32_BLOCK) { + mask = (uint8_t) char_index << (bits_left - BITS_PER_B32_BLOCK); + current_byte = (uint8_t) (current_byte | mask); + bits_left -= BITS_PER_B32_BLOCK; + } else { + mask = (uint8_t) char_index >> (BITS_PER_B32_BLOCK - bits_left); + current_byte = (uint8_t) (current_byte | mask); + decoded_data[j++] = current_byte; + current_byte = (uint8_t) (char_index << (BITS_PER_BYTE - BITS_PER_B32_BLOCK + bits_left)); + bits_left += BITS_PER_BYTE - BITS_PER_B32_BLOCK; + } + } + decoded_data[output_length] = '\0'; + + free(user_data); + + *err = SUCCESS; + return decoded_data; +} + + +static int +is_valid_b32_input(const char *user_data, size_t data_len) +{ + size_t found = 0, b32_alphabet_len = sizeof(b32_alphabet); + for (int i = 0; i < data_len; i++) { + if (user_data[i] == '\0') { + found++; + break; + } + for(int j = 0; j < b32_alphabet_len; j++) { + if(user_data[i] == b32_alphabet[j] || user_data[i] == '=') { + found++; + break; + } + } + } + if (found != data_len) { + return 0; + } else { + return 1; + } +} + + +static int +get_char_index(unsigned char c) +{ + for (int i = 0; i < sizeof(b32_alphabet); i++) { + if (b32_alphabet[i] == c) { + return i; + } + } + return -1; +} diff --git a/external/libbaseencode/src/base64.c b/external/libbaseencode/src/base64.c new file mode 100644 index 0000000..5b729dd --- /dev/null +++ b/external/libbaseencode/src/base64.c @@ -0,0 +1,174 @@ +#include +#include +#include +#include +#include +#include "common.h" + +static int is_valid_b64_input(const char *user_data, size_t data_len); + +static int get_char_index(unsigned char c); + +static const unsigned char b64_alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + +char * +base64_encode(const unsigned char *user_data, size_t data_len, baseencode_error_t *err) +{ + baseencode_error_t error; + check_input(user_data, data_len, MAX_ENCODE_INPUT_LEN, &error); + if (error != SUCCESS) { + *err = error; + if (error == EMPTY_STRING) { + return strdup(""); + } else { + return NULL; + } + } + + size_t user_data_chars = 0, total_bits = 0; + int num_of_equals = 0; + for (int i = 0; i < data_len; i++) { + // As it's not known whether data_len is with or without the +1 for the null byte, a manual check is required. + if (user_data[i] != '\0') { + total_bits += 8; + user_data_chars += 1; + } else { + break; + } + } + switch (total_bits % 24) { + case 8: + num_of_equals = 2; + break; + case 16: + num_of_equals = 1; + break; + default: + break; + } + + size_t output_length = (user_data_chars * 8 + 4) / 6; + char *encoded_data = calloc(output_length + num_of_equals + 1 + 3, 1); + if (encoded_data == NULL) { + *err = MEMORY_ALLOCATION; + return NULL; + } + + uint8_t first_octet, second_octet, third_octet; + for (int i = 0, j = 0, triple = 0; i < user_data_chars + 1;) { + first_octet = (uint8_t) (i < user_data_chars+1 ? user_data[i++] : 0); + second_octet = (uint8_t) (i < user_data_chars+1 ? user_data[i++] : 0); + third_octet = (uint8_t) (i < user_data_chars+1 ? user_data[i++] : 0); + triple = (first_octet << 0x10) + (second_octet << 0x08) + third_octet; + + encoded_data[j++] = b64_alphabet[(triple >> 0x12) & 0x3F]; + encoded_data[j++] = b64_alphabet[(triple >> 0x0C) & 0x3F]; + encoded_data[j++] = b64_alphabet[(triple >> 0x06) & 0x3F]; + encoded_data[j++] = b64_alphabet[(triple >> 0x00) & 0x3F]; + } + + for (int i = 0; i < num_of_equals; i++) { + encoded_data[output_length + i] = '='; + } + encoded_data[output_length + num_of_equals] = '\0'; + + *err = SUCCESS; + return encoded_data; +} + + +unsigned char * +base64_decode(const char *user_data_untrimmed, size_t data_len, baseencode_error_t *err) +{ + baseencode_error_t error; + check_input((unsigned char *)user_data_untrimmed, data_len, MAX_DECODE_BASE64_INPUT_LEN, &error); + if (error != SUCCESS) { + *err = error; + if (error == EMPTY_STRING) { + return (unsigned char *) strdup(""); + } else { + return NULL; + } + } + + char *user_data = strdup(user_data_untrimmed); + data_len -= strip_char(user_data, ' '); + + if (!is_valid_b64_input(user_data, data_len)) { + *err = INVALID_B64_DATA; + free(user_data); + return NULL; + } + + size_t user_data_chars = 0; + for (int z = 0; z < data_len; z++) { + // As it's not known whether data_len is with or without the +1 for the null byte, a manual check is required. + if (user_data[z] != '=' && user_data[z] != '\0') { + user_data_chars += 1; + } + } + + size_t output_length = data_len / 4 * 3; + unsigned char *decoded_data = calloc(output_length + 1, 1); + if (decoded_data == NULL) { + *err = MEMORY_ALLOCATION; + free(user_data); + return NULL; + } + + uint8_t mask = 0, current_byte = 0; + int bits_left = 8; + for (int i = 0, j = 0; i < user_data_chars; i++) { + int char_index = get_char_index((unsigned char)user_data[i]); + if (bits_left > BITS_PER_B64_BLOCK) { + mask = (uint8_t) char_index << (bits_left - BITS_PER_B64_BLOCK); + current_byte = (uint8_t) (current_byte | mask); + bits_left -= BITS_PER_B64_BLOCK; + } else { + mask = (uint8_t) char_index >> (BITS_PER_B64_BLOCK - bits_left); + current_byte = (uint8_t) (current_byte | mask); + decoded_data[j++] = current_byte; + current_byte = (uint8_t) (char_index << (BITS_PER_BYTE - BITS_PER_B64_BLOCK + bits_left)); + bits_left += BITS_PER_BYTE - BITS_PER_B64_BLOCK; + } + } + decoded_data[output_length] = '\0'; + + free(user_data); + + *err = SUCCESS; + return decoded_data; +} + + +static int +is_valid_b64_input(const char *user_data, size_t data_len) +{ + size_t found = 0, b64_alphabet_len = sizeof(b64_alphabet); + for (int i = 0; i < data_len; i++) { + for(int j = 0; j < b64_alphabet_len; j++) { + if(user_data[i] == b64_alphabet[j] || user_data[i] == '=') { + found++; + break; + } + } + } + if (found != data_len) { + return 0; + } else { + return 1; + } +} + + +static int +get_char_index(unsigned char c) +{ + for (int i = 0; i < sizeof(b64_alphabet); i++) { + if (b64_alphabet[i] == c) { + return i; + } + } + return -1; +} diff --git a/external/libbaseencode/src/baseencode.h b/external/libbaseencode/src/baseencode.h new file mode 100644 index 0000000..8c86ed1 --- /dev/null +++ b/external/libbaseencode/src/baseencode.h @@ -0,0 +1,28 @@ +#pragma once + +typedef enum _baseencode_errno { + SUCCESS = 0, + INVALID_INPUT = 1, + EMPTY_STRING = 2, + INPUT_TOO_BIG = 3, + INVALID_B32_DATA = 4, + INVALID_B64_DATA = 5, + MEMORY_ALLOCATION = 6, +} baseencode_error_t; + + +char *base32_encode (const unsigned char *user_data, + size_t data_len, + baseencode_error_t *err); + +unsigned char *base32_decode (const char *user_data, + size_t data_len, + baseencode_error_t *err); + +char *base64_encode (const unsigned char *input_string, + size_t input_length, + baseencode_error_t *err); + +unsigned char *base64_decode (const char *input_string, + size_t input_length, + baseencode_error_t *err); \ No newline at end of file diff --git a/external/libbaseencode/src/common.h b/external/libbaseencode/src/common.h new file mode 100644 index 0000000..be716cb --- /dev/null +++ b/external/libbaseencode/src/common.h @@ -0,0 +1,50 @@ +#pragma once + +#include "baseencode.h" + +#define BITS_PER_BYTE 8 +#define BITS_PER_B32_BLOCK 5 +#define BITS_PER_B64_BLOCK 6 + +// 64 MB should be more than enough +#define MAX_ENCODE_INPUT_LEN 64*1024*1024 +// if 64 MB of data is encoded than it should be also possible to decode it. That's why a bigger input is allowed for decoding +#define MAX_DECODE_BASE32_INPUT_LEN ((MAX_ENCODE_INPUT_LEN * 8 + 4) / 5) +#define MAX_DECODE_BASE64_INPUT_LEN ((MAX_ENCODE_INPUT_LEN * 8 + 4) / 6) + + +static int +strip_char(char *str, char strip) +{ + int found = 0; + char *p, *q; + for (q = p = str; *p; p++) { + if (*p != strip) { + *q++ = *p; + } else { + found++; + } + } + *q = '\0'; + return found; +} + + +static void +check_input(const unsigned char *user_data, size_t data_len, int max_len, baseencode_error_t *err) +{ + if (user_data == NULL || (data_len == 0 && user_data[0] != '\0')) { + *err = INVALID_INPUT; + return; + } else if (user_data[0] == '\0') { + *err = EMPTY_STRING; + return; + } + + if (data_len > max_len) { + *err = INPUT_TOO_BIG; + return; + } + + *err = SUCCESS; +} \ No newline at end of file diff --git a/external/libcotp/.gitignore b/external/libcotp/.gitignore new file mode 100644 index 0000000..c648f9d --- /dev/null +++ b/external/libcotp/.gitignore @@ -0,0 +1,36 @@ +.idea/ +cmake-build-debug/ +build/ + +# Object files +*.o +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ diff --git a/external/libcotp/CMakeLists.txt b/external/libcotp/CMakeLists.txt new file mode 100644 index 0000000..8a06707 --- /dev/null +++ b/external/libcotp/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.5) +project(cotp) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +include(GNUInstallDirs) + +find_package(PkgConfig REQUIRED) +find_package(Gcrypt 1.6.0 REQUIRED) + +include_directories(${GCRYPT_INCLUDE_DIR}) + +link_directories(${GCRYPT_LIBRARY_DIRS}) + +# set up versioning. +set(BUILD_MAJOR "1") +set(BUILD_MINOR "2") +set(BUILD_VERSION "6") +set(BUILD_VERSION ${BUILD_MAJOR}.${BUILD_MINOR}.${BUILD_VERSION}) + +set(CMAKE_C_STANDARD 11) + +set(COTP_HEADERS src/cotp.h) +set(SOURCE_FILES src/otp.c) + +set(CMAKE_C_FLAGS "-Wall -Wextra -O3 -Wno-format-truncation -fstack-protector-strong -fPIC") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3") + +add_library(cotp SHARED ${SOURCE_FILES}) + +target_link_libraries(cotp ${GCRYPT_LIBRARIES} baseencode) +target_include_directories(cotp PUBLIC src) + +set_target_properties(cotp PROPERTIES VERSION ${BUILD_VERSION} SOVERSION ${BUILD_MAJOR}${BUILD_MINOR}) \ No newline at end of file diff --git a/external/libcotp/LICENSE b/external/libcotp/LICENSE new file mode 100644 index 0000000..1c6a7eb --- /dev/null +++ b/external/libcotp/LICENSE @@ -0,0 +1,202 @@ + 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 2018 Paolo Stivanin + + 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. + diff --git a/external/libcotp/README.md b/external/libcotp/README.md new file mode 100644 index 0000000..24c8ecc --- /dev/null +++ b/external/libcotp/README.md @@ -0,0 +1,61 @@ +# libcotp + + Coverity Scan Build Status + + +C library that generates TOTP and HOTP according to [RFC-6238](https://tools.ietf.org/html/rfc6238) + +## Requirements +- [libbaseencode](https://github.com/paolostivanin/libbaseencode) +- GCC/Clang and CMake to build the library +- libgcrypt + +## Build and Install +``` +$ git clone https://github.com/paolostivanin/libcotp.git +$ cd libcotp +$ mkdir build && cd $_ +$ cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr ../ # add -DBUILD_TESTING=ON if you want to compile also the tests +$ make +# make install +``` + +## How To Use It +``` +char *totp = get_totp (const char *base32_encoded_secret, int digits, int period, int algo, cotp_error_t *err); +free (totp); + +char *steam_totp = get_steam_totp (const char *secret, int period, cotp_error_t *err) + +char *hotp = get_hotp (const char *base32_encoded_secret, long counter, int digits, int algo, cotp_error_t *err); +free (hotp); + +char *get_totp_at (const char *base32_encoded_secret, long target_date, int digits, int algo, cotp_error_t *err) + +int is_valid = totp_verify (const har *base32_encoded_secret, const char *totp, int digits, int period, int algo, cotp_error_t *err); + +int is_valid = hotp_verify (const char *base32_encoded_secret, long counter, digits, char *hotp, int algo, cotp_error_t *err); +``` + +where: +- `secret_key` is the **base32 encoded** secret. Usually, a website gives you the secret already base32 encoded, so you should pay attention to not encode the secret again. +The format of the secret can either be `hxdm vjec jjws` or `HXDMVJECJJWS`. In the first case, the library will normalize the secret to second format before computing the OTP. +- `digits` is between `3` and `10` inclusive +- `period` is between `1` and `120` inclusive +- `counter` is a value decided with the server +- `target_date` is the target date specified as the unix epoch format in seconds +- `algo` is either `SHA1`, `SHA256` or `SHA512` + +## Errors +`get_totp`, `get_hotp` and `get_totp_at` return `NULL` if an error occurs and `err` is set accordingly. The following errors are currently supported: +- `GCRYPT_VERSION_MISMATCH`, set if the installed Gcrypt library is too old +- `INVALID_B32_INPUT`, set if the given input is not valid base32 text +- `INVALID_ALGO`, set if the given algo is not supported by the library +- `INVALID_PERIOD`, set if `period` is `<= 0` or `> 120` seconds +- `INVALID_DIGITS`, set if `digits` is `< 3` or `> 10` + +`totp_verify` and `hotp_verify` can return, in addition to one of the previous code, also the error `INVALID_OTP` if the given OTP doesn't match the computed one. + +In case of success, the value returned by `get_totp`, `get_hotp` and `get_totp_at` **must be freed** once no longer needed. + diff --git a/external/libcotp/SECURITY.md b/external/libcotp/SECURITY.md new file mode 100644 index 0000000..9cb1169 --- /dev/null +++ b/external/libcotp/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy + +## Supported Versions + +The following list describes whether a version is eligible or not for security updates. + +| Version | Supported | EOL | +| ------- | ------------------ |-------------| +| 1.2.x | :heavy_check_mark: | - | +| 1.1.x | :x: | 31-Dec-2021 | +| 1.0.x | :x: | 31-Dec-2021 | + +## Reporting a Vulnerability + +Should you find a vulnerability, please report it privately to me via [e-mail](mailto:paolostivanin@users.noreply.github.com). +The following is the workflow: +- security issue is found, an e-mail is sent to me +- within 24 hours I will reply to your e-mail with some info like, for example, whether it actually is a security issue and how serious it is +- within 7 days I will develop and ship a fix +- once the update is out I will open a [security advisory](https://github.com/paolostivanin/OTPClient/security/advisories) diff --git a/external/libcotp/cmake/FindGcrypt.cmake b/external/libcotp/cmake/FindGcrypt.cmake new file mode 100644 index 0000000..0775704 --- /dev/null +++ b/external/libcotp/cmake/FindGcrypt.cmake @@ -0,0 +1,31 @@ +# Copyright (C) 2011 Felix Geyer +# +# 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 . + +find_path(GCRYPT_INCLUDE_DIR gcrypt.h) + +find_library(GCRYPT_LIBRARIES gcrypt) + +mark_as_advanced(GCRYPT_LIBRARIES GCRYPT_INCLUDE_DIR) + +if(GCRYPT_INCLUDE_DIR AND EXISTS "${GCRYPT_INCLUDE_DIR}/gcrypt.h") + file(STRINGS "${GCRYPT_INCLUDE_DIR}/gcrypt.h" GCRYPT_H REGEX "^#define GCRYPT_VERSION \"[^\"]*\"$") + string(REGEX REPLACE "^.*GCRYPT_VERSION \"([0-9]+).*$" "\\1" GCRYPT_VERSION_MAJOR "${GCRYPT_H}") + string(REGEX REPLACE "^.*GCRYPT_VERSION \"[0-9]+\\.([0-9]+).*$" "\\1" GCRYPT_VERSION_MINOR "${GCRYPT_H}") + string(REGEX REPLACE "^.*GCRYPT_VERSION \"[0-9]+\\.[0-9]+\\.([0-9]+).*$" "\\1" GCRYPT_VERSION_PATCH "${GCRYPT_H}") + set(GCRYPT_VERSION_STRING "${GCRYPT_VERSION_MAJOR}.${GCRYPT_VERSION_MINOR}.${GCRYPT_VERSION_PATCH}") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Gcrypt DEFAULT_MSG GCRYPT_LIBRARIES GCRYPT_INCLUDE_DIR) diff --git a/external/libcotp/src/cotp.h b/external/libcotp/src/cotp.h new file mode 100644 index 0000000..47e6238 --- /dev/null +++ b/external/libcotp/src/cotp.h @@ -0,0 +1,65 @@ +#pragma once +#include + +#define SHA1 GCRY_MD_SHA1 +#define SHA256 GCRY_MD_SHA256 +#define SHA512 GCRY_MD_SHA512 + +typedef enum _cotp_errno { + VALID = 0, + GCRYPT_VERSION_MISMATCH = 1, + INVALID_B32_INPUT = 2, + INVALID_ALGO = 3, + INVALID_OTP = 4, + INVALID_DIGITS = 5, + INVALID_PERIOD = 6 +} cotp_error_t; + +#ifdef __cplusplus +extern "C" { +#endif +char *get_hotp (const char *base32_encoded_secret, + long counter, + int digits, + int sha_algo, + cotp_error_t *err_code); + +char *get_totp (const char *base32_encoded_secret, + int digits, + int period, + int sha_algo, + cotp_error_t *err_code); + +char *get_steam_totp (const char *base32_encoded_secret, + int period, + cotp_error_t *err_code); + + +char *get_totp_at (const char *base32_encoded_secret, + long time, + int digits, + int period, + int sha_algo, + cotp_error_t *err_code); + +char *get_steam_totp_at (const char *base32_encoded_secret, + long timestamp, + int period, + cotp_error_t *err_code); + +int totp_verify (const char *base32_encoded_secret, + const char *user_totp, + int digits, + int period, + int sha_algo); + +int hotp_verify (const char *base32_encoded_secret, + long counter, + int digits, + const char *user_hotp, + int sha_algo); + + +#ifdef __cplusplus +} +#endif diff --git a/external/libcotp/src/otp.c b/external/libcotp/src/otp.c new file mode 100644 index 0000000..22ce63f --- /dev/null +++ b/external/libcotp/src/otp.c @@ -0,0 +1,335 @@ +#include +#include +#include +#include +#include +#include "cotp.h" + +#define SHA1_DIGEST_SIZE 20 +#define SHA256_DIGEST_SIZE 32 +#define SHA512_DIGEST_SIZE 64 + +static long long int DIGITS_POWER[] = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000}; + + +static int +check_gcrypt() +{ + if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) { + if (!gcry_check_version("1.6.0")) { + fprintf(stderr, "libgcrypt v1.6.0 and above is required\n"); + return -1; + } + gcry_control(GCRYCTL_DISABLE_SECMEM, 0); + gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); + } + return 0; +} + + +static char * +normalize_secret (const char *K) +{ + char *nK = calloc (1, strlen (K) + 1); + if (nK == NULL) { + fprintf (stderr, "Error during memory allocation\n"); + return nK; + } + + int i = 0, j = 0; + while (K[i] != '\0') { + if (K[i] != ' ') { + if (K[i] >= 'a' && K[i] <= 'z') { + nK[j++] = (char) (K[i] - 32); + } else { + nK[j++] = K[i]; + } + } + i++; + } + return nK; +} + + +static char * +get_steam_code(unsigned const char *hmac) +{ + int offset = (hmac[SHA1_DIGEST_SIZE-1] & 0x0f); + + // Starting from the offset, take the successive 4 bytes while stripping the topmost bit to prevent it being handled as a signed integer + int bin_code = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | ((hmac[offset + 3] & 0xff)); + + const char steam_alphabet[] = "23456789BCDFGHJKMNPQRTVWXY"; + + char code[6]; + size_t steam_alphabet_len = strlen(steam_alphabet); + for (int i = 0; i < 5; i++) { + int mod = bin_code % steam_alphabet_len; + bin_code = bin_code / steam_alphabet_len; + code[i] = steam_alphabet[mod]; + } + code[5] = '\0'; + + return strdup(code); +} + + +static int +truncate(unsigned const char *hmac, int digits_length, int algo) +{ + // take the lower four bits of the last byte + int offset = 0; + switch (algo) { + case SHA1: + offset = (hmac[SHA1_DIGEST_SIZE-1] & 0x0f); + break; + case SHA256: + offset = (hmac[SHA256_DIGEST_SIZE-1] & 0x0f); + break; + case SHA512: + offset = (hmac[SHA512_DIGEST_SIZE-1] & 0x0f); + break; + default: + break; + } + + // Starting from the offset, take the successive 4 bytes while stripping the topmost bit to prevent it being handled as a signed integer + int bin_code = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | ((hmac[offset + 3] & 0xff)); + + int token = bin_code % DIGITS_POWER[digits_length]; + + return token; +} + + +static unsigned char * +compute_hmac(const char *K, long C, int algo) +{ + baseencode_error_t err; + size_t secret_len = (size_t) ((strlen(K) + 1.6 - 1) / 1.6); + + char *normalized_K = normalize_secret (K); + if (normalized_K == NULL) { + return NULL; + } + unsigned char *secret = base32_decode(normalized_K, strlen(normalized_K), &err); + free (normalized_K); + if (secret == NULL) { + return NULL; + } + + unsigned char C_reverse_byte_order[8]; + int j, i; + for (j = 0, i = 7; j < 8 && i >= 0; j++, i--) + C_reverse_byte_order[i] = ((unsigned char *) &C)[j]; + + gcry_md_hd_t hd; + gcry_md_open(&hd, algo, GCRY_MD_FLAG_HMAC); + gcry_md_setkey(hd, secret, secret_len); + gcry_md_write(hd, C_reverse_byte_order, sizeof(C_reverse_byte_order)); + gcry_md_final (hd); + unsigned char *hmac = gcry_md_read(hd, algo); + + free(secret); + + return hmac; +} + + +static char * +finalize(int digits_length, int tk) +{ + char *token = malloc((size_t)digits_length + 1); + if (token == NULL) { + fprintf (stderr, "Error during memory allocation\n"); + return token; + } else { + int extra_char = digits_length < 10 ? 0 : 1; + char *fmt = calloc(1, 5 + extra_char); + if (fmt == NULL) { + fprintf (stderr, "Error during memory allocation\n"); + free (token); + return fmt; + } + memcpy (fmt, "%.", 3); + snprintf (fmt + 2, 2 + extra_char, "%d", digits_length); + memcpy (fmt + 3 + extra_char, "d", 2); + snprintf (token, digits_length + 1, fmt, tk); + free (fmt); + } + return token; +} + + +static int +check_period(int period) +{ + if (period <= 0 || period > 120) { + return INVALID_PERIOD; + } + return VALID; +} + + +static int +check_otp_len(int digits_length) +{ + if (digits_length < 3 || digits_length > 10) { + return INVALID_DIGITS; + } + return VALID; +} + + +static int +check_algo(int algo) +{ + if (algo != SHA1 && algo != SHA256 && algo != SHA512) { + return INVALID_ALGO; + } else { + return VALID; + } +} + + +char * +get_hotp(const char *secret, long timestamp, int digits, int algo, cotp_error_t *err_code) +{ + if (check_gcrypt() == -1) { + *err_code = GCRYPT_VERSION_MISMATCH; + return NULL; + } + + if (check_algo(algo) == INVALID_ALGO) { + *err_code = INVALID_ALGO; + return NULL; + } + + if (check_otp_len(digits) == INVALID_DIGITS) { + *err_code = INVALID_DIGITS; + return NULL; + } + + unsigned char *hmac = compute_hmac(secret, timestamp, algo); + if (hmac == NULL) { + *err_code = INVALID_B32_INPUT; + return NULL; + } + int tk = truncate(hmac, digits, algo); + char *token = finalize(digits, tk); + return token; +} + + +char * +get_totp(const char *secret, int digits, int period, int algo, cotp_error_t *err_code) +{ + return get_totp_at(secret, (long)time(NULL), digits, period, algo, err_code); +} + + +char * +get_steam_totp (const char *secret, int period, cotp_error_t *err_code) +{ + // AFAIK, the secret is stored base64 encoded on the device. As I don't have time to waste on reverse engineering + // this non-standard solution, the user is responsible for decoding the secret in whatever format this is and then + // providing the library with the secret base32 encoded. + return get_steam_totp_at (secret, (long)time(NULL), period, err_code); +} + + +char * +get_totp_at(const char *secret, long current_timestamp, int digits, int period, int algo, cotp_error_t *err_code) +{ + if (check_gcrypt() == -1) { + *err_code = GCRYPT_VERSION_MISMATCH; + return NULL; + } + + if (check_otp_len(digits) == INVALID_DIGITS) { + *err_code = INVALID_DIGITS; + return NULL; + } + + if (check_period(period) == INVALID_PERIOD) { + *err_code = INVALID_PERIOD; + return NULL; + } + + long timestamp = current_timestamp / period; + + cotp_error_t err; + char *token = get_hotp(secret, timestamp, digits, algo, &err); + if (token == NULL) { + *err_code = err; + return NULL; + } + return token; +} + + +char * +get_steam_totp_at (const char *secret, long current_timestamp, int period, cotp_error_t *err_code) +{ + if (check_gcrypt() == -1) { + *err_code = GCRYPT_VERSION_MISMATCH; + return NULL; + } + + if (check_period(period) == INVALID_PERIOD) { + *err_code = INVALID_PERIOD; + return NULL; + } + + long timestamp = current_timestamp / period; + + unsigned char *hmac = compute_hmac(secret, timestamp, SHA1); + if (hmac == NULL) { + *err_code = INVALID_B32_INPUT; + return NULL; + } + + return get_steam_code(hmac); +} + + +int +totp_verify(const char *secret, const char *user_totp, int digits, int period, int algo) +{ + cotp_error_t err; + char *current_totp = get_totp(secret, digits, period, algo, &err); + if (current_totp == NULL) { + return err; + } + + int token_status; + if (strcmp(current_totp, user_totp) != 0) { + token_status = INVALID_OTP; + } else { + token_status = VALID; + } + free(current_totp); + + return token_status; +} + + +int +hotp_verify(const char *K, long C, int N, const char *user_hotp, int algo) +{ + cotp_error_t err; + char *current_hotp = get_hotp(K, C, N, algo, &err); + if (current_hotp == NULL) { + return err; + } + + int token_status; + if (strcmp(current_hotp, user_hotp) != 0) { + token_status = INVALID_OTP; + } else { + token_status = VALID; + } + free(current_hotp); + + return token_status; +} diff --git a/launcher/desktop/CMakeLists.txt b/launcher/desktop/CMakeLists.txt index ee94b4c..ec09198 100644 --- a/launcher/desktop/CMakeLists.txt +++ b/launcher/desktop/CMakeLists.txt @@ -24,4 +24,6 @@ target_link_libraries(astra_desktop PUBLIC astra_core Qt5::Core Qt5::Widgets - Qt5::Network) \ No newline at end of file + Qt5::Network + cotp + crypto) \ No newline at end of file diff --git a/launcher/desktop/src/autologinwindow.cpp b/launcher/desktop/src/autologinwindow.cpp index 20c87d4..2fc6151 100644 --- a/launcher/desktop/src/autologinwindow.cpp +++ b/launcher/desktop/src/autologinwindow.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "launchercore.h" #include "launcherwindow.h" @@ -34,18 +35,18 @@ AutoLoginWindow::AutoLoginWindow(ProfileSettings& profile, LauncherCore& core, Q mainLayout->addWidget(cancelButton); auto autologinTimer = new QTimer(); - connect(autologinTimer, &QTimer::timeout, [&] { - qDebug() << "logging in!"; - + connect(autologinTimer, &QTimer::timeout, [&, this, autologinTimer] { // TODO: this is the second place where I have implemented this. this is a good idea to abstract, maybe? :-) auto loop = new QEventLoop(); + QString username, password; + QString otpSecret; auto usernameJob = new QKeychain::ReadPasswordJob("LauncherWindow"); usernameJob->setKey(profile.name + "-username"); usernameJob->start(); - core.connect( + QObject::connect( usernameJob, &QKeychain::ReadPasswordJob::finished, [loop, usernameJob, &username](QKeychain::Job* j) { username = usernameJob->textData(); loop->quit(); @@ -57,7 +58,7 @@ AutoLoginWindow::AutoLoginWindow(ProfileSettings& profile, LauncherCore& core, Q passwordJob->setKey(profile.name + "-password"); passwordJob->start(); - core.connect( + QObject::connect( passwordJob, &QKeychain::ReadPasswordJob::finished, [loop, passwordJob, &password](QKeychain::Job* j) { password = passwordJob->textData(); loop->quit(); @@ -65,16 +66,41 @@ AutoLoginWindow::AutoLoginWindow(ProfileSettings& profile, LauncherCore& core, Q loop->exec(); + // TODO: handle cases where the user doesn't want to store their OTP secret, so we have to manually prompt them + if(profile.useOneTimePassword && profile.rememberOTPSecret) { + auto otpJob = new QKeychain::ReadPasswordJob("LauncherWindow"); + otpJob->setKey(profile.name + "-otpsecret"); + otpJob->start(); + + QObject::connect( + otpJob, &QKeychain::ReadPasswordJob::finished, [loop, otpJob, &otpSecret](QKeychain::Job* j) { + otpSecret = otpJob->textData(); + loop->quit(); + }); + + loop->exec(); + } + auto info = new LoginInformation(); info->settings = &profile; info->username = username; info->password = password; + if(profile.useOneTimePassword && profile.rememberOTPSecret) { + // generate otp + char *totp = get_totp (otpSecret.toStdString().c_str(), 6, 30, SHA1, nullptr); + info->oneTimePassword = totp; + free (totp); + } + if (profile.isSapphire) { core.sapphireLauncher->login(profile.lobbyURL, *info); } else { core.squareBoot->bootCheck(*info); } + + close(); + autologinTimer->stop(); }); connect(this, &AutoLoginWindow::loginCanceled, [autologinTimer] { autologinTimer->stop(); diff --git a/launcher/desktop/src/settingswindow.cpp b/launcher/desktop/src/settingswindow.cpp index 258d2f4..d02f589 100644 --- a/launcher/desktop/src/settingswindow.cpp +++ b/launcher/desktop/src/settingswindow.cpp @@ -450,7 +450,7 @@ void SettingsWindow::setupLoginTab(QFormLayout& layout) { connect(otpSecretButton, &QPushButton::pressed, [=] { auto otpSecret = QInputDialog::getText(this, "OTP Input", "Enter your OTP Secret:"); - auto job = new QKeychain::WritePasswordJob("SettingsWindow"); + auto job = new QKeychain::WritePasswordJob("LauncherWindow"); job->setTextData(otpSecret); job->setKey(this->getCurrentProfile().name + "-otpsecret"); job->start();