summaryrefslogtreecommitdiff
path: root/src/leap/soledad/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/soledad/__init__.py')
-rw-r--r--src/leap/soledad/__init__.py239
1 files changed, 145 insertions, 94 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()