From 406221dc8d42a6beda5aca96dd7d668751e53635 Mon Sep 17 00:00:00 2001 From: Simeon Simeonov Date: Wed, 8 Apr 2020 21:19:40 +0200 Subject: Initial release based on sgs' private overlay --- mail-client/sylpheed/Manifest | 8 + .../sylpheed/files/sylpheed-3.7.0-ALL.patch | 2149 ++++++++++++++++++++ .../files/sylpheed-3.7.0-PGP-signature-fix.patch | 18 + .../files/sylpheed-3.7.0-master-password.patch | 1850 +++++++++++++++++ .../files/sylpheed-3.7.0-signature-box.patch | 267 +++ .../sylpheed/files/sylpheed-3.7.0-version.patch | 26 + mail-client/sylpheed/metadata.xml | 10 + mail-client/sylpheed/sylpheed-3.7.0-r1.ebuild | 76 + 8 files changed, 4404 insertions(+) create mode 100644 mail-client/sylpheed/Manifest create mode 100644 mail-client/sylpheed/files/sylpheed-3.7.0-ALL.patch create mode 100644 mail-client/sylpheed/files/sylpheed-3.7.0-PGP-signature-fix.patch create mode 100644 mail-client/sylpheed/files/sylpheed-3.7.0-master-password.patch create mode 100644 mail-client/sylpheed/files/sylpheed-3.7.0-signature-box.patch create mode 100644 mail-client/sylpheed/files/sylpheed-3.7.0-version.patch create mode 100644 mail-client/sylpheed/metadata.xml create mode 100644 mail-client/sylpheed/sylpheed-3.7.0-r1.ebuild (limited to 'mail-client/sylpheed') diff --git a/mail-client/sylpheed/Manifest b/mail-client/sylpheed/Manifest new file mode 100644 index 0000000..557767e --- /dev/null +++ b/mail-client/sylpheed/Manifest @@ -0,0 +1,8 @@ +AUX sylpheed-3.7.0-ALL.patch 63415 BLAKE2B c40c55b039d5c457bf555e0b738f8c97925966f75b0357dad7cc4f10f9fbaa9e6156e84e65c7e89880e28b36e286de96761693718faac9d1056f720a2cae8b1e SHA512 96d4edfea92439d2f0dd4dec11305710e8d5513979d4bd96113acdbe2338c9452da64ded98204b7af7c84ed86f22337a0248be109a6a8e20d3d1b62929ce70a3 +AUX sylpheed-3.7.0-PGP-signature-fix.patch 565 BLAKE2B c97f48f7844498323d944d1dc1925601ee470e50da26d4e434eb497b29b59fdf5144d019e30efc7392093422111f7c4bb0ff71a148d553db9fb2a2d26e86599a SHA512 089883841eae1c2ed806db1992ff94dd2906b1e825dbab7ae1b5d484815e446979d9f031851d4506ad0f4b00c462997d3704cbf605f7cbc9292e4b53bff5a0fe +AUX sylpheed-3.7.0-master-password.patch 53668 BLAKE2B c2e898dbbaa4351fa9306a925c5aab56b6b63df4cebc130be7d44a33434524cf61e279fdc9b743d4b9d0c23dc84eda02c56a8c1b33c2309959a4c03c497ad232 SHA512 dcc987a9835ca2c38f06fe6aafdc9d05b750d691b1d1838b818557776be1129cc90b2982169510495ad30bc5e684b69dd614c747fff27b938620c7e0c8133498 +AUX sylpheed-3.7.0-signature-box.patch 9787 BLAKE2B 664b60b3a3421f12cc76a1a1cea64cd773509f17583e32acd9445581e0bed75289674ce82e018dd688204f932228ed379c6f68c31ff25c09e912ad74cbfe7394 SHA512 f1d1c165b701469cacf24eaab5512750d307145a551013e3ecd20e49817b4cd4d4113bd690d8ff6b1e6e9b1d96a45fb25c9417f5aa455e4063f85fc2d48ed464 +AUX sylpheed-3.7.0-version.patch 626 BLAKE2B fe3b90263c384429f9520bc357066f6cea80f81389cdc915ac30932276d5d3dbde5fdea16f649fb553f81d3487afbbdd41151604bd388e9d3ec12f44c7b9097a SHA512 26765397fe73ea03306513363f217f71b9a344590ad8716b34532279b1ade8c5feac0a9b981951bc58e85ae399c8efaab7435534441002c14a963c4c0b001eb8 +DIST sylpheed-3.7.0.tar.bz2 3612328 BLAKE2B bd8182db8a46b956e12b3da4b15d3ee8184a612e2fb216aca20fd4a022610b17416f994d36b390a5a92835915e95f08bd59bf71154a86962c9564162be891f21 SHA512 490837528bf7ba9d26994cd5fff00b6e5390a127419b9d0efd9fc25c38be1291d55c5b8daebdf5ca9d9159a51c938449e76212328f3eae40cc039db88cb5caa4 +EBUILD sylpheed-3.7.0-r1.ebuild 1801 BLAKE2B 35c5232beea2bf460817409bd552e3914b7d5453db4e5b2438cfd70be617b08d278b019873a48cc6430f75319289d077c3840023513cac31d4e4609a1d710a41 SHA512 13a808da1cb04f34c727cf1eaf3c8bca2158073efa459eb446c065fc46a6798d232d18af0aa8c221d2b22399f3c4c47f778ca45bcb5a59bffb24c03a91452e79 +MISC metadata.xml 333 BLAKE2B 45fdb2ceec80729593ac9d58a70399596ebff40c7eb68010f02a6025347b19775c8ec7483006e6c2abe826e6426cd53685835ccf2ed2b4670b16be8ea3da974c SHA512 0e5e8e8dcf6bbb73a907313cbc5bd234bc31c645759bd9ec33193fae5bd9b9b08cd884ea4adcb941b6693049f27522cd210bc3e7e6d14acaf1db595883ddbd4e diff --git a/mail-client/sylpheed/files/sylpheed-3.7.0-ALL.patch b/mail-client/sylpheed/files/sylpheed-3.7.0-ALL.patch new file mode 100644 index 0000000..0a34542 --- /dev/null +++ b/mail-client/sylpheed/files/sylpheed-3.7.0-ALL.patch @@ -0,0 +1,2149 @@ +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, diff --git a/mail-client/sylpheed/files/sylpheed-3.7.0-PGP-signature-fix.patch b/mail-client/sylpheed/files/sylpheed-3.7.0-PGP-signature-fix.patch new file mode 100644 index 0000000..f41a22f --- /dev/null +++ b/mail-client/sylpheed/files/sylpheed-3.7.0-PGP-signature-fix.patch @@ -0,0 +1,18 @@ +diff -rupN sylpheed-3.7.0/libsylph/utils.c sylpheed-3.7.0.new/libsylph/utils.c +--- sylpheed-3.7.0/libsylph/utils.c 2017-11-30 03:34:14.000000000 +0100 ++++ sylpheed-3.7.0.new/libsylph/utils.c 2018-02-08 08:39:06.586381290 +0100 +@@ -3306,10 +3306,10 @@ gint canonicalize_file(const gchar *src, + } + } + +- 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"); diff --git a/mail-client/sylpheed/files/sylpheed-3.7.0-master-password.patch b/mail-client/sylpheed/files/sylpheed-3.7.0-master-password.patch new file mode 100644 index 0000000..b7fc53a --- /dev/null +++ b/mail-client/sylpheed/files/sylpheed-3.7.0-master-password.patch @@ -0,0 +1,1850 @@ +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 91814fd..192964e 100644 +--- a/libsylph/prefs_common.c ++++ b/libsylph/prefs_common.c +@@ -418,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 910f370..6f9c364 100644 +--- a/libsylph/prefs_common.h ++++ b/libsylph/prefs_common.h +@@ -249,6 +249,12 @@ struct _PrefsCommon + 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 6ecf328..3bbcbcf 100644 +--- a/libsylph/utils.c ++++ b/libsylph/utils.c +@@ -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 fa4be4e..119a056 100644 +--- a/src/prefs_common_dialog.c ++++ b/src/prefs_common_dialog.c +@@ -216,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; +@@ -563,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, +@@ -693,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); +@@ -852,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++); +@@ -2648,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; +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/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, diff --git a/mail-client/sylpheed/files/sylpheed-3.7.0-signature-box.patch b/mail-client/sylpheed/files/sylpheed-3.7.0-signature-box.patch new file mode 100644 index 0000000..af8cdaa --- /dev/null +++ b/mail-client/sylpheed/files/sylpheed-3.7.0-signature-box.patch @@ -0,0 +1,267 @@ +diff -x .git -rupN sylpheed-3.7.0/libsylph/prefs_common.c sylpheed-3.7.0.new/libsylph/prefs_common.c +--- sylpheed-3.7.0/libsylph/prefs_common.c 2017-11-24 05:17:23.000000000 +0100 ++++ sylpheed-3.7.0.new/libsylph/prefs_common.c 2018-01-31 10:52:57.094885505 +0100 +@@ -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}, +diff -x .git -rupN sylpheed-3.7.0/libsylph/prefs_common.h sylpheed-3.7.0.new/libsylph/prefs_common.h +--- sylpheed-3.7.0/libsylph/prefs_common.h 2017-11-24 05:16:18.000000000 +0100 ++++ sylpheed-3.7.0.new/libsylph/prefs_common.h 2018-01-31 10:52:57.052886536 +0100 +@@ -243,6 +243,7 @@ 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; +diff -x .git -rupN sylpheed-3.7.0/src/prefs_common_dialog.c sylpheed-3.7.0.new/src/prefs_common_dialog.c +--- sylpheed-3.7.0/src/prefs_common_dialog.c 2014-11-05 08:28:13.000000000 +0100 ++++ sylpheed-3.7.0.new/src/prefs_common_dialog.c 2018-01-31 10:52:57.184883295 +0100 +@@ -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; +@@ -314,6 +315,13 @@ static void prefs_common_attach_toolbtn_ + 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 +548,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, +@@ -2478,10 +2489,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 +2520,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 +2637,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 +@@ -4719,6 +4785,60 @@ static void prefs_common_online_mode_set + } + } + ++#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 -x .git -rupN sylpheed-3.7.0/src/rfc2015.c sylpheed-3.7.0.new/src/rfc2015.c +--- sylpheed-3.7.0/src/rfc2015.c 2014-03-26 06:55:35.000000000 +0100 ++++ sylpheed-3.7.0.new/src/rfc2015.c 2018-01-31 11:10:44.946662147 +0100 +@@ -210,8 +210,12 @@ static void check_signature(MimeInfo *mi + 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 + 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/mail-client/sylpheed/files/sylpheed-3.7.0-version.patch b/mail-client/sylpheed/files/sylpheed-3.7.0-version.patch new file mode 100644 index 0000000..65d560f --- /dev/null +++ b/mail-client/sylpheed/files/sylpheed-3.7.0-version.patch @@ -0,0 +1,26 @@ +diff --git a/configure b/configure +index 6984cce..cc2b770 100755 +--- a/configure ++++ b/configure +@@ -2581,7 +2581,7 @@ MINOR_VERSION=7 + MICRO_VERSION=0 + INTERFACE_AGE=0 + BINARY_AGE=0 +-EXTRA_VERSION= ++EXTRA_VERSION=-sgs + BUILD_REVISION=1185 + VERSION=$MAJOR_VERSION.$MINOR_VERSION.$MICRO_VERSION$EXTRA_VERSION + +diff --git a/configure.ac b/configure.ac +index 090177d..ae65343 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -9,7 +9,7 @@ MINOR_VERSION=7 + MICRO_VERSION=0 + INTERFACE_AGE=0 + BINARY_AGE=0 +-EXTRA_VERSION= ++EXTRA_VERSION=-sgs + BUILD_REVISION=1185 + VERSION=$MAJOR_VERSION.$MINOR_VERSION.$MICRO_VERSION$EXTRA_VERSION + diff --git a/mail-client/sylpheed/metadata.xml b/mail-client/sylpheed/metadata.xml new file mode 100644 index 0000000..2facbf5 --- /dev/null +++ b/mail-client/sylpheed/metadata.xml @@ -0,0 +1,10 @@ + + + + + blackmore@pichove.org + + + Use dev-libs/oniguruma for regular expression + + diff --git a/mail-client/sylpheed/sylpheed-3.7.0-r1.ebuild b/mail-client/sylpheed/sylpheed-3.7.0-r1.ebuild new file mode 100644 index 0000000..f94d992 --- /dev/null +++ b/mail-client/sylpheed/sylpheed-3.7.0-r1.ebuild @@ -0,0 +1,76 @@ +# Copyright 2018 Simeon Simeonov +# Distributed under the terms of the GNU General Public License v2 + +EAPI="6" + +inherit eutils + +DESCRIPTION="A lightweight email client and newsreader (patched by sgs)" +HOMEPAGE="http://sylpheed.sraoss.jp/" +SRC_URI="http://${PN}.sraoss.jp/${PN}/v${PV%.*}/${P}.tar.bz2" + +LICENSE="GPL-2 LGPL-2.1" +SLOT="0" +KEYWORDS="amd64 x86" +IUSE="crypt ipv6 ldap libressl nls oniguruma pda spell ssl xface" + +CDEPEND="net-libs/liblockfile + x11-libs/gtk+:2 + crypt? ( app-crypt/gpgme ) + ldap? ( net-nds/openldap ) + nls? ( sys-devel/gettext ) + oniguruma? ( dev-libs/oniguruma:= ) + pda? ( app-pda/jpilot ) + spell? ( + app-text/gtkspell:2 + dev-libs/dbus-glib + ) + ssl? ( + !libressl? ( dev-libs/openssl:0 ) + libressl? ( dev-libs/libressl ) + )" +RDEPEND="${CDEPEND} + app-misc/mime-types + net-misc/curl" +DEPEND="${CDEPEND} + virtual/pkgconfig + xface? ( media-libs/compface )" + +PATCHES=( + "${FILESDIR}/${P}-signature-box.patch" + "${FILESDIR}/${P}-PGP-signature-fix.patch" + "${FILESDIR}/${P}-master-password.patch" + "${FILESDIR}/${P}-version.patch" +) + +DOCS="AUTHORS ChangeLog* NEW* PLUGIN* README* TODO*" + +src_configure() { + local htmldir="${EPREFIX}"/usr/share/doc/${PF}/html + econf \ + $(use_enable crypt gpgme) \ + $(use_enable ipv6) \ + $(use_enable ldap) \ + $(use_enable oniguruma) \ + $(use_enable pda jpilot) \ + $(use_enable spell gtkspell) \ + $(use_enable ssl) \ + $(use_enable xface compface) \ + --with-plugindir="${EPREFIX}"/usr/$(get_libdir)/${PN}/plugins \ + --with-manualdir="${htmldir}"/manual \ + --with-faqdir="${htmldir}"/faq \ + --disable-updatecheckplugin \ + --disable-updatecheck +} + +src_install() { + default + + doicon *.png + domenu *.desktop + + cd plugin/attachment_tool + emake DESTDIR="${D}" install-plugin + docinto plugin/attachment_tool + dodoc README +} -- cgit v1.3