From 268f2445d7b614f3b95f8f17708f43de03f42415 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Fri, 15 Feb 2019 07:33:20 +0100 Subject: [PATCH 1/9] Add andOTP export feature - fix #43 --- CMakeLists.txt | 10 +++-- src/andotp.c | 96 ++++++++++++++++++++++++++++++++++++++++----- src/app.c | 7 ++-- src/exports.c | 58 +++++++++++++++++++++++++++ src/exports.h | 22 +++++++++++ src/imports.c | 12 +++--- src/imports.h | 5 ++- src/otpclient.h | 2 +- src/password-cb.c | 9 +++-- src/password-cb.h | 3 +- src/ui/otpclient.ui | 27 ++++++++++++- 11 files changed, 221 insertions(+), 30 deletions(-) create mode 100644 src/exports.c create mode 100644 src/exports.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 89fa975..81d6630 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) option(USE_FLATPAK_APP_FOLDER "Use flatpak app's config folder to store the database" OFF) set(OTPCLIENT_VERSION_MAJOR "1") -set(OTPCLIENT_VERSION_MINOR "3") -set(OTPCLIENT_VERSION_PATCH "1") +set(OTPCLIENT_VERSION_MINOR "4") +set(OTPCLIENT_VERSION_PATCH "0") set(OTPCLIENT_VERSION "${OTPCLIENT_VERSION_MAJOR}.${OTPCLIENT_VERSION_MINOR}.${OTPCLIENT_VERSION_PATCH}") set(CMAKE_C_STANDARD 11) @@ -83,7 +83,8 @@ set(HEADER_FILES src/parse-uri.h src/password-cb.h src/qrcode-parser.h - src/treeview.h) + src/treeview.h + src/exports.h) set(SOURCE_FILES src/add-common.c @@ -110,7 +111,8 @@ set(SOURCE_FILES src/settings.c src/shortcuts.c src/treeview.c - src/webcam-add-cb.c) + src/webcam-add-cb.c + src/exports.c) add_executable(${PROJECT_NAME} ${SOURCE_FILES} ${HEADER_FILES}) target_link_libraries(${PROJECT_NAME} diff --git a/src/andotp.c b/src/andotp.c index b294cbd..67b9181 100644 --- a/src/andotp.c +++ b/src/andotp.c @@ -7,8 +7,8 @@ #include "common.h" #include "gquarks.h" -#define ANDOTP_IV_SIZE 12 -#define TAG_SIZE 16 +#define ANDOTP_IV_SIZE 12 +#define ANDOTP_TAG_SIZE 16 static guchar *get_sha256 (const gchar *password); @@ -21,8 +21,6 @@ get_andotp_data (const gchar *path, gint32 max_file_size, GError **err) { - gcry_cipher_hd_t hd; - GFile *in_file = g_file_new_for_path (path); GFileInputStream *in_stream = g_file_read (in_file, NULL, err); if (*err != NULL) { @@ -38,19 +36,19 @@ get_andotp_data (const gchar *path, } goffset input_file_size = get_file_size (path); - guchar tag[TAG_SIZE]; - if (!g_seekable_seek (G_SEEKABLE (in_stream), input_file_size - TAG_SIZE, G_SEEK_SET, NULL, err)) { + 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, TAG_SIZE, NULL, err) == -1) { + 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; } - gsize enc_buf_size = (gsize) input_file_size - ANDOTP_IV_SIZE - TAG_SIZE; + gsize enc_buf_size = (gsize) input_file_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); @@ -81,13 +79,14 @@ get_andotp_data (const gchar *path, guchar *hashed_key = get_sha256 (password); + gcry_cipher_hd_t hd; gcry_cipher_open (&hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_GCM, GCRY_CIPHER_SECURE); gcry_cipher_setkey (hd, hashed_key, gcry_cipher_get_algo_keylen (GCRY_CIPHER_AES256)); gcry_cipher_setiv (hd, iv, ANDOTP_IV_SIZE); gchar *decrypted_json = gcry_calloc_secure (enc_buf_size, 1); gcry_cipher_decrypt (hd, decrypted_json, enc_buf_size, enc_buf, enc_buf_size); - if (gcry_err_code (gcry_cipher_checktag (hd, tag, TAG_SIZE)) == GPG_ERR_CHECKSUM) { + 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); gcry_free (hashed_key); @@ -106,6 +105,85 @@ get_andotp_data (const gchar *path, } +gchar * +export_andotp ( const gchar *export_path, + const gchar *password, + json_t *json_db_data) +{ + json_t *array = json_array (); + json_t *db_obj, *export_obj; + gsize index; + json_array_foreach (json_db_data, index, db_obj) { + export_obj = json_object (); + json_object_set (export_obj, "type", json_object_get (db_obj, "type")); + gchar *constructed_label = g_strconcat (json_string_value (json_object_get (db_obj, "issuer")), + " - ", + json_string_value (json_object_get (db_obj, "label")), + NULL); + json_object_set (export_obj, "label", json_string (constructed_label)); + g_free (constructed_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, "algorithm", json_object_get (db_obj, "algo")); + if (g_strcmp0 (json_string_value (json_object_get (db_obj, "type")), "TOTP") == 0) { + json_object_set (export_obj, "period", json_object_get (db_obj, "period")); + } else { + json_object_set (export_obj, "counter", json_object_get (db_obj, "counter")); + } + json_array_append (array, export_obj); + } + gchar *json_data = json_dumps (array, JSON_COMPACT); + if (json_data == NULL) { + json_array_clear (array); + return g_strdup ("couldn't dump json data"); + } + gsize json_data_size = g_utf8_strlen (json_data, -1); + + gcry_cipher_hd_t hd; + gcry_cipher_open (&hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_GCM, GCRY_CIPHER_SECURE); + guchar *hashed_key = get_sha256 (password); + gcry_cipher_setkey (hd, hashed_key, gcry_cipher_get_algo_keylen (GCRY_CIPHER_AES256)); + guchar *iv = g_malloc0 (ANDOTP_IV_SIZE); + gcry_create_nonce (iv, ANDOTP_IV_SIZE); + gcry_cipher_setiv (hd, iv, ANDOTP_IV_SIZE); + + gchar *enc_buf = gcry_calloc_secure (json_data_size, 1); + gcry_cipher_encrypt (hd, enc_buf, json_data_size, json_data, json_data_size); + guchar tag[ANDOTP_TAG_SIZE]; + gcry_cipher_gettag (hd, tag, ANDOTP_TAG_SIZE); + gcry_cipher_close (hd); + + GError *err = NULL; + GFile *out_gfile = g_file_new_for_path (export_path); + GFileOutputStream *out_stream = g_file_append_to (out_gfile, G_FILE_CREATE_REPLACE_DESTINATION, NULL, &err); + if (err != NULL) { + goto cleanup_before_exiting; + } + g_output_stream_write (G_OUTPUT_STREAM (out_stream), iv, ANDOTP_IV_SIZE, NULL, &err); + if (err != NULL) { + goto cleanup_before_exiting; + } + g_output_stream_write (G_OUTPUT_STREAM (out_stream), enc_buf, json_data_size, NULL, &err); + if (err != NULL) { + goto cleanup_before_exiting; + } + g_output_stream_write (G_OUTPUT_STREAM (out_stream), tag, ANDOTP_TAG_SIZE, NULL, &err); + if (err != NULL) { + goto cleanup_before_exiting; + } + + cleanup_before_exiting: + g_free (iv); + gcry_free (json_data); + gcry_free (enc_buf); + json_array_clear (array); + g_object_unref (out_stream); + g_object_unref (out_gfile); + + return (err != NULL ? g_strdup (err->message) : NULL); +} + + static guchar * get_sha256 (const gchar *password) { diff --git a/src/app.c b/src/app.c index 3845bb4..54a0dc2 100644 --- a/src/app.c +++ b/src/app.c @@ -5,6 +5,7 @@ #include "common.h" #include "gquarks.h" #include "imports.h" +#include "exports.h" #include "message-dialogs.h" #include "password-cb.h" #include "get-builder.h" @@ -119,7 +120,7 @@ activate (GtkApplication *app, app_data->db_data->last_hotp_update = g_date_time_add_seconds (g_date_time_new_now_local (), -(G_TIME_SPAN_SECOND * HOTP_RATE_LIMIT_IN_SEC)); retry: - app_data->db_data->key = prompt_for_password (app_data, NULL, NULL); + app_data->db_data->key = prompt_for_password (app_data, NULL, NULL, FALSE); if (app_data->db_data->key == NULL) { g_free (app_data->db_data); g_application_quit (G_APPLICATION(app)); @@ -222,7 +223,7 @@ set_action_group (GtkBuilder *builder, static GActionEntry settings_menu_entries[] = { { .name = ANDOTP_IMPORT_ACTION_NAME, .activate = select_file_cb }, { .name = AUTHPLUS_IMPORT_ACTION_NAME, .activate = select_file_cb }, - { .name = "export", .activate = NULL }, + { .name = ANDOTP_EXPORT_ACTION_NAME, .activate = export_data_cb }, { .name = "change_pwd", .activate = change_password_cb }, { .name = "edit_row", .activate = edit_selected_row_cb }, { .name = "settings", .activate = settings_dialog_cb }, @@ -357,7 +358,7 @@ change_password_cb (GSimpleAction *simple __attribute__((unused)), { AppData *app_data = (AppData *)user_data; gchar *tmp_key = secure_strdup (app_data->db_data->key); - gchar *pwd = prompt_for_password (app_data, tmp_key, NULL); + gchar *pwd = prompt_for_password (app_data, tmp_key, NULL, FALSE); if (pwd != NULL) { app_data->db_data->key = pwd; GError *err = NULL; diff --git a/src/exports.c b/src/exports.c new file mode 100644 index 0000000..67dbf78 --- /dev/null +++ b/src/exports.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include "password-cb.h" +#include "message-dialogs.h" +#include "gquarks.h" +#include "db-misc.h" +#include "exports.h" + + +void +export_data_cb (GSimpleAction *simple, + GVariant *parameter __attribute__((unused)), + gpointer user_data) +{ + const gchar *action_name = g_action_get_name (G_ACTION(simple)); + AppData *app_data = (AppData *)user_data; + + const gchar *base_dir = NULL; +#ifndef USE_FLATPAK_APP_FOLDER + base_dir = g_get_home_dir (); +#else + base_dir = g_get_user_data_dir (); +#endif + + gchar *password = prompt_for_password (app_data, NULL, NULL, TRUE); + + gchar *exported_file_path = NULL; + if (g_strcmp0 (action_name, "export_andotp") == 0) { + exported_file_path = g_build_filename (base_dir, "andotp_exports.json.aes", NULL); + gchar *message = NULL; + GtkMessageType msg_type; + gchar *ret_msg = export_andotp (exported_file_path, password, app_data->db_data->json_data); + if (ret_msg != NULL) { + message = g_strconcat ("Error while exporting data: ", ret_msg, NULL); + msg_type = GTK_MESSAGE_ERROR; + } else { + message = g_strconcat ("Data successfully exported to ", exported_file_path, NULL); + msg_type = GTK_MESSAGE_INFO; + } + show_message_dialog (app_data->main_window, message, msg_type); + g_free (message); + g_free (ret_msg); + }/* else if (g_strcmp0 (action_name, "export_authy") == 0) { + // TODO: check authy format + exported_file_path = g_build_filename (base_dir, "", NULL); + export_authy (exported_file_path); + } else if (g_strcmp0 (action_name, "export_winauth") == 0) { + // TODO: check winauth format + exported_file_path = g_build_filename (base_dir, "", NULL); + export_winauth (exported_file_path); + }*/ else { + show_message_dialog (app_data->main_window, "Invalid export action.", GTK_MESSAGE_ERROR); + return; + } + + g_free (exported_file_path); +} diff --git a/src/exports.h b/src/exports.h new file mode 100644 index 0000000..2981bf7 --- /dev/null +++ b/src/exports.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define ANDOTP_EXPORT_ACTION_NAME "export_andotp" +#define AUTHY_EXPORT_ACTION_NAME "export_authy" +#define WINAUTH_EXPORT_ACTION_NAME "export_winauth" + +void +export_data_cb (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data); + +gchar * +export_andotp ( const gchar *export_path, + const gchar *password, + json_t *json_db_data); + +G_END_DECLS \ No newline at end of file diff --git a/src/imports.c b/src/imports.c index 8d42fbc..58373aa 100644 --- a/src/imports.c +++ b/src/imports.c @@ -19,11 +19,11 @@ select_file_cb (GSimpleAction *simple, GVariant *parameter __attribute__((unused)), gpointer user_data) { - const gchar *action_name = g_action_get_name (G_ACTION (simple)); + const gchar *action_name = g_action_get_name (G_ACTION(simple)); AppData *app_data = (AppData *)user_data; GtkWidget *dialog = gtk_file_chooser_dialog_new ("Open File", - GTK_WINDOW (app_data->main_window), + GTK_WINDOW(app_data->main_window), GTK_FILE_CHOOSER_ACTION_OPEN, "Cancel", GTK_RESPONSE_CANCEL, "Open", GTK_RESPONSE_ACCEPT, @@ -33,9 +33,9 @@ select_file_cb (GSimpleAction *simple, gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), g_get_user_data_dir ()); #endif - gint res = gtk_dialog_run (GTK_DIALOG (dialog)); + gint res = gtk_dialog_run (GTK_DIALOG(dialog)); if (res == GTK_RESPONSE_ACCEPT) { - GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog); + GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog); gchar *filename = gtk_file_chooser_get_filename (chooser); parse_data_and_update_db (app_data, filename, action_name); g_free (filename); @@ -54,7 +54,7 @@ update_db_from_otps (GSList *otps, AppData *app_data) otp_t *otp = g_slist_nth_data (otps, i); obj = build_json_obj (otp->type, otp->label, 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) { + 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_memdup (&hash, sizeof (guint))); app_data->db_data->data_to_add = g_slist_append (app_data->db_data->data_to_add, obj); } else { @@ -97,7 +97,7 @@ parse_data_and_update_db (AppData *app_data, { GError *err = NULL; GSList *content = NULL; - gchar *pwd = prompt_for_password (app_data, NULL, action_name); + gchar *pwd = prompt_for_password (app_data, NULL, action_name, FALSE); if (pwd == NULL) { return FALSE; } diff --git a/src/imports.h b/src/imports.h index eeca58f..8aab02c 100644 --- a/src/imports.h +++ b/src/imports.h @@ -5,8 +5,11 @@ G_BEGIN_DECLS -#define ANDOTP_IMPORT_ACTION_NAME "import_andotp" +#define ANDOTP_IMPORT_ACTION_NAME "import_andotp" #define AUTHPLUS_IMPORT_ACTION_NAME "import_authplus" +#define AUTHY_IMPORT_ACTION_NAME "import_authy" +#define WINAUTH_IMPORT_ACTION_NAME "import_winauth" + typedef struct _otp_t { gchar *type; diff --git a/src/otpclient.h b/src/otpclient.h index 2a97edd..0977bb5 100644 --- a/src/otpclient.h +++ b/src/otpclient.h @@ -5,7 +5,7 @@ G_BEGIN_DECLS #define APP_NAME "OTPClient" -#define APP_VERSION "1.3.1" +#define APP_VERSION "1.4.0-beta" #define HOTP_RATE_LIMIT_IN_SEC 3 diff --git a/src/password-cb.c b/src/password-cb.c index c4128c2..13ab37c 100644 --- a/src/password-cb.c +++ b/src/password-cb.c @@ -25,9 +25,10 @@ static void password_cb (GtkWidget *entry, gchar * -prompt_for_password (AppData *app_data, - gchar *current_key, - const gchar *action_name) +prompt_for_password (AppData *app_data, + gchar *current_key, + const gchar *action_name, + gboolean is_export_pwd) { EntryWidgets *entry_widgets = g_new0 (EntryWidgets, 1); entry_widgets->retry = FALSE; @@ -52,7 +53,7 @@ prompt_for_password (AppData *app_data, entry_widgets->entry1 = GTK_WIDGET(gtk_builder_get_object (builder,"decpwddiag_entry_id")); g_signal_connect (entry_widgets->entry1, "activate", G_CALLBACK (send_ok_cb), NULL); g_signal_connect (entry_widgets->entry1, "icon-press", G_CALLBACK (icon_press_cb), NULL); - } else if (file_exists == FALSE && current_key == NULL) { + } else if ((file_exists == FALSE && current_key == NULL) || is_export_pwd == TRUE) { // new db dialog, 2 fields dialog = GTK_WIDGET(gtk_builder_get_object (builder, "newdb_pwd_diag_id")); entry_widgets->entry1 = GTK_WIDGET(gtk_builder_get_object (builder,"newdb_pwd_diag_entry1_id")); diff --git a/src/password-cb.h b/src/password-cb.h index 2d1cc54..efafc3b 100644 --- a/src/password-cb.h +++ b/src/password-cb.h @@ -1,9 +1,10 @@ #pragma once #include +#include "data.h" G_BEGIN_DECLS -gchar *prompt_for_password (AppData *app_data, gchar *current_key, const gchar *action_name); +gchar *prompt_for_password (AppData *app_data, gchar *current_key, const gchar *action_name, gboolean is_export_pwd); G_END_DECLS \ No newline at end of file diff --git a/src/ui/otpclient.ui b/src/ui/otpclient.ui index 9019552..3121907 100644 --- a/src/ui/otpclient.ui +++ b/src/ui/otpclient.ui @@ -1165,6 +1165,31 @@ Please note that <b>there is no way to recover a forgotten password.</b 1 + + + True + False + vertical + + + True + True + True + settings_menu.export_andotp + andOTP + + + False + True + 0 + + + + + export_menu + 2 + + True @@ -1263,7 +1288,7 @@ Please note that <b>there is no way to recover a forgotten password.</b main - 2 + 3 From 6b42c060a466136046cef49b680ad8e5c2dc5e04 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Fri, 15 Feb 2019 15:19:29 +0100 Subject: [PATCH 2/9] Use monospace for database path - fix #116 --- src/password-cb.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/password-cb.c b/src/password-cb.c index 13ab37c..56ddd3f 100644 --- a/src/password-cb.c +++ b/src/password-cb.c @@ -38,18 +38,24 @@ prompt_for_password (AppData *app_data, gboolean pwd_must_be_checked = TRUE; gboolean file_exists = g_file_test (app_data->db_data->db_path, G_FILE_TEST_EXISTS); - if ((file_exists == TRUE || action_name != NULL) && current_key == NULL) { + if ((file_exists == TRUE || action_name != NULL) && current_key == NULL && is_export_pwd == FALSE) { // decrypt dialog, just one field pwd_must_be_checked = FALSE; dialog = GTK_WIDGET(gtk_builder_get_object (builder, "decpwd_diag_id")); - gchar *text; + gchar *text = NULL, *markup = NULL; if (action_name == NULL){ - text = g_strconcat ("Enter the decryption password for ", app_data->db_data->db_path, NULL); + markup = g_markup_printf_escaped ("%s %s", "Enter the decryption password for ", app_data->db_data->db_path); } else { text = g_strdup ("Enter the decryption password"); } - gtk_label_set_text (GTK_LABEL(gtk_builder_get_object (builder, "decpwd_label_id")), text); - g_free (text); + GtkLabel *label = GTK_LABEL(gtk_builder_get_object (builder, "decpwd_label_id")); + if (markup != NULL) { + gtk_label_set_markup (label, markup); + g_free (markup); + } else { + gtk_label_set_text (label, text); + g_free (text); + } entry_widgets->entry1 = GTK_WIDGET(gtk_builder_get_object (builder,"decpwddiag_entry_id")); g_signal_connect (entry_widgets->entry1, "activate", G_CALLBACK (send_ok_cb), NULL); g_signal_connect (entry_widgets->entry1, "icon-press", G_CALLBACK (icon_press_cb), NULL); From 7b1e86cc54db938122083a906e958bf5b1cef48a Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Sat, 23 Feb 2019 07:55:04 +0100 Subject: [PATCH 3/9] Update license year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 8c9aa57..831e50a 100644 --- a/LICENSE +++ b/LICENSE @@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. OTPClient - Simple GTK+ software to generate OTPs - Copyright (C) 2017 Paolo Stivanin + Copyright (C) 2019 Paolo Stivanin This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by From 7e81637b7a06f50bf108c3999a9e964d07be90de Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Sat, 23 Feb 2019 07:56:08 +0100 Subject: [PATCH 4/9] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4c04ff..ba82bf3 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Highly secure and easy to use GTK+ software for two-factor authentication that s - support SHA1, SHA256 and SHA512 algorithms - support for Steam codes (please read [THIS PAGE](https://github.com/paolostivanin/OTPClient/wiki/Steam-Support)) - import encrypted [Authenticator Plus](https://www.authenticatorplus.com/) backup -- import encrypted [andOTP](https://github.com/flocke/andOTP) backup +- import and export encrypted [andOTP](https://github.com/flocke/andOTP) backup - local database is encrypted using AES256-GCM - key is derived using PBKDF2 with SHA512 and 100k iterations - decrypted file is never saved (and hopefully never swapped) to disk. While the app is running, the decrypted content resides in a "secure memory" buffer allocated by Gcrypt From 028a531b746a9eb66e74703e4b312880fd78776c Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Sat, 23 Feb 2019 08:05:42 +0100 Subject: [PATCH 5/9] Update flatpak manifest --- flatpak/com.github.paolostivanin.OTPClient.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flatpak/com.github.paolostivanin.OTPClient.yaml b/flatpak/com.github.paolostivanin.OTPClient.yaml index 19f6a98..2091a4b 100644 --- a/flatpak/com.github.paolostivanin.OTPClient.yaml +++ b/flatpak/com.github.paolostivanin.OTPClient.yaml @@ -38,8 +38,8 @@ modules: - "/lib/*.la" sources: - type: archive - url: http://www.digip.org/jansson/releases/jansson-2.11.tar.gz - sha256: 6e85f42dabe49a7831dbdd6d30dca8a966956b51a9a50ed534b82afc3fa5b2f4 + url: http://www.digip.org/jansson/releases/jansson-2.12.tar.gz + sha256: 5f8dec765048efac5d919aded51b26a32a05397ea207aa769ff6b53c7027d2c9 - name: zbar config-opts: - "--without-qt" @@ -81,8 +81,8 @@ modules: - "/include" sources: - type: archive - url: https://github.com/paolostivanin/libcotp/archive/v1.2.1.tar.gz - sha256: 52ea9d876d8465aad666bdb38a59d85f183a0e8d2aa14b14d07e4e6bc471648e + url: https://github.com/paolostivanin/libcotp/archive/v1.2.2.tar.gz + sha256: 25b45ffa4aece5cc689503ebea7356a2f760c194f0c41805934495d2fe7165b1 - name: OTPClient buildsystem: cmake-ninja config-opts: From 84fcca598f716780e9b1781ddc4f6adb476cb590 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Sat, 23 Feb 2019 08:08:09 +0100 Subject: [PATCH 6/9] Updatea appdata --- data/com.github.paolostivanin.OTPClient.appdata.xml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/data/com.github.paolostivanin.OTPClient.appdata.xml b/data/com.github.paolostivanin.OTPClient.appdata.xml index e042917..910ae64 100644 --- a/data/com.github.paolostivanin.OTPClient.appdata.xml +++ b/data/com.github.paolostivanin.OTPClient.appdata.xml @@ -1,5 +1,5 @@ - + com.github.paolostivanin.OTPClient.desktop CC-BY-4.0 @@ -83,6 +83,15 @@ It's also possible to import encrypted backups from andOTP and Authenticator+. + + +

OTPClient 1.4.0 brings full support to andOTP.

+
    +
  • it's now possible to export encrypted andOTP backups
  • +
  • use monospace to show the database path on startup
  • +
+
+

OTPClient 1.3.1 brings some fixes to bugs that were introduced with the previous version.

From 57ad37503b49b75ea5907ebf7ed0e87f0ad5e831 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Sat, 23 Feb 2019 08:36:58 +0100 Subject: [PATCH 7/9] Add additional check on prompt-password.c --- src/otpclient.h | 2 +- src/password-cb.c | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/otpclient.h b/src/otpclient.h index 0977bb5..ff72995 100644 --- a/src/otpclient.h +++ b/src/otpclient.h @@ -5,7 +5,7 @@ G_BEGIN_DECLS #define APP_NAME "OTPClient" -#define APP_VERSION "1.4.0-beta" +#define APP_VERSION "1.4.0" #define HOTP_RATE_LIMIT_IN_SEC 3 diff --git a/src/password-cb.c b/src/password-cb.c index 56ddd3f..04bb7fb 100644 --- a/src/password-cb.c +++ b/src/password-cb.c @@ -69,6 +69,12 @@ prompt_for_password (AppData *app_data, g_signal_connect (entry_widgets->entry2, "icon-press", G_CALLBACK (icon_press_cb), NULL); } else { // change pwd dialog, 3 fields + if (current_key == NULL) { + show_message_dialog (app_data->main_window, "ERROR: current_key cannot be NULL", GTK_MESSAGE_ERROR); + g_free (entry_widgets); + g_object_unref (builder); + return NULL; + } dialog = GTK_WIDGET(gtk_builder_get_object (builder, "changepwd_diag_id")); entry_widgets->cur_pwd = secure_strdup (current_key); entry_widgets->entry_old = GTK_WIDGET(gtk_builder_get_object (builder,"changepwd_diag_currententry_id")); From a8ca425aaf9131721d500b1658522ce33560118b Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Tue, 26 Feb 2019 20:49:28 +0100 Subject: [PATCH 8/9] Multiple steam fixes - andOTP sets a special type for Steam called "Steam", while in OTPClient a Steam token is just a TOTP. This commit fixes #119 by checking the type when importing an andOTP backup. - a steam token must have the issuer set to "Steam" in order for OTPClient to correctly work with these kind of tokens. Therefore, editing a Steam row is only possible for the label and no longer for the issuer --- src/andotp.c | 5 +++++ src/edit-data.c | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/andotp.c b/src/andotp.c index 67b9181..58b949f 100644 --- a/src/andotp.c +++ b/src/andotp.c @@ -240,6 +240,11 @@ parse_json_data (const gchar *data, } else if (g_ascii_strcasecmp (type, "HOTP") == 0) { otp->type = g_strdup (type); otp->counter = json_integer_value (json_object_get (obj, "counter")); + } else if (g_ascii_strcasecmp (type, "Steam") == 0) { + otp->type = g_strdup ("TOTP"); + otp->period = (guint32)json_integer_value (json_object_get (obj, "period")); + 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); diff --git a/src/edit-data.c b/src/edit-data.c index fba3938..426d419 100644 --- a/src/edit-data.c +++ b/src/edit-data.c @@ -74,6 +74,10 @@ show_edit_dialog (EditData *edit_data, AppData *app_data, gchar *current_label, gtk_entry_set_text (GTK_ENTRY(new_lab_entry), current_label); gtk_entry_set_text (GTK_ENTRY(new_iss_entry), current_issuer); + if (g_ascii_strcasecmp (current_issuer, "steam") == 0) { + gtk_widget_set_sensitive (new_iss_entry, FALSE); + } + gchar *err_msg = NULL; gint res = gtk_dialog_run (GTK_DIALOG (diag)); switch (res) { From e3e40374eea43ded025dbce1b86a776d4c694bb9 Mon Sep 17 00:00:00 2001 From: Paolo Stivanin Date: Thu, 28 Feb 2019 14:28:21 +0100 Subject: [PATCH 9/9] Fix andotp export and import for steam - when importing an andotp backup, if the field is steam there might not be the period value. We default to 30 in such a case - when exporting the database, andotp expects Steam code to have the STEAM type set and not TOTP This commit should completely fix #119 --- src/andotp.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/andotp.c b/src/andotp.c index 58b949f..36a4f9f 100644 --- a/src/andotp.c +++ b/src/andotp.c @@ -115,7 +115,13 @@ export_andotp ( const gchar *export_path, gsize index; json_array_foreach (json_db_data, index, db_obj) { export_obj = json_object (); - json_object_set (export_obj, "type", json_object_get (db_obj, "type")); + + if (g_ascii_strcasecmp (json_string_value (json_object_get (db_obj, "issuer")), "steam") == 0) { + json_object_set (export_obj, "type", json_string ("STEAM")); + } else { + json_object_set (export_obj, "type", json_object_get (db_obj, "type")); + } + gchar *constructed_label = g_strconcat (json_string_value (json_object_get (db_obj, "issuer")), " - ", json_string_value (json_object_get (db_obj, "label")), @@ -243,6 +249,10 @@ parse_json_data (const gchar *data, } else if (g_ascii_strcasecmp (type, "Steam") == 0) { otp->type = g_strdup ("TOTP"); otp->period = (guint32)json_integer_value (json_object_get (obj, "period")); + if (otp->period == 0) { + // andOTP exported backup for Steam might not contain the period field, + otp->period = 30; + } g_free (otp->issuer); otp->issuer = g_strdup ("Steam"); } else {