From bd8ececb46b48403a62ef27fc8139d31366af39e Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Sat, 29 Sep 2007 20:51:01 +0000 Subject: [PATCH] encrypted secret storage and an improved SAS verification dialog --- src/common/config.py | 1 + src/common/configpaths.py | 1 + src/common/connection_handlers.py | 4 + src/common/exceptions.py | 4 + src/common/gajim.py | 7 +- src/common/stanza_session.py | 53 ++++--- src/dialogs.py | 25 +++- src/gajim.py | 224 +++++++++++++----------------- src/negotiation.py | 18 ++- src/secrets.py | 194 ++++++++++++++++++++++++++ 10 files changed, 368 insertions(+), 163 deletions(-) create mode 100644 src/secrets.py diff --git a/src/common/config.py b/src/common/config.py index 7d965308e..7f51d4000 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -177,6 +177,7 @@ class Config: '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], diff --git a/src/common/configpaths.py b/src/common/configpaths.py index f7dbb49d4..f71e6c9b2 100644 --- a/src/common/configpaths.py +++ b/src/common/configpaths.py @@ -110,6 +110,7 @@ class ConfigPaths: 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) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 4fbba26da..a95ac1d29 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1240,6 +1240,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, 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 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, 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: diff --git a/src/common/exceptions.py b/src/common/exceptions.py index 79deaf04c..20ac2c09d 100644 --- a/src/common/exceptions.py +++ b/src/common/exceptions.py @@ -62,6 +62,10 @@ class DecryptionError(Exception): '''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=''): diff --git a/src/common/gajim.py b/src/common/gajim.py index 443e96d46..f43967edf 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -138,14 +138,9 @@ for status in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): 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('@') diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 834223fa5..e27c9b529 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -7,11 +7,8 @@ from common import exceptions import random import string -import math -import os import time -from common import dh import xmpp.c14n import base64 @@ -73,6 +70,9 @@ class StanzaSession(object): 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 @@ if gajim.HAVE_PYCRYPTO: 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 @@ class EncryptedStanzaSession(StanzaSession): 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 @@ class EncryptedStanzaSession(StanzaSession): 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 @@ class EncryptedStanzaSession(StanzaSession): 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 @@ class EncryptedStanzaSession(StanzaSession): 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 @@ class EncryptedStanzaSession(StanzaSession): 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 @@ class EncryptedStanzaSession(StanzaSession): 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 @@ class EncryptedStanzaSession(StanzaSession): 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,10 +808,18 @@ class EncryptedStanzaSession(StanzaSession): 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 = [] diff --git a/src/dialogs.py b/src/dialogs.py index 994485b46..5b5b841a2 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -294,7 +294,10 @@ class PassphraseDialog: 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 YesNoDialog(HigDialog): 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 @@ class ConfirmationDialogCheck(ConfirmationDialog): 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() diff --git a/src/gajim.py b/src/gajim.py index c89427d54..7f809a8a3 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -126,8 +126,6 @@ from common import dbus_support 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 @@ gajimpaths = common.configpaths.gajimpaths pid_filename = gajimpaths['PID_FILE'] config_filename = gajimpaths['CONFIG_FILE'] -secrets_filename = gajimpaths['SECRETS_FILE'] import traceback import errno @@ -1529,48 +1526,6 @@ class Interface: 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 @@ class Interface: # 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): + 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) + + 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 + + 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) + + 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: + + %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) + + def ignore_negotiation(widget): 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) - - 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 - negotiated, not_acceptable, ask_user = session.verify_options_bob(form) + continue_with_negotiation() + + return + + # alice accepts + elif session.status == 'requested-e2e' and form.getType() == 'submit': + negotiated, not_acceptable, ask_user = session.verify_options_alice(form) + + if session.sigmai: + def _cb(on_success): + negotiation.show_sas_dialog(session, jid, session.sas, on_success) + + session.check_identity = _cb if ask_user: def accept_nondefault_options(widget): - self.dialog.destroy() + dialog.destroy() + negotiated.update(ask_user) - session.respond_e2e_bob(form, negotiated, not_acceptable) + + try: + session.accept_e2e_alice(form, negotiated) + except exceptions.NegotiationError, details: + session.fail_bad_negotiation(details) 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) + session.reject_negotiation() + dialog.destroy() - self.dialog = dialogs.YesNoDialog(_('Confirm these session options'), - _('''The remote client wants to negotiate an session with these features: - - %s - - Are these options acceptable?''') % (negotiation.describe_features(ask_user)), + 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: - session.respond_e2e_bob(form, negotiated, not_acceptable) - - def ignore_negotiation(widget): - self.dialog.destroy() - return - - continue_with_negotiation() - - return - - # alice accepts - elif session.status == 'requested-e2e' and form.getType() == 'submit': - negotiated, not_acceptable, ask_user = session.verify_options_alice(form) - - if session.sigmai: - session.check_identity = lambda: negotiation.show_sas_dialog(jid, session.sas) - - 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: + 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': + + def _cb(on_success): + negotiation.show_sas_dialog(session, jid, session.sas, on_success) + + session.check_identity = _cb - 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) + 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) + return + except exceptions.Cancelled: + # user cancelled the negotiation + + session.cancelled_negotiation() - try: - session.final_steps_alice(form) - except exceptions.NegotiationError, details: - session.fail_bad_negotiation(details) - return if form.getField('terminate'): @@ -2824,11 +2796,5 @@ if __name__ == '__main__': 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() diff --git a/src/negotiation.py b/src/negotiation.py index 73917400c..d0473266c 100644 --- a/src/negotiation.py +++ b/src/negotiation.py @@ -14,10 +14,22 @@ def describe_features(features): 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''' diff --git a/src/secrets.py b/src/secrets.py new file mode 100644 index 000000000..a95f4cd0d --- /dev/null +++ b/src/secrets.py @@ -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