Skip to content

Commit

Permalink
Merge pull request #118 from paolostivanin/develop-1.4.0
Browse files Browse the repository at this point in the history
Release 1.4.0
  • Loading branch information
paolostivanin authored Mar 4, 2019
2 parents 071bd7d + e3e4037 commit 7dbe461
Show file tree
Hide file tree
Showing 16 changed files with 273 additions and 42 deletions.
10 changes: 6 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion data/com.github.paolostivanin.OTPClient.appdata.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2018 Paolo Stivanin <info@paolostivanin.com> -->
<!-- Copyright 2019 Paolo Stivanin <info@paolostivanin.com> -->
<component type="desktop-application">
<id type="desktop">com.github.paolostivanin.OTPClient.desktop</id>
<metadata_license>CC-BY-4.0</metadata_license>
Expand Down Expand Up @@ -83,6 +83,15 @@ It's also possible to import encrypted backups from andOTP and Authenticator+.
</content_rating>

<releases>
<release version="1.4.0" date="2019-02-23">
<description>
<p>OTPClient 1.4.0 brings full support to andOTP.</p>
<ul>
<li>it's now possible to export encrypted andOTP backups</li>
<li>use monospace to show the database path on startup</li>
</ul>
</description>
</release>
<release version="1.3.1" date="2018-10-31">
<description>
<p>OTPClient 1.3.1 brings some fixes to bugs that were introduced with the previous version.</p>
Expand Down
8 changes: 4 additions & 4 deletions flatpak/com.github.paolostivanin.OTPClient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
111 changes: 102 additions & 9 deletions src/andotp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -106,6 +105,91 @@ 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 ();

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")),
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)
{
Expand Down Expand Up @@ -162,6 +246,15 @@ 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"));
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 {
g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "otp type is neither TOTP nor HOTP");
gcry_free (otp->secret);
Expand Down
7 changes: 4 additions & 3 deletions src/app.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/edit-data.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
58 changes: 58 additions & 0 deletions src/exports.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include <gtk/gtk.h>
#include <gcrypt.h>
#include <jansson.h>
#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);
}
22 changes: 22 additions & 0 deletions src/exports.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <gtk/gtk.h>
#include <jansson.h>

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
Loading

0 comments on commit 7dbe461

Please sign in to comment.