From f0117969b19e05d568a108b12390c47a011576f6 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Thu, 8 Jun 2017 11:14:38 +0200 Subject: [feat] push private key updates into nicknym Deal as well with sending key if key is outdated in the providers nicknym. - Resolves: #8819, #8832 --- src/leap/bitmask/core/mail_services.py | 125 +++++++++++++----------------- src/leap/bitmask/core/service.py | 2 + src/leap/bitmask/keymanager/__init__.py | 125 +++++++++--------------------- src/leap/bitmask/keymanager/openpgp.py | 2 +- src/leap/bitmask/mail/testing/__init__.py | 2 + src/leap/bitmask/mua/pixelizer.py | 4 +- 6 files changed, 97 insertions(+), 163 deletions(-) (limited to 'src/leap/bitmask') diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py index 16b20ed3..d1ccdb95 100644 --- a/src/leap/bitmask/core/mail_services.py +++ b/src/leap/bitmask/core/mail_services.py @@ -29,8 +29,6 @@ from collections import namedtuple from twisted.application import service from twisted.internet import defer -from twisted.internet import reactor -from twisted.internet import task from twisted.logger import Logger from leap.common.events import catalog, emit_async @@ -74,6 +72,8 @@ class ImproperlyConfigured(Exception): class SoledadContainer(Container): + log = Logger() + def __init__(self, service=None, basedir=DEFAULT_BASEDIR): self._basedir = os.path.expanduser(basedir) self._usermap = UserMap() @@ -105,6 +105,14 @@ class SoledadContainer(Container): 'soledad': soledad} self.service.trigger_hook('on_new_soledad_instance', **data) + self.log.debug('Syncing soledad for the first time...') + d = soledad.sync() + d.addCallbacks( + lambda _: + self.service.trigger_hook('on_soledad_first_sync', **data), + lambda _: + self.log.failure('Something failed on soledad first sync')) + def _create_soledad_instance(self, uuid, passphrase, soledad_path, server_url, cert_file, token): # setup soledad info @@ -230,10 +238,6 @@ class KeymanagerContainer(Container): keymanager = self._create_keymanager_instance( userid, token, uuid, soledad) super(KeymanagerContainer, self).add_instance(userid, keymanager) - d = self._get_or_generate_keys(keymanager, userid) - d.addCallback(self._on_keymanager_ready_cb, userid, soledad) - d.addCallback(lambda _: self._set_status(userid, "on", keys="found")) - return d def set_remote_auth_token(self, userid, token): self.get_instance(userid).token = token @@ -243,87 +247,58 @@ class KeymanagerContainer(Container): return {'status': 'off', 'error': None, 'keys': None} return self._status[userid] - def _set_status(self, address, status, error=None, keys=None): - self._status[address] = {"status": status, - "error": error, "keys": keys} - emit_async(catalog.MAIL_STATUS_CHANGED, address) - - def _on_keymanager_ready_cb(self, keymanager, userid, soledad): - data = {'userid': userid, 'soledad': soledad, 'keymanager': keymanager} - self.service.trigger_hook('on_new_keymanager_instance', **data) - - def _get_or_generate_keys(self, keymanager, userid): - - def _get_key(_): - self.log.info('Looking up private key for %s' % userid) - return keymanager.get_key(userid, private=True, fetch_remote=False) + def get_or_generate_keys(self, userid): + keymanager = self.get_instance(userid) def _found_key(key): self.log.info('Found key: %r' % key) + return key def _if_not_found_generate(failure): failure.trap(KeyNotFound) self.log.info('Key not found, generating key for %s' % (userid,)) self._set_status(userid, "starting", keys="generating") d = keymanager.gen_key() - d.addCallbacks(_send_key, _log_key_error("generating")) + d.addErrback(_log_key_error) return d - def _send_key(ignored): - # ---------------------------------------------------------------- - # It might be the case that we have generated a key-pair - # but this hasn't been successfully uploaded. How do we know that? - # XXX Should this be a method of bonafide instead? - # ----------------------------------------------------------------- - self.log.info('Key generated for %s' % userid) - - if not keymanager.token: - self.log.debug( - 'Token not available, scheduling ' - 'a new key sending attempt...') - return task.deferLater(reactor, 5, _send_key, None) - - self.log.info('Sending public key to server') - d = keymanager.send_key() - d.addCallbacks( - lambda _: self.log.info('Key sent to server'), - _log_key_error("sending")) - return d + def _log_key_error(failure): + self.log.failure('Error while generating key!') + error = "Error generating key: %s" % failure.getErrorMessage() + self._set_status(userid, "failure", error=error) + return failure - def _log_key_error(step): - def log_error(failure): - self.log.error('Error while %s key!' % step) - self.log.failure('error!') - error = "Error generating key: %s" % failure.getErrorMessage() - self._set_status(userid, "failure", error=error) - return failure - return log_error - - def _sync_if_never_synced(ever_synced): - if ever_synced: - self.log.debug('Soledad has synced in the past') - return defer.succeed(None) - - self.log.debug('Soledad has never synced') - - if not keymanager.token: - self.log.debug('No token to sync now, scheduling a new check') - d = task.deferLater(reactor, 5, keymanager.ever_synced) - d.addCallback(_sync_if_never_synced) - return d - - self.log.debug('Syncing soledad for the first time...') - self._set_status(userid, "starting", keys="sync") - return keymanager._soledad.sync() - - self.log.debug('Checking if soledad has ever synced...') - d = keymanager.ever_synced() - d.addCallback(_sync_if_never_synced) - d.addCallback(_get_key) + self.log.info('Looking up private key for %s' % userid) + d = keymanager.get_key(userid, private=True, fetch_remote=False) d.addCallbacks(_found_key, _if_not_found_generate) - d.addCallback(lambda _: keymanager) + d.addCallback(self._on_keymanager_ready_cb, keymanager, userid) + self._set_status(userid, "on", keys="found") return d + @defer.inlineCallbacks + def send_if_outdated_key_in_nicknym(self, userid): + keymanager = self.get_instance(userid) + key = yield keymanager.get_key(userid, fetch_remote=False) + try: + remote = yield keymanager._nicknym.fetch_key_with_address(userid) + except Exception: + remote = {} + + if (keymanager.OPENPGP_KEY not in remote or + key.key_data != remote[KeyManager.OPENPGP_KEY]): + yield keymanager.send_key() + + def _set_status(self, address, status, error=None, keys=None): + self._status[address] = {"status": status, + "error": error, "keys": keys} + emit_async(catalog.MAIL_STATUS_CHANGED, address) + + def _on_keymanager_ready_cb(self, key, keymanager, userid): + soledad = keymanager._soledad + data = {'userid': userid, 'soledad': soledad, 'keymanager': keymanager} + self.service.trigger_hook('on_new_keymanager_instance', **data) + return key + def _create_keymanager_instance(self, userid, token, uuid, soledad): user, provider = userid.split('@') nickserver_uri = self._get_nicknym_uri(provider) @@ -384,6 +359,12 @@ class KeymanagerService(HookableService): token = self.tokens.get(user) container.add_instance(user, token, uuid, soledad) + def hook_on_soledad_first_sync(self, **kw): + userid = kw['user'] + d = self._container.get_or_generate_keys(userid) + d.addCallback( + lambda _: self._container.send_if_outdated_key_in_nicknym(userid)) + def hook_on_bonafide_auth(self, **kw): userid = kw['username'] provider = _get_provider_from_full_userid(userid) diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py index 310ac08e..0a3ac6bd 100644 --- a/src/leap/bitmask/core/service.py +++ b/src/leap/bitmask/core/service.py @@ -178,6 +178,8 @@ class BitmaskBackend(configurable.ConfigurableService): if sol: sol.register_hook( 'on_new_soledad_instance', listener='keymanager') + sol.register_hook( + 'on_soledad_first_sync', listener='keymanager') # XXX this might not be the right place for hooking the sessions. # If we want to be offline, we need to authenticate them after diff --git a/src/leap/bitmask/keymanager/__init__.py b/src/leap/bitmask/keymanager/__init__.py index 190e26ff..68184175 100644 --- a/src/leap/bitmask/keymanager/__init__.py +++ b/src/leap/bitmask/keymanager/__init__.py @@ -25,7 +25,7 @@ import tempfile from urlparse import urlparse from twisted.logger import Logger -from twisted.internet import defer +from twisted.internet import defer, task, reactor from twisted.web import client from twisted.web._responses import NOT_FOUND @@ -131,70 +131,6 @@ class KeyManager(object): return tmp_file.name - @defer.inlineCallbacks - def _get_key_from_nicknym(self, address): - """ - Send a GET request to C{uri} containing C{data}. - - :param address: The URI of the request. - :type address: str - - :return: A deferred that will be fired with GET content as json (dict) - :rtype: Deferred - """ - try: - uri = self._nickserver_uri + '?address=' + address - content = yield self._fetch_and_handle_404_from_nicknym( - uri, address) - json_content = json.loads(content) - - except keymanager_errors.KeyNotFound: - raise - except IOError as e: - self.log.warn("HTTP error retrieving key: %r" % (e,)) - self.log.warn("%s" % (content,)) - raise keymanager_errors.KeyNotFound(e.message), \ - None, sys.exc_info()[2] - except ValueError as v: - self.log.warn("Invalid JSON data from key: %s" % (uri,)) - raise keymanager_errors.KeyNotFound(v.message + ' - ' + uri), \ - None, sys.exc_info()[2] - - except Exception as e: - self.log.warn("Error retrieving key: %r" % (e,)) - raise keymanager_errors.KeyNotFound(e.message), \ - None, sys.exc_info()[2] - # Responses are now text/plain, although it's json anyway, but - # this will fail when it shouldn't - # leap_assert( - # res.headers['content-type'].startswith('application/json'), - # 'Content-type is not JSON.') - defer.returnValue(json_content) - - def _fetch_and_handle_404_from_nicknym(self, uri, address): - """ - Send a GET request to C{uri} containing C{data}. - - :param uri: The URI of the request. - :type uri: str - :param address: The email corresponding to the key. - :type address: str - - :return: A deferred that will be fired with GET content as json (dict) - :rtype: Deferred - """ - def check_404(response): - if response.code == NOT_FOUND: - message = '%s: %s key not found.' % (response.code, address) - self.log.warn(message) - raise KeyNotFound(message), None, sys.exc_info()[2] - return response - - d = self._nicknym._async_client_pinned.request( - str(uri), 'GET', callback=check_404) - d.addCallback(client.readBody) - return d - @defer.inlineCallbacks def _get_with_combined_ca_bundle(self, uri, data=None): """ @@ -224,6 +160,7 @@ class KeyManager(object): # key management # + @defer.inlineCallbacks def send_key(self): """ Send user's key to provider. @@ -237,18 +174,20 @@ class KeyManager(object): :raise UnsupportedKeyTypeError: if invalid key type """ - def send(pubkey): - d = self._nicknym.put_key(self.uid, pubkey.key_data, - self._api_uri, self._api_version) - d.addCallback(lambda _: - emit_async(catalog.KEYMANAGER_DONE_UPLOADING_KEYS, - self._address)) - return d - - d = self.get_key( - self._address, private=False, fetch_remote=False) - d.addCallback(send) - return d + if not self.token: + self.log.debug( + 'Token not available, scheduling ' + 'a new key sending attempt...') + yield task.deferLater(reactor, 5, self.send_key) + + self.log.info('Sending public key to server') + key = yield self.get_key(self._address, fetch_remote=False) + yield self._nicknym.put_key(self.uid, key.key_data, + self._api_uri, self._api_version) + emit_async(catalog.KEYMANAGER_DONE_UPLOADING_KEYS, + self._address) + self.log.info('Key sent to server') + defer.returnValue(key) @defer.inlineCallbacks def _fetch_keys_from_server_and_store_local(self, address): @@ -276,7 +215,7 @@ class KeyManager(object): validation_level = ValidationLevels.Provider_Trust yield self.put_raw_key( - server_keys['openpgp'], + server_keys[OPENPGP_KEY], address=address, validation=validation_level) @@ -355,6 +294,7 @@ class KeyManager(object): :raise UnsupportedKeyTypeError: if invalid key type """ def signal_finished(key): + self.log.info('Key generated for %s' % self._address) emit_async( catalog.KEYMANAGER_FINISHED_KEY_GENERATION, self._address) return key @@ -662,6 +602,7 @@ class KeyManager(object): d.addCallback(check_upgrade) return d + @defer.inlineCallbacks def put_raw_key(self, key, address, validation=ValidationLevels.Weak_Chain): """ @@ -688,13 +629,26 @@ class KeyManager(object): pubkey, privkey = self._openpgp.parse_key(key, address) if pubkey is None: - return defer.fail(keymanager_errors.KeyNotFound(key)) + raise keymanager_errors.KeyNotFound(key) + + if address == self._address and not privkey: + try: + existing = yield self.get_key(address, fetch_remote=False) + except KeyNotFound: + existing = None + if (existing is not None or + pubkey.fingerprint != existing.fingerprint): + raise keymanager_errors.KeyNotValidUpgrade( + "Cannot update your %s key without the private part" + % (address,)) pubkey.validation = validation - d = self.put_key(pubkey) + yield self.put_key(pubkey) if privkey is not None: - d.addCallback(lambda _: self.put_key(privkey)) - return d + yield self.put_key(privkey) + + if address == self._address: + yield self.send_key() @defer.inlineCallbacks def fetch_key(self, address, uri, validation=ValidationLevels.Weak_Chain): @@ -731,13 +685,6 @@ class KeyManager(object): pubkey.validation = validation yield self.put_key(pubkey) - def ever_synced(self): - # TODO: provide this method in soledad api, avoid using a private - # attribute here - d = self._soledad._dbpool.runQuery('SELECT * FROM sync_log') - d.addCallback(lambda result: bool(result)) - return d - def _split_email(address): """ diff --git a/src/leap/bitmask/keymanager/openpgp.py b/src/leap/bitmask/keymanager/openpgp.py index 8c9dc1e8..a27eb3de 100644 --- a/src/leap/bitmask/keymanager/openpgp.py +++ b/src/leap/bitmask/keymanager/openpgp.py @@ -226,7 +226,7 @@ class OpenPGPScheme(object): d = self.get_key(address) d.addCallbacks(key_already_exists, _gen_key) - d.addCallback(lambda _: self.get_key(address, private=True)) + d.addCallback(lambda _: self.get_key(address, private=False)) return d def get_key(self, address, private=False): diff --git a/src/leap/bitmask/mail/testing/__init__.py b/src/leap/bitmask/mail/testing/__init__.py index 2fc4c073..e430f6aa 100644 --- a/src/leap/bitmask/mail/testing/__init__.py +++ b/src/leap/bitmask/mail/testing/__init__.py @@ -67,6 +67,8 @@ class KeyManagerWithSoledadTestCase(unittest.TestCase, BaseLeapTest): self.km._nicknym._async_client_pinned.request = Mock( return_value=defer.succeed(Response())) + self.km.send_key = Mock( + return_value=defer.succeed(Response())) d1 = self.km.put_raw_key(PRIVATE_KEY, ADDRESS) d2 = self.km.put_raw_key(PRIVATE_KEY_2, ADDRESS_2) diff --git a/src/leap/bitmask/mua/pixelizer.py b/src/leap/bitmask/mua/pixelizer.py index 98333bd2..819ffa70 100644 --- a/src/leap/bitmask/mua/pixelizer.py +++ b/src/leap/bitmask/mua/pixelizer.py @@ -215,7 +215,9 @@ class NickNym(object): return self.keymanager.gen_key() def _send_key_to_leap(self): - return self.keymanager.send_key() + # XXX: this needs to be removed in pixels side + # km.send_key doesn't exist anymore + return defer.succeed(None) class LeapSessionAdapter(object): -- cgit v1.2.3