diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9742351 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +*.o +*.lo +*.la +Makefile +config.h +config.log +config.status +libsylph/.deps/ +libsylph/.libs/ +libtool +plugin/attachment_tool/.deps/ +plugin/attachment_tool/.libs/ +plugin/test/.deps/ +plugin/test/.libs/ +po/Makefile.in +po/POTFILES +src/.deps/ +src/.libs/ +src/sylpheed +src/sylpheed.rc +stamp-h1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1cf6d21 --- /dev/null +++ b/README.md @@ -0,0 +1,219 @@ +## Implemented features and fixes not present in the official Sylpheed release + +- (fix) + PGP signature not verified properly when the message has no newline + at the end. https://sylpheed.sraoss.jp/redmine/issues/288 + +- (feature) + Make it possible to select "Show signature check result in a popup window" + only for bad signatures. + +- (feature) + Support for encrypting and storing encrypted passwords using a master password. + Read bellow for more details! + + +## Master password + +The master password feature is developed by Simeon Simeonov (sgs) +and is currently in an experimental state. + + +### Motivation + +Currently Sylpheed is storing passwords in plain-text. One can always refrain +from storing passwords and let Sylpheed prompt for them, but the more accounts +one has, the more annoying this becomes. + +The goal is to have the passwords stored in a secure way and let Sylpheed only +prompt for the master password. + + +### Security goals + +- attacker (A) should not be able to derive the password from the digest. + +- A should not be able to derive the master password even if she has + read and write access to the storage. + +- A should not be able to determine the length of the encrypted password + even if she has read and write access to the storage. + +- A should not be able to craft an edited password without obtaining the + master password. + +- A should not be able to compromise the master password or force Sylpheed + to store decrypted passwords even if she has access to a running Sylpheed. + +- a warning / prompt should be given if a user accidentally types in a "wrong" + master password before decryption is initiated. + + +### Usage in Sylpheed + +- backup your Sylpheed profile (often $HOME/.sylpheed-2.0)! + +- start Sylpheed and open "Configuration" -> "Common preferences..."! + +- select the "Master password" tab, enable "Use master password" and + apply the changes! + +- restart Sylpheed (exit and then start Sylpheed again)! + +- you will be asked to type and verify a new master password. + +- set your new passwords from the "Configuration" -> "Edit accounts..."! + +- select "Automatically unload master password after session initialization" + for increased security and decreased convenience. + +Note: +Sylpheed will automatically convert existing stored passwords, but it will not +touch your backups. You will have to remove all remnants of plain-text +passwords manually. + + +### Choice of cryptographic primitives + +The primary concern when selecting cryptographic primitives was portability. +The desire was to go for primitives that are both strong and available in all +supported production distributions of OpenSSL and LibreSSL. + + +#### Cipher + +When it comes to implementation, there are several advantages in using stream +cipher or a block cipher that behaves like a stream cipher when used in a +certain mode of operation. One is avoiding to deal with padding. + +AES-256 operating in CFB was selected for these reasons. +ChaCha20 should be considered as a replacement in the future. + + +#### Hash-function + +Hash-functions are used for: +- key derivation +- plain-text digest + +Those operations do not have to use the same hash-function. +(See the "Encryption & decryption scheme" section for more details!) + +Key-derivation: +Since AES-256 uses a 256 bits key, we need a hash-function with at least +the same digest size or bigger. +Since the digest (which is the key itself) is considered confidential and is +stored only in memory for only a limited amount of time, SHA-256 is considered +sufficiently strong for that purpose. + +Size + plain-text + padding digest: +Since the digest is created of both the plain-text and the plain-text size, +as well as being encrypted, SHA-256 is considered sufficiently strong. +SHA-512 may increase security at the price of adding additional 32 bytes +to the encrypted password digest. + +Stronger hash-functions like SHA-3 or BLAKE2b can be considered as a +replacement in the future. + + +#### Master password digest + +In order to be able to decide whether the user typed a "wrong" master password, +before attempting to decrypt, Sylpheed stores a digest of the master password +in 'master_password_hash' in sylpheedrc. +100000 iterations of PBKDF2_HMAC with SHA-512 and 16 bytes salt is used. +Note that this digest is useless as a key and even if a plain-text that +produces the same digest is found, it will most probably be useless as a +master-password. + + +### Encryption & decryption scheme + + +#### Encryption + + +Input: + +- plain-text password to be encrypted (P) + +- plain-text master-password used for key derivation (M) + +- integer minimum password length (0 < L < 100) + + +Output: + +- an encrypted password digest (base64) (B) + + +Operation: + +- generate 16 bytes of random data to be used as a salt (S) + +- derive the key (K): K = SHA_256(S + M) + +- produce a 2 byte string (N) indicating the length of P + +- generate random padding and set N: + - if the length of P < L, produce L - P bytes of random data (R), N = "%02d" + - if the length of P >= L, N = "-1" + +- produce a hash digest (H): H = SHA_256(N + P + R (if the length of P < L)) + +- encrypt (E): E = AES_256_CFB_ENCRYPT(H + N + P + R (if the length of P < L), K) + +- B = mpes1:BASE64_ENCODE(S + E) + +example: +mpes1:vo7lsIpD7i6byBA6+vlUoF4OVDfEe+aYRRk4FRtfJ2gMY8M43Kj6WfdfgbViIOl83bI4XEc96okhPW5Mla813aAR1gbPjDg0xmCyIbWOiUv/dg== + + +#### Decryption + + +Input: + +- encrypted password digest (base64) (B) + +- plain-text master-password used for key derivation (M) + + +Output: + +- plain-text password (P) + + +Operation: + +- remove the prefix (mpes1:) and base64-decode the rest of the digest: B = BASE64_DECODE(B) + +- fetch the first 16 bytes for the salt: S = B[0 : 15] + +- derive the key (K): K = SHA_256(S + M) + +- decrypt the rest of B (D): D = AES_256_CFB_DECRYPT(B[16 :], K) + +- extract the first 16 bytes for the hash digest (H): H = D[0 : 15] + +- in order to detect data-inconsistency, assert H == SHA_256(D[16 :]) + +- extract the next 2 bytes for the length of P (N): N = D[16 : 17] + +- fetch the plain-text: + - if N == "-1" the password is the remaining bytes of D: P = D[18 :] + - if N != "-1", extract the next N-bytes from D: P = D[18 : (18 + N)] + + +### Limitations + +- currently only the 'password' and 'smtp_password' keys in accountrc + are encrypted. + A mechanism that allows for any key and even folders to be encrypted + should be considered in the future. + +- currently it is not possible to select alternative ciphers, hash-functions + and modes of operation (without editing the source code). + +- currently it is not possible to change your master password without having to + (re)set your passwords manually. diff --git a/libsylph/Makefile.am b/libsylph/Makefile.am index 8f2b76b..40d26a9 100644 --- a/libsylph/Makefile.am +++ b/libsylph/Makefile.am @@ -19,6 +19,7 @@ libsylph_0_la_SOURCES = \ folder.c \ html.c \ imap.c \ + masterpassword.c \ mbox.c \ md5.c \ md5_hmac.c \ @@ -62,6 +63,7 @@ libsylph_0include_HEADERS = \ folder.h \ html.h \ imap.h \ + masterpassword.h \ mbox.h \ md5.h \ md5_hmac.h \ diff --git a/libsylph/Makefile.in b/libsylph/Makefile.in index 6e3c999..ac8fefd 100644 --- a/libsylph/Makefile.in +++ b/libsylph/Makefile.in @@ -129,8 +129,8 @@ libsylph_0_la_DEPENDENCIES = $(am__DEPENDENCIES_1) \ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) am_libsylph_0_la_OBJECTS = account.lo base64.lo codeconv.lo \ customheader.lo displayheader.lo filter.lo folder.lo html.lo \ - imap.lo mbox.lo md5.lo md5_hmac.lo mh.lo news.lo nntp.lo \ - pop.lo prefs.lo prefs_account.lo prefs_common.lo procheader.lo \ + imap.lo masterpassword.lo mbox.lo md5.lo md5_hmac.lo mh.lo news.lo \ + nntp.lo pop.lo prefs.lo prefs_account.lo prefs_common.lo procheader.lo \ procmime.lo procmsg.lo quoted-printable.lo recv.lo session.lo \ smtp.lo socket.lo socks.lo ssl.lo ssl_hostname_validation.lo \ stringtable.lo sylmain.lo unmime.lo utils.lo uuencode.lo \ @@ -402,6 +402,7 @@ libsylph_0_la_SOURCES = \ folder.c \ html.c \ imap.c \ + masterpassword.c \ mbox.c \ md5.c \ md5_hmac.c \ @@ -445,6 +446,7 @@ libsylph_0include_HEADERS = \ folder.h \ html.h \ imap.h \ + masterpassword.h \ mbox.h \ md5.h \ md5_hmac.h \ @@ -579,6 +581,7 @@ distclean-compile: @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/folder.Plo@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/html.Plo@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap.Plo@am__quote@ +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/masterpassword.Plo@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox.Plo@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/md5.Plo@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/md5_hmac.Plo@am__quote@ diff --git a/libsylph/defs.h b/libsylph/defs.h index 9e3f82b..67325bf 100644 --- a/libsylph/defs.h +++ b/libsylph/defs.h @@ -58,6 +58,9 @@ #define DISPLAY_HEADER_RC "dispheaderrc" #define MENU_RC "menurc" #define ACTIONS_RC "actionsrc" +#ifdef USE_SSL +#define SECURE_RC "securerc" +#endif #define COMMAND_HISTORY "command_history" #define TEMPLATE_DIR "templates" #define TMP_DIR "tmp" diff --git a/libsylph/imap.c b/libsylph/imap.c index aa0d737..e1c7cfd 100644 --- a/libsylph/imap.c +++ b/libsylph/imap.c @@ -52,6 +52,7 @@ #include "utils.h" #include "prefs_common.h" #include "virtual.h" +#include "masterpassword.h" #define IMAP4_PORT 143 #if USE_SSL @@ -705,9 +706,13 @@ static gint imap_session_connect(IMAPSession *session) account = (PrefsAccount *)(SESSION(session)->data); log_message(_("creating IMAP4 connection to %s:%d ...\n"), - SESSION(session)->server, SESSION(session)->port); - - pass = account->passwd; + SESSION(session)->server, SESSION(session)->port); + if (master_password_active()) { + pass = decrypt_with_master_password(account->passwd); + /* a new string is allocated. To be removed ... */ + } else { + pass = account->passwd; + } if (!pass) pass = account->tmp_pass; if (!pass) { @@ -770,8 +775,11 @@ static gint imap_session_connect(IMAPSession *session) #endif if (!session->authenticated && - imap_auth(session, account->userid, pass, account->imap_auth_type) - != IMAP_SUCCESS) { + imap_auth(session, account->userid, pass, account->imap_auth_type) + != IMAP_SUCCESS) { + if (master_password_active()) { + g_free(pass); /* remove the decrypted password */ + } if (account->tmp_pass) { g_free(account->tmp_pass); account->tmp_pass = NULL; @@ -780,6 +788,10 @@ static gint imap_session_connect(IMAPSession *session) return IMAP_AUTHFAIL; } + if (master_password_active()) { + g_free(pass); /* remove the decrypted password */ + } + return IMAP_SUCCESS; } diff --git a/libsylph/masterpassword.c b/libsylph/masterpassword.c new file mode 100644 index 0000000..3d7af94 --- /dev/null +++ b/libsylph/masterpassword.c @@ -0,0 +1,216 @@ +/* + * LibSylph -- E-Mail client library + * Copyright (C) 1999-2018 Hiroyuki Yamamoto + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include "prefs_common.h" +#include "ssl.h" +#include "utils.h" +#include "masterpassword.h" + + +gchar *master_password; +gboolean master_password_enabled_on_init; + +void set_master_password(const char *password) { + master_password = password; +} + +gchar *get_master_password(void) { + return master_password; +} + +void cleanse_buffer(void *buf, size_t len) { +#if USE_SSL + OPENSSL_cleanse(buf, len); +#else + memset(buf, 0, len); /* better than nothing */ +#endif +} + +void unload_master_password(void) { + + debug_print("Unloading master password\n"); + if (master_password == NULL) { + /* not loaded / already unloaded */ + return; + } + cleanse_buffer(master_password, strlen(master_password)); + g_free(master_password); + master_password = NULL; + debug_print("Master password unloaded\n"); + +} + +gint mpes_string_prefix(const gchar *str) { + + /* this function will be expanded in time as the format changes */ + if (g_str_has_prefix(str, "mpes1:")) + return 6; + return 0; + +} + +gboolean master_password_active(void) { + +#if USE_SSL + return ((master_password != NULL) && + prefs_common.use_master_password); +#else + return FALSE; +#endif + +} + +gchar *decrypt_with_master_password(const gchar *str) { + +#if USE_SSL + gchar *new_str; + gint str_prefix; + + if ((!str) || (!prefs_common.use_master_password)) + return g_strdup(str); + + if (master_password == NULL) { + /* we have empty or auto unloaded master password */ + if ((!prefs_common.auto_unload_master_password) || + (check_master_password_interactively(3) != MP_RC_OK)) { + return g_strdup(str); + } + debug_print("Reloaded master password\n"); + } + + str_prefix = mpes_string_prefix(str); + if (!str_prefix) + return g_strdup(str); + + if (decrypt_data(&new_str, + str + str_prefix, + master_password, + strlen(str) + 1 - str_prefix) != MP_RC_OK) { + OPENSSL_cleanse(new_str, strlen(new_str)); + g_free(new_str); + return g_strdup(str); + } + + return new_str; +#else + return g_strdup(str); +#endif + +} + +gchar *encrypt_with_master_password(const gchar *str) { + +#if USE_SSL + gchar *new_str, *mpes1_str; + gint length_encrypted; + + if ((!str) || (!master_password_active())) + return NULL; + + /* + * unlike the decrypt function, here it is up to the caller + * to make sure that auto unloaded master password is handled properly + */ + + if (encrypt_data(&new_str, + &length_encrypted, + str, + master_password, + strlen(str) + 1, + prefs_common.encrypted_password_min_length, + TRUE) != MP_RC_OK) { + g_free(new_str); + return NULL; + } + + mpes1_str = g_strdup_printf("mpes1:%s", new_str); + g_free(new_str); + + return mpes1_str; +#else + return NULL; +#endif + +} + +#if USE_SSL +gint set_master_password_interactively(guint max_attempts) { + + if (master_password == NULL) + master_password = input_set_new_password(max_attempts); + + if (master_password == NULL) + return 1; + + if (generate_password_hash( + &prefs_common.master_password_hash, + master_password, + NULL) != MP_RC_OK) { + /* should not really happen unless buggy code / library */ + g_free(prefs_common.master_password_hash); + prefs_common.master_password_hash = NULL; + debug_print(_("Could not generate master password hash")); + return 1; + } + + prefs_common_write_config(); + return 0; + +} + +gint check_master_password_interactively(guint max_attempts) { + + guint cnt; + + g_return_val_if_fail(max_attempts > 0, 1); + g_return_val_if_fail(prefs_common.master_password_hash != NULL, 1); + + if (master_password != NULL) { + /* password already cached */ + debug_print("Master password already cached\n"); + return check_password(master_password, + prefs_common.master_password_hash); + } + + for (cnt = 0; cnt < max_attempts; ++cnt) { + master_password = input_query_master_password(); + if (master_password == NULL) { + /* input canceled or query_master_password_func not set */ + continue; + } + if (check_password(master_password, + prefs_common.master_password_hash) == MP_RC_OK) { + return MP_RC_OK; /* match */ + } + debug_print(_("Wrong master password entered (%d)\n"), cnt); + OPENSSL_cleanse(master_password, strlen(master_password)); + g_free(master_password); + master_password = NULL; + } + + return 1; /* no match */ + +} +#endif /* USE_SSL */ diff --git a/libsylph/masterpassword.h b/libsylph/masterpassword.h new file mode 100644 index 0000000..7254643 --- /dev/null +++ b/libsylph/masterpassword.h @@ -0,0 +1,47 @@ +/* + * LibSylph -- E-Mail client library + * Copyright (C) 1999-2018 Hiroyuki Yamamoto + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __MASTERPASSWORD_H__ +#define __MASTERPASSWORD_H__ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#define MP_RC_OK 0 +#define MP_RC_WRONG_HASH_OR_KEY 1 +#define MP_RC_INVALID_FORMAT 2 /* invalid digest format */ + +extern gchar *master_password; +extern gboolean master_password_enabled_on_init; /* m.p. enabled on init? */ +void set_master_password(const char *password); +gchar *get_master_password(void); +void cleanse_buffer(void *buf, size_t len); +void unload_master_password(void); +gint mpes_string_prefix(const gchar *str); +gboolean master_password_active(void); +gchar *decrypt_with_master_password(const gchar *str); +gchar *encrypt_with_master_password(const gchar *str); + +#if USE_SSL +gint set_master_password_interactively(guint max_attempts); +gint check_master_password_interactively(guint max_attempts); +#endif /* USE_SSL */ + +#endif /* __MASTERPASSWORD_H__ */ diff --git a/libsylph/news.c b/libsylph/news.c index ffff9f9..36d3b10 100644 --- a/libsylph/news.c +++ b/libsylph/news.c @@ -44,6 +44,7 @@ #include "utils.h" #include "prefs_common.h" #include "prefs_account.h" +#include "masterpassword.h" #if USE_SSL # include "ssl.h" #endif @@ -249,10 +250,11 @@ static Session *news_session_new_for_folder(Folder *folder) ac = folder->account; if (ac->use_nntp_auth && ac->userid && ac->userid[0]) { userid = ac->userid; - if (ac->passwd && ac->passwd[0]) - passwd = g_strdup(ac->passwd); - else + if (ac->passwd && ac->passwd[0]) { + passwd = decrypt_with_master_password(ac->passwd); + } else { passwd = input_query_password(ac->nntp_server, userid); + } } if (ac->use_socks && ac->use_socks_for_recv && ac->proxy_host) { diff --git a/libsylph/pop.c b/libsylph/pop.c index 8cb7f5c..85f3ed9 100644 --- a/libsylph/pop.c +++ b/libsylph/pop.c @@ -39,6 +39,7 @@ #include "prefs_account.h" #include "utils.h" #include "recv.h" +#include "masterpassword.h" gint pop3_greeting_recv (Pop3Session *session, const gchar *msg); @@ -437,8 +438,9 @@ Session *pop3_session_new(PrefsAccount *account) session->error_msg = NULL; session->user = g_strdup(account->userid); - session->pass = account->passwd ? g_strdup(account->passwd) : - account->tmp_pass ? g_strdup(account->tmp_pass) : NULL; + session->pass = account->passwd ? decrypt_with_master_password( + account->passwd) : account->tmp_pass ? g_strdup( + account->tmp_pass) : NULL; SESSION(session)->server = g_strdup(account->recv_server); diff --git a/libsylph/prefs_account.c b/libsylph/prefs_account.c index 1aecba9..54cbc89 100644 --- a/libsylph/prefs_account.c +++ b/libsylph/prefs_account.c @@ -34,6 +34,7 @@ #include "customheader.h" #include "account.h" #include "utils.h" +#include "masterpassword.h" static PrefsAccount tmp_ac_prefs; @@ -205,7 +206,7 @@ PrefsAccount *prefs_account_new(void) void prefs_account_read_config(PrefsAccount *ac_prefs, const gchar *label) { const gchar *p = label; - gchar *rcpath; + gchar *rcpath, *tmp_str; gint id; g_return_if_fail(ac_prefs != NULL); @@ -229,6 +230,34 @@ void prefs_account_read_config(PrefsAccount *ac_prefs, const gchar *label) ac_prefs->use_apop_auth = TRUE; } + if (master_password_active()) { + if ((ac_prefs->passwd != NULL) && + !mpes_string_prefix(ac_prefs->passwd)) { + /* TODO: Perhaps some prompt? */ + debug_print( + "%s -> converting passwd cleartext to encrypted\n", + label); + tmp_str = ac_prefs->passwd; + ac_prefs->passwd = encrypt_with_master_password(ac_prefs->passwd); + cleanse_buffer(tmp_str, strlen(tmp_str)); + g_free(tmp_str); + } + + if ((ac_prefs->smtp_passwd != NULL) && + !mpes_string_prefix(ac_prefs->smtp_passwd)) { + /* TODO: Perhaps some prompt? */ + debug_print( + "%s -> converting smtp_passwd from cleartext to encrypted\n", + label); + tmp_str = ac_prefs->smtp_passwd; + ac_prefs->smtp_passwd = encrypt_with_master_password( + ac_prefs->smtp_passwd); + cleanse_buffer(tmp_str, strlen(tmp_str)); + g_free(tmp_str); + } + + } + custom_header_read_config(ac_prefs); } diff --git a/libsylph/prefs_common.c b/libsylph/prefs_common.c index 8ed74a3..192964e 100644 --- a/libsylph/prefs_common.c +++ b/libsylph/prefs_common.c @@ -406,6 +406,8 @@ static PrefParam param[] = { P_BOOL}, {"gpg_signature_popup", "FALSE", &prefs_common.gpg_signature_popup, P_BOOL}, + {"gpg_signature_popup_mode", "1", &prefs_common.gpg_signature_popup_mode, + P_INT}, {"store_passphrase", "FALSE", &prefs_common.store_passphrase, P_BOOL}, {"store_passphrase_timeout", "0", &prefs_common.store_passphrase_timeout, P_INT}, @@ -416,6 +418,17 @@ static PrefParam param[] = { {"show_gpg_warning", "TRUE", &prefs_common.gpg_warning, P_BOOL}, #endif + /* Master password */ + {"use_master_password", "FALSE", &prefs_common.use_master_password, + P_BOOL}, + {"master_password_hash", NULL, &prefs_common.master_password_hash, + P_STRING}, + {"encrypted_password_min_length", "32", + &prefs_common.encrypted_password_min_length, P_INT}, + {"auto_unload_master_password", "FALSE", + &prefs_common.auto_unload_master_password, + P_BOOL}, + /* Interface */ {"separate_folder", "FALSE", &prefs_common.sep_folder, P_BOOL}, {"separate_message", "FALSE", &prefs_common.sep_msg, P_BOOL}, diff --git a/libsylph/prefs_common.h b/libsylph/prefs_common.h index ba9ddb5..6f9c364 100644 --- a/libsylph/prefs_common.h +++ b/libsylph/prefs_common.h @@ -243,11 +243,18 @@ struct _PrefsCommon /* Privacy */ gboolean auto_check_signatures; gboolean gpg_signature_popup; + gint gpg_signature_popup_mode; gboolean store_passphrase; gint store_passphrase_timeout; gboolean passphrase_grab; gboolean gpg_warning; + /* Master password */ + gboolean use_master_password; + gchar *master_password_hash; + guint encrypted_password_min_length; + gboolean auto_unload_master_password; + /* Interface */ gboolean sep_folder; gboolean sep_msg; diff --git a/libsylph/ssl.c b/libsylph/ssl.c index 8413925..e782b0e 100644 --- a/libsylph/ssl.c +++ b/libsylph/ssl.c @@ -32,6 +32,13 @@ #include "ssl.h" #include "ssl_hostname_validation.h" +#define SALT_SIZE 16 +#define CIPHER EVP_aes_256_cfb() +#define KEY_HASH EVP_sha256() +#define DIGEST_HASH EVP_sha256() +#define PBKDF2_DIGEST_SIZE 64 +#define PBKDF2_ITERATIONS 100000 + static SSL_CTX *ssl_ctx_SSLv23 = NULL; static SSL_CTX *ssl_ctx_TLSv1 = NULL; @@ -402,4 +409,468 @@ void ssl_set_verify_func(SSLVerifyFunc func) verify_ui_func = func; } +/* master password related functions */ +static gint secure_derive_key(guchar *key, + gint length_key, + const gchar *passphrase, + const guchar *salt) { + + guint length_buffer, length_hash; + guchar *buffer, *ptr_hash; + + EVP_MD_CTX *mdctx; + + OPENSSL_cleanse(key, length_key); + + length_buffer = SALT_SIZE + strlen(passphrase); + buffer = OPENSSL_malloc(length_buffer); + OPENSSL_cleanse(buffer, length_buffer); + + memcpy(buffer, salt, SALT_SIZE); + memcpy(buffer + SALT_SIZE, passphrase, strlen(passphrase)); + + mdctx = EVP_MD_CTX_create(); + EVP_DigestInit_ex(mdctx, KEY_HASH, NULL); + EVP_DigestUpdate(mdctx, buffer, length_buffer); + OPENSSL_cleanse(buffer, length_buffer); + + ptr_hash = OPENSSL_malloc(EVP_MD_size(KEY_HASH)); + EVP_DigestFinal_ex(mdctx, ptr_hash, &length_hash); + + memcpy(key, + ptr_hash, + (length_hash > length_key) ? length_key : length_hash); + + OPENSSL_cleanse(ptr_hash, length_hash); + OPENSSL_free(ptr_hash); + OPENSSL_free(buffer); + EVP_MD_CTX_destroy(mdctx); + + return SSL_RC_OK; + +} + +gint encrypt_data(gchar **encrypted, + gint *length_encrypted, + const gchar *data, + const gchar *passphrase, + gint length_data, + guint min_data_length, + gboolean rnd_salt) { + + gint crypt_buffer_cnt, rc; + guint key_size, length_hash; + guint length_cleartext, length_ciphertext, length_total; + guchar salt[SALT_SIZE]; + guchar *ciphertext_buffer, *total_buffer; + /* sensitive buffers and counters */ + guint length_data_payload, length_padding; + gchar str_data_size[3]; + guchar *data_payload_buffer, *hash_buffer, *padding_buffer, *key; + guchar *cleartext_buffer; + + EVP_CIPHER_CTX *ctx; + EVP_MD_CTX *mdctx; + + rc = SSL_RC_ERROR; + + if (length_data < 1) { + return -1; + } + if (rnd_salt) { + if (RAND_bytes(salt, SALT_SIZE) != 1) { + debug_print("Random problems...\n"); + goto cleanup; + } + } else { + strncpy((gchar *)salt, "FOR TESTING ONLY", SALT_SIZE); + } + + key_size = EVP_CIPHER_key_length(CIPHER); + key = OPENSSL_malloc(key_size); + OPENSSL_cleanse(key, key_size); + if (secure_derive_key(key, + key_size, + passphrase, + salt) != SSL_RC_OK) { + OPENSSL_cleanse(key, key_size); + debug_print("Could not generate secure key\n"); + goto cleanup; + } + + /* prepare the data-buffer */ + length_padding = 0; + if (length_data < min_data_length) { + g_snprintf(str_data_size, 3, "%02d", length_data); + length_padding = min_data_length - length_data; + padding_buffer = OPENSSL_malloc(length_padding); + OPENSSL_cleanse(padding_buffer, length_padding); + if (RAND_bytes(padding_buffer, length_padding) != 1) { + debug_print("Random problems...\n"); + goto cleanup; + } + } else { + g_snprintf(str_data_size, 3, "-1"); + } + + length_data_payload = 2 + length_data + length_padding; + data_payload_buffer = OPENSSL_malloc(length_data_payload); + OPENSSL_cleanse(data_payload_buffer, length_data_payload); + + memcpy(data_payload_buffer, str_data_size, 2); + memcpy(data_payload_buffer + 2, data, length_data); + if (length_padding > 0) { + memcpy(data_payload_buffer + (2 + length_data), + padding_buffer, + length_padding); + } + + mdctx = EVP_MD_CTX_create(); + EVP_DigestInit_ex(mdctx, DIGEST_HASH, NULL); + EVP_DigestUpdate(mdctx, data_payload_buffer, length_data_payload); + + length_hash = EVP_MD_size(DIGEST_HASH); + hash_buffer = OPENSSL_malloc(length_hash); + OPENSSL_cleanse(hash_buffer, length_hash); + EVP_DigestFinal_ex(mdctx, hash_buffer, NULL); + + length_cleartext = length_hash + length_data_payload; + + cleartext_buffer = OPENSSL_malloc(length_cleartext); + OPENSSL_cleanse(cleartext_buffer, length_cleartext); + + /* assemble cleartext-buffer */ + memcpy(cleartext_buffer, hash_buffer, length_hash); + memcpy(cleartext_buffer + length_hash, + data_payload_buffer, + length_data_payload); + OPENSSL_cleanse(data_payload_buffer, length_data_payload); /* sensitive */ + + /* encryption */ + if (!(ctx = EVP_CIPHER_CTX_new())) { + debug_print("New ctx failed\n"); + goto cleanup; + } + + length_ciphertext = 0; + if (EVP_EncryptInit_ex(ctx, CIPHER, NULL, key, salt) != 1) { + debug_print("EVP_EncryptInit_ex failed\n"); + goto cleanup; + } + + ciphertext_buffer = OPENSSL_malloc(length_cleartext); + + crypt_buffer_cnt = 0; + while(1) { + if (EVP_EncryptUpdate(ctx, + ciphertext_buffer + length_ciphertext, + &crypt_buffer_cnt, + cleartext_buffer + length_ciphertext, + 1) != 1) { /* one byte at a time */ + debug_print("EVP_EncryptUpdate failed\n"); + goto cleanup; + } + + if (crypt_buffer_cnt != 1) { /* paranoia */ + debug_print("The sizes of enc and dec text do not correspond\n"); + goto cleanup; + } + + ++length_ciphertext; + + if (length_ciphertext >= length_cleartext) { + break; + } + } + /* No padding required for the CFB mode */ + + OPENSSL_cleanse(cleartext_buffer, length_cleartext); /* sensitive */ + length_total = SALT_SIZE + length_ciphertext; + total_buffer = OPENSSL_malloc(length_total); + + memcpy(total_buffer, salt, SALT_SIZE); + memcpy(total_buffer + SALT_SIZE, ciphertext_buffer, length_ciphertext); + + *encrypted = g_base64_encode(total_buffer, length_total); + *length_encrypted = strlen(*encrypted); + + rc = SSL_RC_OK; + +cleanup: + /* key */ + OPENSSL_cleanse(key, key_size); + OPENSSL_free(key); + /* cleartext buffer */ + OPENSSL_cleanse(cleartext_buffer, length_cleartext); + OPENSSL_free(cleartext_buffer); + /* payload buffer */ + OPENSSL_cleanse(data_payload_buffer, length_data_payload); + OPENSSL_free(data_payload_buffer); + /* hash */ + OPENSSL_cleanse(hash_buffer, length_hash); + OPENSSL_free(hash_buffer); + /* padding */ + if (length_padding > 0) { + OPENSSL_cleanse(padding_buffer, length_padding); + OPENSSL_free(padding_buffer); + } + + OPENSSL_cleanse(str_data_size, 3); /* paranoia */ + + /* ciphertext buffer */ + OPENSSL_free(ciphertext_buffer); + + /* total buffer */ + OPENSSL_free(total_buffer); + + length_data_payload = 0; + length_padding = 0; + + EVP_CIPHER_CTX_free(ctx); + EVP_MD_CTX_destroy(mdctx); + + return rc; + +} + +gint decrypt_data(gchar **decrypted, + const gchar *data, + const gchar *passphrase, + gint length_data) { + + gint rc; /* return code */ + gint decrypt_buffer_cnt, length_ciphertext, length_decrypted; + guint key_size, length_hash, length_cleartext; + gsize length_total; + guchar salt[SALT_SIZE]; + guchar *ciphertext_buffer, *total_buffer; + /* sensitive buffers and counters */ + gint data_size; + guint length_data_payload; + gchar str_data_size[3]; + guchar *data_payload_buffer, *hash_buffer, *key; + guchar *cleartext_buffer; + + EVP_CIPHER_CTX *ctx; + EVP_MD_CTX *mdctx; + + rc = -1; + + if (length_data < 1) { + return -1; + } + + total_buffer = g_base64_decode(data, &length_total); + length_ciphertext = length_total - SALT_SIZE; + ciphertext_buffer = OPENSSL_malloc(length_ciphertext); + OPENSSL_cleanse(ciphertext_buffer, length_ciphertext); + + memcpy(salt, total_buffer, SALT_SIZE); + memcpy(ciphertext_buffer, total_buffer + SALT_SIZE, length_ciphertext); + + key_size = EVP_CIPHER_key_length(CIPHER); + + /* decryption */ + if(!(ctx = EVP_CIPHER_CTX_new())) { + debug_print("New ctx failed\n"); + goto cleanup; + } + + key = OPENSSL_malloc(key_size); + OPENSSL_cleanse(key, key_size); + if (secure_derive_key(key, + key_size, + passphrase, + salt) != 0) { + OPENSSL_cleanse(key, key_size); + debug_print("Could not generate secure key\n"); + goto cleanup; + } + + if (EVP_DecryptInit_ex(ctx, CIPHER, NULL, key, salt) != 1) { + debug_print("EVP_DecryptInit_ex failed\n"); + goto cleanup; + } + OPENSSL_cleanse(key, key_size); /* highly sensitive */ + cleartext_buffer = OPENSSL_malloc(length_ciphertext); + OPENSSL_cleanse(cleartext_buffer, length_ciphertext); + + length_cleartext = 0; + decrypt_buffer_cnt = 0; + + while(1) { + if (EVP_DecryptUpdate(ctx, + cleartext_buffer + length_cleartext, + &decrypt_buffer_cnt, + ciphertext_buffer + length_cleartext, + 1) != 1) { /* one byte at a time */ + debug_print("EVP_EncryptUpdate failed\n"); + goto cleanup; + } + + if (decrypt_buffer_cnt != 1) { /* paranoia */ + debug_print("The sizes of enc and dec text do not correspond"); + goto cleanup; + } + + ++length_cleartext; + + if (length_cleartext >= length_ciphertext) { + break; + } + } + + + length_hash = EVP_MD_size(DIGEST_HASH); + hash_buffer = OPENSSL_malloc(length_hash); + length_data_payload = length_cleartext - length_hash; + data_payload_buffer = OPENSSL_malloc(length_data_payload); + OPENSSL_cleanse(data_payload_buffer, length_data_payload); + + memcpy(data_payload_buffer, + cleartext_buffer + length_hash, + length_data_payload); + + mdctx = EVP_MD_CTX_create(); + EVP_DigestInit_ex(mdctx, DIGEST_HASH, NULL); + EVP_DigestUpdate(mdctx, data_payload_buffer, length_data_payload); + EVP_DigestFinal_ex(mdctx, hash_buffer, &length_hash); + + if (strncmp((const gchar*) hash_buffer, + (const gchar*) cleartext_buffer, + length_hash) != 0) { + debug_print("Invalid hash\n"); + rc = SSL_RC_WRONG_HASH_OR_KEY; + goto cleanup; + } + + memcpy(str_data_size, cleartext_buffer + length_hash, 2); + data_size = atoi(str_data_size); + + if (data_size < 0) { + length_decrypted = length_data_payload - 2; + } else { + length_decrypted = data_size; + } + *decrypted = OPENSSL_malloc(length_decrypted); + memcpy(*decrypted, data_payload_buffer + 2, length_decrypted); + + rc = SSL_RC_OK; + +cleanup: + + /* key */ + OPENSSL_cleanse(key, key_size); + OPENSSL_free(key); + /* payload buffer */ + OPENSSL_cleanse(data_payload_buffer, length_data_payload); + OPENSSL_free(data_payload_buffer); + /* hash */ + OPENSSL_cleanse(hash_buffer, length_hash); + OPENSSL_free(hash_buffer); + /* ciphertext */ + OPENSSL_free(ciphertext_buffer); + OPENSSL_free(total_buffer); + + EVP_CIPHER_CTX_free(ctx); + EVP_MD_CTX_destroy(mdctx); + + return rc; + +} + +gint generate_password_hash(gchar **password_hash, + const gchar *password, + const guchar *salt) { + /* + * Hashes 'password' using PKCS5_PBKDF2_HMAC with SHA512 and 'salt', + * and assigns a string to 'password_hash' with the following format: + * pbkdf2_sha512$iterations$base64(salt)$base64(password_hash) + */ + guchar lsalt[SALT_SIZE]; + gchar *PBKDF2_digest, *salt_b64, *digest_b64; + + if (salt == NULL) { + if (RAND_bytes(lsalt, SALT_SIZE) != 1) { + debug_print("Random problems...\n"); + return SSL_RC_ERROR; + } + } else { + memcpy(lsalt, salt, SALT_SIZE); + } + + PBKDF2_digest = OPENSSL_malloc(PBKDF2_DIGEST_SIZE); + OPENSSL_cleanse(PBKDF2_digest, PBKDF2_DIGEST_SIZE); + + PKCS5_PBKDF2_HMAC(password, + strlen(password), + lsalt, + SALT_SIZE, + PBKDF2_ITERATIONS, + EVP_sha512(), + PBKDF2_DIGEST_SIZE, + (guchar *) PBKDF2_digest); + digest_b64 = g_base64_encode((guchar *) PBKDF2_digest, PBKDF2_DIGEST_SIZE); + OPENSSL_cleanse(PBKDF2_digest, PBKDF2_DIGEST_SIZE); + OPENSSL_free(PBKDF2_digest); + + salt_b64 = g_base64_encode(lsalt, SALT_SIZE); + + *password_hash = g_strdup_printf("pbkdf2_sha512$%d$%s$%s", + PBKDF2_ITERATIONS, + salt_b64, + digest_b64); + + OPENSSL_cleanse(digest_b64, strlen(digest_b64)); + OPENSSL_free(digest_b64); + + OPENSSL_cleanse(salt_b64, strlen(salt_b64)); + OPENSSL_free(salt_b64); + + return SSL_RC_OK; + +} + +gint check_password(const gchar *password, const gchar *password_hash) { + + gint token_counter, rc; + guchar *salt; + gchar **tokens, *new_hash; + gsize salt_length; + + rc = SSL_RC_ERROR; + tokens = g_strsplit(password_hash, + "$", + -1); + token_counter = 0; + while (*(tokens + token_counter) != NULL) { + ++token_counter; + } + + if (token_counter != 4) { + debug_print("Invalid password hash...\n"); + goto cleanup; + } + + salt = g_base64_decode(*(tokens + 2), &salt_length); + if (salt_length != SALT_SIZE) { + debug_print("Salt size does not match\n"); + goto cleanup; + } + + if (generate_password_hash(&new_hash, password, salt) != SSL_RC_OK) { + debug_print("Password hash generation failed\n"); + goto cleanup; + } + + rc = g_strcmp0(password_hash, new_hash); + +cleanup: + g_free(salt); + g_free(new_hash); + g_strfreev(tokens); + return rc; + +} + #endif /* USE_SSL */ diff --git a/libsylph/ssl.h b/libsylph/ssl.h index a9f690d..4c0ccd1 100644 --- a/libsylph/ssl.h +++ b/libsylph/ssl.h @@ -32,9 +32,15 @@ #include #include #include +#include +#include #include "socket.h" +#define SSL_RC_OK 0 +#define SSL_RC_ERROR -1 +#define SSL_RC_WRONG_HASH_OR_KEY 1 + typedef enum { SSL_METHOD_SSLv23, SSL_METHOD_TLSv1 @@ -60,6 +66,26 @@ void ssl_done_socket (SockInfo *sockinfo); void ssl_set_verify_func (SSLVerifyFunc func); +/* master password related code */ +gint encrypt_data(gchar **encrypted, + gint *length_encrypted, + const gchar *data, + const gchar *passphrase, + gint length_data, + guint min_data_length, + gboolean rnd_salt); + +gint decrypt_data(gchar **decrypted, + const gchar *data, + const gchar *passphrase, + gint length_data); + +gint generate_password_hash(gchar **password_hash, + const gchar *password, + const guchar *salt); + +gint check_password(const gchar *password, const gchar *password_hash); +/* ---------------------------- */ #endif /* USE_SSL */ #endif /* __SSL_H__ */ diff --git a/libsylph/utils.c b/libsylph/utils.c index aabce06..3bbcbcf 100644 --- a/libsylph/utils.c +++ b/libsylph/utils.c @@ -3306,10 +3306,10 @@ gint canonicalize_file(const gchar *src, const gchar *dest) } } - if (last_linebreak == TRUE) { - if (fputs("\r\n", dest_fp) == EOF) - err = TRUE; - } + /* if (last_linebreak == TRUE) { */ + /* if (fputs("\r\n", dest_fp) == EOF) */ + /* err = TRUE; */ + /* } */ if (ferror(src_fp)) { FILE_OP_ERROR(src, "fgets"); @@ -4598,6 +4598,36 @@ gchar *input_query_password(const gchar *server, const gchar *user) return NULL; } +static QueryMasterPasswordFunc query_master_password_func = NULL; + +void set_input_query_master_password_func(QueryMasterPasswordFunc func) +{ + query_master_password_func = func; +} + +gchar *input_query_master_password(void) +{ + if (query_master_password_func) + return query_master_password_func(); + else + return NULL; +} + +static SetNewPasswordFunc set_new_password_func = NULL; + +void set_input_set_new_password_func(SetNewPasswordFunc func) +{ + set_new_password_func = func; +} + +gchar *input_set_new_password(guint max_attempts) +{ + if (set_new_password_func) + return set_new_password_func(max_attempts); + else + return NULL; +} + /* logging */ static FILE *log_fp = NULL; diff --git a/libsylph/utils.h b/libsylph/utils.h index 9ac65cf..ede55ff 100644 --- a/libsylph/utils.h +++ b/libsylph/utils.h @@ -202,6 +202,8 @@ typedef void (*ProgressFunc) (gint cur, gint total); typedef gchar * (*QueryPasswordFunc) (const gchar *server, const gchar *user); +typedef gchar * (*QueryMasterPasswordFunc) (void); +typedef gchar * (*SetNewPasswordFunc) (guint max_attempts); typedef void (*LogFunc) (const gchar *str); typedef void (*LogFlushFunc) (void); @@ -560,9 +562,12 @@ void progress_show (gint cur, /* user input */ void set_input_query_password_func (QueryPasswordFunc func); - gchar *input_query_password (const gchar *server, const gchar *user); +void set_input_query_master_password_func(QueryMasterPasswordFunc func); +gchar *input_query_master_password(void); +void set_input_set_new_password_func(SetNewPasswordFunc func); +gchar *input_set_new_password(guint max_attempts); /* logging */ void set_log_file (const gchar *filename); diff --git a/src/inputdialog.c b/src/inputdialog.c index a601085..cdfb997 100644 --- a/src/inputdialog.c +++ b/src/inputdialog.c @@ -44,6 +44,7 @@ #include "filesel.h" #include "prefs_common.h" #include "gtkutils.h" +#include "masterpassword.h" #include "utils.h" #define DIALOG_WIDTH 420 @@ -156,6 +157,57 @@ gchar *input_dialog_query_password(const gchar *server, const gchar *user) return pass; } +gchar *input_dialog_query_master_password(void) +{ + gchar *mp_input; + + mp_input = input_dialog_with_invisible(_("Input password"), + _("Master password"), + NULL); + + if (prefs_common.use_master_password && + prefs_common.auto_unload_master_password) + { + g_timeout_add(1000 * 30, unload_master_password, NULL); + debug_print("Master password to be unloaded in 30 seconds\n"); + } + + return mp_input; +} + +gchar *input_dialog_set_new_password(guint max_attempts) +{ + guint cnt; + gchar *pass1, *pass2; + + if (max_attempts < 1) + return NULL; + + for (cnt = 0; cnt < max_attempts; ++cnt) { + pass1 = input_dialog_with_invisible( + _("Input password"), + _("New password"), + NULL); + pass2 = input_dialog_with_invisible( + _("Input password"), + _("Confirm new password"), + NULL); + + if (pass1 != NULL && pass2 != NULL && strcmp(pass1, pass2) == 0) { + cleanse_buffer(pass2, strlen(pass2)); + g_free(pass2); + break; + } + cleanse_buffer(pass1, strlen(pass1)); + cleanse_buffer(pass2, strlen(pass2)); + g_free(pass2); + g_free(pass1); + pass1 = NULL; + } + + return pass1; +} + gchar *input_dialog_with_filesel(const gchar *title, const gchar *message, const gchar *default_string, GtkFileChooserAction action) diff --git a/src/inputdialog.h b/src/inputdialog.h index 02bdda3..5364d1a 100644 --- a/src/inputdialog.h +++ b/src/inputdialog.h @@ -36,7 +36,8 @@ gchar *input_dialog_combo (const gchar *title, gboolean case_sensitive); gchar *input_dialog_query_password (const gchar *server, const gchar *user); - +gchar *input_dialog_query_master_password(void); +gchar *input_dialog_set_new_password(guint max_attempts); gchar *input_dialog_with_filesel (const gchar *title, const gchar *message, const gchar *default_string, diff --git a/src/main.c b/src/main.c index 77d8192..97a4bd0 100644 --- a/src/main.c +++ b/src/main.c @@ -87,6 +87,7 @@ #include "foldersel.h" #include "update_check.h" #include "colorlabel.h" +#include "masterpassword.h" #if USE_GPGME # include "rfc2015.h" @@ -265,14 +266,54 @@ int main(int argc, char *argv[]) set_ui_update_func(gtkut_events_flush); set_progress_func(main_window_progress_show); set_input_query_password_func(input_dialog_query_password); + set_input_set_new_password_func(input_dialog_set_new_password); #if USE_SSL ssl_init(); ssl_set_verify_func(ssl_manager_verify_cert); + set_input_query_master_password_func(input_dialog_query_master_password); #endif CHDIR_EXIT_IF_FAIL(get_home_dir(), 1); prefs_common_read_config(); + set_master_password(NULL); +#if USE_SSL + if (prefs_common.use_master_password) { + if (prefs_common.master_password_hash != NULL) { + if (check_master_password_interactively(3) != MP_RC_OK) { + if (alertpanel(_("Master password"), + _("Invalid master password"), + GTK_STOCK_DISCARD, + GTK_STOCK_QUIT, + NULL) != G_ALERTDEFAULT) { + exit(1); + } + } + } else { + alertpanel_notice( + _("Master password enabled but not set. Setting one now")); + if (set_master_password_interactively(3) != MP_RC_OK) { + if (alertpanel(_("Master password"), + _("Unable to set master password"), + GTK_STOCK_DISCARD, + GTK_STOCK_QUIT, + NULL) != G_ALERTDEFAULT) { + exit(1); + } + } + } + } + /* security goal: + * if the master password is enabled and loaded on init, + * an attacker can potentially set use_master_password - disabled + * afterwards and then force Sylpheed to save account data - the result + * being passwords saved to accountrc in plain-text. + * possible solution: + * Do not allow the passwords to be stored in plain-text if Sylpheed + * was started with use_master_password - enabled. + */ + master_password_enabled_on_init = prefs_common.use_master_password; +#endif filter_set_addressbook_func(addressbook_has_address); filter_read_config(); prefs_actions_read_config(); @@ -381,6 +422,13 @@ int main(int argc, char *argv[]) remote_command_exec(); +#if USE_SSL + if (prefs_common.auto_unload_master_password && master_password_active()) { + debug_print("Auto unloading master password\n"); + unload_master_password(); + } +#endif + #if USE_UPDATE_CHECK if (prefs_common.auto_update_check) update_check(FALSE); @@ -993,6 +1041,7 @@ void app_will_exit(gboolean force) /* remove temporary files, close log file, socket cleanup */ #if USE_SSL + unload_master_password(); ssl_done(); #endif syl_cleanup(); diff --git a/src/prefs_account_dialog.c b/src/prefs_account_dialog.c index e9cba13..e215b8e 100644 --- a/src/prefs_account_dialog.c +++ b/src/prefs_account_dialog.c @@ -267,7 +267,7 @@ static PrefsUIData ui_data[] = { {"user_id", &basic.uid_entry, prefs_set_data_from_entry, prefs_set_entry}, {"password", &basic.pass_entry, - prefs_set_data_from_entry, prefs_set_entry}, + prefs_set_data_from_epass_entry, prefs_set_entry}, /* Receive */ {"use_apop_auth", &receive.use_apop_chkbtn, @@ -313,7 +313,7 @@ static PrefsUIData ui_data[] = { {"smtp_user_id", &p_send.smtp_uid_entry, prefs_set_data_from_entry, prefs_set_entry}, {"smtp_password", &p_send.smtp_pass_entry, - prefs_set_data_from_entry, prefs_set_entry}, + prefs_set_data_from_epass_entry, prefs_set_entry}, {"pop_before_smtp", &p_send.pop_bfr_smtp_chkbtn, prefs_set_data_from_toggle, prefs_set_toggle}, diff --git a/src/prefs_common_dialog.c b/src/prefs_common_dialog.c index b46c9b7..119a056 100644 --- a/src/prefs_common_dialog.c +++ b/src/prefs_common_dialog.c @@ -205,6 +205,7 @@ static struct JunkMail { static struct Privacy { GtkWidget *checkbtn_auto_check_signatures; GtkWidget *checkbtn_gpg_signature_popup; + GtkWidget *radiobtn_gpg_signature_all; GtkWidget *checkbtn_store_passphrase; GtkWidget *spinbtn_store_passphrase; GtkObject *spinbtn_store_passphrase_adj; @@ -215,6 +216,14 @@ static struct Privacy { } privacy; #endif +#if USE_SSL +static struct MasterPassword { + GtkWidget *checkbtn_use_master_password; + GtkWidget *checkbtn_auto_unload_master_password; + GtkWidget *spinbtn_encrypted_password_min_length; +} master_password; +#endif + static struct Interface { GtkWidget *checkbtn_always_show_msg; GtkWidget *checkbtn_always_mark_read; @@ -314,6 +323,13 @@ static void prefs_common_attach_toolbtn_pos_set_radiobtn (PrefParam *pparam); static void prefs_common_online_mode_set_data_from_radiobtn(PrefParam *pparam); static void prefs_common_online_mode_set_radiobtn (PrefParam *pparam); +#if USE_GPGME +static void prefs_common_gpg_signature_popup_mode_set_data_from_radiobtn( + PrefParam *pparam); +static void prefs_common_gpg_signature_popup_mode_set_radiobtn( + PrefParam *pparam); +#endif + static PrefsUIData ui_data[] = { /* Receive */ {"autochk_newmail", &receive.checkbtn_autochk, @@ -540,6 +556,9 @@ static PrefsUIData ui_data[] = { prefs_set_data_from_toggle, prefs_set_toggle}, {"gpg_signature_popup", &privacy.checkbtn_gpg_signature_popup, prefs_set_data_from_toggle, prefs_set_toggle}, + {"gpg_signature_popup_mode", &privacy.radiobtn_gpg_signature_all, + prefs_common_gpg_signature_popup_mode_set_data_from_radiobtn, + prefs_common_gpg_signature_popup_mode_set_radiobtn}, {"store_passphrase", &privacy.checkbtn_store_passphrase, prefs_set_data_from_toggle, prefs_set_toggle}, {"store_passphrase_timeout", &privacy.spinbtn_store_passphrase, @@ -552,6 +571,18 @@ static PrefsUIData ui_data[] = { prefs_set_data_from_toggle, prefs_set_toggle}, #endif /* USE_GPGME */ +#if USE_SSL + {"use_master_password", &master_password.checkbtn_use_master_password, + prefs_set_data_from_toggle, prefs_set_toggle}, + {"auto_unload_master_password", + &master_password.checkbtn_auto_unload_master_password, + prefs_set_data_from_toggle, prefs_set_toggle}, + {"encrypted_password_min_length", + &master_password.spinbtn_encrypted_password_min_length, + prefs_set_data_from_spinbtn, + prefs_set_spinbtn}, +#endif + /* Interface */ {"always_show_message_when_selected", &iface.checkbtn_always_show_msg, @@ -682,6 +713,9 @@ static void prefs_junk_create (void); #if USE_GPGME static void prefs_privacy_create (void); #endif +#if USE_SSL +static void prefs_master_password_create (void); +#endif static void prefs_details_create (void); static GtkWidget *prefs_other_create (void); static GtkWidget *prefs_extcmd_create (void); @@ -841,6 +875,10 @@ static void prefs_common_create(void) #if USE_GPGME prefs_privacy_create(); SET_NOTEBOOK_LABEL(dialog.notebook, _("Privacy"), page++); +#endif +#if USE_SSL + prefs_master_password_create(); + SET_NOTEBOOK_LABEL(dialog.notebook, _("Master password"), page++); #endif prefs_details_create(); SET_NOTEBOOK_LABEL(dialog.notebook, _("Details"), page++); @@ -2478,10 +2516,14 @@ static void prefs_privacy_create(void) GtkWidget *vbox2; GtkWidget *vbox3; GtkWidget *hbox1; + GtkWidget *vbox_sign_popup_mode; + GtkWidget *hbox_sign_popup_mode; GtkWidget *hbox_spc; GtkWidget *label; GtkWidget *checkbtn_auto_check_signatures; GtkWidget *checkbtn_gpg_signature_popup; + GtkWidget *radiobtn_gpg_signature_all; + GtkWidget *radiobtn_gpg_signature_bad; GtkWidget *checkbtn_store_passphrase; GtkObject *spinbtn_store_passphrase_adj; GtkWidget *spinbtn_store_passphrase; @@ -2505,6 +2547,56 @@ static void prefs_privacy_create(void) PACK_CHECK_BUTTON (vbox2, checkbtn_gpg_signature_popup, _("Show signature check result in a popup window")); + vbox_sign_popup_mode = gtk_vbox_new (FALSE, VSPACING_NARROW); + gtk_widget_show (vbox_sign_popup_mode); + gtk_box_pack_start (GTK_BOX (vbox2), vbox_sign_popup_mode, FALSE, FALSE, 0); + + hbox_sign_popup_mode = gtk_hbox_new (FALSE, 8); + gtk_widget_show (hbox_sign_popup_mode); + gtk_box_pack_start (GTK_BOX (vbox_sign_popup_mode), + hbox_sign_popup_mode, + FALSE, + FALSE, + 0); + + hbox_spc = gtk_hbox_new (FALSE, 0); + gtk_widget_show (hbox_spc); + gtk_box_pack_start (GTK_BOX (hbox_sign_popup_mode), + hbox_spc, + FALSE, + FALSE, + 0); + gtk_widget_set_size_request (hbox_spc, 12, -1); + + radiobtn_gpg_signature_all = gtk_radio_button_new_with_label( + NULL, + _("All signatures")); + gtk_widget_show(radiobtn_gpg_signature_all); + gtk_box_pack_start(GTK_BOX (hbox_sign_popup_mode), + radiobtn_gpg_signature_all, + FALSE, + FALSE, + 0); + g_object_set_data(G_OBJECT (radiobtn_gpg_signature_all), + MENU_VAL_ID, + GINT_TO_POINTER (1)); + + radiobtn_gpg_signature_bad = gtk_radio_button_new_with_label_from_widget( + GTK_RADIO_BUTTON (radiobtn_gpg_signature_all), + _("Bad signatures only")); + gtk_widget_show(radiobtn_gpg_signature_bad); + gtk_box_pack_start(GTK_BOX (hbox_sign_popup_mode), + radiobtn_gpg_signature_bad, + FALSE, + FALSE, + 0); + g_object_set_data(G_OBJECT (radiobtn_gpg_signature_bad), + MENU_VAL_ID, + GINT_TO_POINTER (0)); + + SET_TOGGLE_SENSITIVITY (checkbtn_gpg_signature_popup, + hbox_sign_popup_mode); + PACK_CHECK_BUTTON (vbox2, checkbtn_store_passphrase, _("Store passphrase in memory temporarily")); @@ -2572,7 +2664,8 @@ static void prefs_privacy_create(void) = checkbtn_auto_check_signatures; privacy.checkbtn_gpg_signature_popup = checkbtn_gpg_signature_popup; - privacy.checkbtn_store_passphrase = checkbtn_store_passphrase; + privacy.radiobtn_gpg_signature_all = radiobtn_gpg_signature_all; + privacy.checkbtn_store_passphrase = checkbtn_store_passphrase; privacy.spinbtn_store_passphrase = spinbtn_store_passphrase; privacy.spinbtn_store_passphrase_adj = spinbtn_store_passphrase_adj; #ifndef G_OS_WIN32 @@ -2582,6 +2675,93 @@ static void prefs_privacy_create(void) } #endif /* USE_GPGME */ +#if USE_SSL +static void prefs_master_password_create(void) +{ + GtkWidget *vbox_main; + GtkWidget *vbox_master_password_options; + GtkWidget *vbox_master_password_suboptions; + GtkWidget *hbox1; + GtkWidget *hbox_spc; + GtkWidget *label; + GtkWidget *checkbtn_use_master_password; + GtkWidget *checkbtn_auto_unload_master_password; + GtkWidget *spinbtn_encrypted_password_min_length; + GtkObject *spinbtn_encrypted_password_min_length_adj; + + vbox_main = gtk_vbox_new (FALSE, VSPACING); + gtk_widget_show (vbox_main); + gtk_container_add (GTK_CONTAINER (dialog.notebook), vbox_main); + gtk_container_set_border_width (GTK_CONTAINER (vbox_main), VBOX_BORDER); + + vbox_master_password_options = gtk_vbox_new (FALSE, 0); + gtk_widget_show (vbox_master_password_options); + gtk_box_pack_start ( + GTK_BOX (vbox_main), vbox_master_password_options, FALSE, FALSE, 0); + + PACK_CHECK_BUTTON (vbox_master_password_options, + checkbtn_use_master_password, + _("Use master password")); + + vbox_master_password_suboptions = gtk_vbox_new (FALSE, VSPACING_NARROW); + gtk_widget_show (vbox_master_password_suboptions); + gtk_box_pack_start (GTK_BOX (vbox_master_password_options), + vbox_master_password_suboptions, + FALSE, + FALSE, + 0); + + PACK_CHECK_BUTTON (vbox_master_password_suboptions, + checkbtn_auto_unload_master_password, + _("Automatically unload master password " + "after session initialization")); + + hbox1 = gtk_hbox_new (FALSE, 8); + gtk_widget_show (hbox1); + gtk_box_pack_start (GTK_BOX (vbox_master_password_suboptions), + hbox1, + FALSE, + FALSE, + 0); + + hbox_spc = gtk_hbox_new (FALSE, 0); + gtk_widget_show (hbox_spc); + gtk_box_pack_start (GTK_BOX (hbox1), hbox_spc, FALSE, FALSE, 0); + gtk_widget_set_size_request (hbox_spc, 12, -1); + + label = gtk_label_new (_("Min. password length")); + gtk_widget_show (label); + gtk_box_pack_start ( + GTK_BOX (hbox1), label, FALSE, FALSE, 0); + + spinbtn_encrypted_password_min_length_adj = gtk_adjustment_new ( + 32, 0, 99, 1, 5, 0); + spinbtn_encrypted_password_min_length = gtk_spin_button_new( + GTK_ADJUSTMENT (spinbtn_encrypted_password_min_length_adj), 1, 0); + gtk_widget_show (spinbtn_encrypted_password_min_length); + gtk_box_pack_start (GTK_BOX (hbox1), + spinbtn_encrypted_password_min_length, + FALSE, + FALSE, + 0); + gtk_spin_button_set_numeric ( + GTK_SPIN_BUTTON (spinbtn_encrypted_password_min_length), TRUE); + gtk_widget_set_size_request (spinbtn_encrypted_password_min_length, + 64, + -1); + + SET_TOGGLE_SENSITIVITY (checkbtn_use_master_password, + vbox_master_password_suboptions); + + master_password.checkbtn_use_master_password + = checkbtn_use_master_password; + master_password.checkbtn_auto_unload_master_password + = checkbtn_auto_unload_master_password; + master_password.spinbtn_encrypted_password_min_length + = spinbtn_encrypted_password_min_length; +} +#endif /* USE_SSL */ + static void prefs_details_create(void) { GtkWidget *vbox1; @@ -4719,6 +4899,60 @@ static void prefs_common_online_mode_set_radiobtn(PrefParam *pparam) } } +#if USE_GPGME +static void prefs_common_gpg_signature_popup_mode_set_data_from_radiobtn( + PrefParam *pparam) +{ + PrefsUIData *ui_data; + GtkRadioButton *radiobtn; + GSList *group; + + ui_data = (PrefsUIData *)pparam->ui_data; + g_return_if_fail(ui_data != NULL); + g_return_if_fail(*ui_data->widget != NULL); + + radiobtn = GTK_RADIO_BUTTON(*ui_data->widget); + group = gtk_radio_button_get_group(radiobtn); + while (group != NULL) { + GtkToggleButton *btn = GTK_TOGGLE_BUTTON(group->data); + + if (gtk_toggle_button_get_active(btn)) { + prefs_common.gpg_signature_popup_mode = + GPOINTER_TO_INT(g_object_get_data(G_OBJECT(btn), MENU_VAL_ID)); + break; + } + group = group->next; + } +} + +static void prefs_common_gpg_signature_popup_mode_set_radiobtn( + PrefParam *pparam) +{ + PrefsUIData *ui_data; + GtkRadioButton *radiobtn; + GSList *group; + + ui_data = (PrefsUIData *)pparam->ui_data; + g_return_if_fail(ui_data != NULL); + g_return_if_fail(*ui_data->widget != NULL); + + radiobtn = GTK_RADIO_BUTTON(*ui_data->widget); + group = gtk_radio_button_get_group(radiobtn); + while (group != NULL) { + GtkToggleButton *btn = GTK_TOGGLE_BUTTON(group->data); + gint data; + + data = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(btn), + MENU_VAL_ID)); + if (data == prefs_common.gpg_signature_popup_mode) { + gtk_toggle_button_set_active(btn, TRUE); + break; + } + group = group->next; + } +} +#endif + static void prefs_common_dispitem_clicked(void) { prefs_summary_column_open(FOLDER_ITEM_IS_SENT_FOLDER diff --git a/src/prefs_ui.c b/src/prefs_ui.c index a3a8f74..2ab1d45 100644 --- a/src/prefs_ui.c +++ b/src/prefs_ui.c @@ -31,11 +31,13 @@ #include #include "prefs.h" +#include "prefs_common.h" #include "prefs_ui.h" #include "menu.h" #include "codeconv.h" #include "utils.h" #include "gtkutils.h" +#include "masterpassword.h" typedef enum { @@ -268,6 +270,65 @@ void prefs_set_data_from_entry(PrefParam *pparam) } } +void prefs_set_data_from_epass_entry(PrefParam *pparam) +{ +#if USE_SSL + PrefsUIData *ui_data; + gchar **str; + const gchar *entry_str; + + /* This is where decrypted passwords are encrypted and stored again */ + + /* master_password == NULL for any of the following reasons: + * - use_master_password is not enabled (we just save without encrypting) + * - master_password is auto unloaded (prompt for the master password) + * - undefined reason (refure to store the password) + */ + if (!prefs_common.use_master_password) { + /* check if use_master_password was enabled when Sylpheed started */ + if (!master_password_enabled_on_init) { + prefs_set_data_from_entry(pparam); + } + return; + } else if (master_password == NULL) { + if (!prefs_common.auto_unload_master_password) { + debug_print("Master password enabled, but not loaded for no " + "apparent reason. Not storing\n"); + return; + } else { + if (check_master_password_interactively(3) != MP_RC_OK) { + debug_print("Failed to reload the master password\n"); + return; + } + } + } + + ui_data = (PrefsUIData *)pparam->ui_data; + g_return_if_fail(ui_data != NULL); + g_return_if_fail(*ui_data->widget != NULL); + + entry_str = gtk_entry_get_text(GTK_ENTRY(*ui_data->widget)); + + switch (pparam->type) { + case P_STRING: + str = (gchar **)pparam->data; + g_free(*str); + if ((entry_str != NULL) && (!mpes_string_prefix(entry_str))) { + debug_print("Encrypting GTK password entry\n"); + *str = encrypt_with_master_password(entry_str); + } else { + *str = entry_str[0] ? g_strdup(entry_str) : NULL; + } + break; + default: + g_warning("Invalid PrefType for GtkEntry widget: %d\n", + pparam->type); + } +#else + prefs_set_data_from_entry(pparam); +#endif +} + void prefs_set_entry(PrefParam *pparam) { PrefsUIData *ui_data; diff --git a/src/prefs_ui.h b/src/prefs_ui.h index cfdf353..f7bd903 100644 --- a/src/prefs_ui.h +++ b/src/prefs_ui.h @@ -164,6 +164,7 @@ void prefs_set_data_from_dialog (PrefParam *param); void prefs_set_dialog_to_default(PrefParam *param); void prefs_set_data_from_entry (PrefParam *pparam); +void prefs_set_data_from_epass_entry(PrefParam *pparam); void prefs_set_entry (PrefParam *pparam); void prefs_set_data_from_text (PrefParam *pparam); void prefs_set_text (PrefParam *pparam); diff --git a/src/rfc2015.c b/src/rfc2015.c index ebfa96c..48825e2 100644 --- a/src/rfc2015.c +++ b/src/rfc2015.c @@ -210,8 +210,12 @@ static void check_signature(MimeInfo *mimeinfo, MimeInfo *partinfo, FILE *fp) gchar *tmp_file; gint n_exclude_chars = 0; - if (prefs_common.gpg_signature_popup) + if (prefs_common.gpg_signature_popup && + prefs_common.gpg_signature_popup_mode == 1) + { + /* FIXME: Perhaps some macro instead of 1 */ statuswindow = gpgmegtk_sig_status_create(); + } err = gpgme_new(&ctx); if (err) { @@ -309,8 +313,21 @@ leave: result = _("Error verifying the signature"); } debug_print("verification status: %s\n", result); - if (prefs_common.gpg_signature_popup) + if (prefs_common.gpg_signature_popup && + prefs_common.gpg_signature_popup_mode == 1) + { + /* FIXME: Perhaps some macro instead of 1 */ + gpgmegtk_sig_status_update(statuswindow, ctx); + } else if (prefs_common.gpg_signature_popup && + verifyresult->signatures->status != GPG_ERR_NO_DATA && + verifyresult->signatures->status != GPG_ERR_NO_ERROR) + { + /* prefs_common.gpg_signature_popup_mode == 0 is useless as long as + * there are only two modes (0 and 1) + */ + statuswindow = gpgmegtk_sig_status_create(); gpgmegtk_sig_status_update(statuswindow, ctx); + } g_free (partinfo->sigstatus); partinfo->sigstatus = g_strdup (result); @@ -319,8 +336,11 @@ leave: gpgme_data_release(text); if (ctx) gpgme_release(ctx); - if (prefs_common.gpg_signature_popup) + if (prefs_common.gpg_signature_popup) { + /* gpgmegtk_sig_status_destroy handles NULL params so no complicated + check is needed */ gpgmegtk_sig_status_destroy(statuswindow); + } } /* @@ -427,7 +447,11 @@ static gpgme_data_t pgp_decrypt(MsgInfo *msginfo, MimeInfo *partinfo, FILE *fp) debug_print("verification status: %s\n", result); debug_print("full status: %s\n", msginfo->encinfo->sigstatus_full); - if (prefs_common.gpg_signature_popup) { + if (prefs_common.gpg_signature_popup && + ((prefs_common.gpg_signature_popup_mode == 1) || + (verifyresult->signatures->status != GPG_ERR_NO_DATA && + verifyresult->signatures->status != GPG_ERR_NO_ERROR))) + { GpgmegtkSigStatus statuswindow; statuswindow = gpgmegtk_sig_status_create(); gpgmegtk_sig_status_update(statuswindow, ctx); diff --git a/src/send_message.c b/src/send_message.c index a8c2c7a..537c3c8 100644 --- a/src/send_message.c +++ b/src/send_message.c @@ -58,6 +58,7 @@ #include "inc.h" #include "mainwindow.h" #include "summaryview.h" +#include "masterpassword.h" #define SMTP_PORT 25 #if USE_SSL @@ -648,13 +649,13 @@ static gint send_message_smtp(PrefsAccount *ac_prefs, GSList *to_list, FILE *fp) if (ac_prefs->smtp_userid) { smtp_session->user = g_strdup(ac_prefs->smtp_userid); - if (ac_prefs->smtp_passwd) - smtp_session->pass = - g_strdup(ac_prefs->smtp_passwd); - else if (ac_prefs->tmp_smtp_pass) + if (ac_prefs->smtp_passwd) { + smtp_session->pass = decrypt_with_master_password( + ac_prefs->smtp_passwd); + } else if (ac_prefs->tmp_smtp_pass) { smtp_session->pass = g_strdup(ac_prefs->tmp_smtp_pass); - else { + } else { smtp_session->pass = input_query_password (ac_prefs->smtp_server,