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
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..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
-
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/src/app.c b/src/app.c
index 18970834..475b0038 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 },
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..39a67e8c
--- /dev/null
+++ b/src/common/authpro.c
@@ -0,0 +1,310 @@
+#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;
+ 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,
+ 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..3144b192 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,194 @@ 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");
+ 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..a56e7c7c 100644
--- a/src/common/common.h
+++ b/src/common/common.h
@@ -3,18 +3,25 @@
#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
+
+#define TWOFAS_SALT 256
+#define TWOFAS_IV 12
+
gint32 get_max_file_size_from_memlock (void);
gchar *init_libs (gint32 max_file_size);
@@ -37,11 +44,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 +56,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..71c353f4
--- /dev/null
+++ b/src/common/twofas.c
@@ -0,0 +1,449 @@
+#include
+#include
+#include
+#include
+#include "common.h"
+#include "../gquarks.h"
+#include "../imports.h"
+#include "../parse-uri.h"
+
+#define TWOFAS_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,
+ 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,
+ guchar *iv);
+
+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;
+ 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));
+ }
+ }
+ 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 ("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, "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);
+ }
+
+ 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_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));
+ 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");
+ 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:
+ 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 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, 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 (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) {
+ g_printerr ("Failed to decrypt data: %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,
+ 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)
+{
+ 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;
+}
\ No newline at end of file
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..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
@@ -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;
@@ -68,32 +81,22 @@ export_data_cb (GSimpleAction *simple,
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..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;
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..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);
}
@@ -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_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