diff --git a/.circleci/config.yml b/.circleci/config.yml index 1fa70338..09807970 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,9 +1,9 @@ version: 2.0 jobs: - ubuntu2004: + ubuntu2304: docker: - - image: ubuntu:20.04 + - image: ubuntu:23.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 + - ubuntu2304 - ubuntuLatestRolling - debianLatestStable - fedoraLatestStable diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bb62692..7bdcf854 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") @@ -44,14 +44,14 @@ 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) 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) @@ -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..f03b8cb4 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 | @@ -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/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 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 | diff --git a/data/com.github.paolostivanin.OTPClient.appdata.xml b/data/com.github.paolostivanin.OTPClient.appdata.xml index ba2dd639..f851dea6 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 @@ -89,6 +89,20 @@ + + +

OTPClient 3.5.0 brings some new features and improvements:

+
    +
  • NEW: add support for importing and exporting plain/encrypted 2FAS backups (#322)
  • +
  • NEW: add support for importing and exporting plain/encrypted AuthenticatorPro backups (#322)
  • +
  • CHANGE: show warning when exporting a plain backup
  • +
  • CHANGE: remove support for older Glib and GCrypt
  • +
  • FIX: add 2fa keyword to the desktop file (#349)
  • +
  • FIX: remove custom keywords from metadata file (#348)
  • +
  • FIX: returning to a dialog won't crash the widget
  • +
+
+

OTPClient 3.4.1 brings a single fix::

@@ -561,8 +575,4 @@
- - workstation - mobile - 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 diff --git a/flatpak/com.github.paolostivanin.OTPClient.yaml b/flatpak/com.github.paolostivanin.OTPClient.yaml index 5c78ddc7..4cba5f7c 100644 --- a/flatpak/com.github.paolostivanin.OTPClient.yaml +++ b/flatpak/com.github.paolostivanin.OTPClient.yaml @@ -1,11 +1,11 @@ app-id: com.github.paolostivanin.OTPClient runtime: org.gnome.Platform -runtime-version: '44' +runtime-version: '45' sdk: org.gnome.Sdk command: otpclient finish-args: - "--share=ipc" -- "--socket=x11" +- "--socket=fallback-x11" - "--socket=wayland" - "--device=all" - "--talk-name=org.freedesktop.secrets" @@ -42,8 +42,8 @@ modules: - "/lib/*.la" sources: - type: archive - url: https://github.com/protobuf-c/protobuf-c/archive/refs/tags/v1.4.1.tar.gz - sha256: 99be336cdb15dfc5827efe34e5ac9aaa962e2485db547dd254d2a122a7d23102 + url: https://github.com/protobuf-c/protobuf-c/archive/refs/tags/v1.5.0.tar.gz + sha256: 7b404c63361ed35b3667aec75cc37b54298d56dd2bcf369de3373212cc06fd98 - name: qrencode buildsystem: cmake-ninja config-opts: @@ -66,8 +66,8 @@ modules: - "--enable-codes=qrcode" sources: - type: archive - url: https://www.linuxtv.org/downloads/zbar/zbar-0.23.90.tar.gz - sha256: ff857dd7e3dbe043dac3765b5182c91dfd0477800713a75d15287d797cee60fa + url: https://www.linuxtv.org/downloads/zbar/zbar-0.23.93.tar.gz + sha256: 78ae427a529f0399561bc198de5c2c7ca3f11d05fa9e903e65e501168433d218 - name: libcotp buildsystem: cmake-ninja config-opts: @@ -77,8 +77,8 @@ modules: - "/include" sources: - type: archive - url: https://github.com/paolostivanin/libcotp/archive/v2.0.1.tar.gz - sha256: b111d528bbde7c1a0a392f49293b25ae33e6e78fbcbe378e0cf8bc6d59743d11 + url: https://github.com/paolostivanin/libcotp/archive/v3.0.0.tar.gz + sha256: ff0b9ce208c4c6542a0f1e739cf31978fbf28848c573837c671a6cb7b56b2c12 - name: OTPClient buildsystem: cmake-ninja config-opts: diff --git a/src/app.c b/src/app.c index 18970834..33ec1aed 100644 --- a/src/app.c +++ b/src/app.c @@ -536,11 +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_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 }, @@ -601,6 +609,8 @@ get_db_path (AppData *app_data) gchar *msg = g_strconcat ("Database file/location:\n", db_path, "\ndoes not exist. A new database will be created.", NULL); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); g_free (msg); + g_free (db_path); + db_path = NULL; goto new_db; } goto end; diff --git a/src/change-db-cb.c b/src/change-db-cb.c index a1afa16f..d2edee83 100644 --- a/src/change-db-cb.c +++ b/src/change-db-cb.c @@ -60,10 +60,9 @@ change_db (AppData *app_data) g_free (old_db_path); break; case GTK_RESPONSE_CANCEL: - gtk_widget_destroy (changedb_diag); - return QUIT_APP; default: - break; + gtk_widget_hide (changedb_diag); + return QUIT_APP; } gtk_widget_destroy (changedb_diag); 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/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 3bb6953b..363c0c49 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); @@ -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); } @@ -53,8 +52,8 @@ get_otps_from_plain_backup (const gchar *path, return NULL; } - gchar *dumped_json = json_dumps(json_object_get (json, "db"), 0); - GSList *otps = parse_json_data (dumped_json, err); + gchar *dumped_json = json_dumps (json_object_get (json, "db"), 0); + GSList *otps = parse_aegis_json_data (dumped_json, err); gcry_free (dumped_json); return otps; @@ -204,7 +203,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; @@ -213,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 (); @@ -422,8 +421,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 +450,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 +468,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,16 +478,20 @@ 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"); + g_printerr ("Skipping token due to unsupported algo: %s\n", algo); + skip = TRUE; + } + + if (!skip) { + 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); - json_decref (obj); - json_decref (info_obj); - return NULL; } - - otps = g_slist_append (otps, g_memdupX (otp, sizeof (otp_t))); - g_free (otp); } json_decref (root); diff --git a/src/common/andotp.c b/src/common/andotp.c index 2b6711ae..a827818b 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); @@ -37,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); @@ -47,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); } @@ -75,95 +70,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); - 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); + 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; } - - 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 +91,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 +173,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); + 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 +211,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 +239,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); @@ -419,9 +314,7 @@ parse_json_data (const gchar *data, json_decref (obj); return NULL; } - - otps = g_slist_append (otps, g_memdupX (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 new file mode 100644 index 00000000..14172ecc --- /dev/null +++ b/src/common/authpro.c @@ -0,0 +1,312 @@ +#include +#include +#include +#include "common.h" +#include "../gquarks.h" +#include "../imports.h" + +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 GSList *parse_authpro_json_data (const gchar *data, + 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 (password != NULL) ? get_otps_from_encrypted_backup (path, password, max_file_size, in_file, in_stream, err) : get_otps_from_plain_backup (path, err); +} + + +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; + gboolean is_steam = FALSE; + 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")); + is_steam = TRUE; + } 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", is_steam ? json_integer (4) : 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, + gint32 max_file_size, + GFile *in_file, + GFileInputStream *in_stream, + GError **err) +{ + 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 * +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) +{ + 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, 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); + } + } + + json_decref (root); + + return otps; +} diff --git a/src/common/common.c b/src/common/common.c index a969590d..61d948a3 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) @@ -173,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) { @@ -448,3 +408,195 @@ get_kf_ptr (void) g_key_file_free (kf); return NULL; } + + +guchar * +get_authpro_derived_key (const gchar *password, + const guchar *salt) +{ + 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, AUTHPRO_SALT_TAG, + NULL, 0, NULL, 0) != GPG_ERR_NO_ERROR) { + g_printerr ("Error while opening the KDF handler\n"); + gcry_free (derived_key); + 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) +{ + 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, 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); + 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 = 0, iv_size = 0, tag_size = 0; + switch (provider) { + case ANDOTP: + salt_size = iv_size = ANDOTP_IV_SALT; + tag_size = ANDOTP_TAG; + break; + case AUTHPRO: + salt_size = tag_size = AUTHPRO_SALT_TAG; + iv_size = AUTHPRO_IV; + 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 = 0; + switch (provider) { + case ANDOTP: + // 4 is the size of iterations (int32) + offset = 4; + break; + case AUTHPRO: + // 16 is the size of the header + offset = 16; + 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); + 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 = NULL; + switch (provider) { + case ANDOTP: + derived_key = get_andotp_derived_key (password, salt, andotp_be_iterations); + break; + case AUTHPRO: + derived_key = get_authpro_derived_key (password, salt); + 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); + 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..9b931e03 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -3,18 +3,22 @@ #include #include #include +#include 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 +#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); gchar *init_libs (gint32 max_file_size); @@ -37,11 +41,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); @@ -54,4 +53,20 @@ 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); + +guchar *get_authpro_derived_key (const gchar *password, + const guchar *salt); + +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 diff --git a/src/common/exports.h b/src/common/exports.h index 0f3023ce..dca6beb6 100644 --- a/src/common/exports.h +++ b/src/common/exports.h @@ -10,6 +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_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, @@ -24,7 +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, + const gchar *password, + json_t *json_db_data); + +gchar *export_twofas (const gchar *export_path, + const gchar *password, + json_t *json_db_data); G_END_DECLS diff --git a/src/common/get-providers-data.h b/src/common/get-providers-data.h index df7fce13..7ff9160b 100644 --- a/src/common/get-providers-data.h +++ b/src/common/get-providers-data.h @@ -4,19 +4,26 @@ 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, + 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, + 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, + GError **err); G_END_DECLS diff --git a/src/common/twofas.c b/src/common/twofas.c new file mode 100644 index 00000000..f65645d3 --- /dev/null +++ b/src/common/twofas.c @@ -0,0 +1,487 @@ +#include +#include +#include +#include +#include "common.h" +#include "../gquarks.h" +#include "../imports.h" + +#define TWOFAS_KDF_ITERS 10000 +#define TWOFAS_SALT 256 +#define TWOFAS_IV 12 +#define TWOFAS_TAG 16 + +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, + 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 gchar *get_encoded_data (guchar *enc_buf, + gsize enc_buf_len, + guchar *salt, + guchar *iv); + +static gchar *get_reference_data (guchar *derived_key, + guchar *salt); + +static GSList *parse_twofas_json_data (const gchar *data, + GError **err); + + +GSList * +get_twofas_data (const gchar *path, + const gchar *password, + GError **err) +{ + return (password != NULL) ? get_otps_from_encrypted_backup (path, password, err) : get_otps_from_plain_backup (path, err); +} + + +gchar * +export_twofas (const gchar *export_path, + const gchar *password, + json_t *json_db_data) +{ + GError *err = NULL; + gint64 epoch_time = g_get_real_time(); + + 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, "updatedAt", json_integer (epoch_time)); + 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)); + } + } + json_object_set (export_obj, "updatedAt", json_integer (epoch_time)); + 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)); + 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_string (algo)); + g_free (algo); + + json_object_set (otp_obj, "digits", json_object_get (db_obj, "digits")); + json_object_set (otp_obj, "source", json_string ("Manual")); + + 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, "otp", otp_obj); + json_object_set (export_obj, "order", order_obj); + + 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 = g_malloc0 (json_data_size); + 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); + g_free (enc_buf); + g_free (iv); + g_free (salt); + gcry_cipher_close (hd); + goto end; + } + guchar tag[TWOFAS_TAG]; + gcry_cipher_gettag (hd, tag, TWOFAS_TAG); + gcry_cipher_close (hd); + + guchar *enc_data_with_tag = g_malloc0 (json_data_size + TWOFAS_TAG); + memcpy (enc_data_with_tag, enc_buf, json_data_size); + memcpy (enc_data_with_tag+json_data_size, tag, TWOFAS_TAG); + g_free (enc_buf); + + 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)); + gchar *encoded_data = get_encoded_data (enc_data_with_tag, json_data_size + TWOFAS_TAG, salt, iv); + json_object_set (enc_root, "servicesEncrypted", json_string (encoded_data)); + gchar *encoded_ref_data = get_reference_data (derived_key, salt); + 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"); + 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 (enc_data_with_tag); + gcry_free (derived_key); + 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); +} + + +static GSList * +get_otps_from_encrypted_backup (const gchar *path, + const gchar *password, + GError **err) +{ + if (!is_schema_supported (path)) { + return NULL; + } + + 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) { + otps = parse_twofas_json_data (twofas_data->json_data, err); + gcry_free (twofas_data->json_data); + } + g_strfreev (b64_encoded_data); + g_free (twofas_data->salt); + g_free (twofas_data->iv); + g_free (twofas_data); + json_decref (root); + + return otps; +} + + +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) +{ + // TWOFAS ignores the tag, so we don't have to check it (sigh!) + gsize enc_data_with_tag_size, salt_out_len, iv_out_len; + guchar *enc_data_with_tag = g_base64_decode (b64_data[0], &enc_data_with_tag_size); + 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 tag[TWOFAS_TAG]; + gsize enc_buf_size = enc_data_with_tag_size - TWOFAS_TAG; + guchar *enc_data = g_malloc0 (enc_buf_size); + memcpy (enc_data, enc_data_with_tag, enc_buf_size); + memcpy (tag, enc_data_with_tag+enc_buf_size, TWOFAS_TAG); + g_free (enc_data_with_tag); + + 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, 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); + 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) { + gcry_free (derived_key); + g_free (enc_data); + return; + } + + twofas_data->json_data = gcry_calloc_secure (enc_buf_size, 1); + gpg_error_t gpg_err = gcry_cipher_decrypt (hd, twofas_data->json_data, enc_buf_size, enc_data, enc_buf_size); + if (gpg_err) { + g_printerr ("Failed to decrypt data: %s/%s\n", gcry_strsource (g_err), gcry_strerror (g_err)); + gcry_free (derived_key); + g_free (enc_data); + gcry_cipher_close (hd); + return; + } + + gpg_err = gcry_cipher_checktag (hd, tag, TWOFAS_TAG); + if (gpg_err) { + g_printerr ("Failed to verify the tag: %s/%s\n", gcry_strsource (g_err), gcry_strerror (g_err)); + } + + gcry_cipher_close (hd); + gcry_free (derived_key); + g_free (enc_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 gchar * +get_reference_data (guchar *derived_key, + guchar *salt) +{ + // 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"; + + // 2FAS requires a new IV for this reference data + guchar *iv = g_malloc0 (TWOFAS_IV); + gcry_create_nonce (iv, TWOFAS_IV); + + 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 = g_malloc0 (buf_size); + 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)); + g_free (enc_ref_buf); + gcry_cipher_close (hd); + return NULL; + } + guchar tag[TWOFAS_TAG]; + gcry_cipher_gettag (hd, tag, TWOFAS_TAG); + gcry_cipher_close (hd); + + gsize enc_data_with_tag_size = buf_size + TWOFAS_TAG; + guchar *enc_data_with_tag = g_malloc0 (enc_data_with_tag_size); + memcpy (enc_data_with_tag, enc_ref_buf, buf_size); + memcpy (enc_data_with_tag+buf_size, tag, TWOFAS_TAG); + g_free (enc_ref_buf); + + gchar *encoded_data = get_encoded_data (enc_data_with_tag, enc_data_with_tag_size, salt, iv); + g_free (enc_data_with_tag); + g_free (iv); + + return encoded_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, 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); + } + } + + json_decref (array); + + return otps; +} 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/exports.c b/src/exports.c index 09f83a4d..4fa68e55 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 @@ -26,11 +25,21 @@ export_data_cb (GSimpleAction *simple, base_dir = g_get_user_data_dir (); #endif - gboolean encrypted; - if ((g_strcmp0 (action_name, "export_andotp") == 0) || (g_strcmp0 (action_name, "export_aegis") == 0)) { + 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; + 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", @@ -49,6 +58,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; @@ -65,35 +78,28 @@ export_data_cb (GSimpleAction *simple, if (export_file_abs_path == NULL) { show_message_dialog (app_data->main_window, "Invalid export file name/path.", GTK_MESSAGE_ERROR); + if (encrypted == TRUE) { + gcry_free (password); + } return; } - gchar *password = NULL, *ret_msg = NULL; + gchar *ret_msg = NULL; if (g_strcmp0 (action_name, ANDOTP_EXPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, ANDOTP_EXPORT_PLAIN_ACTION_NAME) == 0) { - if (encrypted == TRUE) { - password = prompt_for_password (app_data, NULL, NULL, TRUE); - if (password == NULL) { - return; - } - } 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) { diff --git a/src/imports.c b/src/imports.c index 8e59bb31..881be297 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"); @@ -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); } @@ -97,7 +96,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; @@ -105,11 +105,15 @@ 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) { + content = get_twofas_data (filename, pwd, &err); } if (content == NULL) { diff --git a/src/imports.h b/src/imports.h index f5e74e9b..f1e666ca 100644 --- a/src/imports.h +++ b/src/imports.h @@ -10,6 +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_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/message-dialogs.c b/src/message-dialogs.c index 0780cc19..ccdfd945 100644 --- a/src/message-dialogs.c +++ b/src/message-dialogs.c @@ -23,20 +23,21 @@ 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); + gtk_box_set_spacing(GTK_BOX(gtk_dialog_get_content_area (GTK_DIALOG(dialog))), 8); - 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; diff --git a/src/new-db-cb.c b/src/new-db-cb.c index e4cda812..8019828c 100644 --- a/src/new-db-cb.c +++ b/src/new-db-cb.c @@ -43,6 +43,7 @@ new_db (AppData *app_data) if (app_data->db_data->key == NULL) { gtk_widget_hide (newdb_diag); revert_db_path (app_data, old_db_path); + g_string_free (new_db_path_with_suffix, TRUE); return RETRY_CHANGE; } secret_password_store (OTPCLIENT_SCHEMA, SECRET_COLLECTION_DEFAULT, "main_pwd", app_data->db_data->key, NULL, on_password_stored, NULL, "string", "main_pwd", NULL); @@ -53,6 +54,7 @@ new_db (AppData *app_data) g_clear_error (&err); gtk_widget_hide (newdb_diag); revert_db_path (app_data, old_db_path); + g_string_free (new_db_path_with_suffix, TRUE); return RETRY_CHANGE; } load_new_db (app_data, &err); @@ -61,6 +63,7 @@ new_db (AppData *app_data) g_clear_error (&err); gtk_widget_hide (newdb_diag); revert_db_path (app_data, old_db_path); + g_string_free (new_db_path_with_suffix, TRUE); return RETRY_CHANGE; } g_free (old_db_path); @@ -68,10 +71,9 @@ new_db (AppData *app_data) g_string_free (new_db_path_with_suffix, TRUE); break; case GTK_RESPONSE_CANCEL: - gtk_widget_destroy (newdb_diag); - return QUIT_APP; default: - break; + gtk_widget_hide (newdb_diag); + return QUIT_APP; } gtk_widget_destroy (newdb_diag); 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); } 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