diff options
author | drebs <drebs@leap.se> | 2013-03-12 16:31:33 -0300 |
---|---|---|
committer | drebs <drebs@leap.se> | 2013-03-12 16:31:33 -0300 |
commit | 163c0500a4a6fb9b11fe667fb7ee6d2822916cdb (patch) | |
tree | e9a85ddec162c8aba2b00638358c0ee490e17f12 /src/leap/soledad | |
parent | ab1e1c8862b70d47c7e5f10267639ef71b3bc536 (diff) | |
parent | 0b0384c4985210ba2763dc31de98afa59e3936e4 (diff) |
Merge branch 'feature/bootstrap' into develop
Diffstat (limited to 'src/leap/soledad')
-rw-r--r-- | src/leap/soledad/__init__.py | 239 | ||||
-rw-r--r-- | src/leap/soledad/server.py | 3 | ||||
-rw-r--r-- | src/leap/soledad/shared_db.py | 4 | ||||
-rw-r--r-- | src/leap/soledad/tests/__init__.py | 14 | ||||
-rw-r--r-- | src/leap/soledad/tests/test_crypto.py (renamed from src/leap/soledad/tests/test_encrypted.py) | 90 | ||||
-rw-r--r-- | src/leap/soledad/util.py | 17 |
6 files changed, 249 insertions, 118 deletions
diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index 316eeaf7..f79e1c31 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -70,11 +70,23 @@ class Soledad(object): def __init__(self, user_email, prefix=None, gnupg_home=None, secret_path=None, local_db_path=None, config_file=None, shared_db_url=None, auth_token=None, - initialize=True): - """ - Bootstrap Soledad, initialize cryptographic material and open - underlying U1DB database. - """ + bootstrap=True): + """ + Initialize crypto and dbs. + + :param user_email: Email address of the user (username@provider). + :param prefix: Path to use as prefix for files. + :param gnupg_home: Home directory for gnupg. + :param secret_path: Path for storing gpg-encrypted key used for + symmetric encryption. + :param local_db_path: Path for local encrypted storage db. + :param config_file: Path for configuration file. + :param shared_db_url: URL for shared Soledad DB for key storage and + unauth retrieval. + :param auth_token: Authorization token for accessing remote databases. + :param bootstrap: True/False, should bootstrap keys? + """ + # TODO: allow for fingerprint enforcing. self._user_email = user_email self._auth_token = auth_token self._init_config( @@ -86,52 +98,59 @@ class Soledad(object): 'shared_db_url': shared_db_url, } ) - if self.shared_db_url: - # TODO: eliminate need to create db here. - self._shared_db = SoledadSharedDatabase.open_database( - shared_db_url, - True, - token=auth_token) - if initialize: + if bootstrap: self._bootstrap() def _bootstrap(self): """ Bootstrap local Soledad instance. - There are 3 stages for Soledad Client bootstrap: - - 1. No key material has been generated, so we need to generate and - upload to the server. - - 2. Key material has already been generated and uploaded to the - server, but has not been downloaded to this device/installation - yet. + Soledad Client bootstrap is the following sequence of stages: - 3. Key material has already been generated and uploaded, and is - also stored locally, so we just need to load it from disk. + Stage 0 - Local environment setup. + - directory initialization. + - gnupg wrapper initialization. + Stage 1 - Keys generation/loading: + - if keys exists locally, load them. + - else, if keys exists in server, download them. + - else, generate keys. + Stage 2 - Keys synchronization: + - if keys exist in server, confirm we have the same keys + locally. + - else, send keys to server. + Stage 3 - Database initialization. - This method decides which bootstrap stage has to be performed and - performs it. + This method decides which bootstrap stages have already been performed + and performs the missing ones in order. """ # TODO: make sure key storage always happens (even if this method is # interrupted). # TODO: write tests for bootstrap stages. + # TODO: log each bootstrap step. + # Stage 0 - Local environment setup self._init_dirs() self._gpg = GPGWrapper(gnupghome=self.gnupg_home) - if not self._has_keys(): - try: - # stage 2 bootstrap - self._retrieve_keys() - except Exception: - # stage 1 bootstrap + # Stage 1 - Keys generation/loading + if self._has_keys(): + self._load_keys() + else: + doc = self._get_keys_doc() + if not doc: self._init_keys() - # TODO: change key below - self._send_keys(self._secret) - # stage 3 bootstrap - self._load_keys() - self._send_keys(self._secret) + else: + self._set_privkey(self.decrypt(doc.content['_privkey'], + passphrase=self._user_hash())) + self._set_symkey(self.decrypt(doc.content['_symkey'])) + # Stage 2 - Keys synchronization + self._assert_server_keys() + # Stage 3 -Database initialization self._init_db() + if self.shared_db_url: + # TODO: eliminate need to create db here. + self._shared_db = SoledadSharedDatabase.open_database( + self.shared_db_url, + True, + token=auth_token) def _init_config(self, param_conf): """ @@ -152,7 +171,7 @@ class Soledad(object): config = configparser.ConfigParser() config.read(self.config_file) if 'soledad-client' in config: - for key in default_conf: + for key in self.DEFAULT_CONF: if key in config['soledad-client'] and not param_conf[key]: setattr(self, key, config['soledad-client'][key]) @@ -170,13 +189,13 @@ class Soledad(object): """ # TODO: write tests for methods below. # load/generate OpenPGP keypair - if not self._has_openpgp_keypair(): - self._gen_openpgp_keypair() - self._load_openpgp_keypair() + if not self._has_privkey(): + self._gen_privkey() + self._load_privkey() # load/generate secret - if not self._has_secret(): - self._gen_secret() - self._load_secret() + if not self._has_symkey(): + self._gen_symkey() + self._load_symkey() def _init_db(self): """ @@ -187,7 +206,7 @@ class Soledad(object): # one for symmetric encryption. self._db = sqlcipher.open( self.local_db_path, - self._secret, + self._symkey, create=True, document_factory=LeapDocument, soledad=self) @@ -205,7 +224,7 @@ class Soledad(object): # TODO: refactor the following methods to somewhere out of here # (SoledadCrypto, maybe?) - def _has_secret(self): + def _has_symkey(self): """ Verify if secret for symmetric encryption exists in a local encrypted file. @@ -227,36 +246,39 @@ class Soledad(object): "which we don't have." % fp) return True - def _load_secret(self): + def _load_symkey(self): """ Load secret for symmetric encryption from local encrypted file. """ - if not self._has_secret(): + if not self._has_symkey(): raise KeyDoesNotExist("Tried to load key for symmetric " "encryption but it does not exist on disk.") try: with open(self.secret_path) as f: - self._secret = str(self._gpg.decrypt(f.read())) + self._symkey = str(self._gpg.decrypt(f.read())) except IOError: raise IOError('Failed to open secret file %s.' % self.secret_path) - def _gen_secret(self): + def _gen_symkey(self): """ Generate a secret for symmetric encryption and store in a local encrypted file. """ - if self._has_secret(): - raise KeyAlreadyExists("Tried to generate secret for symmetric " - "encryption but it already exists on " - "disk.") - self._secret = ''.join( + self._set_symkey(''.join( random.choice( string.ascii_letters + - string.digits) for x in range(self.SECRET_LENGTH)) - self._store_secret() - - def _store_secret(self): - ciphertext = self._gpg.encrypt(self._secret, self._fingerprint, + string.digits) for x in range(self.SECRET_LENGTH))) + + def _set_symkey(self, symkey): + if self._has_symkey(): + raise KeyAlreadyExists("Tried to set the value of the key for " + "symmetric encryption but it already " + "exists on disk.") + self._symkey = symkey + self._store_symkey() + + def _store_symkey(self): + ciphertext = self._gpg.encrypt(self._symkey, self._fingerprint, self._fingerprint) f = open(self.secret_path, 'w') f.write(str(ciphertext)) @@ -266,21 +288,21 @@ class Soledad(object): # Management of OpenPGP keypair #------------------------------------------------------------------------- - def _has_openpgp_keypair(self): + def _has_privkey(self): """ Verify if there exists an OpenPGP keypair for this user. """ try: - self._load_openpgp_keypair() + self._load_privkey() return True except: return False - def _gen_openpgp_keypair(self): + def _gen_privkey(self): """ Generate an OpenPGP keypair for this user. """ - if self._has_openpgp_keypair(): + if self._has_privkey(): raise KeyAlreadyExists("Tried to generate OpenPGP keypair but it " "already exists on disk.") params = self._gpg.gen_key_input( @@ -289,17 +311,30 @@ class Soledad(object): name_real=self._user_email, name_email=self._user_email, name_comment='Generated by LEAP Soledad.') - self._gpg.gen_key(params) + fingerprint = self._gpg.gen_key(params).fingerprint + return self._load_privkey(fingerprint) - def _load_openpgp_keypair(self): + def _set_privkey(self, raw_data): + if self._has_privkey(): + raise KeyAlreadyExists("Tried to generate OpenPGP keypair but it " + "already exists on disk.") + fingerprint = self._gpg.import_keys(raw_data).fingerprints[0] + return self._load_privkey(fingerprint) + + def _load_privkey(self, fingerprint=None): """ Find fingerprint for this user's OpenPGP keypair. """ - # TODO: verify if we have the corresponding private key. + # TODO: guarantee encrypted storage of private keys. try: - self._fingerprint = self._gpg.find_key_by_email( - self._user_email, - secret=True)['fingerprint'] + if fingerprint: + self._fingerprint = self._gpg.find_key_by_fingerprint( + fingerprint, + secret=True)['fingerprint'] + else: + self._fingerprint = self._gpg.find_key_by_email( + self._user_email, + secret=True)['fingerprint'] return self._fingerprint except LookupError: raise KeyDoesNotExist("Tried to load OpenPGP keypair but it does " @@ -317,36 +352,52 @@ class Soledad(object): #------------------------------------------------------------------------- def _has_keys(self): - return self._has_openpgp_keypair() and self._has_secret() + return self._has_privkey() and self._has_symkey() def _load_keys(self): - self._load_openpgp_keypair() - self._load_secret() + self._load_privkey() + self._load_symkey() def _gen_keys(self): - self._gen_openpgp_keypair() - self._gen_secret() + self._gen_privkey() + self._gen_symkey() def _user_hash(self): return hmac.new(self._user_email, 'user').hexdigest() - def _retrieve_keys(self): + def _get_keys_doc(self): return self._shared_db.get_doc_unauth(self._user_hash()) - # TODO: create corresponding error on server side - - def _send_keys(self, passphrase): - # TODO: change this method's name to something more meaningful. - privkey = self._gpg.export_keys(self._fingerprint, secret=True) - content = { - '_privkey': self.encrypt(privkey, passphrase=passphrase, - symmetric=True), - '_symkey': self.encrypt(self._secret), - } - doc = self._retrieve_keys() - if not doc: + + def _assert_server_keys(self): + """ + Assert our key copies are the same as server's ones. + """ + assert self._has_keys() + doc = self._get_keys_doc() + if doc: + remote_privkey = self.decrypt(doc.content['_privkey'], + # TODO: change passphrase. + passphrase=self._user_hash()) + remote_symkey = self.decrypt(doc.content['_symkey']) + result = self._gpg.import_keys(remote_privkey) + # TODO: is the following behaviour expected in any scenario? + assert result.fingerprints[0] == self._fingerprint + assert remote_symkey == self._symkey + else: + privkey = self._gpg.export_keys(self._fingerprint, secret=True) + content = { + '_privkey': self.encrypt(privkey, + # TODO: change passphrase + passphrase=self._user_hash(), + symmetric=True), + '_symkey': self.encrypt(self._symkey), + } doc = LeapDocument(doc_id=self._user_hash(), soledad=self) - doc.content = content - self._shared_db.put_doc(doc) + doc.content = content + self._shared_db.put_doc(doc) + + def _assert_remote_keys(self): + privkey, symkey = self._retrieve_keys() #------------------------------------------------------------------------- # Data encryption and decryption @@ -368,7 +419,7 @@ class Soledad(object): passphrase=self._hmac_passphrase(doc_id), symmetric=True) - def decrypt(self, data, passphrase=None, symmetric=False): + def decrypt(self, data, passphrase=None): """ Decrypt data. """ @@ -381,7 +432,7 @@ class Soledad(object): return self.decrypt(data, passphrase=self._hmac_passphrase(doc_id)) def _hmac_passphrase(self, doc_id): - return hmac.new(self._secret, doc_id).hexdigest() + return hmac.new(self._symkey, doc_id).hexdigest() def is_encrypted(self, data): return self._gpg.is_encrypted(data) @@ -478,7 +529,7 @@ class Soledad(object): data = json.dumps({ 'user_email': self._user_email, 'privkey': self._gpg.export_keys(self._fingerprint, secret=True), - 'secret': self._secret, + 'symkey': self._symkey, }) if passphrase: data = str(self._gpg.encrypt(data, None, sign=None, @@ -498,9 +549,9 @@ class Soledad(object): data = json.loads(data) self._user_email = data['user_email'] self._gpg.import_keys(data['privkey']) - self._load_openpgp_keypair() - self._secret = data['secret'] - self._store_secret() + self._load_privkey() + self._symkey = data['symkey'] + self._store_symkey() # TODO: make this work well with bootstrap. self._load_keys() diff --git a/src/leap/soledad/server.py b/src/leap/soledad/server.py index eaa5e964..159f4768 100644 --- a/src/leap/soledad/server.py +++ b/src/leap/soledad/server.py @@ -86,7 +86,8 @@ class SoledadAuthMiddleware(object): Verify if token is valid for authenticating this action. """ # TODO: implement token verification - raise NotImplementedError(self.verify_token) + return True + #raise NotImplementedError(self.verify_token) def need_auth(self, environ): """ diff --git a/src/leap/soledad/shared_db.py b/src/leap/soledad/shared_db.py index c27bba71..27018701 100644 --- a/src/leap/soledad/shared_db.py +++ b/src/leap/soledad/shared_db.py @@ -99,6 +99,6 @@ class SoledadSharedDatabase(http_database.HTTPDatabase): """ Modified method to allow for unauth request. """ - db = http_database.HTTPDatabase(self._url, factory=self._factory, - creds=self._creds) + db = http_database.HTTPDatabase(self._url.geturl(), + document_factory=self._factory) return db.get_doc(doc_id) diff --git a/src/leap/soledad/tests/__init__.py b/src/leap/soledad/tests/__init__.py index fdde8c78..1ed2e248 100644 --- a/src/leap/soledad/tests/__init__.py +++ b/src/leap/soledad/tests/__init__.py @@ -32,16 +32,16 @@ class BaseSoledadTest(BaseLeapTest): document_factory=LeapDocument) # initialize soledad by hand so we can control keys self._soledad = Soledad(self.email, gnupg_home=self.gnupg_home, - initialize=False, + bootstrap=False, prefix=self.tempdir) self._soledad._init_dirs() self._soledad._gpg = GPGWrapper(gnupghome=self.gnupg_home) - self._soledad._gpg.import_keys(PUBLIC_KEY) - self._soledad._gpg.import_keys(PRIVATE_KEY) - self._soledad._load_openpgp_keypair() - if not self._soledad._has_secret(): - self._soledad._gen_secret() - self._soledad._load_secret() + #self._soledad._gpg.import_keys(PUBLIC_KEY) + if not self._soledad._has_privkey(): + self._soledad._set_privkey(PRIVATE_KEY) + if not self._soledad._has_symkey(): + self._soledad._gen_symkey() + self._soledad._load_symkey() self._soledad._init_db() def tearDown(self): diff --git a/src/leap/soledad/tests/test_encrypted.py b/src/leap/soledad/tests/test_crypto.py index eb78fbe3..52cc0315 100644 --- a/src/leap/soledad/tests/test_encrypted.py +++ b/src/leap/soledad/tests/test_crypto.py @@ -1,6 +1,11 @@ +import os +from leap.testing.basetest import BaseLeapTest from leap.soledad.backends.leap_backend import LeapDocument from leap.soledad.tests import BaseSoledadTest -from leap.soledad.tests import KEY_FINGERPRINT +from leap.soledad.tests import ( + KEY_FINGERPRINT, + PRIVATE_KEY, +) from leap.soledad import ( Soledad, KeyAlreadyExists, @@ -43,6 +48,9 @@ class EncryptedSyncTestCase(BaseSoledadTest): self._soledad._gpg.is_encrypted_sym(enc_json), "could not encrypt with passphrase.") + +class RecoveryDocumentTestCase(BaseSoledadTest): + def test_export_recovery_document_raw(self): rd = self._soledad.export_recovery_document(None) self.assertEqual( @@ -51,7 +59,7 @@ class EncryptedSyncTestCase(BaseSoledadTest): 'privkey': self._soledad._gpg.export_keys( self._soledad._fingerprint, secret=True), - 'secret': self._soledad._secret + 'symkey': self._soledad._symkey }, json.loads(rd), "Could not export raw recovery document." @@ -66,7 +74,7 @@ class EncryptedSyncTestCase(BaseSoledadTest): 'privkey': self._soledad._gpg.export_keys( self._soledad._fingerprint, secret=True), - 'secret': self._soledad._secret, + 'symkey': self._soledad._symkey, } raw_data = json.loads(str(self._soledad._gpg.decrypt( rd, @@ -86,14 +94,14 @@ class EncryptedSyncTestCase(BaseSoledadTest): rd = self._soledad.export_recovery_document(None) gnupg_home = self.gnupg_home = "%s/gnupg2" % self.tempdir s = Soledad('anotheruser@leap.se', gnupg_home=gnupg_home, - initialize=False, prefix=self.tempdir) + bootstrap=False, prefix=self.tempdir) s._init_dirs() s._gpg = GPGWrapper(gnupghome=gnupg_home) s.import_recovery_document(rd, None) self.assertEqual(self._soledad._user_email, s._user_email, 'Failed setting user email.') - self.assertEqual(self._soledad._secret, - s._secret, + self.assertEqual(self._soledad._symkey, + s._symkey, 'Failed settinng secret for symmetric encryption.') self.assertEqual(self._soledad._fingerprint, s._fingerprint, @@ -112,14 +120,14 @@ class EncryptedSyncTestCase(BaseSoledadTest): rd = self._soledad.export_recovery_document('123456') gnupg_home = self.gnupg_home = "%s/gnupg2" % self.tempdir s = Soledad('anotheruser@leap.se', gnupg_home=gnupg_home, - initialize=False, prefix=self.tempdir) + bootstrap=False, prefix=self.tempdir) s._init_dirs() s._gpg = GPGWrapper(gnupghome=gnupg_home) s.import_recovery_document(rd, '123456') self.assertEqual(self._soledad._user_email, s._user_email, 'Failed setting user email.') - self.assertEqual(self._soledad._secret, - s._secret, + self.assertEqual(self._soledad._symkey, + s._symkey, 'Failed settinng secret for symmetric encryption.') self.assertEqual(self._soledad._fingerprint, s._fingerprint, @@ -133,3 +141,67 @@ class EncryptedSyncTestCase(BaseSoledadTest): pk2, 'Failed settinng private key.' ) + + +class SoledadAuxMethods(BaseLeapTest): + + def setUp(self): + pass + + def tearDown(self): + pass + + def _soledad_instance(self): + return Soledad('leap@leap.se', bootstrap=False, + prefix=self.tempdir+'/soledad') + def _gpgwrapper_instance(self): + return GPGWrapper(gnupghome="%s/gnupg" % self.tempdir) + + def test__init_dirs(self): + sol = self._soledad_instance() + sol._init_dirs() + self.assertTrue(os.path.isdir(sol.prefix)) + + def test__init_db(self): + sol = self._soledad_instance() + sol._init_dirs() + sol._gpg = self._gpgwrapper_instance() + #self._soledad._gpg.import_keys(PUBLIC_KEY) + if not sol._has_privkey(): + sol._set_privkey(PRIVATE_KEY) + if not sol._has_symkey(): + sol._gen_symkey() + sol._load_symkey() + sol._init_db() + from leap.soledad.backends.sqlcipher import SQLCipherDatabase + self.assertIsInstance(sol._db, SQLCipherDatabase) + + def test__has_privkey(self): + sol = self._soledad_instance() + sol._init_dirs() + sol._gpg = GPGWrapper(gnupghome="%s/gnupg2" % self.tempdir) + self.assertFalse(sol._has_privkey()) + sol._set_privkey(PRIVATE_KEY) + self.assertTrue(sol._has_privkey()) + + def test__has_symkey(self): + sol = Soledad('leap@leap.se', bootstrap=False, + prefix=self.tempdir+'/soledad3') + sol._init_dirs() + sol._gpg = GPGWrapper(gnupghome="%s/gnupg3" % self.tempdir) + if not sol._has_privkey(): + sol._set_privkey(PRIVATE_KEY) + self.assertFalse(sol._has_symkey()) + sol._gen_symkey() + self.assertTrue(sol._has_symkey()) + + def test__has_keys(self): + sol = self._soledad_instance() + sol._init_dirs() + sol._gpg = self._gpgwrapper_instance() + self.assertFalse(sol._has_keys()) + sol._set_privkey(PRIVATE_KEY) + self.assertFalse(sol._has_keys()) + sol._gen_symkey() + self.assertTrue(sol._has_keys()) + diff --git a/src/leap/soledad/util.py b/src/leap/soledad/util.py index c64d4c5f..b8ee4cf3 100644 --- a/src/leap/soledad/util.py +++ b/src/leap/soledad/util.py @@ -70,20 +70,27 @@ class GPGWrapper(gnupg.GPG): return key raise LookupError("GnuPG public key for email %s not found!" % email) - def find_key_by_subkey(self, subkey): - for key in self.list_keys(): + def find_key_by_subkey(self, subkey, secret=False): + for key in self.list_keys(secret=secret): for sub in key['subkeys']: if sub[0] == subkey: return key raise LookupError( "GnuPG public key for subkey %s not found!" % subkey) - def find_key_by_keyid(self, keyid): - for key in self.list_keys(): + def find_key_by_keyid(self, keyid, secret=False): + for key in self.list_keys(secret=secret): if keyid == key['keyid']: return key raise LookupError( - "GnuPG public key for subkey %s not found!" % subkey) + "GnuPG public key for keyid %s not found!" % keyid) + + def find_key_by_fingerprint(self, fingerprint, secret=False): + for key in self.list_keys(secret=secret): + if fingerprint == key['fingerprint']: + return key + raise LookupError( + "GnuPG public key for fingerprint %s not found!" % fingerprint) def encrypt(self, data, recipient, sign=None, always_trust=True, passphrase=None, symmetric=False): |