From f661466d602b02aa063b648c970628e6567287f2 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Fri, 16 Feb 2024 14:06:10 +0100 Subject: [PATCH 01/20] Add new providers --- CMakeLists.txt | 10 ++++++--- README.md | 2 ++ src/common/authpro.c | 37 ++++++++++++++++++++++++++++++++ src/common/exports.h | 10 +++++++++ src/common/get-providers-data.h | 38 +++++++++++++++++++++------------ src/common/twofas.c | 37 ++++++++++++++++++++++++++++++++ src/imports.c | 4 ++++ src/imports.h | 2 ++ 8 files changed, 123 insertions(+), 17 deletions(-) create mode 100644 src/common/authpro.c create mode 100644 src/common/twofas.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bb62692..3cca46ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(OTPClient VERSION "3.4.1" LANGUAGES "C") +project(OTPClient VERSION "3.5.0" LANGUAGES "C") include(GNUInstallDirs) configure_file("src/common/version.h.in" "version.h") @@ -130,7 +130,9 @@ set(GUI_SOURCE_FILES src/show-qr-cb.c src/setup-signals-shortcuts.c src/change-pwd-cb.c - src/dbinfo-cb.c) + src/dbinfo-cb.c + src/common/twofas.c + src/common/authpro.c) set(CLI_HEADER_FILES src/cli/get-data.h @@ -159,7 +161,9 @@ set(CLI_SOURCE_FILES src/common/aegis.c src/common/freeotp.c src/secret-schema.c - src/google-migration.pb-c.c) + src/google-migration.pb-c.c + src/common/twofas.c + src/common/authpro.c) if(BUILD_GUI AND BUILD_CLI) list(APPEND CLI_SOURCE_FILES diff --git a/README.md b/README.md index 43913865..dc50566d 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ See this [wiki section](https://github.com/paolostivanin/OTPClient/wiki/Secure-M - import and export encrypted/plain [andOTP](https://github.com/flocke/andOTP) backup - import and export encrypted/plain [Aegis](https://github.com/beemdevelopment/Aegis) backup - import and export plain [FreeOTPPlus](https://github.com/helloworld1/FreeOTPPlus) backup (key URI format only) +- import and export encrypted [AuthenticatorPro](https://github.com/jamie-mh/AuthenticatorPro) backup +- import and export encrypted [2FAS](https://github.com/twofas) backup - import of Google's migration QR codes - local database is encrypted using AES256-GCM - key is derived using PBKDF2 with SHA512 and 100k iterations diff --git a/src/common/authpro.c b/src/common/authpro.c new file mode 100644 index 00000000..24ae026c --- /dev/null +++ b/src/common/authpro.c @@ -0,0 +1,37 @@ +#include +#include + +static GSList *get_otps_from_encrypted_authpro_backup (const gchar *path, + const gchar *password, + gint32 max_file_size, + GFile *in_file, + GFileInputStream *in_stream, + GError **err); + + +GSList * +get_authpro_data (const gchar *path, + const gchar *password, + gint32 max_file_size, + GError **err) +{ + GFile *in_file = g_file_new_for_path(path); + GFileInputStream *in_stream = g_file_read(in_file, NULL, err); + if (*err != NULL) { + g_object_unref(in_file); + return NULL; + } + return get_otps_from_encrypted_authpro_backup (path, password, max_file_size, in_file, in_stream, err); +} + + +static GSList * +get_otps_from_encrypted_authpro_backup (const gchar *path, + const gchar *password, + gint32 max_file_size, + GFile *in_file, + GFileInputStream *in_stream, + GError **err) +{ + return NULL; +} \ No newline at end of file diff --git a/src/common/exports.h b/src/common/exports.h index 0f3023ce..4d6aecd6 100644 --- a/src/common/exports.h +++ b/src/common/exports.h @@ -10,6 +10,8 @@ G_BEGIN_DECLS #define FREEOTPPLUS_EXPORT_ACTION_NAME "export_freeotpplus" #define AEGIS_EXPORT_ACTION_NAME "export_aegis" #define AEGIS_EXPORT_PLAIN_ACTION_NAME "export_aegis_plain" +#define AUTHPRO_EXPORT_PLAIN_ACTION_NAME "export_authpro" +#define TWOFAS_EXPORT_PLAIN_ACTION_NAME "export_2fas" void export_data_cb (GSimpleAction *simple, @@ -27,4 +29,12 @@ gchar *export_aegis (const gchar *export_path, json_t *json_db_data, const gchar *password); +gchar *export_authpro (const gchar *export_path, + json_t *json_db_data, + const gchar *password); + +gchar *export_2fas (const gchar *export_path, + json_t *json_db_data, + const gchar *password); + G_END_DECLS diff --git a/src/common/get-providers-data.h b/src/common/get-providers-data.h index df7fce13..e48a138a 100644 --- a/src/common/get-providers-data.h +++ b/src/common/get-providers-data.h @@ -4,19 +4,29 @@ G_BEGIN_DECLS -GSList *get_andotp_data (const gchar *path, - const gchar *password, - gint32 max_file_size, - gboolean encrypted, - GError **err); - -GSList *get_freeotpplus_data (const gchar *path, - GError **err); - -GSList *get_aegis_data (const gchar *path, - const gchar *password, - gint32 max_file_size, - gboolean encrypted, - GError **err); +GSList *get_andotp_data (const gchar *path, + const gchar *password, + gint32 max_file_size, + gboolean encrypted, + GError **err); + +GSList *get_freeotpplus_data (const gchar *path, + GError **err); + +GSList *get_aegis_data (const gchar *path, + const gchar *password, + gint32 max_file_size, + gboolean encrypted, + GError **err); + +GSList *get_authpro_data (const gchar *path, + const gchar *password, + gint32 max_file_size, + GError **err); + +GSList *get_twofas_data (const gchar *path, + const gchar *password, + gint32 max_file_size, + GError **err); G_END_DECLS diff --git a/src/common/twofas.c b/src/common/twofas.c new file mode 100644 index 00000000..046d544f --- /dev/null +++ b/src/common/twofas.c @@ -0,0 +1,37 @@ +#include +#include + +static GSList *get_otps_from_encrypted_2fas_backup (const gchar *path, + const gchar *password, + gint32 max_file_size, + GFile *in_file, + GFileInputStream *in_stream, + GError **err); + + +GSList * +get_twofas_data (const gchar *path, + const gchar *password, + gint32 max_file_size, + GError **err) +{ + GFile *in_file = g_file_new_for_path(path); + GFileInputStream *in_stream = g_file_read(in_file, NULL, err); + if (*err != NULL) { + g_object_unref(in_file); + return NULL; + } + return get_otps_from_encrypted_2fas_backup (path, password, max_file_size, in_file, in_stream, err); +} + + +static GSList * +get_otps_from_encrypted_2fas_backup (const gchar *path, + const gchar *password, + gint32 max_file_size, + GFile *in_file, + GFileInputStream *in_stream, + GError **err) +{ + return NULL; +} \ No newline at end of file diff --git a/src/imports.c b/src/imports.c index 8e59bb31..4a4d6753 100644 --- a/src/imports.c +++ b/src/imports.c @@ -110,6 +110,10 @@ parse_data_and_update_db (AppData *app_data, content = get_freeotpplus_data (filename, &err); } else if (g_strcmp0 (action_name, AEGIS_IMPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, AEGIS_IMPORT_ENC_ACTION_NAME) == 0) { content = get_aegis_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, g_strcmp0 (action_name, AEGIS_IMPORT_ENC_ACTION_NAME) == 0 ? TRUE : FALSE , &err); + } else if (g_strcmp0 (action_name, AUTHPRO_IMPORT_ACTION_NAME) == 0) { + content = get_authpro_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, &err); + } else if (g_strcmp0 (action_name, TWOFAS_IMPORT_ACTION_NAME) == 0) { + content = get_twofas_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, &err); } if (content == NULL) { diff --git a/src/imports.h b/src/imports.h index f5e74e9b..efa5abd9 100644 --- a/src/imports.h +++ b/src/imports.h @@ -10,6 +10,8 @@ G_BEGIN_DECLS #define FREEOTPPLUS_IMPORT_ACTION_NAME "import_freeotpplus" #define AEGIS_IMPORT_ACTION_NAME "import_aegis" #define AEGIS_IMPORT_ENC_ACTION_NAME "import_aegis_enc" +#define AUTHPRO_IMPORT_ACTION_NAME "import_authpro" +#define TWOFAS_IMPORT_ACTION_NAME "import_2fas" #define GOOGLE_MIGRATION_FILE_ACTION_NAME "import_google_qr_file" #define GOOGLE_MIGRATION_WEBCAM_ACTION_NAME "import_google_qr_webcam" From f0236e1d5b3c7c5f341fd28da6198c00cc695bf1 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Tue, 20 Feb 2024 10:07:22 +0100 Subject: [PATCH 02/20] Add Authenticator Pro import support --- CMakeLists.txt | 2 +- src/app.c | 2 + src/common/authpro.c | 138 +++++++++++++++++++++++++++---- src/common/common.c | 190 +++++++++++++++++++++++++++++++++++++++++++ src/common/common.h | 22 +++++ 5 files changed, 338 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3cca46ca..8034f7e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,7 +44,7 @@ endif() find_package(PkgConfig REQUIRED) find_package(Protobuf 3.6.0 REQUIRED) -find_package(Gcrypt 1.8.0 REQUIRED) +find_package(Gcrypt 1.10.1 REQUIRED) pkg_check_modules(COTP REQUIRED cotp>=3.0.0) pkg_check_modules(PNG REQUIRED libpng>=1.6.30) pkg_check_modules(JANSSON REQUIRED jansson>=2.12) diff --git a/src/app.c b/src/app.c index 18970834..ec5c1f55 100644 --- a/src/app.c +++ b/src/app.c @@ -536,6 +536,8 @@ set_action_group (GtkBuilder *builder, { .name = FREEOTPPLUS_IMPORT_ACTION_NAME, .activate = select_file_cb }, { .name = AEGIS_IMPORT_ACTION_NAME, .activate = select_file_cb }, { .name = AEGIS_IMPORT_ENC_ACTION_NAME, .activate = select_file_cb }, + { .name = AUTHPRO_IMPORT_ACTION_NAME, .activate = select_file_cb }, + { .name = TWOFAS_IMPORT_ACTION_NAME, .activate = select_file_cb }, { .name = ANDOTP_EXPORT_ACTION_NAME, .activate = export_data_cb }, { .name = ANDOTP_EXPORT_PLAIN_ACTION_NAME, .activate = export_data_cb }, { .name = FREEOTPPLUS_EXPORT_ACTION_NAME, .activate = export_data_cb }, diff --git a/src/common/authpro.c b/src/common/authpro.c index 24ae026c..abe64cac 100644 --- a/src/common/authpro.c +++ b/src/common/authpro.c @@ -1,12 +1,19 @@ #include #include +#include +#include "common.h" +#include "../gquarks.h" +#include "../imports.h" -static GSList *get_otps_from_encrypted_authpro_backup (const gchar *path, - const gchar *password, - gint32 max_file_size, - GFile *in_file, - GFileInputStream *in_stream, - GError **err); +static GSList *get_otps_from_encrypted_backup (const gchar *path, + const gchar *password, + gint32 max_file_size, + GFile *in_file, + GFileInputStream *in_stream, + GError **err); + +static GSList *parse_authpro_json_data (const gchar *data, + GError **err); GSList * @@ -21,17 +28,118 @@ get_authpro_data (const gchar *path, g_object_unref(in_file); return NULL; } - return get_otps_from_encrypted_authpro_backup (path, password, max_file_size, in_file, in_stream, err); + + return get_otps_from_encrypted_backup (path, password, max_file_size, in_file, in_stream, err); } static GSList * -get_otps_from_encrypted_authpro_backup (const gchar *path, - const gchar *password, - gint32 max_file_size, - GFile *in_file, - GFileInputStream *in_stream, - GError **err) +get_otps_from_encrypted_backup (const gchar *path, + const gchar *password, + gint32 max_file_size, + GFile *in_file, + GFileInputStream *in_stream, + GError **err) { - return NULL; -} \ No newline at end of file + guchar header[16]; + if (g_input_stream_read (G_INPUT_STREAM (in_stream), header, 16, NULL, err) == -1) { + g_object_unref (in_stream); + g_object_unref (in_file); + return NULL; + } + + gchar *decrypted_json = get_data_from_encrypted_backup (path, password, max_file_size, AUTHPRO, 0, in_file, in_stream, err); + if (decrypted_json == NULL) { + return NULL; + } + + GSList *otps = parse_authpro_json_data (decrypted_json, err); + gcry_free (decrypted_json); + + return otps; +} + + +static GSList * +parse_authpro_json_data (const gchar *data, + GError **err) +{ + json_error_t jerr; + json_t *root = json_loads (data, JSON_DISABLE_EOF_CHECK, &jerr); + if (root == NULL) { + g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "%s", jerr.text); + return NULL; + } + + json_t *array = json_object_get (root, "Authenticators"); + if (array == NULL) { + g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "%s", jerr.text); + json_decref (root); + return NULL; + } + + GSList *otps = NULL; + for (guint i = 0; i < json_array_size (array); i++) { + json_t *obj = json_array_get (array, i); + + otp_t *otp = g_new0 (otp_t, 1); + otp->issuer = g_strdup (json_string_value (json_object_get (obj, "Issuer"))); + otp->account_name = g_strdup (json_string_value (json_object_get (obj, "Username"))); + otp->secret = secure_strdup (json_string_value (json_object_get (obj, "Secret"))); + otp->digits = (guint32)json_integer_value (json_object_get(obj, "Digits")); + otp->counter = json_integer_value (json_object_get (obj, "Counter")); + otp->period = (guint32)json_integer_value (json_object_get (obj, "Period")); + + gboolean skip = FALSE; + guint32 algo = (guint32)json_integer_value (json_object_get(obj, "Algorithm")); + switch (algo) { + case 0: + otp->algo = g_strdup ("SHA1"); + break; + case 1: + otp->algo = g_strdup ("SHA256"); + break; + case 2: + otp->algo = g_strdup ("SHA512"); + break; + default: + g_printerr ("Skipping token due to unsupported algo: %d\n", algo); + skip = TRUE; + break; + } + + guint32 type = (guint32)json_integer_value (json_object_get(obj, "Type")); + switch (type) { + case 1: + otp->type = g_strdup ("HOTP"); + break; + case 2: + otp->type = g_strdup ("TOTP"); + break; + case 4: + otp->type = g_strdup ("TOTP"); + g_free (otp->issuer); + otp->issuer = g_strdup ("Steam"); + break; + default: + g_printerr ("Skipping token due to unsupported type: %d (3=Mobile-OTP, 5=Yandex)\n", type); + skip = TRUE; + break; + } + + if (!skip) { + otps = g_slist_append (otps, g_memdupX (otp, sizeof (otp_t))); + } + + gcry_free (otp->secret); + g_free (otp->issuer); + g_free (otp->account_name); + g_free (otp->algo); + g_free (otp->type); + g_free (otp); + } + + json_decref (root); + + return otps; +} diff --git a/src/common/common.c b/src/common/common.c index a969590d..d053a48a 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -6,6 +7,8 @@ #include "jansson.h" #include "common.h" #include "../google-migration.pb-c.h" +#include "../file-size.h" +#include "../gquarks.h" gint32 get_max_file_size_from_memlock (void) @@ -448,3 +451,190 @@ get_kf_ptr (void) g_key_file_free (kf); return NULL; } + + +guchar * +get_authpro_derived_key (const gchar *password, + const guchar *salt, + gint32 salt_size) +{ + guchar *derived_key = gcry_malloc_secure (32); + // taglen, iterations, memory_cost (65536=64MiB), parallelism + const unsigned long params[4] = {32, 3, 65536, 4}; + gcry_kdf_hd_t hd; + if (gcry_kdf_open (&hd, GCRY_KDF_ARGON2, GCRY_KDF_ARGON2ID, + params, 4, + password, (gsize)g_utf8_strlen (password, -1), + salt, salt_size, + NULL, 0, NULL, 0) != GPG_ERR_NO_ERROR) { + g_printerr ("Error while opening the KDF handler\n"); + return NULL; + } + if (gcry_kdf_compute (hd, NULL) != GPG_ERR_NO_ERROR) { + g_printerr ("Error while computing the KDF\n"); + gcry_free (derived_key); + gcry_kdf_close (hd); + return NULL; + } + if (gcry_kdf_final (hd, 32, derived_key) != GPG_ERR_NO_ERROR) { + g_printerr ("Error while finalizing the KDF handler\n"); + gcry_free (derived_key); + gcry_kdf_close (hd); + return NULL; + } + + gcry_kdf_close (hd); + + return derived_key; +} + + +guchar * +get_andotp_derived_key (const gchar *password, + const guchar *salt, + guint32 iterations, + guint32 salt_size) +{ + guchar *derived_key = gcry_malloc_secure (32); + if (gcry_kdf_derive (password, (gsize)g_utf8_strlen (password, -1), GCRY_KDF_PBKDF2, GCRY_MD_SHA1, + salt, salt_size, iterations, 32, derived_key) != 0) { + gcry_free (derived_key); + return NULL; + } + + return derived_key; +} + + +gchar * +get_data_from_encrypted_backup (const gchar *path, + const gchar *password, + gint32 max_file_size, + gint32 provider, + guint32 andotp_be_iterations, + GFile *in_file, + GFileInputStream *in_stream, + GError **err) +{ + gint32 salt_size, iv_size, tag_size; + switch (provider) { + case ANDOTP: + salt_size = iv_size = 12; + tag_size = 16; + break; + case AUTHPRO: + salt_size = tag_size = 16; + iv_size = 12; + break; + } + + guchar salt[salt_size]; + if (g_input_stream_read (G_INPUT_STREAM (in_stream), salt, salt_size, NULL, err) == -1) { + g_object_unref (in_stream); + g_object_unref (in_file); + return NULL; + } + + guchar iv[iv_size]; + if (g_input_stream_read (G_INPUT_STREAM (in_stream), iv, iv_size, NULL, err) == -1) { + g_object_unref (in_stream); + g_object_unref (in_file); + return NULL; + } + + goffset input_file_size = get_file_size (path); + if (!g_seekable_seek (G_SEEKABLE (in_stream), input_file_size - tag_size, G_SEEK_SET, NULL, err)) { + g_object_unref (in_stream); + g_object_unref (in_file); + return NULL; + } + guchar tag[tag_size]; + if (g_input_stream_read (G_INPUT_STREAM (in_stream), tag, tag_size, NULL, err) == -1) { + g_object_unref (in_stream); + g_object_unref (in_file); + return NULL; + } + + gsize enc_buf_size; + gint32 offset; + switch (provider) { + case ANDOTP: + // 4 is the size of iterations (int32) + offset = 4; + enc_buf_size = (gsize)input_file_size - offset - salt_size - iv_size - tag_size; + break; + case AUTHPRO: + // 16 is the size of the header + offset = 16; + enc_buf_size = (gsize)(input_file_size - offset - salt_size - iv_size - tag_size); + break; + } + if (enc_buf_size < 1) { + g_printerr ("A non-encrypted file has been selected\n"); + g_object_unref (in_stream); + g_object_unref (in_file); + return NULL; + } else if (enc_buf_size > max_file_size) { + g_object_unref (in_stream); + g_object_unref (in_file); + g_set_error (err, file_too_big_gquark (), FILE_TOO_BIG, "File is too big"); + return NULL; + } + + guchar *enc_buf = g_malloc0 (enc_buf_size); + if (!g_seekable_seek (G_SEEKABLE(in_stream), offset + salt_size + iv_size, G_SEEK_SET, NULL, err)) { + g_object_unref (in_stream); + g_object_unref (in_file); + g_free (enc_buf); + return NULL; + } + if (g_input_stream_read (G_INPUT_STREAM (in_stream), enc_buf, enc_buf_size, NULL, err) == -1) { + g_object_unref (in_stream); + g_object_unref (in_file); + g_free (enc_buf); + return NULL; + } + g_object_unref (in_stream); + g_object_unref (in_file); + + guchar *derived_key; + switch (provider) { + case ANDOTP: + derived_key = get_andotp_derived_key (password, salt, andotp_be_iterations, salt_size); + break; + case AUTHPRO: + derived_key = get_authpro_derived_key (password, salt, salt_size); + break; + } + + gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, iv, iv_size); + if (hd == NULL) { + gcry_free (derived_key); + g_free (enc_buf); + return NULL; + } + + gchar *decrypted_data = gcry_calloc_secure (enc_buf_size, 1); + gpg_error_t gpg_err = gcry_cipher_decrypt (hd, decrypted_data, enc_buf_size, enc_buf, enc_buf_size); + if (gpg_err) { + g_free (enc_buf); + gcry_free (derived_key); + gcry_free (decrypted_data); + gcry_cipher_close (hd); + return NULL; + } + if (gcry_err_code (gcry_cipher_checktag (hd, tag, tag_size)) == GPG_ERR_CHECKSUM) { + g_set_error (err, bad_tag_gquark (), BAD_TAG_ERRCODE, "Either the file is corrupted or the password is wrong"); + gcry_cipher_close (hd); + g_free (enc_buf); + gcry_free (derived_key); + gcry_free (decrypted_data); + return NULL; + } + + gcry_cipher_close (hd); + gcry_free (derived_key); + g_free (enc_buf); + + return decrypted_data; +} \ No newline at end of file diff --git a/src/common/common.h b/src/common/common.h index 540d332f..828d35e0 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -3,6 +3,7 @@ #include #include #include +#include G_BEGIN_DECLS @@ -15,6 +16,9 @@ G_BEGIN_DECLS #define LOW_MEMLOCK_VALUE 65536 //64KB #define MEMLOCK_VALUE 67108864 //64MB +#define ANDOTP 100 +#define AUTHPRO 101 + gint32 get_max_file_size_from_memlock (void); gchar *init_libs (gint32 max_file_size); @@ -54,4 +58,22 @@ gcry_cipher_hd_t open_cipher_and_set_data (guchar *derived_key, GKeyFile *get_kf_ptr (void); +guchar *get_andotp_derived_key (const gchar *password, + const guchar *salt, + guint32 iterations, + guint32 salt_size); + +guchar *get_authpro_derived_key (const gchar *password, + const guchar *salt, + gint32 salt_size); + +gchar *get_data_from_encrypted_backup (const gchar *path, + const gchar *password, + gint32 max_file_size, + gint32 provider, + guint32 andotp_be_iterations, + GFile *in_file, + GFileInputStream *in_stream, + GError **err); + G_END_DECLS From 3e55cd20785978bf3a4fcfa5e5a2a9ef0c46f243 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Tue, 20 Feb 2024 10:07:44 +0100 Subject: [PATCH 03/20] Improve code on andotp.c and aegis.c --- src/common/aegis.c | 36 ++++++------ src/common/andotp.c | 138 ++++++-------------------------------------- 2 files changed, 36 insertions(+), 138 deletions(-) diff --git a/src/common/aegis.c b/src/common/aegis.c index 3bb6953b..f5dd8827 100644 --- a/src/common/aegis.c +++ b/src/common/aegis.c @@ -22,7 +22,7 @@ static GSList *get_otps_from_encrypted_backup (const gchar *path, gint32 max_file_size, GError **err); -static GSList *parse_json_data (const gchar *data, +static GSList *parse_aegis_json_data (const gchar *data, GError **err); @@ -54,7 +54,7 @@ get_otps_from_plain_backup (const gchar *path, } gchar *dumped_json = json_dumps(json_object_get (json, "db"), 0); - GSList *otps = parse_json_data (dumped_json, err); + GSList *otps = parse_aegis_json_data (dumped_json, err); gcry_free (dumped_json); return otps; @@ -204,7 +204,7 @@ get_otps_from_encrypted_backup (const gchar *path, g_regex_unref (regex); gcry_free (decrypted_db); - GSList *otps = parse_json_data (cleaned_db, err); + GSList *otps = parse_aegis_json_data (cleaned_db, err); gcry_free (cleaned_db); return otps; @@ -422,8 +422,8 @@ export_aegis (const gchar *export_path, static GSList * -parse_json_data (const gchar *data, - GError **err) +parse_aegis_json_data (const gchar *data, + GError **err) { json_error_t jerr; json_t *root = json_loads (data, JSON_DISABLE_EOF_CHECK, &jerr); @@ -451,6 +451,7 @@ parse_json_data (const gchar *data, otp->secret = secure_strdup (json_string_value (json_object_get (info_obj, "secret"))); otp->digits = (guint32) json_integer_value (json_object_get(info_obj, "digits")); + gboolean skip = FALSE; const gchar *type = json_string_value (json_object_get (obj, "type")); if (g_ascii_strcasecmp (type, "TOTP") == 0) { otp->type = g_strdup (type); @@ -468,11 +469,8 @@ parse_json_data (const gchar *data, g_free (otp->issuer); otp->issuer = g_strdup ("Steam"); } else { - g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "otp type is neither TOTP nor HOTP"); - gcry_free (otp->secret); - g_free (otp); - json_decref (obj); - return NULL; + g_printerr ("Skipping token due to unsupported type: %s\n", type); + skip = TRUE; } const gchar *algo = json_string_value (json_object_get (info_obj, "algo")); @@ -481,15 +479,19 @@ parse_json_data (const gchar *data, g_ascii_strcasecmp (algo, "SHA512") == 0) { otp->algo = g_ascii_strup (algo, -1); } else { - g_printerr ("algo not supported (must be either one of: sha1, sha256 or sha512\n"); - gcry_free (otp->secret); - g_free (otp); - json_decref (obj); - json_decref (info_obj); - return NULL; + g_printerr ("Skipping token due to unsupported algo: %s\n", algo); + skip = TRUE; + } + + if (!skip) { + otps = g_slist_append (otps, g_memdupX (otp, sizeof (otp_t))); } - otps = g_slist_append (otps, g_memdupX (otp, sizeof (otp_t))); + gcry_free (otp->secret); + g_free (otp->issuer); + g_free (otp->account_name); + g_free (otp->algo); + g_free (otp->type); g_free (otp); } diff --git a/src/common/andotp.c b/src/common/andotp.c index 2b6711ae..8706a0c0 100644 --- a/src/common/andotp.c +++ b/src/common/andotp.c @@ -9,8 +9,8 @@ #include "../gquarks.h" #include "common.h" -#define ANDOTP_IV_SIZE 12 -#define ANDOTP_SALT_SIZE 12 +// salt and iv are both 12 bytes +#define ANDOTP_SI_SIZE 12 #define ANDOTP_TAG_SIZE 16 #define PBKDF2_MIN_BACKUP_ITERATIONS 140000 #define PBKDF2_MAX_BACKUP_ITERATIONS 160000 @@ -25,11 +25,7 @@ static GSList *get_otps_from_encrypted_backup (const gchar *path, static GSList *get_otps_from_plain_backup (const gchar *path, GError **err); -static guchar *get_derived_key (const gchar *password, - const guchar *salt, - guint32 iterations); - -static GSList *parse_json_data (const gchar *data, +static GSList *parse_andotp_json_data (const gchar *data, GError **err); @@ -75,95 +71,11 @@ get_otps_from_encrypted_backup (const gchar *path, return NULL; } - guchar salt[ANDOTP_SALT_SIZE]; - if (g_input_stream_read (G_INPUT_STREAM (in_stream), salt, ANDOTP_SALT_SIZE, NULL, err) == -1) { - g_object_unref (in_stream); - g_object_unref (in_file); - return NULL; - } - - guchar iv[ANDOTP_IV_SIZE]; - if (g_input_stream_read (G_INPUT_STREAM (in_stream), iv, ANDOTP_IV_SIZE, NULL, err) == -1) { - g_object_unref (in_stream); - g_object_unref (in_file); - return NULL; - } - - goffset input_file_size = get_file_size (path); - guchar tag[ANDOTP_TAG_SIZE]; - if (!g_seekable_seek (G_SEEKABLE (in_stream), input_file_size - ANDOTP_TAG_SIZE, G_SEEK_SET, NULL, err)) { - g_object_unref (in_stream); - g_object_unref (in_file); - return NULL; - } - if (g_input_stream_read (G_INPUT_STREAM (in_stream), tag, ANDOTP_TAG_SIZE, NULL, err) == -1) { - g_object_unref (in_stream); - g_object_unref (in_file); - return NULL; - } - - // 4 is the size of iterations (int32) - gsize enc_buf_size = (gsize) input_file_size - 4 - ANDOTP_SALT_SIZE - ANDOTP_IV_SIZE - ANDOTP_TAG_SIZE; - if (enc_buf_size < 1) { - g_printerr ("A non-encrypted file has been selected\n"); - g_object_unref (in_stream); - g_object_unref (in_file); - return NULL; - } else if (enc_buf_size > max_file_size) { - g_object_unref (in_stream); - g_object_unref (in_file); - g_set_error (err, file_too_big_gquark (), FILE_TOO_BIG, "File is too big"); - return NULL; - } - - guchar *enc_buf = g_malloc0 (enc_buf_size); - if (!g_seekable_seek (G_SEEKABLE (in_stream), 4 + ANDOTP_SALT_SIZE + ANDOTP_IV_SIZE, G_SEEK_SET, NULL, err)) { - g_object_unref (in_stream); - g_object_unref (in_file); - g_free (enc_buf); + gchar *decrypted_json = get_data_from_encrypted_backup (path, password, max_file_size, ANDOTP, be_iterations, in_file, in_stream, err); + if (decrypted_json == NULL) { return NULL; } - if (g_input_stream_read (G_INPUT_STREAM (in_stream), enc_buf, enc_buf_size, NULL, err) == -1) { - g_object_unref (in_stream); - g_object_unref (in_file); - g_free (enc_buf); - return NULL; - } - g_object_unref (in_stream); - g_object_unref (in_file); - - guchar *derived_key = get_derived_key (password, salt, be_iterations); - - gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, iv, ANDOTP_IV_SIZE); - if (hd == NULL) { - gcry_free (derived_key); - g_free (enc_buf); - return NULL; - } - - gchar *decrypted_json = gcry_calloc_secure (enc_buf_size, 1); - gpg_error_t gpg_err = gcry_cipher_decrypt (hd, decrypted_json, enc_buf_size, enc_buf, enc_buf_size); - if (gpg_err) { - g_free (enc_buf); - gcry_free (derived_key); - gcry_free (decrypted_json); - gcry_cipher_close (hd); - return NULL; - } - if (gcry_err_code (gcry_cipher_checktag (hd, tag, ANDOTP_TAG_SIZE)) == GPG_ERR_CHECKSUM) { - g_set_error (err, bad_tag_gquark (), BAD_TAG_ERRCODE, "Either the file is corrupted or the password is wrong"); - gcry_cipher_close (hd); - g_free (enc_buf); - gcry_free (derived_key); - gcry_free (decrypted_json); - return NULL; - } - - gcry_cipher_close (hd); - gcry_free (derived_key); - g_free (enc_buf); - - GSList *otps = parse_json_data (decrypted_json, err); + GSList *otps = parse_andotp_json_data (decrypted_json, err); gcry_free (decrypted_json); return otps; @@ -180,7 +92,7 @@ get_otps_from_plain_backup (const gchar *path, return NULL; } - GSList *otps = parse_json_data (plain_json_data, err); + GSList *otps = parse_andotp_json_data (plain_json_data, err); g_free (plain_json_data); return otps; @@ -262,14 +174,14 @@ export_andotp (const gchar *export_path, guint32 le_iterations = (g_random_int () % (PBKDF2_MAX_BACKUP_ITERATIONS - PBKDF2_MIN_BACKUP_ITERATIONS + 1)) + PBKDF2_MIN_BACKUP_ITERATIONS; gint32 be_iterations = (gint32)__builtin_bswap32 (le_iterations); - guchar *iv = g_malloc0 (ANDOTP_IV_SIZE); - gcry_create_nonce (iv, ANDOTP_IV_SIZE); + guchar *iv = g_malloc0 (ANDOTP_SI_SIZE); + gcry_create_nonce (iv, ANDOTP_SI_SIZE); - guchar *salt = g_malloc0 (ANDOTP_SALT_SIZE); - gcry_create_nonce (salt, ANDOTP_SALT_SIZE); + guchar *salt = g_malloc0 (ANDOTP_SI_SIZE); + gcry_create_nonce (salt, ANDOTP_SI_SIZE); - guchar *derived_key = get_derived_key (password, salt, le_iterations); - gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, iv, ANDOTP_IV_SIZE); + guchar *derived_key = get_andotp_derived_key (password, salt, le_iterations, ANDOTP_SI_SIZE); + gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, iv, ANDOTP_SI_SIZE); if (hd == NULL) { gcry_free (derived_key); g_free (iv); @@ -300,10 +212,10 @@ export_andotp (const gchar *export_path, if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), &be_iterations, 4, NULL, &err) == -1) { goto cleanup_before_exiting; } - if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), salt, ANDOTP_SALT_SIZE, NULL, &err) == -1) { + if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), salt, ANDOTP_SI_SIZE, NULL, &err) == -1) { goto cleanup_before_exiting; } - if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), iv, ANDOTP_IV_SIZE, NULL, &err) == -1) { + if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), iv, ANDOTP_SI_SIZE, NULL, &err) == -1) { goto cleanup_before_exiting; } if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), enc_buf, json_data_size, NULL, &err) == -1) { @@ -328,25 +240,9 @@ export_andotp (const gchar *export_path, } -static guchar * -get_derived_key (const gchar *password, - const guchar *salt, - guint32 iterations) -{ - guchar *derived_key = gcry_malloc_secure (32); - if (gcry_kdf_derive (password, (gsize) g_utf8_strlen (password, -1), GCRY_KDF_PBKDF2, GCRY_MD_SHA1, - salt, ANDOTP_SALT_SIZE, iterations, 32, derived_key) != 0) { - gcry_free (derived_key); - return NULL; - } - - return derived_key; -} - - static GSList * -parse_json_data (const gchar *data, - GError **err) +parse_andotp_json_data (const gchar *data, + GError **err) { json_error_t jerr; json_t *array = json_loads (data, JSON_DISABLE_EOF_CHECK, &jerr); From ce080b33cb3ee3ee63a5b5dda360b0cd03c3ca30 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Tue, 20 Feb 2024 10:11:12 +0100 Subject: [PATCH 04/20] Remove support for older glib, update docs --- CMakeLists.txt | 4 ++-- README.md | 4 ++-- src/cli/get-data.c | 8 -------- src/common/aegis.c | 2 +- src/common/andotp.c | 2 +- src/common/authpro.c | 2 +- src/common/common.c | 45 +------------------------------------------- src/common/common.h | 11 ----------- src/db-misc.c | 2 +- src/imports.c | 2 +- src/parse-data.c | 2 +- src/parse-uri.c | 2 +- src/treeview.c | 2 +- 13 files changed, 13 insertions(+), 75 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8034f7e7..7bdcf854 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,8 +50,8 @@ pkg_check_modules(PNG REQUIRED libpng>=1.6.30) pkg_check_modules(JANSSON REQUIRED jansson>=2.12) pkg_check_modules(ZBAR REQUIRED zbar>=0.20) pkg_check_modules(GTK3 REQUIRED gtk+-3.0>=3.24.0) -pkg_check_modules(GLIB2 REQUIRED glib-2.0>=2.64.0) -pkg_check_modules(GIO REQUIRED gio-2.0>=2.64.0) +pkg_check_modules(GLIB2 REQUIRED glib-2.0>=2.68.0) +pkg_check_modules(GIO REQUIRED gio-2.0>=2.68.0) pkg_check_modules(UUID REQUIRED uuid>=2.34.0) pkg_check_modules(PROTOC REQUIRED libprotobuf-c>=1.3.0) pkg_check_modules(LIBSECRET REQUIRED libsecret-1>=0.20.0) diff --git a/README.md b/README.md index dc50566d..b26e32c5 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ Highly secure and easy to use GTK+ software for two-factor authentication that s | Name | Min Version | |----------------------------------------------------|-------------| | GTK+ | 3.24 | -| Glib | 2.64.0 | +| Glib | 2.68.0 | | jansson | 2.12 | -| libgcrypt | 1.8.0 | +| libgcrypt | 1.10.1 | | libpng | 1.6.30 | | [libcotp](https://github.com/paolostivanin/libcotp) | 3.0.0 | | zbar | 0.20 | diff --git a/src/cli/get-data.c b/src/cli/get-data.c index a401d3ab..5d4bdc9c 100644 --- a/src/cli/get-data.c +++ b/src/cli/get-data.c @@ -59,21 +59,13 @@ show_token (DatabaseData *db_data, // Translators: please do not translate 'account' GString *msg = g_string_new (_("Given account: %s")); -#if GLIB_CHECK_VERSION(2, 68, 0) g_string_replace (msg, "%s", account != NULL ? account : "", 0); -#else - g_string_replace_backported (msg, "%s", account != NULL ? account : "", 0); -#endif g_printerr ("%s\n", msg->str); g_string_free (msg, TRUE); // Translators: please do not translate 'issuer' msg = g_string_new (_("Given issuer: %s")); -#if GLIB_CHECK_VERSION(2, 68, 0) g_string_replace (msg, "%s", issuer != NULL ? issuer : "", 0); -#else - g_string_replace_backported (msg, "%s", issuer != NULL ? issuer : "", 0); -#endif g_printerr ("%s\n", msg->str); g_string_free (msg, TRUE); diff --git a/src/common/aegis.c b/src/common/aegis.c index f5dd8827..2f54a248 100644 --- a/src/common/aegis.c +++ b/src/common/aegis.c @@ -484,7 +484,7 @@ parse_aegis_json_data (const gchar *data, } if (!skip) { - otps = g_slist_append (otps, g_memdupX (otp, sizeof (otp_t))); + otps = g_slist_append (otps, g_memdup2 (otp, sizeof (otp_t))); } gcry_free (otp->secret); diff --git a/src/common/andotp.c b/src/common/andotp.c index 8706a0c0..243f2f8e 100644 --- a/src/common/andotp.c +++ b/src/common/andotp.c @@ -316,7 +316,7 @@ parse_andotp_json_data (const gchar *data, return NULL; } - otps = g_slist_append (otps, g_memdupX (otp, sizeof (otp_t))); + otps = g_slist_append (otps, g_memdup2 (otp, sizeof (otp_t))); g_free (otp); } diff --git a/src/common/authpro.c b/src/common/authpro.c index abe64cac..594fba5c 100644 --- a/src/common/authpro.c +++ b/src/common/authpro.c @@ -128,7 +128,7 @@ parse_authpro_json_data (const gchar *data, } if (!skip) { - otps = g_slist_append (otps, g_memdupX (otp, sizeof (otp_t))); + otps = g_slist_append (otps, g_memdup2 (otp, sizeof (otp_t))); } gcry_free (otp->secret); diff --git a/src/common/common.c b/src/common/common.c index d053a48a..76466f2a 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -176,50 +176,7 @@ bytes_to_hexstr (const guchar *data, size_t datalen) } -// Backported from Glib 2.68 in order to support Debian "bullseye" and Ubuntu 20.04 -guint -g_string_replace_backported (GString *string, - const gchar *find, - const gchar *replace, - guint limit) -{ - gsize f_len, r_len, pos; - gchar *cur, *next; - guint n = 0; - - g_return_val_if_fail (string != NULL, 0); - g_return_val_if_fail (find != NULL, 0); - g_return_val_if_fail (replace != NULL, 0); - - f_len = g_utf8_strlen (find, -1); - r_len = g_utf8_strlen (replace, -1); - cur = string->str; - - while ((next = strstr (cur, find)) != NULL) - { - pos = next - string->str; - g_string_erase (string, (gssize)pos, (gssize)f_len); - g_string_insert (string, (gssize)pos, replace); - cur = string->str + pos + r_len; - n++; - /* Only match the empty string once at any given position, to - * avoid infinite loops */ - if (f_len == 0) - { - if (cur[0] == '\0') - break; - else - cur++; - } - if (n == limit) - break; - } - - return n; -} - - -// Backported from Glib. The only difference is that it's using gcrypt to allocate a secure buffer. +// Backported from Glib (needed by below function) static int unescape_character (const char *scanner) { diff --git a/src/common/common.h b/src/common/common.h index 828d35e0..e0c45d14 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -7,12 +7,6 @@ G_BEGIN_DECLS -#if GLIB_CHECK_VERSION(2, 68, 0) - #define g_memdupX g_memdup2 -#else - #define g_memdupX g_memdup -#endif - #define LOW_MEMLOCK_VALUE 65536 //64KB #define MEMLOCK_VALUE 67108864 //64MB @@ -41,11 +35,6 @@ gchar *bytes_to_hexstr (const guchar *data, GSList *decode_migration_data (const gchar *encoded_uri); -guint g_string_replace_backported (GString *string, - const gchar *find, - const gchar *replace, - guint limit); - gchar *g_uri_unescape_string_secure (const gchar *escaped_string, const gchar *illegal_characters); diff --git a/src/db-misc.c b/src/db-misc.c index 291103db..cbe4d04e 100644 --- a/src/db-misc.c +++ b/src/db-misc.c @@ -70,7 +70,7 @@ load_db (DatabaseData *db_data, json_t *obj; json_array_foreach (db_data->json_data, index, obj) { guint32 hash = json_object_get_hash (obj); - db_data->objects_hash = g_slist_append (db_data->objects_hash, g_memdupX (&hash, sizeof (guint32))); + db_data->objects_hash = g_slist_append (db_data->objects_hash, g_memdup2 (&hash, sizeof (guint32))); } } diff --git a/src/imports.c b/src/imports.c index 4a4d6753..9575f5e3 100644 --- a/src/imports.c +++ b/src/imports.c @@ -53,7 +53,7 @@ update_db_from_otps (GSList *otps, AppData *app_data) obj = build_json_obj (otp->type, otp->account_name, otp->issuer, otp->secret, otp->digits, otp->algo, otp->period, otp->counter); guint hash = json_object_get_hash (obj); if (g_slist_find_custom (app_data->db_data->objects_hash, GUINT_TO_POINTER(hash), check_duplicate) == NULL) { - app_data->db_data->objects_hash = g_slist_append (app_data->db_data->objects_hash, g_memdupX (&hash, sizeof (guint))); + app_data->db_data->objects_hash = g_slist_append (app_data->db_data->objects_hash, g_memdup2 (&hash, sizeof (guint))); app_data->db_data->data_to_add = g_slist_append (app_data->db_data->data_to_add, obj); } else { g_print ("[INFO] Duplicate element not added\n"); diff --git a/src/parse-data.c b/src/parse-data.c index f95ef132..a23decf1 100644 --- a/src/parse-data.c +++ b/src/parse-data.c @@ -52,7 +52,7 @@ parse_user_data (Widgets *widgets, obj = get_json_obj (widgets, acc_label, acc_iss, acc_key_trimmed, digits, period, counter); guint32 hash = json_object_get_hash (obj); if (g_slist_find_custom (db_data->objects_hash, GUINT_TO_POINTER(hash), check_duplicate) == NULL) { - db_data->objects_hash = g_slist_append (db_data->objects_hash, g_memdupX(&hash, sizeof (guint))); + db_data->objects_hash = g_slist_append (db_data->objects_hash, g_memdup2(&hash, sizeof (guint))); db_data->data_to_add = g_slist_append (db_data->data_to_add, obj); } else { g_print ("[INFO] Duplicate element not added\n"); diff --git a/src/parse-uri.c b/src/parse-uri.c index 32201cc1..6ec597cf 100644 --- a/src/parse-uri.c +++ b/src/parse-uri.c @@ -142,7 +142,7 @@ parse_uri (const gchar *uri, } parse_parameters (uri_copy, otp); - *otps = g_slist_append (*otps, g_memdupX (otp, sizeof (otp_t))); + *otps = g_slist_append (*otps, g_memdup2 (otp, sizeof (otp_t))); g_free (otp); } diff --git a/src/treeview.c b/src/treeview.c index 40955253..67f4b34e 100644 --- a/src/treeview.c +++ b/src/treeview.c @@ -204,7 +204,7 @@ reorder_db (AppData *app_data) json_t *obj = json_array_get (app_data->db_data->json_data, current_db_pos); node_info->newpos = gtk_tree_path_get_indices (path)[0]; node_info->hash = json_object_get_hash (obj); - nodes_order_slist = g_slist_append (nodes_order_slist, g_memdupX (node_info, sizeof (NodeInfo))); + nodes_order_slist = g_slist_append (nodes_order_slist, g_memdup2 (node_info, sizeof (NodeInfo))); slist_len++; g_free (node_info); } From c7d95dd1511f2632e720e37be30d1b2f899c2129 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Wed, 21 Feb 2024 09:15:14 +0100 Subject: [PATCH 05/20] Add import/export menus and actions --- src/app.c | 10 +++- src/common/exports.h | 8 +-- src/exports.c | 10 +++- src/imports.c | 7 +-- src/imports.h | 6 ++- src/ui/otpclient.ui | 119 +++++++++++++++++++++++++++++++++++++++---- 6 files changed, 138 insertions(+), 22 deletions(-) diff --git a/src/app.c b/src/app.c index ec5c1f55..475b0038 100644 --- a/src/app.c +++ b/src/app.c @@ -536,13 +536,19 @@ set_action_group (GtkBuilder *builder, { .name = FREEOTPPLUS_IMPORT_ACTION_NAME, .activate = select_file_cb }, { .name = AEGIS_IMPORT_ACTION_NAME, .activate = select_file_cb }, { .name = AEGIS_IMPORT_ENC_ACTION_NAME, .activate = select_file_cb }, - { .name = AUTHPRO_IMPORT_ACTION_NAME, .activate = select_file_cb }, - { .name = TWOFAS_IMPORT_ACTION_NAME, .activate = select_file_cb }, + { .name = AUTHPRO_IMPORT_ENC_ACTION_NAME, .activate = select_file_cb }, + { .name = AUTHPRO_IMPORT_PLAIN_ACTION_NAME, .activate = select_file_cb }, + { .name = TWOFAS_IMPORT_ENC_ACTION_NAME, .activate = select_file_cb }, + { .name = TWOFAS_IMPORT_PLAIN_ACTION_NAME, .activate = select_file_cb }, { .name = ANDOTP_EXPORT_ACTION_NAME, .activate = export_data_cb }, { .name = ANDOTP_EXPORT_PLAIN_ACTION_NAME, .activate = export_data_cb }, { .name = FREEOTPPLUS_EXPORT_ACTION_NAME, .activate = export_data_cb }, { .name = AEGIS_EXPORT_ACTION_NAME, .activate = export_data_cb }, { .name = AEGIS_EXPORT_PLAIN_ACTION_NAME, .activate = export_data_cb }, + { .name = AUTHPRO_EXPORT_ENC_ACTION_NAME, .activate = export_data_cb }, + { .name = AUTHPRO_EXPORT_PLAIN_ACTION_NAME, .activate = export_data_cb }, + { .name = TWOFAS_EXPORT_ENC_ACTION_NAME, .activate = export_data_cb }, + { .name = TWOFAS_EXPORT_PLAIN_ACTION_NAME, .activate = export_data_cb }, { .name = GOOGLE_MIGRATION_FILE_ACTION_NAME, .activate = add_qr_from_file }, { .name = GOOGLE_MIGRATION_WEBCAM_ACTION_NAME, .activate = webcam_add_cb }, { .name = "create_newdb", .activate = new_db_cb }, diff --git a/src/common/exports.h b/src/common/exports.h index 4d6aecd6..437a51e0 100644 --- a/src/common/exports.h +++ b/src/common/exports.h @@ -10,8 +10,10 @@ G_BEGIN_DECLS #define FREEOTPPLUS_EXPORT_ACTION_NAME "export_freeotpplus" #define AEGIS_EXPORT_ACTION_NAME "export_aegis" #define AEGIS_EXPORT_PLAIN_ACTION_NAME "export_aegis_plain" -#define AUTHPRO_EXPORT_PLAIN_ACTION_NAME "export_authpro" -#define TWOFAS_EXPORT_PLAIN_ACTION_NAME "export_2fas" +#define AUTHPRO_EXPORT_ENC_ACTION_NAME "export_authpro_enc" +#define AUTHPRO_EXPORT_PLAIN_ACTION_NAME "export_authpro_plain" +#define TWOFAS_EXPORT_ENC_ACTION_NAME "export_twofas_enc" +#define TWOFAS_EXPORT_PLAIN_ACTION_NAME "export_twofas_plain" void export_data_cb (GSimpleAction *simple, @@ -33,7 +35,7 @@ gchar *export_authpro (const gchar *export_path, json_t *json_db_data, const gchar *password); -gchar *export_2fas (const gchar *export_path, +gchar *export_twofas (const gchar *export_path, json_t *json_db_data, const gchar *password); diff --git a/src/exports.c b/src/exports.c index 09f83a4d..6f73e58c 100644 --- a/src/exports.c +++ b/src/exports.c @@ -27,7 +27,8 @@ export_data_cb (GSimpleAction *simple, #endif gboolean encrypted; - if ((g_strcmp0 (action_name, "export_andotp") == 0) || (g_strcmp0 (action_name, "export_aegis") == 0)) { + if (g_strcmp0 (action_name, "export_andotp") == 0 || g_strcmp0 (action_name, "export_aegis") == 0 || + g_strcmp0 (action_name, "export_authpro_enc") == 0 || g_strcmp0 (action_name, "export_twofas_enc") == 0) { encrypted = TRUE; } else { encrypted = FALSE; @@ -49,6 +50,10 @@ export_data_cb (GSimpleAction *simple, filename = "freeotpplus-exports.txt"; } else if (g_strcmp0 (action_name, AEGIS_EXPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, AEGIS_EXPORT_PLAIN_ACTION_NAME) == 0) { filename = (encrypted == TRUE) ? "aegis_encrypted.json" : "aegis_export_plain.json"; + } else if (g_strcmp0 (action_name, AUTHPRO_EXPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, AUTHPRO_EXPORT_PLAIN_ACTION_NAME) == 0) { + filename = (encrypted == TRUE) ? "authpro_encrypted.bin" : "authpro_plain.json"; + } else if (g_strcmp0 (action_name, TWOFAS_EXPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, TWOFAS_EXPORT_PLAIN_ACTION_NAME) == 0) { + filename = (encrypted == TRUE) ? "twofas_encrypted_v4.2fas" : "twofas_plain_v4.2fas"; } else { show_message_dialog (app_data->main_window, "Invalid export action.", GTK_MESSAGE_ERROR); return; @@ -69,7 +74,8 @@ export_data_cb (GSimpleAction *simple, } gchar *password = NULL, *ret_msg = NULL; - if (g_strcmp0 (action_name, ANDOTP_EXPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, ANDOTP_EXPORT_PLAIN_ACTION_NAME) == 0) { + if (g_strcmp0 (action_name, ANDOTP_EXPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, ANDOTP_EXPORT_PLAIN_ACTION_NAME) == 0 || + g_strcmp0 (action_name, AUTHPRO_EXPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, TWOFAS_EXPORT_ENC_ACTION_NAME) == 0) { if (encrypted == TRUE) { password = prompt_for_password (app_data, NULL, NULL, TRUE); if (password == NULL) { diff --git a/src/imports.c b/src/imports.c index 9575f5e3..49889569 100644 --- a/src/imports.c +++ b/src/imports.c @@ -97,7 +97,8 @@ parse_data_and_update_db (AppData *app_data, GSList *content = NULL; gchar *pwd = NULL; - if (g_strcmp0 (action_name, ANDOTP_IMPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, AEGIS_IMPORT_ENC_ACTION_NAME) == 0) { + if (g_strcmp0 (action_name, ANDOTP_IMPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, AEGIS_IMPORT_ENC_ACTION_NAME) == 0 || + g_strcmp0 (action_name, AUTHPRO_IMPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, TWOFAS_IMPORT_ENC_ACTION_NAME) == 0) { pwd = prompt_for_password (app_data, NULL, action_name, FALSE); if (pwd == NULL) { return FALSE; @@ -110,9 +111,9 @@ parse_data_and_update_db (AppData *app_data, content = get_freeotpplus_data (filename, &err); } else if (g_strcmp0 (action_name, AEGIS_IMPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, AEGIS_IMPORT_ENC_ACTION_NAME) == 0) { content = get_aegis_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, g_strcmp0 (action_name, AEGIS_IMPORT_ENC_ACTION_NAME) == 0 ? TRUE : FALSE , &err); - } else if (g_strcmp0 (action_name, AUTHPRO_IMPORT_ACTION_NAME) == 0) { + } else if (g_strcmp0 (action_name, AUTHPRO_IMPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, AUTHPRO_IMPORT_PLAIN_ACTION_NAME) == 0) { content = get_authpro_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, &err); - } else if (g_strcmp0 (action_name, TWOFAS_IMPORT_ACTION_NAME) == 0) { + } else if (g_strcmp0 (action_name, TWOFAS_IMPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, TWOFAS_IMPORT_PLAIN_ACTION_NAME) == 0) { content = get_twofas_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, &err); } diff --git a/src/imports.h b/src/imports.h index efa5abd9..f1e666ca 100644 --- a/src/imports.h +++ b/src/imports.h @@ -10,8 +10,10 @@ G_BEGIN_DECLS #define FREEOTPPLUS_IMPORT_ACTION_NAME "import_freeotpplus" #define AEGIS_IMPORT_ACTION_NAME "import_aegis" #define AEGIS_IMPORT_ENC_ACTION_NAME "import_aegis_enc" -#define AUTHPRO_IMPORT_ACTION_NAME "import_authpro" -#define TWOFAS_IMPORT_ACTION_NAME "import_2fas" +#define AUTHPRO_IMPORT_ENC_ACTION_NAME "import_authpro_enc" +#define AUTHPRO_IMPORT_PLAIN_ACTION_NAME "import_authpro_plain" +#define TWOFAS_IMPORT_ENC_ACTION_NAME "import_twofas_enc" +#define TWOFAS_IMPORT_PLAIN_ACTION_NAME "import_twofas_plain" #define GOOGLE_MIGRATION_FILE_ACTION_NAME "import_google_qr_file" #define GOOGLE_MIGRATION_WEBCAM_ACTION_NAME "import_google_qr_webcam" diff --git a/src/ui/otpclient.ui b/src/ui/otpclient.ui index 246812a0..4d2eb7ff 100644 --- a/src/ui/otpclient.ui +++ b/src/ui/otpclient.ui @@ -2170,12 +2170,12 @@ but not the number of digits and/or the period/counter. - + True True True - settings_menu.import_authplus - Authenticator Plus + settings_menu.import_freeotpplus + FreeOTP+ (key URI) False @@ -2184,12 +2184,12 @@ but not the number of digits and/or the period/counter. - + True True True - settings_menu.import_freeotpplus - FreeOTP+ (key URI) + settings_menu.import_aegis_enc + Aegis (encrypted json) False @@ -2212,12 +2212,12 @@ but not the number of digits and/or the period/counter. - + True True True - settings_menu.import_aegis_enc - Aegis (encrypted json) + settings_menu.import_authpro_enc + Authenticator Pro (encrypted) False @@ -2225,6 +2225,49 @@ but not the number of digits and/or the period/counter. 5 + + + True + True + True + settings_menu.import_authpro_plain + Authenticator Pro (plain json) + + + False + True + 6 + + + + + True + True + True + settings_menu.import_twofas_enc + 2FAS (encrypted json) + + + False + True + 7 + + + + + True + True + True + settings_menu.import_twofas_plain + 2FAS (plain json) + + + False + True + 8 + + + True @@ -2236,7 +2279,7 @@ but not the number of digits and/or the period/counter. False True - 6 + 9 @@ -2320,6 +2363,62 @@ but not the number of digits and/or the period/counter. 4 + + + True + True + True + settings_menu.export_authpro_enc + Authenticator Pro (encrypted) + + + False + True + 5 + + + + + True + True + True + settings_menu.export_authpro_plain + Authenticator Pro (plain json) + + + False + True + 6 + + + + + True + True + True + settings_menu.export_twofas_enc + 2FAS (encrypted json) + + + False + True + 7 + + + + + True + True + True + settings_menu.export_twofas_plain + 2FAS (plain json) + + + False + True + 8 + + export_menu From 0432f66f4fb222e67892a7f0be69918bfe534c33 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Wed, 21 Feb 2024 09:33:00 +0100 Subject: [PATCH 06/20] Code cleanup for aegis and andotp --- src/common/aegis.c | 3 +-- src/common/andotp.c | 3 +-- src/common/get-providers-data.h | 2 -- src/imports.c | 4 ++-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/common/aegis.c b/src/common/aegis.c index 2f54a248..949a5167 100644 --- a/src/common/aegis.c +++ b/src/common/aegis.c @@ -30,7 +30,6 @@ GSList * get_aegis_data (const gchar *path, const gchar *password, gint32 max_file_size, - gboolean encrypted, GError **err) { if (g_file_test (path, G_FILE_TEST_IS_SYMLINK | G_FILE_TEST_IS_DIR) ) { @@ -38,7 +37,7 @@ get_aegis_data (const gchar *path, return NULL; } - return (encrypted == TRUE) ? get_otps_from_encrypted_backup(path, password, max_file_size, err) : get_otps_from_plain_backup(path, err); + return (password != NULL) ? get_otps_from_encrypted_backup (path, password, max_file_size, err) : get_otps_from_plain_backup (path, err); } diff --git a/src/common/andotp.c b/src/common/andotp.c index 243f2f8e..41956ac8 100644 --- a/src/common/andotp.c +++ b/src/common/andotp.c @@ -33,7 +33,6 @@ GSList * get_andotp_data (const gchar *path, const gchar *password, gint32 max_file_size, - gboolean encrypted, GError **err) { GFile *in_file = g_file_new_for_path(path); @@ -43,7 +42,7 @@ get_andotp_data (const gchar *path, return NULL; } - return (encrypted == TRUE) ? get_otps_from_encrypted_backup (path, password, max_file_size, in_file, in_stream, err) : get_otps_from_plain_backup (path, err); + return (password != NULL) ? get_otps_from_encrypted_backup (path, password, max_file_size, in_file, in_stream, err) : get_otps_from_plain_backup (path, err); } diff --git a/src/common/get-providers-data.h b/src/common/get-providers-data.h index e48a138a..a41b728f 100644 --- a/src/common/get-providers-data.h +++ b/src/common/get-providers-data.h @@ -7,7 +7,6 @@ G_BEGIN_DECLS GSList *get_andotp_data (const gchar *path, const gchar *password, gint32 max_file_size, - gboolean encrypted, GError **err); GSList *get_freeotpplus_data (const gchar *path, @@ -16,7 +15,6 @@ GSList *get_freeotpplus_data (const gchar *path, GSList *get_aegis_data (const gchar *path, const gchar *password, gint32 max_file_size, - gboolean encrypted, GError **err); GSList *get_authpro_data (const gchar *path, diff --git a/src/imports.c b/src/imports.c index 49889569..cd3412c2 100644 --- a/src/imports.c +++ b/src/imports.c @@ -106,11 +106,11 @@ parse_data_and_update_db (AppData *app_data, } if (g_strcmp0 (action_name, ANDOTP_IMPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, ANDOTP_IMPORT_PLAIN_ACTION_NAME) == 0) { - content = get_andotp_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, g_strcmp0 (action_name, ANDOTP_IMPORT_ACTION_NAME) == 0 ? TRUE : FALSE , &err); + content = get_andotp_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, &err); } else if (g_strcmp0 (action_name, FREEOTPPLUS_IMPORT_ACTION_NAME) == 0) { content = get_freeotpplus_data (filename, &err); } else if (g_strcmp0 (action_name, AEGIS_IMPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, AEGIS_IMPORT_ENC_ACTION_NAME) == 0) { - content = get_aegis_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, g_strcmp0 (action_name, AEGIS_IMPORT_ENC_ACTION_NAME) == 0 ? TRUE : FALSE , &err); + content = get_aegis_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, &err); } else if (g_strcmp0 (action_name, AUTHPRO_IMPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, AUTHPRO_IMPORT_PLAIN_ACTION_NAME) == 0) { content = get_authpro_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, &err); } else if (g_strcmp0 (action_name, TWOFAS_IMPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, TWOFAS_IMPORT_PLAIN_ACTION_NAME) == 0) { From 562a0c716ec610488be4da8078cca816f6107bc2 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Wed, 21 Feb 2024 09:33:12 +0100 Subject: [PATCH 07/20] Add authpro plain import --- src/common/authpro.c | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/common/authpro.c b/src/common/authpro.c index 594fba5c..03d75542 100644 --- a/src/common/authpro.c +++ b/src/common/authpro.c @@ -12,6 +12,9 @@ static GSList *get_otps_from_encrypted_backup (const gchar *path, GFileInputStream *in_stream, GError **err); +static GSList *get_otps_from_plain_backup (const gchar *path, + GError **err); + static GSList *parse_authpro_json_data (const gchar *data, GError **err); @@ -29,7 +32,7 @@ get_authpro_data (const gchar *path, return NULL; } - return get_otps_from_encrypted_backup (path, password, max_file_size, in_file, in_stream, err); + return (password != NULL) ? get_otps_from_encrypted_backup (path, password, max_file_size, in_file, in_stream, err) : get_otps_from_plain_backup (path, err); } @@ -60,6 +63,25 @@ get_otps_from_encrypted_backup (const gchar *path, } +static GSList * +get_otps_from_plain_backup (const gchar *path, + GError **err) +{ + json_error_t j_err; + json_t *json = json_load_file (path, 0, &j_err); + if (!json) { + g_printerr ("Error loading json: %s\n", j_err.text); + return NULL; + } + + gchar *dumped_json = json_dumps (json, 0); + GSList *otps = parse_authpro_json_data (dumped_json, err); + gcry_free (dumped_json); + + return otps; +} + + static GSList * parse_authpro_json_data (const gchar *data, GError **err) From 349fbbaf1f51e2e881967f43a8734d4d6c2faf6a Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Wed, 21 Feb 2024 14:33:55 +0100 Subject: [PATCH 08/20] Add import support for enc/plain twofas --- src/common/aegis.c | 2 +- src/common/common.c | 11 ++- src/common/twofas.c | 236 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 233 insertions(+), 16 deletions(-) diff --git a/src/common/aegis.c b/src/common/aegis.c index 949a5167..b6bcb83d 100644 --- a/src/common/aegis.c +++ b/src/common/aegis.c @@ -52,7 +52,7 @@ get_otps_from_plain_backup (const gchar *path, return NULL; } - gchar *dumped_json = json_dumps(json_object_get (json, "db"), 0); + gchar *dumped_json = json_dumps (json_object_get (json, "db"), 0); GSList *otps = parse_aegis_json_data (dumped_json, err); gcry_free (dumped_json); diff --git a/src/common/common.c b/src/common/common.c index 76466f2a..c7dfd6cd 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -453,8 +453,10 @@ get_andotp_derived_key (const gchar *password, guint32 salt_size) { guchar *derived_key = gcry_malloc_secure (32); - if (gcry_kdf_derive (password, (gsize)g_utf8_strlen (password, -1), GCRY_KDF_PBKDF2, GCRY_MD_SHA1, - salt, salt_size, iterations, 32, derived_key) != 0) { + gpg_error_t g_err = gcry_kdf_derive (password, (gsize)g_utf8_strlen (password, -1), GCRY_KDF_PBKDF2, GCRY_MD_SHA1, + salt, salt_size, iterations, 32, derived_key); + if (g_err != GPG_ERR_NO_ERROR) { + g_printerr ("Failed to derive key: %s/%s\n", gcry_strsource (g_err), gcry_strerror (g_err)); gcry_free (derived_key); return NULL; } @@ -564,6 +566,11 @@ get_data_from_encrypted_backup (const gchar *path, break; } + if (derived_key == NULL) { + g_free (enc_buf); + return NULL; + } + gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, iv, iv_size); if (hd == NULL) { gcry_free (derived_key); diff --git a/src/common/twofas.c b/src/common/twofas.c index 046d544f..3f8ac782 100644 --- a/src/common/twofas.c +++ b/src/common/twofas.c @@ -1,12 +1,39 @@ #include #include +#include +#include +#include "common.h" +#include "../gquarks.h" +#include "../imports.h" -static GSList *get_otps_from_encrypted_2fas_backup (const gchar *path, - const gchar *password, - gint32 max_file_size, - GFile *in_file, - GFileInputStream *in_stream, - GError **err); +#define KDF_ITERS 10000 + +typedef struct twofas_data_t { + guchar *salt; + guchar *iv; + gchar *json_data; +} TwofasData; + +static GSList *get_otps_from_encrypted_backup (const gchar *path, + const gchar *password, + gint32 max_file_size, + GFile *in_file, + GFileInputStream *in_stream, + GError **err); + +static GSList *get_otps_from_plain_backup (const gchar *path, + GError **err); + +static gboolean is_schema_supported (const gchar *path); + +static json_t *get_json_root (const gchar *path); + +static void decrypt_data (const gchar **b64_data, + const gchar *pwd, + TwofasData *twofas_data); + +static GSList *parse_twofas_json_data (const gchar *data, + GError **err); GSList * @@ -21,17 +48,200 @@ get_twofas_data (const gchar *path, g_object_unref(in_file); return NULL; } - return get_otps_from_encrypted_2fas_backup (path, password, max_file_size, in_file, in_stream, err); + + return (password != NULL) ? get_otps_from_encrypted_backup (path, password, max_file_size, in_file, in_stream, err) : get_otps_from_plain_backup (path, err); } static GSList * -get_otps_from_encrypted_2fas_backup (const gchar *path, - const gchar *password, - gint32 max_file_size, - GFile *in_file, - GFileInputStream *in_stream, - GError **err) +get_otps_from_encrypted_backup (const gchar *path, + const gchar *password, + gint32 max_file_size, + GFile *in_file, + GFileInputStream *in_stream, + GError **err) { + if (!is_schema_supported (path)) { + return NULL; + } + + TwofasData *twofas_data = g_new0 (TwofasData, 1); + + json_t *root = get_json_root (path); + gchar **b64_encoded_data = g_strsplit (json_string_value (json_object_get (root, "servicesEncrypted")), ":", 3); + decrypt_data ((const gchar **)b64_encoded_data, password, twofas_data); + if (twofas_data->json_data != NULL) { + parse_twofas_json_data (twofas_data->json_data, err); + } + g_strfreev (b64_encoded_data); + gcry_free (twofas_data->json_data); + g_free (twofas_data->salt); + g_free (twofas_data->iv); + g_free (twofas_data); + json_decref (root); + return NULL; +} + + +static GSList * +get_otps_from_plain_backup (const gchar *path, + GError **err) +{ + if (!is_schema_supported (path)) { + return NULL; + } + + json_error_t j_err; + json_t *json = json_load_file (path, 0, &j_err); + if (!json) { + g_printerr ("Error loading json: %s\n", j_err.text); + return NULL; + } + + gchar *dumped_json = json_dumps (json_object_get (json, "services"), 0); + GSList *otps = parse_twofas_json_data (dumped_json, err); + gcry_free (dumped_json); + + return otps; +} + + +static gboolean +is_schema_supported (const gchar *path) +{ + json_t *root = get_json_root (path); + gint32 schema_version = (gint32)json_integer_value (json_object_get (root, "schemaVersion")); + if (schema_version != 4) { + g_printerr ("Unsupported schema version: %d\n", schema_version); + json_decref (root); + return FALSE; + } + json_decref (root); + return TRUE; +} + + +static json_t * +get_json_root (const gchar *path) +{ + json_error_t jerr; + json_t *json = json_load_file (path, 0, &jerr); + if (!json) { + g_printerr ("Error loading json: %s\n", jerr.text); + return FALSE; + } + + gchar *dumped_json = json_dumps (json, 0); + json_t *root = json_loads (dumped_json, JSON_DISABLE_EOF_CHECK, &jerr); + gcry_free (dumped_json); + + return root; +} + + +static void +decrypt_data (const gchar **b64_data, + const gchar *pwd, + TwofasData *twofas_data) +{ + gsize data_out_len, salt_out_len, iv_out_len; + guchar *enc_data = g_base64_decode (b64_data[0], &data_out_len); + twofas_data->salt = g_base64_decode (b64_data[1], &salt_out_len); + twofas_data->iv = g_base64_decode (b64_data[2], &iv_out_len); + + guchar *derived_key = gcry_malloc_secure (32); + gpg_error_t g_err = gcry_kdf_derive (pwd, (gsize)g_utf8_strlen (pwd, -1), GCRY_KDF_PBKDF2, GCRY_MD_SHA256, + twofas_data->salt, salt_out_len, KDF_ITERS, 32, derived_key); + if (g_err != GPG_ERR_NO_ERROR) { + g_printerr ("Failed to derive key: %s/%s\n", gcry_strsource (g_err), gcry_strerror (g_err)); + // TODO: cleanup + return; + } + + gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, twofas_data->iv, iv_out_len); + if (hd == NULL) { + // TODO: cleanup + return; + } + + twofas_data->json_data = gcry_calloc_secure (data_out_len, 1); + gpg_error_t gpg_err = gcry_cipher_decrypt (hd, twofas_data->json_data, data_out_len, enc_data, data_out_len); + if (gpg_err) { + // TODO: cleanup + return; + } + + gcry_cipher_close (hd); + gcry_free (derived_key); + g_free (enc_data); +} + + +static GSList * +parse_twofas_json_data (const gchar *data, + GError **err) +{ + json_error_t jerr; + json_t *array = json_loads (data, JSON_DISABLE_EOF_CHECK, &jerr); + if (array == NULL) { + g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "%s", jerr.text); + return NULL; + } + + GSList *otps = NULL; + for (guint i = 0; i < json_array_size (array); i++) { + json_t *obj = json_array_get (array, i); + + otp_t *otp = g_new0 (otp_t, 1); + otp->secret = secure_strdup (json_string_value (json_object_get (obj, "secret"))); + + json_t *otp_obj = json_object_get (obj, "otp"); + otp->issuer = g_strdup (json_string_value (json_object_get (otp_obj, "issuer"))); + otp->account_name = g_strdup (json_string_value (json_object_get (otp_obj, "account"))); + otp->digits = (guint32) json_integer_value (json_object_get (otp_obj, "digits")); + + gboolean skip = FALSE; + const gchar *type = json_string_value (json_object_get (otp_obj, "tokenType")); + if (g_ascii_strcasecmp (type, "TOTP") == 0) { + otp->type = g_strdup ("TOTP"); + otp->period = (guint32)json_integer_value (json_object_get (otp_obj, "period")); + } else if (g_ascii_strcasecmp (type, "HOTP") == 0) { + otp->type = g_strdup ("HOTP"); + otp->counter = json_integer_value (json_object_get (otp_obj, "counter")); + } else if (g_ascii_strcasecmp (type, "Steam") == 0) { + otp->type = g_strdup ("TOTP"); + otp->period = (guint32)json_integer_value (json_object_get (otp_obj, "period")); + g_free (otp->issuer); + otp->issuer = g_strdup ("Steam"); + } else { + g_printerr ("Skipping token due to unsupported type: %s\n", type); + skip = TRUE; + } + + const gchar *algo = json_string_value (json_object_get (otp_obj, "algorithm")); + if (g_ascii_strcasecmp (algo, "SHA1") == 0 || + g_ascii_strcasecmp (algo, "SHA256") == 0 || + g_ascii_strcasecmp (algo, "SHA512") == 0) { + otp->algo = g_utf8_strup (algo, -1); + } else { + g_printerr ("Skipping token due to unsupported algo: %s\n", algo); + skip = TRUE; + } + + if (!skip) { + otps = g_slist_append (otps, g_memdup2 (otp, sizeof (otp_t))); + } + + gcry_free (otp->secret); + g_free (otp->issuer); + g_free (otp->account_name); + g_free (otp->algo); + g_free (otp->type); + g_free (otp); + } + + json_decref (array); + + return otps; } \ No newline at end of file From 5bf8f9985bfdef95f2e477299d33f9d78c3a918c Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Wed, 21 Feb 2024 14:37:23 +0100 Subject: [PATCH 09/20] Use ubuntu 22.04 on circleci --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1fa70338..03217b98 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,9 +1,9 @@ version: 2.0 jobs: - ubuntu2004: + ubuntu2204: docker: - - image: ubuntu:20.04 + - image: ubuntu:22.04 steps: - checkout - run: apt update && DEBIAN_FRONTEND=noninteractive apt -y install git gcc clang cmake libgcrypt20-dev libgtk-3-dev libzip-dev libjansson-dev libpng-dev libzbar-dev libprotobuf-c-dev libsecret-1-dev uuid-dev libprotobuf-dev libqrencode-dev @@ -50,7 +50,7 @@ workflows: version: 2 build: jobs: - - ubuntu2004 + - ubuntu2204 - ubuntuLatestRolling - debianLatestStable - fedoraLatestStable From 47fd4e0dbb3bdfb988bf416e629200947d50cf75 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Thu, 22 Feb 2024 15:47:58 +0100 Subject: [PATCH 10/20] Update metadata fixes #348 --- data/com.github.paolostivanin.OTPClient.appdata.xml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/data/com.github.paolostivanin.OTPClient.appdata.xml b/data/com.github.paolostivanin.OTPClient.appdata.xml index ba2dd639..02318ce8 100644 --- a/data/com.github.paolostivanin.OTPClient.appdata.xml +++ b/data/com.github.paolostivanin.OTPClient.appdata.xml @@ -5,7 +5,7 @@ CC-BY-4.0 GPL-3.0+ OTPClient - GTK+ application for managing TOTP and HOTP tokens with built-in encryption. + Application for managing TOTP/HOTP tokens with built-in encryption otp @@ -561,8 +561,4 @@ - - workstation - mobile - From 9ff2945bb0505fa823d203365462445b94228e4e Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Fri, 23 Feb 2024 20:59:23 +0100 Subject: [PATCH 11/20] Finished 2FAS import support --- src/common/get-providers-data.h | 1 - src/common/twofas.c | 32 ++++++++++---------------------- src/imports.c | 2 +- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/src/common/get-providers-data.h b/src/common/get-providers-data.h index a41b728f..7ff9160b 100644 --- a/src/common/get-providers-data.h +++ b/src/common/get-providers-data.h @@ -24,7 +24,6 @@ GSList *get_authpro_data (const gchar *path, GSList *get_twofas_data (const gchar *path, const gchar *password, - gint32 max_file_size, GError **err); G_END_DECLS diff --git a/src/common/twofas.c b/src/common/twofas.c index 3f8ac782..0be50bed 100644 --- a/src/common/twofas.c +++ b/src/common/twofas.c @@ -16,9 +16,6 @@ typedef struct twofas_data_t { static GSList *get_otps_from_encrypted_backup (const gchar *path, const gchar *password, - gint32 max_file_size, - GFile *in_file, - GFileInputStream *in_stream, GError **err); static GSList *get_otps_from_plain_backup (const gchar *path, @@ -39,26 +36,15 @@ static GSList *parse_twofas_json_data (const gchar *data, GSList * get_twofas_data (const gchar *path, const gchar *password, - gint32 max_file_size, GError **err) { - GFile *in_file = g_file_new_for_path(path); - GFileInputStream *in_stream = g_file_read(in_file, NULL, err); - if (*err != NULL) { - g_object_unref(in_file); - return NULL; - } - - return (password != NULL) ? get_otps_from_encrypted_backup (path, password, max_file_size, in_file, in_stream, err) : get_otps_from_plain_backup (path, err); + return (password != NULL) ? get_otps_from_encrypted_backup (path, password, err) : get_otps_from_plain_backup (path, err); } static GSList * get_otps_from_encrypted_backup (const gchar *path, const gchar *password, - gint32 max_file_size, - GFile *in_file, - GFileInputStream *in_stream, GError **err) { if (!is_schema_supported (path)) { @@ -66,21 +52,22 @@ get_otps_from_encrypted_backup (const gchar *path, } TwofasData *twofas_data = g_new0 (TwofasData, 1); + GSList *otps = NULL; json_t *root = get_json_root (path); gchar **b64_encoded_data = g_strsplit (json_string_value (json_object_get (root, "servicesEncrypted")), ":", 3); decrypt_data ((const gchar **)b64_encoded_data, password, twofas_data); if (twofas_data->json_data != NULL) { - parse_twofas_json_data (twofas_data->json_data, err); + otps = parse_twofas_json_data (twofas_data->json_data, err); + gcry_free (twofas_data->json_data); } g_strfreev (b64_encoded_data); - gcry_free (twofas_data->json_data); g_free (twofas_data->salt); g_free (twofas_data->iv); g_free (twofas_data); json_decref (root); - return NULL; + return otps; } @@ -155,21 +142,22 @@ decrypt_data (const gchar **b64_data, twofas_data->salt, salt_out_len, KDF_ITERS, 32, derived_key); if (g_err != GPG_ERR_NO_ERROR) { g_printerr ("Failed to derive key: %s/%s\n", gcry_strsource (g_err), gcry_strerror (g_err)); - // TODO: cleanup + gcry_free (derived_key); + g_free (enc_data); return; } gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, twofas_data->iv, iv_out_len); if (hd == NULL) { - // TODO: cleanup + gcry_free (derived_key); + g_free (enc_data); return; } twofas_data->json_data = gcry_calloc_secure (data_out_len, 1); gpg_error_t gpg_err = gcry_cipher_decrypt (hd, twofas_data->json_data, data_out_len, enc_data, data_out_len); if (gpg_err) { - // TODO: cleanup - return; + g_printerr ("Failed to decrypt data: %s/%s\n", gcry_strsource (g_err), gcry_strerror (g_err)); } gcry_cipher_close (hd); diff --git a/src/imports.c b/src/imports.c index cd3412c2..b2aae7de 100644 --- a/src/imports.c +++ b/src/imports.c @@ -114,7 +114,7 @@ parse_data_and_update_db (AppData *app_data, } else if (g_strcmp0 (action_name, AUTHPRO_IMPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, AUTHPRO_IMPORT_PLAIN_ACTION_NAME) == 0) { content = get_authpro_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, &err); } else if (g_strcmp0 (action_name, TWOFAS_IMPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, TWOFAS_IMPORT_PLAIN_ACTION_NAME) == 0) { - content = get_twofas_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, &err); + content = get_twofas_data (filename, pwd, &err); } if (content == NULL) { From fea111923ec2f4f36ef8faa6a493f95bca0c72c2 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Mon, 26 Feb 2024 07:33:16 +0100 Subject: [PATCH 12/20] Add 2fa keyword fixes #349 --- data/com.github.paolostivanin.OTPClient.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/com.github.paolostivanin.OTPClient.desktop b/data/com.github.paolostivanin.OTPClient.desktop index 701c859e..3e09b6c0 100644 --- a/data/com.github.paolostivanin.OTPClient.desktop +++ b/data/com.github.paolostivanin.OTPClient.desktop @@ -2,7 +2,7 @@ Type=Application Exec=otpclient Icon=com.github.paolostivanin.OTPClient -Keywords=otp;totp;hotp; +Keywords=otp;totp;hotp;2fa Terminal=false Name=OTPClient Comment=GTK+ TOTP and HOTP client From bbee9c366bf475b6ad313076a0a204e46ed2370d Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Mon, 26 Feb 2024 11:16:48 +0100 Subject: [PATCH 13/20] Add export plain/encrypted Authenticator Pro --- src/common/aegis.c | 4 +- src/common/andotp.c | 2 +- src/common/authpro.c | 143 +++++++++++++++++++++++++++++++++++++++++++ src/common/common.c | 25 ++++---- src/common/common.h | 16 +++-- src/common/exports.h | 12 ++-- src/common/twofas.c | 17 +++++ src/exports.c | 36 +++++------ 8 files changed, 204 insertions(+), 51 deletions(-) diff --git a/src/common/aegis.c b/src/common/aegis.c index b6bcb83d..b47cf1ce 100644 --- a/src/common/aegis.c +++ b/src/common/aegis.c @@ -212,8 +212,8 @@ get_otps_from_encrypted_backup (const gchar *path, gchar * export_aegis (const gchar *export_path, - json_t *json_db_data, - const gchar *password) + const gchar *password, + json_t *json_db_data) { GError *err = NULL; json_t *root = json_object (); diff --git a/src/common/andotp.c b/src/common/andotp.c index 41956ac8..0868d580 100644 --- a/src/common/andotp.c +++ b/src/common/andotp.c @@ -179,7 +179,7 @@ export_andotp (const gchar *export_path, guchar *salt = g_malloc0 (ANDOTP_SI_SIZE); gcry_create_nonce (salt, ANDOTP_SI_SIZE); - guchar *derived_key = get_andotp_derived_key (password, salt, le_iterations, ANDOTP_SI_SIZE); + guchar *derived_key = get_andotp_derived_key (password, salt, le_iterations); gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, iv, ANDOTP_SI_SIZE); if (hd == NULL) { gcry_free (derived_key); diff --git a/src/common/authpro.c b/src/common/authpro.c index 03d75542..ed6fcdd3 100644 --- a/src/common/authpro.c +++ b/src/common/authpro.c @@ -36,6 +36,149 @@ get_authpro_data (const gchar *path, } +gchar * +export_authpro (const gchar *export_path, + const gchar *password, + json_t *json_db_data) +{ + GError *err = NULL; + json_t *root = json_object (); + json_t *auth_array = json_array (); + json_object_set (root, "Authenticators", auth_array); + json_object_set (root, "Categories", json_array()); + json_object_set (root, "AuthenticatorCategories", json_array()); + json_object_set (root, "CustomIcons", json_array()); + + json_t *db_obj, *export_obj; + gsize index; + json_array_foreach (json_db_data, index, db_obj) { + export_obj = json_object (); + const gchar *issuer = json_string_value (json_object_get (db_obj, "issuer")); + if (issuer != NULL) { + if (g_ascii_strcasecmp (issuer, "steam") == 0) { + json_object_set (export_obj, "Issuer", json_string ("Steam")); + } else { + json_object_set(export_obj, "Issuer", json_object_get (db_obj, "issuer")); + } + } + const gchar *label = json_string_value (json_object_get (db_obj, "issuer")); + if (label != NULL) { + json_object_set (export_obj, "Username", json_object_get (db_obj, "label")); + } + json_object_set (export_obj, "Secret", json_object_get (db_obj, "secret")); + json_object_set (export_obj, "Digits", json_object_get (db_obj, "digits")); + json_object_set (export_obj, "Ranking", json_integer (0)); + json_object_set (export_obj, "Icon", json_null()); + json_object_set (export_obj, "Pin", json_null()); + if (g_ascii_strcasecmp (json_string_value (json_object_get (db_obj, "algo")), "SHA1") == 0) { + json_object_set (export_obj, "Algorithm", json_integer (0)); + } else if (g_ascii_strcasecmp (json_string_value (json_object_get (db_obj, "algo")), "SHA256") == 0) { + json_object_set (export_obj, "Algorithm", json_integer (1)); + } else if (g_ascii_strcasecmp (json_string_value (json_object_get (db_obj, "algo")), "SHA512") == 0) { + json_object_set (export_obj, "Algorithm", json_integer (2)); + } + if (g_ascii_strcasecmp (json_string_value (json_object_get (db_obj, "type")), "TOTP") == 0) { + json_object_set (export_obj, "Period", json_object_get (db_obj, "period")); + json_object_set (export_obj, "Counter", json_integer (0)); + json_object_set (export_obj, "Type", json_integer (2)); + } else { + json_object_set (export_obj, "Counter", json_object_get (db_obj, "counter")); + json_object_set (export_obj, "Period", json_integer (0)); + json_object_set (export_obj, "Type", json_integer (1)); + } + json_array_append (auth_array, export_obj); + } + + gchar *json_data = json_dumps (root, JSON_COMPACT); + if (json_data == NULL) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't dump json data"); + goto end; + } + gsize json_data_size = strlen (json_data); + + GFile *out_gfile = g_file_new_for_path (export_path); + GFileOutputStream *out_stream = g_file_replace (out_gfile, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION | G_FILE_CREATE_PRIVATE, NULL, &err); + if (password != NULL) { + // encrypt the content and write the encrypted file to disk + const gchar *header = "AUTHENTICATORPRO"; + guchar *salt = g_malloc0 (AUTHPRO_SALT_TAG); + gcry_create_nonce (salt, AUTHPRO_SALT_TAG); + guchar *iv = g_malloc0 (AUTHPRO_IV); + gcry_create_nonce (iv, AUTHPRO_SALT_TAG); + guchar *derived_key = get_authpro_derived_key (password, salt); + if (derived_key == NULL) { + g_free (salt); + g_free (iv); + g_set_error (&err, key_deriv_gquark (), KEY_DERIVATION_ERRCODE, "Error while deriving the key."); + goto end; + } + gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, iv, AUTHPRO_IV); + if (hd == NULL) { + gcry_free (derived_key); + g_free (salt); + g_free (iv); + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Error while opening the cipher."); + goto end; + } + gchar *enc_buf = gcry_calloc_secure (json_data_size, 1); + gpg_error_t gpg_err = gcry_cipher_encrypt (hd, enc_buf, json_data_size, json_data, json_data_size); + if (gpg_err != GPG_ERR_NO_ERROR) { + g_printerr ("Failed to encrypt data: %s/%s\n", gcry_strsource (gpg_err), gcry_strerror (gpg_err)); + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Failed to encrypt data."); + gcry_free (derived_key); + gcry_free (enc_buf); + g_free (iv); + g_free (salt); + gcry_cipher_close (hd); + goto end; + } + guchar tag[AUTHPRO_SALT_TAG]; + gcry_cipher_gettag (hd, tag, AUTHPRO_SALT_TAG); + gcry_cipher_close (hd); + + if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), header, 16, NULL, &err) == -1) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't write header to file."); + goto enc_end; + } + if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), salt, AUTHPRO_SALT_TAG, NULL, &err) == -1) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't write salt to file."); + goto enc_end; + } + if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), iv, AUTHPRO_IV, NULL, &err) == -1) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't write iv to file."); + goto enc_end; + } + if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), enc_buf, json_data_size, NULL, &err) == -1) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't write payload to file."); + goto enc_end; + } + if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), tag, AUTHPRO_SALT_TAG, NULL, &err) == -1) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't write tag to file"); + goto enc_end; + } + enc_end: + gcry_free (derived_key); + gcry_free (enc_buf); + g_free (iv); + g_free (salt); + } else { + // write the plain json to disk + if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), json_data, json_data_size, NULL, &err) == -1) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "couldn't dump json data to file"); + } + } + g_object_unref (out_stream); + g_object_unref (out_gfile); + + end: + gcry_free (json_data); + json_decref (auth_array); + json_decref (root); + + return (err != NULL ? g_strdup (err->message) : NULL); +} + + static GSList * get_otps_from_encrypted_backup (const gchar *path, const gchar *password, diff --git a/src/common/common.c b/src/common/common.c index c7dfd6cd..08c544e8 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -412,8 +412,7 @@ get_kf_ptr (void) guchar * get_authpro_derived_key (const gchar *password, - const guchar *salt, - gint32 salt_size) + const guchar *salt) { guchar *derived_key = gcry_malloc_secure (32); // taglen, iterations, memory_cost (65536=64MiB), parallelism @@ -422,7 +421,7 @@ get_authpro_derived_key (const gchar *password, if (gcry_kdf_open (&hd, GCRY_KDF_ARGON2, GCRY_KDF_ARGON2ID, params, 4, password, (gsize)g_utf8_strlen (password, -1), - salt, salt_size, + salt, AUTHPRO_SALT_TAG, NULL, 0, NULL, 0) != GPG_ERR_NO_ERROR) { g_printerr ("Error while opening the KDF handler\n"); return NULL; @@ -449,12 +448,11 @@ get_authpro_derived_key (const gchar *password, guchar * get_andotp_derived_key (const gchar *password, const guchar *salt, - guint32 iterations, - guint32 salt_size) + guint32 iterations) { guchar *derived_key = gcry_malloc_secure (32); gpg_error_t g_err = gcry_kdf_derive (password, (gsize)g_utf8_strlen (password, -1), GCRY_KDF_PBKDF2, GCRY_MD_SHA1, - salt, salt_size, iterations, 32, derived_key); + salt, ANDOTP_IV_SALT, iterations, 32, derived_key); if (g_err != GPG_ERR_NO_ERROR) { g_printerr ("Failed to derive key: %s/%s\n", gcry_strsource (g_err), gcry_strerror (g_err)); gcry_free (derived_key); @@ -478,12 +476,12 @@ get_data_from_encrypted_backup (const gchar *path, gint32 salt_size, iv_size, tag_size; switch (provider) { case ANDOTP: - salt_size = iv_size = 12; - tag_size = 16; + salt_size = iv_size = ANDOTP_IV_SALT; + tag_size = ANDOTP_TAG; break; case AUTHPRO: - salt_size = tag_size = 16; - iv_size = 12; + salt_size = tag_size = AUTHPRO_SALT_TAG; + iv_size = AUTHPRO_IV; break; } @@ -520,14 +518,13 @@ get_data_from_encrypted_backup (const gchar *path, case ANDOTP: // 4 is the size of iterations (int32) offset = 4; - enc_buf_size = (gsize)input_file_size - offset - salt_size - iv_size - tag_size; break; case AUTHPRO: // 16 is the size of the header offset = 16; - enc_buf_size = (gsize)(input_file_size - offset - salt_size - iv_size - tag_size); break; } + enc_buf_size = (gsize)(input_file_size - offset - salt_size - iv_size - tag_size); if (enc_buf_size < 1) { g_printerr ("A non-encrypted file has been selected\n"); g_object_unref (in_stream); @@ -559,10 +556,10 @@ get_data_from_encrypted_backup (const gchar *path, guchar *derived_key; switch (provider) { case ANDOTP: - derived_key = get_andotp_derived_key (password, salt, andotp_be_iterations, salt_size); + derived_key = get_andotp_derived_key (password, salt, andotp_be_iterations); break; case AUTHPRO: - derived_key = get_authpro_derived_key (password, salt, salt_size); + derived_key = get_authpro_derived_key (password, salt); break; } diff --git a/src/common/common.h b/src/common/common.h index e0c45d14..9b931e03 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -10,8 +10,14 @@ G_BEGIN_DECLS #define LOW_MEMLOCK_VALUE 65536 //64KB #define MEMLOCK_VALUE 67108864 //64MB -#define ANDOTP 100 -#define AUTHPRO 101 +#define ANDOTP 100 +#define AUTHPRO 101 + +#define AUTHPRO_IV 12 +#define AUTHPRO_SALT_TAG 16 + +#define ANDOTP_IV_SALT 12 +#define ANDOTP_TAG 16 gint32 get_max_file_size_from_memlock (void); @@ -49,12 +55,10 @@ GKeyFile *get_kf_ptr (void); guchar *get_andotp_derived_key (const gchar *password, const guchar *salt, - guint32 iterations, - guint32 salt_size); + guint32 iterations); guchar *get_authpro_derived_key (const gchar *password, - const guchar *salt, - gint32 salt_size); + const guchar *salt); gchar *get_data_from_encrypted_backup (const gchar *path, const gchar *password, diff --git a/src/common/exports.h b/src/common/exports.h index 437a51e0..dca6beb6 100644 --- a/src/common/exports.h +++ b/src/common/exports.h @@ -28,15 +28,15 @@ gchar *export_freeotpplus (const gchar *export_path, json_t *json_db_data); gchar *export_aegis (const gchar *export_path, - json_t *json_db_data, - const gchar *password); + const gchar *password, + json_t *json_db_data); gchar *export_authpro (const gchar *export_path, - json_t *json_db_data, - const gchar *password); + const gchar *password, + json_t *json_db_data); gchar *export_twofas (const gchar *export_path, - json_t *json_db_data, - const gchar *password); + const gchar *password, + json_t *json_db_data); G_END_DECLS diff --git a/src/common/twofas.c b/src/common/twofas.c index 0be50bed..75e1c13b 100644 --- a/src/common/twofas.c +++ b/src/common/twofas.c @@ -42,6 +42,23 @@ get_twofas_data (const gchar *path, } +gchar * +export_twofas (const gchar *export_path, + const gchar *password, + json_t *json_db_data) +{ + // TODO: create the otps json (services => array(name, secret, + // otp =>object(link, label=account, issuer, digits, period, algorithm, tokenType, source=Link), + // order => object(position=0++), + // icon=null), + // groups => array(), + // schemaVersion: 4 + // TODO: create the encrypted format json (services => array(), groups => array(), schemaVersion: 4, servicesEncrypted: base64(otps_json), reference: ) + // TODO: encrypt the otps json + // TODO: add encrypted otps json to encrypted format json +} + + static GSList * get_otps_from_encrypted_backup (const gchar *path, const gchar *password, diff --git a/src/exports.c b/src/exports.c index 6f73e58c..a08e6ec2 100644 --- a/src/exports.c +++ b/src/exports.c @@ -26,12 +26,15 @@ export_data_cb (GSimpleAction *simple, base_dir = g_get_user_data_dir (); #endif - gboolean encrypted; + gboolean encrypted = FALSE; + gchar *password = NULL; if (g_strcmp0 (action_name, "export_andotp") == 0 || g_strcmp0 (action_name, "export_aegis") == 0 || g_strcmp0 (action_name, "export_authpro_enc") == 0 || g_strcmp0 (action_name, "export_twofas_enc") == 0) { + password = prompt_for_password (app_data, NULL, NULL, TRUE); + if (password == NULL) { + return; + } encrypted = TRUE; - } else { - encrypted = FALSE; } GtkFileChooserNative *fl_diag = gtk_file_chooser_native_new ("Export file", @@ -73,33 +76,22 @@ export_data_cb (GSimpleAction *simple, return; } - gchar *password = NULL, *ret_msg = NULL; - if (g_strcmp0 (action_name, ANDOTP_EXPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, ANDOTP_EXPORT_PLAIN_ACTION_NAME) == 0 || - g_strcmp0 (action_name, AUTHPRO_EXPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, TWOFAS_EXPORT_ENC_ACTION_NAME) == 0) { - if (encrypted == TRUE) { - password = prompt_for_password (app_data, NULL, NULL, TRUE); - if (password == NULL) { - return; - } - } + gchar *ret_msg = NULL; + if (g_strcmp0 (action_name, ANDOTP_EXPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, ANDOTP_EXPORT_PLAIN_ACTION_NAME) == 0) { ret_msg = export_andotp (export_file_abs_path, password, app_data->db_data->json_data); - show_ret_msg_dialog (app_data->main_window, export_file_abs_path, ret_msg); } else if (g_strcmp0 (action_name, FREEOTPPLUS_EXPORT_ACTION_NAME) == 0) { ret_msg = export_freeotpplus (export_file_abs_path, app_data->db_data->json_data); - show_ret_msg_dialog (app_data->main_window, export_file_abs_path, ret_msg); } else if (g_strcmp0 (action_name, AEGIS_EXPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, AEGIS_EXPORT_PLAIN_ACTION_NAME) == 0) { - if (encrypted == TRUE) { - password = prompt_for_password (app_data, NULL, NULL, TRUE); - if (password == NULL) { - return; - } - } - ret_msg = export_aegis (export_file_abs_path, app_data->db_data->json_data, password); - show_ret_msg_dialog (app_data->main_window, export_file_abs_path, ret_msg); + ret_msg = export_aegis (export_file_abs_path, password, app_data->db_data->json_data); + } else if (g_strcmp0 (action_name, AUTHPRO_EXPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, AUTHPRO_EXPORT_PLAIN_ACTION_NAME) == 0) { + ret_msg = export_authpro (export_file_abs_path, password, app_data->db_data->json_data); + } else if (g_strcmp0 (action_name, TWOFAS_EXPORT_ENC_ACTION_NAME) == 0 || g_strcmp0 (action_name, TWOFAS_EXPORT_PLAIN_ACTION_NAME) == 0) { + ret_msg = export_twofas (export_file_abs_path, password, app_data->db_data->json_data); } else { show_message_dialog (app_data->main_window, "Invalid export action.", GTK_MESSAGE_ERROR); return; } + show_ret_msg_dialog (app_data->main_window, export_file_abs_path, ret_msg); g_free (ret_msg); g_free (export_file_abs_path); if (encrypted == TRUE) { From 4ca5a5df34d5fd10492019da1fd0cf0eaa01cf48 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Mon, 26 Feb 2024 15:23:00 +0100 Subject: [PATCH 14/20] Add export plain/encrypted 2FAS --- src/common/common.h | 3 + src/common/twofas.c | 179 +++++++++++++++++++++++++++++++++++++++++--- src/parse-uri.c | 2 +- 3 files changed, 174 insertions(+), 10 deletions(-) diff --git a/src/common/common.h b/src/common/common.h index 9b931e03..a56e7c7c 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -19,6 +19,9 @@ G_BEGIN_DECLS #define ANDOTP_IV_SALT 12 #define ANDOTP_TAG 16 +#define TWOFAS_SALT 256 +#define TWOFAS_IV 12 + gint32 get_max_file_size_from_memlock (void); gchar *init_libs (gint32 max_file_size); diff --git a/src/common/twofas.c b/src/common/twofas.c index 75e1c13b..dc9c0b9c 100644 --- a/src/common/twofas.c +++ b/src/common/twofas.c @@ -5,8 +5,9 @@ #include "common.h" #include "../gquarks.h" #include "../imports.h" +#include "../parse-uri.h" -#define KDF_ITERS 10000 +#define TWOFAS_KDF_ITERS 10000 typedef struct twofas_data_t { guchar *salt; @@ -29,6 +30,11 @@ static void decrypt_data (const gchar **b64_data, const gchar *pwd, TwofasData *twofas_data); +static gchar *get_encoded_data (guchar *enc_buf, + gsize enc_buf_len, + guchar *salt, + guchar *iv); + static GSList *parse_twofas_json_data (const gchar *data, GError **err); @@ -47,15 +53,151 @@ export_twofas (const gchar *export_path, const gchar *password, json_t *json_db_data) { - // TODO: create the otps json (services => array(name, secret, - // otp =>object(link, label=account, issuer, digits, period, algorithm, tokenType, source=Link), + // TODO: create the otps json (services => array(secret, // order => object(position=0++), // icon=null), - // groups => array(), - // schemaVersion: 4 - // TODO: create the encrypted format json (services => array(), groups => array(), schemaVersion: 4, servicesEncrypted: base64(otps_json), reference: ) - // TODO: encrypt the otps json - // TODO: add encrypted otps json to encrypted format json + GError *err = NULL; + json_t *root = json_object (); + json_t *services_array = json_array (); + json_object_set (root, "services", services_array); + json_object_set (root, "groups", json_array()); + json_object_set (root, "schemaVersion", json_integer (4)); + + json_t *db_obj, *export_obj, *otp_obj, *order_obj; + gsize index; + json_array_foreach (json_db_data, index, db_obj) { + export_obj = json_object (); + otp_obj = json_object (); + order_obj = json_object (); + const gchar *issuer = json_string_value (json_object_get (db_obj, "issuer")); + if (issuer != NULL) { + if (g_ascii_strcasecmp (issuer, "steam") == 0) { + json_object_set (export_obj, "name", json_string ("Steam")); + json_object_set (otp_obj, "issuer", json_string ("Steam")); + json_object_set (otp_obj, "tokenType", json_string ("STEAM")); + } else { + json_object_set(export_obj, "name", json_string (issuer)); + json_object_set (otp_obj, "issuer", json_string (issuer)); + } + } + const gchar *label = json_string_value (json_object_get (db_obj, "label")); + if (label != NULL) { + json_object_set (otp_obj, "label", json_string (label)); + json_object_set (otp_obj, "account", json_string (label)); + } + + gchar *algo = g_ascii_strup (json_string_value (json_object_get (db_obj, "algo")), -1); + json_object_set (otp_obj, "algorithm", json_object_get (db_obj, "digits")); + g_free (algo); + + json_object_set (otp_obj, "digits", json_string (algo)); + json_object_set (otp_obj, "source", json_string ("Link")); + gchar *otpauth_uri = secure_strdup (get_otpauth_uri (NULL, db_obj)); + json_object_set (otp_obj, "link", json_string (otpauth_uri)); + gcry_free (otpauth_uri); + + if (g_ascii_strcasecmp (json_string_value (json_object_get (db_obj, "type")), "TOTP") == 0) { + json_object_set (otp_obj, "period", json_object_get (db_obj, "period")); + json_object_set (otp_obj, "tokenType", json_string ("TOTP")); + } else { + json_object_set (otp_obj, "counter", json_object_get (db_obj, "counter")); + json_object_set (otp_obj, "tokenType", json_string ("HOTP")); + } + + json_object_set (order_obj, "position", json_integer ((json_int_t)index)); + json_object_set (export_obj, "order", order_obj); + json_object_set (export_obj, "otp", otp_obj); + json_object_set (export_obj, "icon", json_null()); + + json_array_append (services_array, export_obj); + } + + gchar *json_data = json_dumps ((password == NULL) ? root : services_array, JSON_COMPACT); + if (json_data == NULL) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't dump json data"); + goto end; + } + gsize json_data_size = strlen (json_data); + + GFile *out_gfile = g_file_new_for_path (export_path); + GFileOutputStream *out_stream = g_file_replace (out_gfile, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION | G_FILE_CREATE_PRIVATE, NULL, &err); + if (password != NULL) { + guchar *salt = g_malloc0 (TWOFAS_SALT); + gcry_create_nonce (salt, TWOFAS_SALT); + guchar *iv = g_malloc0 (TWOFAS_IV); + gcry_create_nonce (iv, TWOFAS_IV); + guchar *derived_key = gcry_malloc_secure (32); + gpg_error_t g_err = gcry_kdf_derive (password, (gsize)g_utf8_strlen (password, -1), GCRY_KDF_PBKDF2, GCRY_MD_SHA256, + salt, TWOFAS_SALT, TWOFAS_KDF_ITERS, 32, derived_key); + if (g_err != GPG_ERR_NO_ERROR) { + g_printerr ("Failed to derive key: %s/%s\n", gcry_strsource (g_err), gcry_strerror (g_err)); + g_set_error (&err, key_deriv_gquark (), KEY_DERIVATION_ERRCODE, "Error while deriving the key."); + gcry_free (derived_key); + g_free (salt); + g_free (iv); + goto end; + } + gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, iv, TWOFAS_IV); + if (hd == NULL) { + gcry_free (derived_key); + g_free (salt); + g_free (iv); + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Error while opening the cipher."); + goto end; + } + guchar *enc_buf = gcry_calloc_secure (json_data_size, 1); + gpg_error_t gpg_err = gcry_cipher_encrypt (hd, enc_buf, json_data_size, json_data, json_data_size); + if (gpg_err != GPG_ERR_NO_ERROR) { + g_printerr ("Failed to encrypt data: %s/%s\n", gcry_strsource (gpg_err), gcry_strerror (gpg_err)); + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Failed to encrypt data."); + gcry_free (derived_key); + gcry_free (enc_buf); + g_free (iv); + g_free (salt); + gcry_cipher_close (hd); + goto end; + } + // TWOFAS ignores the tag, so we don't have to append it (sigh!) + gcry_free (derived_key); + gcry_cipher_close (hd); + + json_t *enc_root = json_object (); + json_object_set (enc_root, "services", json_array ()); + json_object_set (enc_root, "groups", json_array()); + json_object_set (enc_root, "schemaVersion", json_integer (4)); + json_object_set (enc_root, "reference", json_null ()); + gchar *encoded_data = get_encoded_data (enc_buf, json_data_size, salt, iv); + json_object_set (enc_root, "servicesEncrypted", json_string (encoded_data)); + gchar *json_enc_data = json_dumps (enc_root, JSON_COMPACT); + if (json_enc_data == NULL) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't dump json data"); + goto enc_end; + } + if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), json_enc_data, strlen (json_enc_data), NULL, &err) == -1) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't write the json data to file"); + } + gcry_free (json_enc_data); + + enc_end: + g_free (iv); + g_free (salt); + g_free (encoded_data); + json_decref (enc_root); + } else { + // write the plain json to disk + if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), json_data, json_data_size, NULL, &err) == -1) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't write the json data to file"); + } + } + g_object_unref (out_stream); + g_object_unref (out_gfile); + + end: + gcry_free (json_data); + json_decref (services_array); + json_decref (root); + + return (err != NULL ? g_strdup (err->message) : NULL); } @@ -149,6 +291,7 @@ decrypt_data (const gchar **b64_data, const gchar *pwd, TwofasData *twofas_data) { + // TWOFAS ignores the tag, so we don't have to check it (sigh!) gsize data_out_len, salt_out_len, iv_out_len; guchar *enc_data = g_base64_decode (b64_data[0], &data_out_len); twofas_data->salt = g_base64_decode (b64_data[1], &salt_out_len); @@ -156,7 +299,7 @@ decrypt_data (const gchar **b64_data, guchar *derived_key = gcry_malloc_secure (32); gpg_error_t g_err = gcry_kdf_derive (pwd, (gsize)g_utf8_strlen (pwd, -1), GCRY_KDF_PBKDF2, GCRY_MD_SHA256, - twofas_data->salt, salt_out_len, KDF_ITERS, 32, derived_key); + twofas_data->salt, salt_out_len, TWOFAS_KDF_ITERS, 32, derived_key); if (g_err != GPG_ERR_NO_ERROR) { g_printerr ("Failed to derive key: %s/%s\n", gcry_strsource (g_err), gcry_strerror (g_err)); gcry_free (derived_key); @@ -183,6 +326,24 @@ decrypt_data (const gchar **b64_data, } +static gchar * +get_encoded_data (guchar *enc_buf, + gsize enc_buf_len, + guchar *salt, + guchar *iv) +{ + gchar *payload = g_base64_encode (enc_buf, enc_buf_len); + gchar *encoded_salt = g_base64_encode (salt, TWOFAS_SALT); + gchar *encoded_iv = g_base64_encode (iv, TWOFAS_IV); + gchar *encoded_data = g_strconcat (payload, ":", encoded_salt, ":", encoded_iv, NULL); + g_free (payload); + g_free (encoded_salt); + g_free (encoded_iv); + + return encoded_data; +} + + static GSList * parse_twofas_json_data (const gchar *data, GError **err) diff --git a/src/parse-uri.c b/src/parse-uri.c index 6ec597cf..21a63ca8 100644 --- a/src/parse-uri.c +++ b/src/parse-uri.c @@ -108,7 +108,7 @@ get_otpauth_uri (AppData *app_data, g_free (constructed_label); g_free (escaped_label); - return g_string_free (uri, FALSE); + return g_string_free_and_steal (uri); } From 5f2db61f94caae0954280a64fe2d2748f3209edf Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Mon, 26 Feb 2024 15:27:44 +0100 Subject: [PATCH 15/20] Small fixes --- src/cli/exec-action.c | 2 +- src/common/common.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/exec-action.c b/src/cli/exec-action.c index 3626cb04..d9065833 100644 --- a/src/cli/exec-action.c +++ b/src/cli/exec-action.c @@ -120,7 +120,7 @@ gboolean exec_action (CmdlineOpts *cmdline_opts, } } exported_file_path = g_build_filename (export_directory, export_pwd != NULL ? "aegis_exports.json.aes" : "aegis_exports.json", NULL); - ret_msg = export_aegis (exported_file_path, db_data->json_data, export_pwd); + ret_msg = export_aegis (exported_file_path, export_pwd, db_data->json_data); gcry_free (export_pwd); exported = TRUE; } diff --git a/src/common/common.c b/src/common/common.c index 08c544e8..3144b192 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -473,7 +473,7 @@ get_data_from_encrypted_backup (const gchar *path, GFileInputStream *in_stream, GError **err) { - gint32 salt_size, iv_size, tag_size; + gint32 salt_size = 0, iv_size = 0, tag_size = 0; switch (provider) { case ANDOTP: salt_size = iv_size = ANDOTP_IV_SALT; @@ -513,7 +513,7 @@ get_data_from_encrypted_backup (const gchar *path, } gsize enc_buf_size; - gint32 offset; + gint32 offset = 0; switch (provider) { case ANDOTP: // 4 is the size of iterations (int32) @@ -553,7 +553,7 @@ get_data_from_encrypted_backup (const gchar *path, g_object_unref (in_stream); g_object_unref (in_file); - guchar *derived_key; + guchar *derived_key = NULL; switch (provider) { case ANDOTP: derived_key = get_andotp_derived_key (password, salt, andotp_be_iterations); From aadde96b62df7e4e839b8bf7062a6dafef5cb948 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Tue, 27 Feb 2024 14:32:35 +0100 Subject: [PATCH 16/20] Show warning when exporting to a plain format --- src/exports.c | 13 +++++++++---- src/message-dialogs.c | 14 +++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/exports.c b/src/exports.c index a08e6ec2..72c99fc6 100644 --- a/src/exports.c +++ b/src/exports.c @@ -5,10 +5,9 @@ #include "message-dialogs.h" #include "common/exports.h" - -static void show_ret_msg_dialog (GtkWidget *mainwin, - const gchar *fpath, - const gchar *ret_msg); +static void show_ret_msg_dialog (GtkWidget *mainwin, + const gchar *fpath, + const gchar *ret_msg); void @@ -35,6 +34,12 @@ export_data_cb (GSimpleAction *simple, return; } encrypted = TRUE; + } else { + const gchar *msg = "Please note that exporting to a plain format is a huge security risk.\n" + "If you wish to safely abort the operation, please click the 'Cancel' button below."; + if (get_confirmation_from_dialog (app_data->main_window, msg) == FALSE) { + return; + } } GtkFileChooserNative *fl_diag = gtk_file_chooser_native_new ("Export file", diff --git a/src/message-dialogs.c b/src/message-dialogs.c index 0780cc19..caadd261 100644 --- a/src/message-dialogs.c +++ b/src/message-dialogs.c @@ -23,20 +23,20 @@ get_confirmation_from_dialog (GtkWidget *parent, static GtkWidget *dialog = NULL; gboolean confirm; - dialog = gtk_dialog_new_with_buttons ("Confirm", GTK_WINDOW (parent), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + dialog = gtk_dialog_new_with_buttons ("Confirm", GTK_WINDOW(parent), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "OK", GTK_RESPONSE_OK, "Cancel", GTK_RESPONSE_CANCEL, NULL); - gtk_container_set_border_width (GTK_CONTAINER (dialog), 5); + gtk_container_set_border_width (GTK_CONTAINER(dialog), 5); - GtkWidget *content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + GtkWidget *content_area = gtk_dialog_get_content_area (GTK_DIALOG(dialog)); GtkWidget *label = gtk_label_new (NULL); - gtk_label_set_markup (GTK_LABEL (label), message); - gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_CENTER); - gtk_container_add (GTK_CONTAINER (content_area), label); + gtk_label_set_markup (GTK_LABEL(label), message); + gtk_label_set_justify (GTK_LABEL(label), GTK_JUSTIFY_CENTER); + gtk_container_add (GTK_CONTAINER(content_area), label); gtk_widget_show_all (dialog); - gint result = gtk_dialog_run (GTK_DIALOG (dialog)); + gint result = gtk_dialog_run (GTK_DIALOG(dialog)); switch (result) { case GTK_RESPONSE_OK: confirm = TRUE; From 08d49e31c971053515aa8e36b5f11cb2ea841082 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Tue, 27 Feb 2024 14:32:42 +0100 Subject: [PATCH 17/20] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b26e32c5..f03b8cb4 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ See this [wiki section](https://github.com/paolostivanin/OTPClient/wiki/Secure-M - import and export encrypted/plain [andOTP](https://github.com/flocke/andOTP) backup - import and export encrypted/plain [Aegis](https://github.com/beemdevelopment/Aegis) backup - import and export plain [FreeOTPPlus](https://github.com/helloworld1/FreeOTPPlus) backup (key URI format only) -- import and export encrypted [AuthenticatorPro](https://github.com/jamie-mh/AuthenticatorPro) backup -- import and export encrypted [2FAS](https://github.com/twofas) backup +- import and export encrypted/plain [AuthenticatorPro](https://github.com/jamie-mh/AuthenticatorPro) backup +- import and export encrypted/plain [2FAS](https://github.com/twofas) backup - import of Google's migration QR codes - local database is encrypted using AES256-GCM - key is derived using PBKDF2 with SHA512 and 100k iterations From e1683b91e275bb1245f2c2de028e92600d2fa711 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Tue, 27 Feb 2024 14:35:34 +0100 Subject: [PATCH 18/20] Update SECURITY.md --- SECURITY.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 5516eedf..15164b57 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,8 +6,10 @@ The following list describes whether a version is eligible or not for security u | Version | Supported | EOL | |---------|--------------------|-------------| -| 3.4.x | :white_check_mark: | - | -| 3.3.x | :white_check_mark: | 03-Mar-2024 | +| 3.5.x | :white_check_mark: | - | +| 3.4.1 | :white_check_mark: | 31-May-2024 | +| 3.4.0 | :x: | 29-Feb-2024 | +| 3.3.x | :x: | 29-Feb-2024 | | 3.2.x | :x: | 31-Jan-2024 | | 3.1.x | :x: | 30-Nov-2023 | | 3.0.x | :x: | 31-Dec-2022 | From 7ef0613795528f425b3cd36f5df480a068b35349 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Wed, 28 Feb 2024 09:09:20 +0100 Subject: [PATCH 19/20] Import fixes --- src/common/aegis.c | 16 ++++++++-------- src/common/andotp.c | 4 +--- src/common/authpro.c | 16 ++++++++-------- src/common/twofas.c | 16 ++++++++-------- src/imports.c | 3 +-- 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/common/aegis.c b/src/common/aegis.c index b47cf1ce..363c0c49 100644 --- a/src/common/aegis.c +++ b/src/common/aegis.c @@ -483,15 +483,15 @@ parse_aegis_json_data (const gchar *data, } if (!skip) { - otps = g_slist_append (otps, g_memdup2 (otp, sizeof (otp_t))); + otps = g_slist_append (otps, otp); + } else { + gcry_free (otp->secret); + g_free (otp->issuer); + g_free (otp->account_name); + g_free (otp->algo); + g_free (otp->type); + g_free (otp); } - - gcry_free (otp->secret); - g_free (otp->issuer); - g_free (otp->account_name); - g_free (otp->algo); - g_free (otp->type); - g_free (otp); } json_decref (root); diff --git a/src/common/andotp.c b/src/common/andotp.c index 0868d580..a827818b 100644 --- a/src/common/andotp.c +++ b/src/common/andotp.c @@ -314,9 +314,7 @@ parse_andotp_json_data (const gchar *data, json_decref (obj); return NULL; } - - otps = g_slist_append (otps, g_memdup2 (otp, sizeof (otp_t))); - g_free (otp); + otps = g_slist_append (otps, otp); } json_decref (array); diff --git a/src/common/authpro.c b/src/common/authpro.c index ed6fcdd3..39a67e8c 100644 --- a/src/common/authpro.c +++ b/src/common/authpro.c @@ -293,15 +293,15 @@ parse_authpro_json_data (const gchar *data, } if (!skip) { - otps = g_slist_append (otps, g_memdup2 (otp, sizeof (otp_t))); + otps = g_slist_append (otps, otp); + } else { + gcry_free (otp->secret); + g_free (otp->issuer); + g_free (otp->account_name); + g_free (otp->algo); + g_free (otp->type); + g_free (otp); } - - gcry_free (otp->secret); - g_free (otp->issuer); - g_free (otp->account_name); - g_free (otp->algo); - g_free (otp->type); - g_free (otp); } json_decref (root); diff --git a/src/common/twofas.c b/src/common/twofas.c index dc9c0b9c..74fbe054 100644 --- a/src/common/twofas.c +++ b/src/common/twofas.c @@ -396,15 +396,15 @@ parse_twofas_json_data (const gchar *data, } if (!skip) { - otps = g_slist_append (otps, g_memdup2 (otp, sizeof (otp_t))); + otps = g_slist_append (otps, otp); + } else { + gcry_free (otp->secret); + g_free (otp->issuer); + g_free (otp->account_name); + g_free (otp->algo); + g_free (otp->type); + g_free (otp); } - - gcry_free (otp->secret); - g_free (otp->issuer); - g_free (otp->account_name); - g_free (otp->algo); - g_free (otp->type); - g_free (otp); } json_decref (array); diff --git a/src/imports.c b/src/imports.c index b2aae7de..881be297 100644 --- a/src/imports.c +++ b/src/imports.c @@ -83,8 +83,7 @@ free_otps_gslist (GSList *otps, g_free (otp_data->issuer); gcry_free (otp_data->secret); } - - g_slist_free_full (otps, g_free); + g_slist_free (otps); } From 3d1e2706316f836d5426b57997ef2e36a7442e37 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Wed, 28 Feb 2024 11:18:33 +0100 Subject: [PATCH 20/20] Fix 2FAS export --- src/common/twofas.c | 52 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/common/twofas.c b/src/common/twofas.c index 74fbe054..71c353f4 100644 --- a/src/common/twofas.c +++ b/src/common/twofas.c @@ -35,6 +35,10 @@ static gchar *get_encoded_data (guchar *enc_buf, guchar *salt, guchar *iv); +static gchar *get_reference_data (guchar *derived_key, + guchar *salt, + guchar *iv); + static GSList *parse_twofas_json_data (const gchar *data, GError **err); @@ -53,9 +57,6 @@ export_twofas (const gchar *export_path, const gchar *password, json_t *json_db_data) { - // TODO: create the otps json (services => array(secret, - // order => object(position=0++), - // icon=null), GError *err = NULL; json_t *root = json_object (); json_t *services_array = json_array (); @@ -80,6 +81,7 @@ export_twofas (const gchar *export_path, json_object_set (otp_obj, "issuer", json_string (issuer)); } } + json_object_set (export_obj, "secret", json_object_get (db_obj, "secret")); const gchar *label = json_string_value (json_object_get (db_obj, "label")); if (label != NULL) { json_object_set (otp_obj, "label", json_string (label)); @@ -87,10 +89,10 @@ export_twofas (const gchar *export_path, } gchar *algo = g_ascii_strup (json_string_value (json_object_get (db_obj, "algo")), -1); - json_object_set (otp_obj, "algorithm", json_object_get (db_obj, "digits")); + json_object_set (otp_obj, "algorithm", json_string (algo)); g_free (algo); - json_object_set (otp_obj, "digits", json_string (algo)); + json_object_set (otp_obj, "digits", json_object_get (db_obj, "digits")); json_object_set (otp_obj, "source", json_string ("Link")); gchar *otpauth_uri = secure_strdup (get_otpauth_uri (NULL, db_obj)); json_object_set (otp_obj, "link", json_string (otpauth_uri)); @@ -105,8 +107,8 @@ export_twofas (const gchar *export_path, } json_object_set (order_obj, "position", json_integer ((json_int_t)index)); - json_object_set (export_obj, "order", order_obj); json_object_set (export_obj, "otp", otp_obj); + json_object_set (export_obj, "order", order_obj); json_object_set (export_obj, "icon", json_null()); json_array_append (services_array, export_obj); @@ -158,16 +160,20 @@ export_twofas (const gchar *export_path, goto end; } // TWOFAS ignores the tag, so we don't have to append it (sigh!) - gcry_free (derived_key); gcry_cipher_close (hd); json_t *enc_root = json_object (); json_object_set (enc_root, "services", json_array ()); json_object_set (enc_root, "groups", json_array()); json_object_set (enc_root, "schemaVersion", json_integer (4)); - json_object_set (enc_root, "reference", json_null ()); gchar *encoded_data = get_encoded_data (enc_buf, json_data_size, salt, iv); json_object_set (enc_root, "servicesEncrypted", json_string (encoded_data)); + gchar *encoded_ref_data = get_reference_data (derived_key, salt, iv); + if (encoded_ref_data == NULL) { + g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't encrypt the reference data."); + goto enc_end; + } + json_object_set (enc_root, "reference", json_string (encoded_ref_data)); gchar *json_enc_data = json_dumps (enc_root, JSON_COMPACT); if (json_enc_data == NULL) { g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "Couldn't dump json data"); @@ -179,6 +185,7 @@ export_twofas (const gchar *export_path, gcry_free (json_enc_data); enc_end: + gcry_free (derived_key); g_free (iv); g_free (salt); g_free (encoded_data); @@ -344,6 +351,35 @@ get_encoded_data (guchar *enc_buf, } +static gchar * +get_reference_data (guchar *derived_key, + guchar *salt, + guchar *iv) +{ + // This is taken from https://github.com/twofas/2fas-android/blob/main/data/services/src/main/java/com/twofasapp/data/services/domain/BackupContent.kt + const gchar *reference = "tRViSsLKzd86Hprh4ceC2OP7xazn4rrt4xhfEUbOjxLX8Rc3mkISXE0lWbmnWfggogbBJhtYgpK6fMl1D6mtsy92R3HkdGfwuXbzLebqVFJsR7IZ2w58t938iymwG4824igYy1wi6n2WDpO1Q1P69zwJGs2F5a1qP4MyIiDSD7NCV2OvidXQCBnDlGfmz0f1BQySRkkt4ryiJeCjD2o4QsveJ9uDBUn8ELyOrESv5R5DMDkD4iAF8TXU7KyoJujd"; + gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, iv, TWOFAS_IV); + if (hd == NULL) { + g_printerr ("Failed to open the cipher to encrypt the reference data.\n"); + return NULL; + } + gsize buf_size = strlen (reference); + guchar *enc_ref_buf = gcry_calloc (buf_size, 1); + gpg_error_t gpg_err = gcry_cipher_encrypt (hd, enc_ref_buf, buf_size, reference, buf_size); + if (gpg_err != GPG_ERR_NO_ERROR) { + g_printerr ("Failed to encrypt the data: %s/%s\n", gcry_strsource (gpg_err), gcry_strerror (gpg_err)); + gcry_free (enc_ref_buf); + gcry_cipher_close (hd); + return NULL; + } + gchar *encoded_data = get_encoded_data (enc_ref_buf, buf_size, salt, iv); + gcry_free (enc_ref_buf); + gcry_cipher_close (hd); + + return encoded_data; +} + + static GSList * parse_twofas_json_data (const gchar *data, GError **err)