def _encrypt_message(self, uuid_pubkey, address, message): """ Given a UUID, a public key, address and a message, it encrypts the message to that public key. The address is needed in order to build the OpenPGPKey object. @param uuid_pubkey: tuple that holds the uuid and the public key as it is returned by the previous call in the chain @type uuid_pubkey: tuple (str, str) @param address: mail address for this message @type address: str @param message: message contents @type message: str @return: uuid, doc to sync with Soledad @rtype: tuple(str, SoledadDocument) """ uuid, pubkey = uuid_pubkey log.msg("Encrypting message to %s's pubkey" % (uuid,)) log.msg("Pubkey: %s" % (pubkey,)) doc = SoledadDocument(doc_id=str(pyuuid.uuid4())) data = {'incoming': True, 'content': message} if pubkey is None or len(pubkey) == 0: doc.content = { self.INCOMING_KEY: True, ENC_SCHEME_KEY: EncryptionSchemes.NONE, ENC_JSON_KEY: json.dumps(data) } return uuid, doc openpgp_key = None with openpgp.TempGPGWrapper(gpgbinary='/usr/bin/gpg') as gpg: gpg.import_keys(pubkey) key = gpg.list_keys().pop() openpgp_key = openpgp._build_key_from_gpg(address, key, pubkey) doc.content = { self.INCOMING_KEY: True, ENC_SCHEME_KEY: EncryptionSchemes.PUBKEY, ENC_JSON_KEY: str(gpg.encrypt( json.dumps(data), openpgp_key.fingerprint, symmetric=False)) } return uuid, doc
def _put_secrets_in_shared_db(self): """ Assert local keys are the same as shared db's ones. Try to fetch keys from shared recovery database. If they already exist in the remote db, assert that that data is the same as local data. Otherwise, upload keys to shared recovery database. """ soledad_assert( self._has_secret(), 'Tried to send keys to server but they don\'t exist in local ' 'storage.') # try to get secrets doc from server, otherwise create it doc = self._get_secrets_from_shared_db() if doc is None: doc = SoledadDocument( doc_id=self._shared_db_doc_id()) # fill doc with encrypted secrets doc.content = self.export_recovery_document() # upload secrets to server signal(SOLEDAD_UPLOADING_KEYS, self._uuid) db = self._shared_db if not db: logger.warning('No shared db found') return db.put_doc(doc) signal(SOLEDAD_DONE_UPLOADING_KEYS, self._uuid)
def test_stage2_bootstrap_signals(self): """ Test that if there are keys in server, soledad will download them and emit corresponding signals. """ # get existing instance so we have access to keys sol = self._soledad_instance() # create a document with secrets doc = SoledadDocument(doc_id=sol.secrets._shared_db_doc_id()) doc.content = sol.secrets._export_recovery_document() sol.close() # reset mock soledad.client.secrets.events.emit.reset_mock() # get a fresh instance so it emits all bootstrap signals shared_db = self.get_default_shared_mock(get_doc_return_value=doc) sol = self._soledad_instance(secrets_path='alternative_stage2.json', local_db_path='alternative_stage2.u1db', shared_db_class=shared_db) # reverse call order so we can verify in the order the signals were # expected soledad.client.secrets.events.emit.mock_calls.reverse() soledad.client.secrets.events.emit.call_args = \ soledad.client.secrets.events.emit.call_args_list[0] soledad.client.secrets.events.emit.call_args_list.reverse() # assert download keys signals soledad.client.secrets.events.emit.assert_called_with( catalog.SOLEDAD_DOWNLOADING_KEYS, ADDRESS, ) self._pop_mock_call(soledad.client.secrets.events.emit) soledad.client.secrets.events.emit.assert_called_with( catalog.SOLEDAD_DONE_DOWNLOADING_KEYS, ADDRESS, ) sol.close()
def test_stage2_bootstrap_signals(self): """ Test that if there are keys in server, soledad will download them and emit corresponding signals. """ # get existing instance so we have access to keys sol = self._soledad_instance() # create a document with secrets doc = SoledadDocument(doc_id=sol.secrets._shared_db_doc_id()) doc.content = sol.secrets._export_recovery_document() sol.close() # reset mock soledad.client.secrets.events.emit_async.reset_mock() # get a fresh instance so it emits all bootstrap signals shared_db = self.get_default_shared_mock(get_doc_return_value=doc) sol = self._soledad_instance( secrets_path="alternative_stage2.json", local_db_path="alternative_stage2.u1db", shared_db_class=shared_db ) # reverse call order so we can verify in the order the signals were # expected soledad.client.secrets.events.emit_async.mock_calls.reverse() soledad.client.secrets.events.emit_async.call_args = soledad.client.secrets.events.emit_async.call_args_list[0] soledad.client.secrets.events.emit_async.call_args_list.reverse() # assert download keys signals soledad.client.secrets.events.emit_async.assert_called_with( catalog.SOLEDAD_DOWNLOADING_KEYS, {"userid": ADDRESS, "uuid": ADDRESS} ) self._pop_mock_call(soledad.client.secrets.events.emit_async) soledad.client.secrets.events.emit_async.assert_called_with( catalog.SOLEDAD_DONE_DOWNLOADING_KEYS, {"userid": ADDRESS, "uuid": ADDRESS} ) sol.close()
def _put_secrets_in_shared_db(self): """ Assert local keys are the same as shared db's ones. Try to fetch keys from shared recovery database. If they already exist in the remote db, assert that that data is the same as local data. Otherwise, upload keys to shared recovery database. """ soledad_assert( self._has_secret(), 'Tried to send keys to server but they don\'t exist in local ' 'storage.') # try to get secrets doc from server, otherwise create it doc = self._get_secrets_from_shared_db() if doc is None: doc = SoledadDocument(doc_id=self._shared_db_doc_id()) # fill doc with encrypted secrets doc.content = self.export_recovery_document() # upload secrets to server signal(SOLEDAD_UPLOADING_KEYS, self._uuid) db = self._shared_db if not db: logger.warning('No shared db found') return db.put_doc(doc) signal(SOLEDAD_DONE_UPLOADING_KEYS, self._uuid)
def save_remote(self, encrypted): doc = self._remote_doc if not doc: doc = SoledadDocument(doc_id=self._remote_doc_id()) doc.content = encrypted db = self._shared_db if not db: logger.warn('no shared db found') return db.put_doc(doc)
def test_doc_update_succeeds(self): doc_id = 'some-random-doc' self.assertIsNone(self._db.get_doc(doc_id)) # create a document in shared db doc = SoledadDocument(doc_id=doc_id) self._db.put_doc(doc) # update that document expected = {'new': 'content'} doc.content = expected self._db.put_doc(doc) # ensure expected content was saved doc = self._db.get_doc(doc_id) self.assertEqual(expected, doc.content)
def test_extra_comma(self): doc = SoledadDocument('i', rev='r') doc.content = {'a': 'b'} encrypted_docstr = _crypto.SoledadCrypto('safe').encrypt_doc(doc) with self.assertRaises(l2db.errors.BrokenSyncStream): self.parse("[\r\n{},\r\n]") with self.assertRaises(l2db.errors.BrokenSyncStream): self.parse(('[\r\n{},\r\n{"id": "i", "rev": "r", ' + '"gen": 3, "trans_id": "T-sid"},\r\n' + '%s,\r\n]') % encrypted_docstr)
def test_extra_comma(self): doc = SoledadDocument('i', rev='r') doc.content = {'a': 'b'} encrypted_docstr = _crypto.SoledadCrypto('safe').encrypt_doc(doc) with self.assertRaises(l2db.errors.BrokenSyncStream): self.parse("[\r\n{},\r\n]") with self.assertRaises(l2db.errors.BrokenSyncStream): self.parse( ('[\r\n{},\r\n{"id": "i", "rev": "r", ' + '"gen": 3, "trans_id": "T-sid"},\r\n' + '%s,\r\n]') % encrypted_docstr)
def test_decrypt_with_unknown_mac_method_raises(self): """ Trying to decrypt a document with unknown MAC method should raise. """ simpledoc = {'key': 'val'} doc = SoledadDocument(doc_id='id') doc.content = simpledoc # encrypt doc doc.set_json(self._soledad._crypto.encrypt_doc(doc)) self.assertTrue(MAC_KEY in doc.content) self.assertTrue(MAC_METHOD_KEY in doc.content) # mess with MAC method doc.content[MAC_METHOD_KEY] = 'mymac' # try to decrypt doc self.assertRaises(UnknownMacMethodError, self._soledad._crypto.decrypt_doc, doc)
def test_decrypt_with_wrong_mac_raises(self): """ Trying to decrypt a document with wrong MAC should raise. """ simpledoc = {'key': 'val'} doc = SoledadDocument(doc_id='id') doc.content = simpledoc # encrypt doc doc.set_json(self._soledad._crypto.encrypt_doc(doc)) self.assertTrue(MAC_KEY in doc.content) self.assertTrue(MAC_METHOD_KEY in doc.content) # mess with MAC doc.content[MAC_KEY] = '1234567890ABCDEF' # try to decrypt doc self.assertRaises(WrongMacError, self._soledad._crypto.decrypt_doc, doc)
def testLogErrorIfDecryptFails(self): def assert_failure(_): mock_logger_error.assert_any_call('_decrypt_doc: ' 'Error decrypting document with ' 'ID 1') with patch.object(Logger, 'error') as mock_logger_error: doc = SoledadDocument() doc.doc_id = '1' doc.content = {'_enc_json': ''} self.fetcher._process_decrypted_doc = Mock() self.km.decrypt = Mock(return_value=defer.fail(Exception())) d = self.fetcher._decrypt_doc(doc) d.addCallback(assert_failure) return d
def test_extra_comma(self): """ Test adapted to use encrypted content. """ doc = SoledadDocument('i', rev='r') doc.content = {} enc_json = target.encrypt_doc(self._soledad._crypto, doc) tgt = target.SoledadSyncTarget("http://foo/foo", crypto=self._soledad._crypto) self.assertRaises(u1db.errors.BrokenSyncStream, tgt._parse_sync_stream, "[\r\n{},\r\n]", None) self.assertRaises( u1db.errors.BrokenSyncStream, tgt._parse_sync_stream, '[\r\n{},\r\n{"id": "i", "rev": "r", ' '"content": %s, "gen": 3, "trans_id": "T-sid"}' ',\r\n]' % json.dumps(enc_json), lambda doc, gen, trans_id: None)
def test_stage2_bootstrap_signals(self): """ Test that if there are keys in server, soledad will download them and emit corresponding signals. """ # get existing instance so we have access to keys sol = self._soledad_instance() # create a document with secrets doc = SoledadDocument(doc_id=sol._shared_db_doc_id()) doc.content = sol.export_recovery_document() class Stage2MockSharedDB(object): get_doc = Mock(return_value=doc) put_doc = Mock() lock = Mock(return_value=('atoken', 300)) unlock = Mock() def __call__(self): return self sol.close() # reset mock soledad.client.signal.reset_mock() # get a fresh instance so it emits all bootstrap signals sol = self._soledad_instance( secrets_path='alternative_stage2.json', local_db_path='alternative_stage2.u1db', shared_db_class=Stage2MockSharedDB) # reverse call order so we can verify in the order the signals were # expected soledad.client.signal.mock_calls.reverse() soledad.client.signal.call_args = \ soledad.client.signal.call_args_list[0] soledad.client.signal.call_args_list.reverse() # assert download keys signals soledad.client.signal.assert_called_with( proto.SOLEDAD_DOWNLOADING_KEYS, ADDRESS, ) self._pop_mock_call(soledad.client.signal) soledad.client.signal.assert_called_with( proto.SOLEDAD_DONE_DOWNLOADING_KEYS, ADDRESS, ) sol.close()
def test_decrypt_with_unknown_mac_method_raises(self): """ Trying to decrypt a document with unknown MAC method should raise. """ simpledoc = {'key': 'val'} doc = SoledadDocument(doc_id='id') doc.content = simpledoc # encrypt doc doc.set_json(crypto.encrypt_doc(self._soledad._crypto, doc)) self.assertTrue(MAC_KEY in doc.content) self.assertTrue(MAC_METHOD_KEY in doc.content) # mess with MAC method doc.content[MAC_METHOD_KEY] = 'mymac' # try to decrypt doc self.assertRaises( UnknownMacMethodError, crypto.decrypt_doc, self._soledad._crypto, doc)
def test_decrypt_with_wrong_mac_raises(self): """ Trying to decrypt a document with wrong MAC should raise. """ simpledoc = {'key': 'val'} doc = SoledadDocument(doc_id='id') doc.content = simpledoc # encrypt doc doc.set_json(crypto.encrypt_doc(self._soledad._crypto, doc)) self.assertTrue(MAC_KEY in doc.content) self.assertTrue(MAC_METHOD_KEY in doc.content) # mess with MAC doc.content[MAC_KEY] = '1234567890ABCDEF' # try to decrypt doc self.assertRaises( WrongMacError, crypto.decrypt_doc, self._soledad._crypto, doc)
def test_stage2_bootstrap_signals(self): """ Test that if there are keys in server, soledad will download them and emit corresponding signals. """ # get existing instance so we have access to keys sol = self._soledad_instance() # create a document with secrets doc = SoledadDocument(doc_id=sol._shared_db_doc_id()) doc.content = sol.export_recovery_document() class Stage2MockSharedDB(object): get_doc = Mock(return_value=doc) put_doc = Mock() lock = Mock(return_value=('atoken', 300)) unlock = Mock() def __call__(self): return self sol.close() # reset mock soledad.client.signal.reset_mock() # get a fresh instance so it emits all bootstrap signals sol = self._soledad_instance(secrets_path='alternative_stage2.json', local_db_path='alternative_stage2.u1db', shared_db_class=Stage2MockSharedDB) # reverse call order so we can verify in the order the signals were # expected soledad.client.signal.mock_calls.reverse() soledad.client.signal.call_args = \ soledad.client.signal.call_args_list[0] soledad.client.signal.call_args_list.reverse() # assert download keys signals soledad.client.signal.assert_called_with( proto.SOLEDAD_DOWNLOADING_KEYS, ADDRESS, ) self._pop_mock_call(soledad.client.signal) soledad.client.signal.assert_called_with( proto.SOLEDAD_DONE_DOWNLOADING_KEYS, ADDRESS, ) sol.close()
def test_extra_comma(self): """ Test adapted to use encrypted content. """ doc = SoledadDocument('i', rev='r') doc.content = {} enc_json = target.encrypt_doc(self._soledad._crypto, doc) tgt = target.SoledadSyncTarget( "http://foo/foo", crypto=self._soledad._crypto) self.assertRaises(u1db.errors.BrokenSyncStream, tgt._parse_sync_stream, "[\r\n{},\r\n]", None) self.assertRaises(u1db.errors.BrokenSyncStream, tgt._parse_sync_stream, '[\r\n{},\r\n{"id": "i", "rev": "r", ' '"content": %s, "gen": 3, "trans_id": "T-sid"}' ',\r\n]' % json.dumps(enc_json), lambda doc, gen, trans_id: None)
def test_encrypt_decrypt_json(self): """ Test encrypting and decrypting documents. """ simpledoc = {'key': 'val'} doc1 = SoledadDocument(doc_id='id') doc1.content = simpledoc # encrypt doc doc1.set_json(self._soledad._crypto.encrypt_doc(doc1)) # assert content is different and includes keys self.assertNotEqual(simpledoc, doc1.content, 'incorrect document encryption') self.assertTrue(ENC_JSON_KEY in doc1.content) self.assertTrue(ENC_SCHEME_KEY in doc1.content) # decrypt doc doc1.set_json(self._soledad._crypto.decrypt_doc(doc1)) self.assertEqual(simpledoc, doc1.content, 'incorrect document encryption')
def test_encrypt_decrypt_json(self): """ Test encrypting and decrypting documents. """ simpledoc = {'key': 'val'} doc1 = SoledadDocument(doc_id='id') doc1.content = simpledoc # encrypt doc doc1.set_json(target.encrypt_doc(self._soledad._crypto, doc1)) # assert content is different and includes keys self.assertNotEqual( simpledoc, doc1.content, 'incorrect document encryption') self.assertTrue(target.ENC_JSON_KEY in doc1.content) self.assertTrue(target.ENC_SCHEME_KEY in doc1.content) # decrypt doc doc1.set_json(target.decrypt_doc(self._soledad._crypto, doc1)) self.assertEqual( simpledoc, doc1.content, 'incorrect document encryption')
def test_extra_comma(self): """ Test adapted to use encrypted content. """ doc = SoledadDocument('i', rev='r') doc.content = {} _crypto = self._soledad._crypto key = _crypto.doc_passphrase(doc.doc_id) secret = _crypto.secret enc_json = crypto.encrypt_docstr(doc.get_json(), doc.doc_id, doc.rev, key, secret) with self.assertRaises(u1db.errors.BrokenSyncStream): self.target._parse_received_doc_response("[\r\n{},\r\n]") with self.assertRaises(u1db.errors.BrokenSyncStream): self.target._parse_received_doc_response( ('[\r\n{},\r\n{"id": "i", "rev": "r", ' + '"content": %s, "gen": 3, "trans_id": "T-sid"}' + ',\r\n]') % json.dumps(enc_json))
def testFlagMessageOnBadJsonWhileDecrypting(self): doc = SoledadDocument() doc.doc_id = '1' doc.content = {'_enc_json': ''} err = ValueError('No JSON object could be decoded') def assert_failure(): mock_logger_error.assert_any_call('Error while decrypting 1') mock_logger_error.assert_any_call( 'No JSON object could be decoded') self.assertEquals(doc.content['errdecr'], True) with patch.object(Logger, 'error') as mock_logger_error: with patch.object(utils, 'json_loads') as mock_json_loader: self.fetcher._update_incoming_message = Mock() mock_json_loader.side_effect = err self.fetcher._process_decrypted_doc(doc, '') assert_failure()
def test_extra_comma(self): """ Test adapted to use encrypted content. """ doc = SoledadDocument('i', rev='r') doc.content = {} _crypto = self._soledad._crypto key = _crypto.doc_passphrase(doc.doc_id) secret = _crypto.secret enc_json = crypto.encrypt_docstr( doc.get_json(), doc.doc_id, doc.rev, key, secret) with self.assertRaises(u1db.errors.BrokenSyncStream): self.target._parse_received_doc_response("[\r\n{},\r\n]") with self.assertRaises(u1db.errors.BrokenSyncStream): self.target._parse_received_doc_response( ('[\r\n{},\r\n{"id": "i", "rev": "r", ' + '"content": %s, "gen": 3, "trans_id": "T-sid"}' + ',\r\n]') % json.dumps(enc_json))
def test_stage2_bootstrap_signals(self): """ Test that if there are keys in server, soledad will download them and emit corresponding signals. """ # get existing instance so we have access to keys sol = self._soledad_instance() # create a document with secrets doc = SoledadDocument(doc_id=sol.secrets.storage._remote_doc_id()) doc.content = sol.secrets.crypto.encrypt(sol.secrets._secrets) sol.close() # reset mock soledad.client._secrets.util.events.emit_async.reset_mock() # get a fresh instance so it emits all bootstrap signals shared_db = self.get_default_shared_mock(get_doc_return_value=doc) sol = self._soledad_instance( secrets_path='alternative_stage2.json', local_db_path='alternative_stage2.u1db', shared_db_class=shared_db) # reverse call order so we can verify in the order the signals were # expected mocked = soledad.client._secrets.util.events.emit_async mocked.mock_calls.reverse() mocked.call_args = mocked.call_args_list[0] mocked.call_args_list.reverse() def _assert(*args, **kwargs): mocked = soledad.client._secrets.util.events.emit_async mocked.assert_called_with(*args) pop = kwargs.get('pop') if pop or pop is None: self._pop_mock_call(mocked) # assert download keys signals user_data = {'userid': ADDRESS, 'uuid': ADDRESS} _assert(catalog.SOLEDAD_DOWNLOADING_KEYS, user_data) _assert(catalog.SOLEDAD_DONE_DOWNLOADING_KEYS, user_data, pop=False) sol.close()