Author: bct
Date: 2007-09-29 22:51:01 +0200 (Sat, 29 Sep 2007)
New Revision: 8873
Added:
trunk/src/secrets.py
Modified:
trunk/src/common/config.py
trunk/src/common/configpaths.py
trunk/src/common/connection_handlers.py
trunk/src/common/exceptions.py
trunk/src/common/gajim.py
trunk/src/common/stanza_session.py
trunk/src/dialogs.py
trunk/src/gajim.py
trunk/src/negotiation.py
Log:
encrypted secret storage and an improved SAS verification dialog
Modified: trunk/src/common/config.py
===================================================================
--- trunk/src/common/config.py 2007-09-27 21:34:00 UTC (rev 8872)
+++ trunk/src/common/config.py 2007-09-29 20:51:01 UTC (rev 8873)
@@ -177,6 +177,7 @@
'tabs_border': [opt_bool, False, _('Show tabbed notebook border in chat windows?')],
'tabs_close_button': [opt_bool, True, _('Show close button in tab?')],
'log_encrypted_sessions': [opt_bool, False, _('When negotiating an encrypted session, should Gajim assume you want your messages to be logged?')],
+ 'e2e_public_key': [opt_bool, False, _('When negotiating an encrypted session, should Gajim prefer to use public keys for identification?')],
'chat_avatar_width': [opt_int, 52],
'chat_avatar_height': [opt_int, 52],
'roster_avatar_width': [opt_int, 32],
Modified: trunk/src/common/configpaths.py
===================================================================
--- trunk/src/common/configpaths.py 2007-09-27 21:34:00 UTC (rev 8872)
+++ trunk/src/common/configpaths.py 2007-09-29 20:51:01 UTC (rev 8873)
@@ -110,6 +110,7 @@
if len(profile) > 0:
conffile += u'.' + profile
pidfile += u'.' + profile
+ secretsfile += u'.' + profile
pidfile += u'.pid'
self.add_from_root('CONFIG_FILE', conffile)
self.add_from_root('PID_FILE', pidfile)
Modified: trunk/src/common/connection_handlers.py
===================================================================
--- trunk/src/common/connection_handlers.py 2007-09-27 21:34:00 UTC (rev 8872)
+++ trunk/src/common/connection_handlers.py 2007-09-29 20:51:01 UTC (rev 8873)
@@ -1240,6 +1240,8 @@
reply.addChild(node=xmpp.ErrorNode('service-unavailable', typ='cancel'))
con.send(reply)
+
+ raise common.xmpp.NodeProcessed
def _InitE2ECB(self, con, stanza, session):
gajim.log.debug('InitE2ECB')
@@ -1248,6 +1250,8 @@
self.dispatch('SESSION_NEG', (stanza.getFrom(), session, form))
+ raise common.xmpp.NodeProcessed
+
def _ErrorCB(self, con, iq_obj):
gajim.log.debug('ErrorCB')
if iq_obj.getQueryNS() == common.xmpp.NS_VERSION:
Modified: trunk/src/common/exceptions.py
===================================================================
--- trunk/src/common/exceptions.py 2007-09-27 21:34:00 UTC (rev 8872)
+++ trunk/src/common/exceptions.py 2007-09-29 20:51:01 UTC (rev 8873)
@@ -62,6 +62,10 @@
'''A message couldn't be decrypted into usable XML'''
pass
+class Cancelled(Exception):
+ '''The user cancelled an operation'''
+ pass
+
class GajimGeneralException(Exception):
'''This exception is our general exception'''
def __init__(self, text=''):
Modified: trunk/src/common/gajim.py
===================================================================
--- trunk/src/common/gajim.py 2007-09-27 21:34:00 UTC (rev 8872)
+++ trunk/src/common/gajim.py 2007-09-29 20:51:01 UTC (rev 8873)
@@ -138,14 +138,9 @@
HAVE_PYCRYPTO = True
try:
- from Crypto.PublicKey.RSA import generate
- import os
+ import Crypto
except ImportError:
HAVE_PYCRYPTO = False
-else:
- # public key for XEP-0116
- #FIXME os.urandom is not a cryptographic PRNG
- pubkey = generate(384, os.urandom)
def get_nick_from_jid(jid):
pos = jid.find('@')
Modified: trunk/src/common/stanza_session.py
===================================================================
--- trunk/src/common/stanza_session.py 2007-09-27 21:34:00 UTC (rev 8872)
+++ trunk/src/common/stanza_session.py 2007-09-29 20:51:01 UTC (rev 8873)
@@ -7,11 +7,8 @@
import random
import string
-import math
-import os
import time
-from common import dh
import xmpp.c14n
import base64
@@ -73,6 +70,9 @@
def cancelled_negotiation(self):
'''A negotiation has been cancelled, so reset this session to its default state.'''
+
+ # XXX notify the user
+
self.status = None
self.negotiated = {}
@@ -101,6 +101,9 @@
from Crypto.PublicKey import RSA
from common import crypto
+ from common import dh
+ import secrets
+
# an encrypted stanza negotiation has several states. i've represented them
# as the following values in the 'status'
# attribute of the session object:
@@ -231,11 +234,9 @@
return compressed
def encrypt(self, encryptable):
- len_padding = 16 - (len(encryptable) % 16)
- if len_padding != 16:
- encryptable += len_padding * ' '
+ padded = crypto.pad_to_multiple(encryptable, 16, ' ', False)
- return self.encrypter.encrypt(encryptable)
+ return self.encrypter.encrypt(padded)
def decrypt_stanza(self, stanza):
c = stanza.getTag(name='c',
@@ -355,7 +356,7 @@
def make_identity(self, form, dh_i):
if self.negotiated['send_pubkey']:
if self.negotiated['sign_algs'] == (XmlDsig + 'rsa-sha256'):
- pubkey = gajim.interface.get_pubkey(self.conn.name)
+ pubkey = secrets.secrets().my_pubkey(self.conn.name)
fields = (pubkey.n, pubkey.e)
cb_fields = map(lambda f: base64.b64encode(crypto.encode_mpi(f)), fields)
@@ -392,7 +393,8 @@
self.sas = crypto.sas_28x5(m_s, self.form_o)
if self.sigmai:
- self.check_identity()
+ # XXX save retained secret?
+ self.check_identity(lambda : ())
return (xmpp.DataField(name='identity', value=base64.b64encode(id_s)), \
xmpp.DataField(name='mac', value=base64.b64encode(m_s)))
@@ -664,11 +666,13 @@
self.kc_o, self.km_o, self.ks_o = self.generate_responder_keys(self.k)
self.verify_identity(form, self.d, True, 'b')
else:
- secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped())
- rshashes = [self.hmac(self.n_s, rs) for rs in secrets]
+ srses = secrets.secrets().retained_secrets(self.conn.name, self.jid.getStripped())
+ rshashes = [self.hmac(self.n_s, rs) for (rs,v) in srses]
- # XXX add some random fake rshashes here
- rshashes.sort()
+ if not rshashes:
+ # we've never spoken before, but we'll pretend we have
+ rshash_size = self.hash_alg.digest_size
+ rshashes.append(crypto.random_bytes(rshash_size))
rshashes = [base64.b64encode(rshash) for rshash in rshashes]
result.addChild(node=xmpp.DataField(name='rshashes', value=rshashes))
@@ -676,7 +680,6 @@
self.form_o = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren()))
-
# MUST securely destroy K unless it will be used later to generate the final shared secret
for datafield in self.make_identity(result, e):
@@ -722,10 +725,10 @@
srs = ''
- secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped())
+ srses = secrets.secrets().retained_secrets(self.conn.name, self.jid.getStripped())
rshashes = [base64.b64decode(rshash) for rshash in form.getField('rshashes').getValues()]
- for secret in secrets:
+ for (secret, verified) in srses:
if self.hmac(self.n_o, secret) in rshashes:
srs = secret
break
@@ -766,11 +769,11 @@
def final_steps_alice(self, form):
srs = ''
- secrets = gajim.interface.list_secrets(self.conn.name, self.jid.getStripped())
+ srses = secrets.secrets().retained_secrets(self.conn.name, self.jid.getStripped())
srshash = base64.b64decode(form['srshash'])
- for secret in secrets:
+ for (secret, verified) in srses:
if self.hmac(secret, 'Shared Retained Secret') == srshash:
srs = secret
break
@@ -805,11 +808,19 @@
bjid = self.jid.getStripped()
if srs:
- gajim.interface.replace_secret(account, bjid, srs, new_srs)
+ if secrets.secrets().srs_verified(account, bjid, srs):
+ secrets.secrets().replace_srs(account, bjid, srs, new_srs, True)
+ else:
+ def _cb(verified):
+ secrets.secrets().replace_srs(account, bjid, srs, new_srs, verified)
+
+ self.check_identity(_cb)
else:
- self.check_identity()
- gajim.interface.save_new_secret(account, bjid, new_srs)
+ def _cb(verified):
+ secrets.secrets().save_new_srs(account, bjid, new_srs, verified)
+ self.check_identity(_cb)
+
def make_dhfield(self, modp_options, sigmai):
dhs = []
Modified: trunk/src/dialogs.py
===================================================================
--- trunk/src/dialogs.py 2007-09-27 21:34:00 UTC (rev 8872)
+++ trunk/src/dialogs.py 2007-09-29 20:51:01 UTC (rev 8873)
@@ -294,7 +294,10 @@
self.window.destroy()
- self.ok_handler(passph, checked)
+ if isinstance(self.ok_handler, tuple):
+ self.ok_handler[0](passph, checked, *self.ok_handler[1:])
+ else:
+ self.ok_handler(passph, checked)
def on_cancelbutton_clicked(self, widget):
self.window.destroy()
@@ -1065,10 +1068,13 @@
class ConfirmationDialogCheck(ConfirmationDialog):
'''HIG compliant confirmation dialog with checkbutton.'''
def __init__(self, pritext, sectext='', checktext = '',
- on_response_ok = None, on_response_cancel = None):
+ on_response_ok = None, on_response_cancel = None, is_modal = True):
+ self.user_response_ok = on_response_ok
+ self.user_response_cancel = on_response_cancel
+
HigDialog.__init__(self, None, gtk.MESSAGE_QUESTION,
- gtk.BUTTONS_OK_CANCEL, pritext, sectext, on_response_ok,
- on_response_cancel)
+ gtk.BUTTONS_OK_CANCEL, pritext, sectext, self.on_response_ok,
+ self.on_response_cancel)
self.set_default_response(gtk.RESPONSE_OK)
@@ -1077,8 +1083,19 @@
self.checkbutton = gtk.CheckButton(checktext)
self.vbox.pack_start(self.checkbutton, expand = False, fill = True)
+ self.set_modal(is_modal)
self.popup()
+ # XXX should cancel if somebody closes the dialog
+
+ def on_response_ok(self, widget):
+ self.user_response_ok(self.is_checked())
+ self.destroy()
+
+ def on_response_cancel(self, widget):
+ self.user_response_cancel()
+ self.destroy()
+
def is_checked(self):
''' Get active state of the checkbutton '''
return self.checkbutton.get_active()
Modified: trunk/src/gajim.py
===================================================================
--- trunk/src/gajim.py 2007-09-27 21:34:00 UTC (rev 8872)
+++ trunk/src/gajim.py 2007-09-29 20:51:01 UTC (rev 8873)
@@ -126,8 +126,6 @@
if dbus_support.supported:
import dbus
-import pickle
-
if os.name == 'posix': # dl module is Unix Only
try: # rename the process name to gajim
import dl
@@ -222,7 +220,6 @@
pid_filename = gajimpaths['PID_FILE']
config_filename = gajimpaths['CONFIG_FILE']
-secrets_filename = gajimpaths['SECRETS_FILE']
import traceback
import errno
@@ -1529,48 +1526,6 @@
if os.path.isfile(path_to_file + '_notif_size_bw' + ext):
os.remove(path_to_file + '_notif_size_bw' + ext)
- # list the retained secrets we have for a local account and a remote jid
- def list_secrets(self, account, jid):
- f = open(secrets_filename)
-
- try:
- s = pickle.load(f)[account][jid]
- except KeyError:
- s = []
-
- f.close()
- return s
-
- # save a new retained secret
- def save_new_secret(self, account, jid, secret):
- f = open(secrets_filename, 'r')
- secrets = pickle.load(f)
- f.close()
-
- if not account in secrets:
- secrets[account] = {}
-
- if not jid in secrets[account]:
- secrets[account][jid] = []
-
- secrets[account][jid].append(secret)
-
- f = open(secrets_filename, 'w')
- pickle.dump(secrets, f)
- f.close()
-
- def replace_secret(self, account, jid, old_secret, new_secret):
- f = open(secrets_filename, 'r')
- secrets = pickle.load(f)
- f.close()
-
- this_secrets = secrets[account][jid]
- this_secrets[this_secrets.index(old_secret)] = new_secret
-
- f = open(secrets_filename, 'w')
- pickle.dump(secrets, f)
- f.close()
-
def add_event(self, account, jid, type_, event_args):
'''add an event to the gajim.events var'''
# We add it to the gajim.events queue
@@ -1867,109 +1822,126 @@
# encrypted session states. these are described in stanza_session.py
- # bob responds
- if form.getType() == 'form' and 'security' in form.asDict():
- def continue_with_negotiation(*args):
- if len(args):
- self.dialog.destroy()
+ try:
+ # bob responds
+ if form.getType() == 'form' and 'security' in form.asDict():
+ def continue_with_negotiation(*args):
+ if len(args):
+ self.dialog.destroy()
- # we don't support 3-message negotiation as the responder
- if 'dhkeys' in form.asDict():
- err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED)
+ # we don't support 3-message negotiation as the responder
+ if 'dhkeys' in form.asDict():
+ err = xmpp.Error(xmpp.Message(), xmpp.ERR_FEATURE_NOT_IMPLEMENTED)
- feature = xmpp.Node(xmpp.NS_FEATURE + ' feature')
- field = xmpp.Node('field')
- field['var'] = 'dhkeys'
-
- feature.addChild(node=field)
- err.addChild(node=feature)
+ feature = xmpp.Node(xmpp.NS_FEATURE + ' feature')
+ field = xmpp.Node('field')
+ field['var'] = 'dhkeys'
+
+ feature.addChild(node=field)
+ err.addChild(node=feature)
- session.send(err)
- return
+ session.send(err)
+ return
- negotiated, not_acceptable, ask_user = session.verify_options_bob(form)
+ negotiated, not_acceptable, ask_user = session.verify_options_bob(form)
- if ask_user:
- def accept_nondefault_options(widget):
- self.dialog.destroy()
- negotiated.update(ask_user)
- session.respond_e2e_bob(form, negotiated, not_acceptable)
+ if ask_user:
+ def accept_nondefault_options(widget):
+ self.dialog.destroy()
+ negotiated.update(ask_user)
+ session.respond_e2e_bob(form, negotiated, not_acceptable)
- def reject_nondefault_options(widget):
- self.dialog.destroy()
- for key in ask_user.keys():
- not_acceptable.append(key)
- session.respond_e2e_bob(form, negotiated, not_acceptable)
+ def reject_nondefault_options(widget):
+ self.dialog.destroy()
+ for key in ask_user.keys():
+ not_acceptable.append(key)
+ session.respond_e2e_bob(form, negotiated, not_acceptable)
- self.dialog = dialogs.YesNoDialog(_('Confirm these session options'),
- _('''The remote client wants to negotiate an session with these features:
+ self.dialog = dialogs.YesNoDialog(_('Confirm these session options'),
+ _('''The remote client wants to negotiate an session with these features:
- %s
+ %s
- Are these options acceptable?''') % (negotiation.describe_features(ask_user)),
- on_response_yes = accept_nondefault_options,
- on_response_no = reject_nondefault_options)
- else:
- session.respond_e2e_bob(form, negotiated, not_acceptable)
+ Are these options acceptable?''') % (negotiation.describe_features(ask_user)),
+ on_response_yes = accept_nondefault_options,
+ on_response_no = reject_nondefault_options)
+ else:
+ session.respond_e2e_bob(form, negotiated, not_acceptable)
- def ignore_negotiation(widget):
- self.dialog.destroy()
+ def ignore_negotiation(widget):
+ self.dialog.destroy()
+ return
+
+ continue_with_negotiation()
+
return
- continue_with_negotiation()
+ # alice accepts
+ elif session.status == 'requested-e2e' and form.getType() == 'submit':
+ negotiated, not_acceptable, ask_user = session.verify_options_alice(form)
- return
+ if session.sigmai:
+ def _cb(on_success):
+ negotiation.show_sas_dialog(session, jid, session.sas, on_success)
- # alice accepts
- elif session.status == 'requested-e2e' and form.getType() == 'submit':
- negotiated, not_acceptable, ask_user = session.verify_options_alice(form)
+ session.check_identity = _cb
- if session.sigmai:
- session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas)
+ if ask_user:
+ def accept_nondefault_options(widget):
+ dialog.destroy()
- if ask_user:
- def accept_nondefault_options(widget):
- dialog.destroy()
+ negotiated.update(ask_user)
+
+ try:
+ session.accept_e2e_alice(form, negotiated)
+ except exceptions.NegotiationError, details:
+ session.fail_bad_negotiation(details)
- negotiated.update(ask_user)
-
+ def reject_nondefault_options(widget):
+ session.reject_negotiation()
+ dialog.destroy()
+
+ dialog = dialogs.YesNoDialog(_('Confirm these session options'),
+ _('The remote client selected these options:\n\n%s\n\nContinue with the session?') % (negotiation.describe_features(ask_user)),
+ on_response_yes = accept_nondefault_options,
+ on_response_no = reject_nondefault_options)
+ else:
try:
session.accept_e2e_alice(form, negotiated)
- except exceptions.NegotiationError, details:
+ except exceptions.NegotiationError, details:
session.fail_bad_negotiation(details)
- def reject_nondefault_options(widget):
- session.reject_negotiation()
- dialog.destroy()
+ return
+ elif session.status == 'responded-e2e' and form.getType() == 'result':
- dialog = dialogs.YesNoDialog(_('Confirm these session options'),
- _('The remote client selected these options:\n\n%s\n\nContinue with the session?') % (negotiation.describe_features(ask_user)),
- on_response_yes = accept_nondefault_options,
- on_response_no = reject_nondefault_options)
- else:
+ def _cb(on_success):
+ negotiation.show_sas_dialog(session, jid, session.sas, on_success)
+
+ session.check_identity = _cb
+
try:
- session.accept_e2e_alice(form, negotiated)
+ session.accept_e2e_bob(form)
except exceptions.NegotiationError, details:
session.fail_bad_negotiation(details)
- return
- elif session.status == 'responded-e2e' and form.getType() == 'result':
- session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas)
+ return
+ elif session.status == 'identified-alice' and form.getType() == 'result':
+ def _cb(on_success):
+ negotiation.show_sas_dialog(session, jid, session.sas, on_success)
- try:
- session.accept_e2e_bob(form)
- except exceptions.NegotiationError, details:
- session.fail_bad_negotiation(details)
+ session.check_identity = _cb
+
+ try:
+ session.final_steps_alice(form)
+ except exceptions.NegotiationError, details:
+ session.fail_bad_negotiation(details)
- return
- elif session.status == 'identified-alice' and form.getType() == 'result':
- session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas)
-
- try:
- session.final_steps_alice(form)
- except exceptions.NegotiationError, details:
- session.fail_bad_negotiation(details)
+ return
+ except exceptions.Cancelled:
+ # user cancelled the negotiation
+ session.cancelled_negotiation()
+
return
if form.getField('terminate'):
@@ -2824,11 +2796,5 @@
check_paths.check_and_possibly_create_paths()
- # create secrets file (unless it exists)
- if not os.path.exists(secrets_filename):
- f = open(secrets_filename, 'w')
- pickle.dump({}, f)
- f.close()
-
Interface()
gtk.main()
Modified: trunk/src/negotiation.py
===================================================================
--- trunk/src/negotiation.py 2007-09-27 21:34:00 UTC (rev 8872)
+++ trunk/src/negotiation.py 2007-09-29 20:51:01 UTC (rev 8873)
@@ -14,11 +14,23 @@
elif features['logging'] == 'mustnot':
return _('- messages will not be logged')
-def show_sas_dialog(jid, sas):
- dialogs.InformationDialog(_('''Verify the remote client's identity'''), _('''You've begun an encrypted session with %s, but it can't be guaranteed that you're talking directly to the person you think you are.
+def show_sas_dialog(session, jid, sas, on_success):
+ def success_cb(checked):
+ on_success(checked)
-You should speak with them directly (in person or on the phone) and confirm that their Short Authentication String is identical to this one: %s''') % (jid, sas))
+ def failure_cb():
+ session.cancelled_negotiation()
+ dialogs.ConfirmationDialogCheck(_('''OK to continue with negotiation?'''),
+ _('''You've begun an encrypted session with %s, but it can't be guaranteed that you're talking directly to the person you think you are.
+
+You should speak with them directly (in person or on the phone) and confirm that their Short Authentication String is identical to this one: %s
+
+Would you like to continue with the encrypted session?''') % (jid, sas),
+
+ _('Yes, I verified the Short Authentication String'),
+ on_response_ok=success_cb, on_response_cancel=failure_cb, is_modal=False)
+
class FeatureNegotiationWindow:
'''FeatureNegotiotionWindow class'''
def __init__(self, account, jid, session, form):
Added: trunk/src/secrets.py
===================================================================
--- trunk/src/secrets.py (rev 0)
+++ trunk/src/secrets.py 2007-09-29 20:51:01 UTC (rev 8873)
@@ -0,0 +1,194 @@
+from common.configpaths import gajimpaths
+
+from common import crypto
+from common import exceptions
+
+import dialogs
+
+import os
+import pickle
+
+import gtk
+
+import Crypto.Cipher.AES
+import Crypto.Hash.SHA256
+import Crypto.PublicKey.RSA
+
+secrets_filename = gajimpaths['SECRETS_FILE']
+secrets_cache = None
+
+secrets_cipher = None
+secrets_counter = None
+
+# strength of the encryption used on SECRETS_FILE
+n = 256
+
+class Counter:
+ def __init__(self, n, iv):
+ self.n = n
+ self.c = crypto.decode_mpi(iv)
+
+ def __call__(self):
+ self.c = (self.c + 1) % (2 ** self.n)
+ return crypto.encode_mpi_with_padding(self.c)
+
+# return en/decrypter if it's cached, otherwise create it from the user's
+# passphrase
+def get_key(counter, passph=None):
+ global secrets_cipher, secrets_counter
+
+ if secrets_cipher:
+ return secrets_cipher
+
+ if not passph:
+ passph, checked = dialogs.PassphraseDialog(_('Passphrase Required'),
+ _('To continue, Gajim needs to access your stored secrets. Enter your passphrase')
+ ).run()
+
+ if passph == -1:
+ raise exceptions.Cancelled
+
+ sh = Crypto.Hash.SHA256.new()
+ sh.update(passph)
+ key = sh.digest()
+
+ secrets_counter = counter
+
+ secrets_cipher = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_CTR,
+ counter=secrets_counter)
+
+ return secrets_cipher
+
+class Secrets:
+ def __init__(self, filename):
+ self.filename = filename
+ self.srs = {}
+ self.pubkeys = {}
+ self.privkeys = {}
+
+ def _save(self):
+ global secrets_cipher, secrets_counter
+
+ old_counter = secrets_counter.c
+
+ # pickle doesn't appear to have problems with trailing whitespace
+ padded = crypto.pad_to_multiple(pickle.dumps(self), n / 8, ' ', False)
+ encrypted = secrets_cipher.encrypt(padded)
+
+ f = open(secrets_filename, 'w')
+ f.write(crypto.encode_mpi_with_padding(old_counter) + encrypted)
+ f.close()
+
+ def cancel(self):
+ raise exceptions.Cancelled
+
+ def save(self):
+ passph1 = None
+
+ def _cont1(passph, checked):
+ dialogs.PassphraseDialog(_('Confirm Passphrase'),
+ _('Enter your new passphrase again for confirmation'),
+ is_modal=False, ok_handler=(_cont2, passph), cancel_handler=self.cancel)
+
+ def _cont2(passph, checked, passph1):
+ if passph != passph1:
+ dialogs.PassphraseDialog(_('Create Passphrase'),
+ _('Passphrases did not match.\n') +
+ _('Gajim needs you to create a passphrase to encrypt stored secrets'),
+ is_modal=False, ok_handler=_cont1, cancel_handler=self.cancel)
+ return
+
+ counter = Counter(16, crypto.random_bytes(16))
+ get_key(counter, passph1)
+
+ self._save()
+
+ if not os.path.exists(self.filename):
+ dialogs.PassphraseDialog(_('Create Passphrase'),
+ _('Gajim needs you to create a passphrase to encrypt stored secrets'),
+ is_modal=False, ok_handler=_cont1, cancel_handler=self.cancel)
+ else:
+ self._save()
+
+ def retained_secrets(self, account, bare_jid):
+ try:
+ return self.srs[account][bare_jid]
+ except KeyError:
+ return []
+
+ # retained secrets are stored as a tuple of the secret and whether the user
+ # has verified it
+ def save_new_srs(self, account, jid, secret, verified):
+ if not account in self.srs:
+ self.srs[account] = {}
+
+ if not jid in self.srs[account]:
+ self.srs[account][jid] = []
+
+ self.srs[account][jid].append((secret, verified))
+
+ self.save()
+
+ def find_srs(self, account, jid, srs):
+ our_secrets = self.srs[account][jid]
+ return filter(lambda (x,y): x == srs, our_secrets)[0]
+
+ # has the user verified this retained secret?
+ def srs_verified(self, account, jid, srs):
+ return self.find_srs(account, jid, srs)[1]
+
+ def replace_srs(self, account, jid, old_secret, new_secret, verified):
+ our_secrets = self.srs[account][jid]
+
+ idx = our_secrets.index(self.find_srs(account, jid, old_secret))
+
+ our_secrets[idx] = (new_secret, verified)
+
+ self.save()
+
+ # the public key associated with 'account'
+ def my_pubkey(self, account):
+ try:
+ pk = self.privkeys[account]
+ except KeyError:
+ pk = Crypto.PublicKey.RSA.generate(384, crypto.random_bytes)
+
+ self.privkeys[account] = pk
+ self.save()
+
+ return pk
+
+def load_secrets(filename):
+ f = open(filename, 'r')
+
+ counter = Counter(16, f.read(16))
+
+ decrypted = get_key(counter).decrypt(f.read())
+
+ try:
+ secrets = pickle.loads(decrypted)
+ except:
+ f.close()
+
+ global secrets_cipher
+
+ secrets_cipher = None
+
+ return load_secrets(filename)
+ else:
+ f.close()
+
+ return secrets
+
+def secrets():
+ global secrets_cache
+
+ if secrets_cache:
+ return secrets_cache
+
+ if os.path.exists(secrets_filename):
+ secrets_cache = load_secrets(secrets_filename)
+ else:
+ secrets_cache = Secrets(secrets_filename)
+
+ return secrets_cache