Merge lp:~macieksitarz/calibre/optional-keyring-support into lp:calibre

Proposed by Maciej Sitarz
Status: Needs review
Proposed branch: lp:~macieksitarz/calibre/optional-keyring-support
Merge into: lp:calibre
Diff against target: 287 lines (+92/-32)
4 files modified
src/calibre/gui2/email.py (+2/-2)
src/calibre/gui2/wizard/send_email.py (+32/-10)
src/calibre/gui2/wizard/send_email.ui (+26/-19)
src/calibre/utils/smtp.py (+32/-1)
To merge this branch: bzr merge lp:~macieksitarz/calibre/optional-keyring-support
Reviewer Review Type Date Requested Status
Kovid Goyal Disapprove
Review via email: mp+143047@code.launchpad.net

This proposal supersedes a proposal from 2013-01-11.

Description of the change

This branch introduces python keyring support. Keyring's are used as a common secure password storage.

python-keyring:
http://pypi.python.org/pypi/keyring
https://bitbucket.org/kang/python-keyring-lib

It has wide platform support:

The keyring services supported by the Python keyring lib:

    OSXKeychain: supports the Keychain service in Mac OS X.
    KDEKWallet: supports the KDE's Kwallet service.
    GnomeKeyring: for Gnome 2 environment.
    SecretServiceKeyring: for newer GNOME and KDE environments.

Besides these native password storing services provided by operating systems. Python keyring lib also provides following build-in keyrings.

    Win32CryptoKeyring: for Windows 2k+.
    CryptedFileKeyring: a command line interface keyring base on PyCrypto.
    UncryptedFileKeyring: a keyring which leaves passwords directly in file.

To post a comment you must log in.
Revision history for this message
Maciej Sitarz (macieksitarz) wrote : Posted in a previous version of this proposal

Instead of using hexlified password stored in file keyring would be used.
It improves security and uses common keyrings depending on the platform and/or window manager.

Revision history for this message
Maciej Sitarz (macieksitarz) wrote : Posted in a previous version of this proposal

One more thing about testing done.

I tested this on Linux using KWallet(KDE's keyring), but it should work properly on other OSes and window managers, as python-keyring-lib handles that internaly.

Revision history for this message
Kovid Goyal (kovid) wrote : Posted in a previous version of this proposal

Some problems I see:

1) The patch must not break existing setups. Email cannot stop working on a calibre update.

2) The various keyrings depend on environment variables, X sessions, and the like. Those environemnt variables may not be present in all contexts calibre-smtp runs, for example, in cron jobs. That makes calibre-smtp significantly less robust.

3) I dont see how a keyring is fundamentally more or less secure than plaintext password storage. Once an attacher has read access to a user's data, decrypting password stores is possible by brute force attacks. Reversible password storage can simply *never* be secure. However, I do agree it makes an attackers' life a little less convenient.

4) Someone has to test this witht he various keyring implementations. That someone is not going to be me :)

Given all those problems, I am not willing to accept this patch. I will consider a patch that uses an optional keyring based password storage, for people that want to use it. But the default method will remain plaintext storage.

review: Disapprove
Revision history for this message
Maciej Sitarz (macieksitarz) wrote : Posted in a previous version of this proposal

> 1) The patch must not break existing setups. Email cannot stop working on a
> calibre update.

If compatibility is necessary migration code could be added.

Algorithm like:
if relay_password exists in config:
    unhexlify password
    save password to keyring
    remove relay_password from config

> 2) The various keyrings depend on environment variables, X sessions, and the
> like. Those environemnt variables may not be present in all contexts calibre-
> smtp runs, for example, in cron jobs. That makes calibre-smtp significantly
> less robust.

Rigt now calibre-smtp is not using password from config file. Is there such functionality?
I'm asking because I also have a patch adding such functionality.

After applying the patch calibre-smtp will still have the -p/--password option, so one can use it in cron (after specifying plain text password).

> 3) I dont see how a keyring is fundamentally more or less secure than
> plaintext password storage. Once an attacher has read access to a user's data,
> decrypting password stores is possible by brute force attacks. Reversible
> password storage can simply *never* be secure. However, I do agree it makes an
> attackers' life a little less convenient.

Using strong password for the keyring will increase security. But you're right it won't be 100% secure.

> 4) Someone has to test this witht he various keyring implementations. That
> someone is not going to be me :)

Let's wait for volunteers...

> Given all those problems, I am not willing to accept this patch. I will
> consider a patch that uses an optional keyring based password storage, for
> people that want to use it. But the default method will remain plaintext
> storage.

The setting should be boolean "Use keyring for password storage" and visible in the SMTP settings window?

Revision history for this message
Kovid Goyal (kovid) wrote : Posted in a previous version of this proposal

Yes, you would need to add a boolean option (checkbox) that says "Store password securely in keyring" to the GUI (opts.use_keyring) and similarly have a --use-keyring option for calibre-smtp. If opts.use_keyring is True, the code will try to get the password from the keyring, otherwise it will use opts.password.

Revision history for this message
Maciej Sitarz (macieksitarz) wrote :

Resubmitted merge proposal with new branch.

I added the GUI option to use Keyring and also command line option in calibre-smtp.
Values from config file should be moved to keyring and removed from config when necessary.
Also from keyring to config and removed from keyring.

Additionally I fixed one minor bug with TLS/SSL/None. Changing radio buttons from None to SSL did not enable the "Save" option. The SSL button was not monitored for changes.

Revision history for this message
Kovid Goyal (kovid) wrote :

I'll review this in a bit, I have a local maxima in my workload at the moment.

Revision history for this message
Kovid Goyal (kovid) wrote :

I spent half a day looking into the keyring library and frankly, I am not impressed.

A) Windows

1) keyring does not work on my system. It uses environment variables to try to get the location of standard directories. This is broken. It needs to use the win32 API. And a library that reads config files from random places on the users system is not suitable for embedding. It would be trivial for an attacker to write a config file that would cause keyring to use a plain text file storage backend.

2) Examining the source code, it appears to use the ReadCred/WriteCred API to store secrets. This API means that *any* program that the user launches can trivially and without permission read any secrets that calibre stores. This is in no way better than plaintext storage.

B) Linux

1) It uses third party modules (PyKDE/gnomekeyring) to interface with the two major keyrings. These modules will not be available in a binary calibre install

2) The freedesktop.org based keyring backend (SecretService) does not work. Apparently the standard it is based on was never finalized and at least on my system the default collection alias does not exist, which causes the interface to not work. If its broken on my system, it's going to be broken on other systems. In addition, the SecretService API can require the user to respond to a prompt when storing a secret. The keyring backend does not support this.

3) The KDE Wallet based backend asks for permission by pretending ot be the KDE desktop, which is misleading to say the least. Not to mention that it relies on PyKDE instead of using the portable dbus interface.

C) OS X

Given my previous experiences, I did not even bother with this one.

Given this sad state of affairs, I have to decline this merge request. If you are worried about the safety of your email account, I suggest you create a dummy gmail/hotmail/whatever account and use it only for calibre. Much easier and *much* more secure.

Revision history for this message
Kovid Goyal (kovid) wrote :

Rejecting.

review: Disapprove

Unmerged revisions

14092. By Maciej Sitarz

Merge with trunk

14091. By Maciej Sitarz

Monitor both SSL and TLS radio buttons

Enables "Save" after changing from "None" to "SSL"

14090. By Maciej Sitarz

Keyring support for password storage

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/calibre/gui2/email.py'
2--- src/calibre/gui2/email.py 2012-12-20 04:18:34 +0000
3+++ src/calibre/gui2/email.py 2013-01-13 18:50:27 +0000
4@@ -7,13 +7,13 @@
5 __docformat__ = 'restructuredtext en'
6
7 import os, socket, time
8-from binascii import unhexlify
9 from functools import partial
10 from threading import Thread
11 from itertools import repeat
12
13 from calibre.utils.smtp import (compose_mail, sendmail, extract_email_address,
14 config as email_config)
15+from calibre.utils.smtp import get_password
16 from calibre.utils.filenames import ascii_filename
17 from calibre.customize.ui import available_input_formats, available_output_formats
18 from calibre.ebooks.metadata import authors_to_string
19@@ -109,7 +109,7 @@
20 verbose=1,
21 relay=opts.relay_host,
22 username=opts.relay_username,
23- password=unhexlify(opts.relay_password), port=opts.relay_port,
24+ password=get_password(opts, opts.use_keyring), port=opts.relay_port,
25 encryption=opts.encryption,
26 debug_output=log.debug)
27 finally:
28
29=== modified file 'src/calibre/gui2/wizard/send_email.py'
30--- src/calibre/gui2/wizard/send_email.py 2011-07-10 19:09:11 +0000
31+++ src/calibre/gui2/wizard/send_email.py 2013-01-13 18:50:27 +0000
32@@ -7,7 +7,6 @@
33 __docformat__ = 'restructuredtext en'
34
35 import cStringIO, sys
36-from binascii import hexlify, unhexlify
37 from functools import partial
38
39 from PyQt4.Qt import QWidget, pyqtSignal, QDialog, Qt, QLabel, \
40@@ -15,6 +14,8 @@
41
42 from calibre.gui2.wizard.send_email_ui import Ui_Form
43 from calibre.utils.smtp import config as smtp_prefs
44+from calibre.utils.smtp import get_password
45+from calibre.utils.smtp import delete_password_in_keyring
46 from calibre.gui2.dialogs.test_email_ui import Ui_Dialog as TE_Dialog
47 from calibre.gui2 import error_dialog, question_dialog
48
49@@ -32,7 +33,7 @@
50 self.to.setText(pa)
51 if opts.relay_host:
52 self.label.setText(_('Using: %(un)s:%(pw)s@%(host)s:%(port)s and %(enc)s encryption')%
53- dict(un=opts.relay_username, pw=unhexlify(opts.relay_password),
54+ dict(un=opts.relay_username, pw=get_password(opts, opts.use_keyring),
55 host=opts.relay_host, port=opts.relay_port, enc=opts.encryption))
56
57 def test(self, *args):
58@@ -127,11 +128,15 @@
59 self.relay_port.valueChanged.connect(self.changed)
60 if opts.relay_username:
61 self.relay_username.setText(opts.relay_username)
62+ relay_password = get_password(opts, opts.use_keyring)
63+ if relay_password is not None:
64+ self.relay_password.setText(relay_password)
65 self.relay_username.textChanged.connect(self.changed)
66- if opts.relay_password:
67- self.relay_password.setText(unhexlify(opts.relay_password))
68 self.relay_password.textChanged.connect(self.changed)
69+ self.use_keyring.setCheckState(Qt.Checked if opts.use_keyring else Qt.Unchecked)
70+ self.use_keyring.stateChanged.connect(self.changed)
71 getattr(self, 'relay_'+opts.encryption.lower()).setChecked(True)
72+ self.relay_ssl.toggled.connect(self.changed)
73 self.relay_tls.toggled.connect(self.changed)
74
75 for x in ('gmail', 'hotmail'):
76@@ -151,7 +156,7 @@
77 to_set = pa is not None
78 if self.set_email_settings(to_set):
79 opts = smtp_prefs().parse()
80- if not opts.relay_password or question_dialog(self, _('OK to proceed?'),
81+ if not opts.relay_username or question_dialog(self, _('OK to proceed?'),
82 _('This will display your email password on the screen'
83 '. Is it OK to proceed?'), show_copy_button=False):
84 TestEmail(pa, self).exec_()
85@@ -169,7 +174,7 @@
86 sendmail(msg, from_=opts.from_, to=[to],
87 verbose=3, timeout=30, relay=opts.relay_host,
88 username=opts.relay_username,
89- password=unhexlify(opts.relay_password),
90+ password=get_password(opts, opts.use_keyring),
91 encryption=opts.encryption, port=opts.relay_port)
92 except:
93 import traceback
94@@ -224,6 +229,7 @@
95 username = unicode(self.relay_username.text()).strip()
96 password = unicode(self.relay_password.text()).strip()
97 host = unicode(self.relay_host.text()).strip()
98+ use_keyring = True if self.use_keyring.checkState() else False
99 enc_method = ('TLS' if self.relay_tls.isChecked() else 'SSL'
100 if self.relay_ssl.isChecked() else 'NONE')
101 if host:
102@@ -244,13 +250,29 @@
103 ' mailservers need a username and password. Are you sure?')):
104 return False
105 conf = smtp_prefs()
106+ conf_prev = smtp_prefs().parse()
107 conf.set('from_', from_)
108 conf.set('relay_host', host if host else None)
109 conf.set('relay_port', self.relay_port.value())
110 conf.set('relay_username', username if username else None)
111- conf.set('relay_password', hexlify(password))
112+ conf.set('use_keyring', use_keyring)
113 conf.set('encryption', enc_method)
114+
115+ if use_keyring:
116+ if not conf_prev.relay_username is None:
117+ if username != conf_prev.relay_username:
118+ delete_password_in_keyring(conf_prev.relay_username)
119+ if username:
120+ import keyring
121+ keyring.set_password('Calibre', username, password)
122+ else:
123+ from binascii import hexlify
124+ conf.set('relay_password', hexlify(password) if password else None)
125+
126+ if use_keyring != conf_prev.use_keyring:
127+ if use_keyring:
128+ conf.set('relay_password', None)
129+ if conf_prev.use_keyring:
130+ delete_password_in_keyring(conf_prev.relay_username)
131+
132 return True
133-
134-
135-
136
137=== modified file 'src/calibre/gui2/wizard/send_email.ui'
138--- src/calibre/gui2/wizard/send_email.ui 2010-11-25 16:31:29 +0000
139+++ src/calibre/gui2/wizard/send_email.ui 2013-01-13 18:50:27 +0000
140@@ -44,16 +44,6 @@
141 <string>Mail &amp;Server</string>
142 </property>
143 <layout class="QGridLayout" name="gridLayout_3">
144- <item row="0" column="0" colspan="4">
145- <widget class="QLabel" name="label_16">
146- <property name="text">
147- <string>calibre can &lt;b&gt;optionally&lt;/b&gt; use a server to send mail</string>
148- </property>
149- <property name="wordWrap">
150- <bool>true</bool>
151- </property>
152- </widget>
153- </item>
154 <item row="1" column="0">
155 <widget class="QLabel" name="label_17">
156 <property name="text">
157@@ -145,7 +135,7 @@
158 </property>
159 </widget>
160 </item>
161- <item row="4" column="0">
162+ <item row="5" column="0">
163 <widget class="QLabel" name="label_21">
164 <property name="text">
165 <string>&amp;Encryption:</string>
166@@ -168,7 +158,7 @@
167 </property>
168 </widget>
169 </item>
170- <item row="4" column="2">
171+ <item row="5" column="2">
172 <widget class="QRadioButton" name="relay_ssl">
173 <property name="toolTip">
174 <string>Use SSL encryption when connecting to the mail server.</string>
175@@ -178,6 +168,16 @@
176 </property>
177 </widget>
178 </item>
179+ <item row="5" column="3">
180+ <widget class="QRadioButton" name="relay_none">
181+ <property name="toolTip">
182+ <string>WARNING: Using no encryption is highly insecure</string>
183+ </property>
184+ <property name="text">
185+ <string>&amp;None</string>
186+ </property>
187+ </widget>
188+ </item>
189 <item row="3" column="4">
190 <spacer name="horizontalSpacer_3">
191 <property name="orientation">
192@@ -191,13 +191,20 @@
193 </property>
194 </spacer>
195 </item>
196- <item row="4" column="3">
197- <widget class="QRadioButton" name="relay_none">
198- <property name="toolTip">
199- <string>WARNING: Using no encryption is highly insecure</string>
200- </property>
201- <property name="text">
202- <string>&amp;None</string>
203+ <item row="0" column="0" colspan="5">
204+ <widget class="QLabel" name="label_16">
205+ <property name="text">
206+ <string>calibre can &lt;b&gt;optionally&lt;/b&gt; use a server to send mail</string>
207+ </property>
208+ <property name="wordWrap">
209+ <bool>true</bool>
210+ </property>
211+ </widget>
212+ </item>
213+ <item row="4" column="0" colspan="5">
214+ <widget class="QCheckBox" name="use_keyring">
215+ <property name="text">
216+ <string>Store password securely in &amp;keyring</string>
217 </property>
218 </widget>
219 </item>
220
221=== modified file 'src/calibre/utils/smtp.py'
222--- src/calibre/utils/smtp.py 2013-01-11 09:35:10 +0000
223+++ src/calibre/utils/smtp.py 2013-01-13 18:50:27 +0000
224@@ -160,6 +160,8 @@
225 'encryption method is SSL and 25 otherwise.')
226 r('-u', '--username', help='Username for relay')
227 r('-p', '--password', help='Password for relay')
228+ r('-k', '--use-keyring', help='Use keyring to get password',
229+ action='store_true', default=False)
230 r('-e', '--encryption-method', default='TLS',
231 choices=['TLS', 'SSL', 'NONE'],
232 help='Encryption method to use when connecting to relay. Choices are '
233@@ -179,6 +181,27 @@
234 from email.utils import parseaddr
235 return parseaddr(raw)[-1]
236
237+
238+def get_password(opts=None, use_keyring=False):
239+ if use_keyring:
240+ import keyring
241+ if opts.relay_username:
242+ return keyring.get_password('Calibre', opts.relay_username)
243+ return None
244+ else:
245+ if opts:
246+ from binascii import unhexlify
247+ return unhexlify(opts.relay_password)
248+ return None
249+
250+
251+def delete_password_in_keyring(username):
252+ import keyring
253+ try:
254+ keyring.delete_password('Calibre', username)
255+ except:
256+ pass
257+
258 def compose_mail(from_, to, text, subject=None, attachment=None,
259 attachment_name=None):
260 attachment_type = attachment_data = None
261@@ -241,10 +264,17 @@
262 if os.fork() != 0:
263 return 0
264
265+ if opts.use_keyring:
266+ from calibre.utils.smtp import config as smtp_prefs
267+ smtp_opts = smtp_prefs().parse()
268+ password = get_password(smtp_opts, True)
269+ else:
270+ password = opts.password
271+
272 try:
273 sendmail(msg, efrom, eto, localhost=opts.localhost, verbose=opts.verbose,
274 timeout=opts.timeout, relay=opts.relay, username=opts.username,
275- password=opts.password, port=opts.port,
276+ password=password, port=opts.port,
277 encryption=opts.encryption_method)
278 except:
279 if outbox is not None:
280@@ -265,6 +295,7 @@
281 c.add_opt('relay_host')
282 c.add_opt('relay_port', default=25)
283 c.add_opt('relay_username')
284+ c.add_opt('use_keyring', default=False, action='store_true')
285 c.add_opt('relay_password')
286 c.add_opt('encryption', default='TLS', choices=['TLS', 'SSL'])
287 return c

Subscribers

People subscribed via source and target branches