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
+
+
+
+
+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
+
+
+
+
+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();