summaryrefslogtreecommitdiff
path: root/src/leap/soledad/__init__.py
diff options
context:
space:
mode:
authordrebs <drebs@leap.se>2013-04-08 12:15:30 -0300
committerdrebs <drebs@leap.se>2013-04-08 12:15:30 -0300
commite6ca75a9e2a7a634f92ae54238df7d86100fbdab (patch)
treedd780d232253867ff26074dde1a7acc2dfb63fee /src/leap/soledad/__init__.py
parentd2d35a449beb05799bd68a7679f58382f8599152 (diff)
Add a LOT of docstrings.
Diffstat (limited to 'src/leap/soledad/__init__.py')
-rw-r--r--src/leap/soledad/__init__.py368
1 files changed, 314 insertions, 54 deletions
diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py
index bd5a351c..baf303e3 100644
--- a/src/leap/soledad/__init__.py
+++ b/src/leap/soledad/__init__.py
@@ -1,4 +1,21 @@
# -*- coding: utf-8 -*-
+# __init__.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
"""
Soledad - Synchronization Of Locally Encrypted Data Among Devices.
@@ -19,6 +36,8 @@ try:
import simplejson as json
except ImportError:
import json # noqa
+
+
from leap.soledad.backends import sqlcipher
from leap.soledad.util import GPGWrapper
from leap.soledad.backends.leap_backend import (
@@ -66,37 +85,45 @@ class Soledad(object):
'shared_db_url': '',
}
- # TODO: separate username from provider, currently in user_email.
- def __init__(self, user_email, prefix=None, gnupg_home=None,
+ def __init__(self, user, prefix=None, gnupg_home=None,
secret_path=None, local_db_path=None,
config_file=None, shared_db_url=None, auth_token=None,
bootstrap=True):
"""
Initialize configuration, cryptographic keys 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
+ @param user: Email address of the user (username@provider).
+ @type user: str
+ @param prefix: Path to use as prefix for files.
+ @type prefix: str
+ @param gnupg_home: Home directory for gnupg.
+ @type gnupg_home: str
+ @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
+ @type secret_path: str
+ @param local_db_path: Path for local encrypted storage db.
+ @type local_db_path: str
+ @param config_file: Path for configuration file.
+ @type config_file: str
+ @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?
+ @type shared_db_url: str
+ @param auth_token: Authorization token for accessing remote databases.
+ @type auth_token: str
+ @param bootstrap: True/False, should bootstrap this instance? Mostly
+ for testing purposes but can be useful for initialization control.
+ @type bootstrap: bool
"""
# TODO: allow for fingerprint enforcing.
- self._user_email = user_email
+ self._user = user
self._auth_token = auth_token
self._init_config(
- {'prefix': prefix,
- 'gnupg_home': gnupg_home,
- 'secret_path': secret_path,
- 'local_db_path': local_db_path,
- 'config_file': config_file,
- 'shared_db_url': shared_db_url,
- }
+ prefix=prefix,
+ gnupg_home=gnupg_home,
+ secret_path=secret_path,
+ local_db_path=local_db_path,
+ config_file=config_file,
+ shared_db_url=shared_db_url,
)
if bootstrap:
self._bootstrap()
@@ -107,18 +134,18 @@ class Soledad(object):
Soledad Client bootstrap is the following sequence of stages:
- 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.
+ * 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 stages have already been performed
and performs the missing ones in order.
@@ -152,17 +179,21 @@ class Soledad(object):
True,
token=auth_token)
- def _init_config(self, param_conf):
+ def _init_config(self, **kwargs):
"""
Initialize configuration, with precedence order give by: instance
parameters > config file > default values.
+
+ @param kwargs: a dictionary with parameter values passed when
+ instantiating this Soledad instance.
+ @type kwargs: dict
"""
# TODO: write tests for _init_config()
- self.prefix = param_conf['prefix'] or \
+ self.prefix = kwargs['prefix'] or \
os.environ['HOME'] + '/.config/leap/soledad'
m = re.compile('.*%s.*')
for key, default_value in self.DEFAULT_CONF.iteritems():
- val = param_conf[key] or default_value
+ val = kwargs[key] or default_value
if m.match(val):
val = val % self.prefix
setattr(self, key, val)
@@ -172,7 +203,7 @@ class Soledad(object):
config.read(self.config_file)
if 'soledad-client' in config:
for key in self.DEFAULT_CONF:
- if key in config['soledad-client'] and not param_conf[key]:
+ if key in config['soledad-client'] and not kwargs[key]:
setattr(self, key, config['soledad-client'][key])
def _init_dirs(self):
@@ -199,7 +230,7 @@ class Soledad(object):
def _init_db(self):
"""
- Initialize the database for local storage .
+ Initialize the database for local storage.
"""
# instantiate u1db
# TODO: verify if secret for sqlcipher should be the same as the
@@ -222,12 +253,16 @@ class Soledad(object):
#-------------------------------------------------------------------------
# TODO: refactor the following methods to somewhere out of here
- # (SoledadCrypto, maybe?)
+ # (a new class SoledadCrypto, maybe?)
def _has_symkey(self):
"""
- Verify if secret for symmetric encryption exists in a local encrypted
+ Verify if a key for symmetric encryption exists in a local encrypted
file.
+
+ @return: whether this soledad instance has a key for symmetric
+ encryption
+ @rtype: bool
"""
# does the file exist in disk?
if not os.path.isfile(self.secret_path):
@@ -270,6 +305,12 @@ class Soledad(object):
string.digits) for x in range(self.SECRET_LENGTH)))
def _set_symkey(self, symkey):
+ """
+ Define and store the key to be used for symmetric encryption.
+
+ @param symkey: the symmetric key
+ @type symkey: str
+ """
if self._has_symkey():
raise KeyAlreadyExists("Tried to set the value of the key for "
"symmetric encryption but it already "
@@ -291,6 +332,9 @@ class Soledad(object):
def _has_privkey(self):
"""
Verify if there exists an OpenPGP keypair for this user.
+
+ @return: whether this soledad instance has a private OpenPGP key
+ @rtype: bool
"""
try:
self._load_privkey()
@@ -300,7 +344,10 @@ class Soledad(object):
def _gen_privkey(self):
"""
- Generate an OpenPGP keypair for this user.
+ Generate an OpenPGP keypair for the user.
+
+ @return: the fingerprint of the generated key
+ @rtype: str
"""
if self._has_privkey():
raise KeyAlreadyExists("Tried to generate OpenPGP keypair but it "
@@ -308,22 +355,46 @@ class Soledad(object):
params = self._gpg.gen_key_input(
key_type='RSA',
key_length=4096,
- name_real=self._user_email,
- name_email=self._user_email,
+ name_real=self._user,
+ name_email=self._user,
name_comment='Generated by LEAP Soledad.')
fingerprint = self._gpg.gen_key(params).fingerprint
return self._load_privkey(fingerprint)
def _set_privkey(self, raw_data):
+ """
+ Set private OpenPGP key as the key to be used in this soledad instance.
+
+ @param raw_data: the private key blob
+ @type raw_data: str
+
+ @return: the fingerprint of the key passed as argument
+ @rtype: str
+ """
if self._has_privkey():
- raise KeyAlreadyExists("Tried to generate OpenPGP keypair but it "
- "already exists on disk.")
+ raise KeyAlreadyExists("Tried to define an 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.
+ Assert private key exists in local keyring and load its fingerprint to
+ memory.
+
+ This method either looks for a key with fingerprint given by the
+ parameter or searches for a key bound to the user's email address if
+ no finfgerprint is provided.
+
+ Raises a LookupError if a key (either for the given fingerprint or for
+ self._user if that was not provided) was not found.
+
+ @param fingerprint: optional fingerprint for forcing a specific key to
+ be loaded
+ @type fingerprint: str
+
+ @return: the fingerprint of the loaded key
+ @rtype: str
"""
# TODO: guarantee encrypted storage of private keys.
try:
@@ -333,16 +404,22 @@ class Soledad(object):
secret=True)['fingerprint']
else:
self._fingerprint = self._gpg.find_key_by_email(
- self._user_email,
+ self._user,
secret=True)['fingerprint']
return self._fingerprint
except LookupError:
- raise KeyDoesNotExist("Tried to load OpenPGP keypair but it does "
- "not exist on disk.")
+ raise KeyDoesNotExist('OpenPGP private key but it does not exist '
+ 'on local keyring.')
def publish_pubkey(self, keyserver):
"""
Publish OpenPGP public key to a keyserver.
+
+ @param keyserver: the keyserver url
+ @type keyserver: str
+
+ @return: whether the action succeeded
+ @rtype: bool
"""
# TODO: this has to talk to LEAP's Nickserver.
pass
@@ -352,20 +429,51 @@ class Soledad(object):
#-------------------------------------------------------------------------
def _has_keys(self):
+ """
+ Return whether this instance has both the private OpenPGP key and the
+ key for symmetric encryption.
+
+ @return: whether keys are available for this instance
+ @rtype: bool
+ """
return self._has_privkey() and self._has_symkey()
def _load_keys(self):
+ """
+ Load the OpenPGP private key and the key for symmetric encryption from
+ persistent storage.
+ """
self._load_privkey()
self._load_symkey()
def _gen_keys(self):
+ """
+ Generate an OpenPGP keypair and a key for symmetric encryption.
+ """
self._gen_privkey()
self._gen_symkey()
def _user_hash(self):
- return hmac.new(self._user_email, 'user').hexdigest()
+ """
+ Calculate a hash for storing/retrieving key material on shared
+ database, based on user's email.
+
+ @return: the hash
+ @rtype: str
+ """
+ return hmac.new(self._user, 'user').hexdigest()
def _get_keys_doc(self):
+ """
+ Retrieve the document with encrypted key material from the shared
+ database.
+
+ @return: a document with encrypted key material in its contents
+ @rtype: LeapDocument
+ """
+ # TODO: change below to raise appropriate exceptions
+ #if not hasattr(self, '_shared_db'):
+ # return None
return self._shared_db.get_doc_unauth(self._user_hash())
def _assert_server_keys(self):
@@ -373,6 +481,8 @@ class Soledad(object):
Assert our key copies are the same as server's ones.
"""
assert self._has_keys()
+ #if not hasattr(self, '_shared_db'):
+ # return
doc = self._get_keys_doc()
if doc:
remote_privkey = self.decrypt(doc.content['_privkey'],
@@ -406,6 +516,18 @@ class Soledad(object):
def encrypt(self, data, sign=None, passphrase=None, symmetric=False):
"""
Encrypt data.
+
+ @param data: the data to be encrypted
+ @type data: str
+ @param sign: the fingerprint of key to be used for signature
+ @type sign: str
+ @param passphrase: the passphrase to be used for encryption
+ @type passphrase: str
+ @param symmetric: whether the encryption scheme should be symmetric
+ @type symmetric: bool
+
+ @return: the encrypted data
+ @rtype: str
"""
return str(self._gpg.encrypt(data, self._fingerprint, sign=sign,
passphrase=passphrase,
@@ -413,7 +535,20 @@ class Soledad(object):
def encrypt_symmetric(self, doc_id, data, sign=None):
"""
- Encrypt data using symmetric secret.
+ Encrypt data using a password.
+
+ The password is derived from the document id and the secret for
+ symmetric encryption previously generated/loaded.
+
+ @param doc_id: the document id
+ @type doc_id: str
+ @param data: the data to be encrypted
+ @type data: str
+ @param sign: the fingerprint of key to be used for signature
+ @type sign: str
+
+ @return: the encrypted data
+ @rtype: str
"""
return self.encrypt(data, sign=sign,
passphrase=self._hmac_passphrase(doc_id),
@@ -422,25 +557,75 @@ class Soledad(object):
def decrypt(self, data, passphrase=None):
"""
Decrypt data.
+
+ @param data: the data to be decrypted
+ @type data: str
+ @param passphrase: the passphrase to be used for decryption
+ @type passphrase: str
+
+ @return: the decrypted data
+ @rtype: str
"""
return str(self._gpg.decrypt(data, passphrase=passphrase))
def decrypt_symmetric(self, doc_id, data):
"""
Decrypt data using symmetric secret.
+
+ @param doc_id: the document id
+ @type doc_id: str
+ @param data: the data to be decrypted
+ @type data: str
+
+ @return: the decrypted data
+ @rtype: str
"""
return self.decrypt(data, passphrase=self._hmac_passphrase(doc_id))
def _hmac_passphrase(self, doc_id):
+ """
+ Generate a passphrase for symmetric encryption.
+
+ The password is derived from the document id and the secret for
+ symmetric encryption previously generated/loaded.
+
+ @param doc_id: the document id
+ @type doc_id: str
+
+ @return: the passphrase
+ @rtype: str
+ """
return hmac.new(self._symkey, doc_id).hexdigest()
def is_encrypted(self, data):
+ """
+ Test whether some chunk of data is a cyphertext.
+
+ @param data: the data to be tested
+ @type data: str
+
+ @return: whether the data is a cyphertext
+ @rtype: bool
+ """
return self._gpg.is_encrypted(data)
def is_encrypted_sym(self, data):
+ """
+ Test whether some chunk of data was encrypted with a symmetric key.
+
+ @return: whether data is encrypted to a symmetric key
+ @rtype: bool
+ """
return self._gpg.is_encrypted_sym(data)
def is_encrypted_asym(self, data):
+ """
+ Test whether some chunk of data was encrypted to an OpenPGP private
+ key.
+
+ @return: whether data is encrypted to an OpenPGP private key
+ @rtype: bool
+ """
return self._gpg.is_encrypted_asym(data)
#-------------------------------------------------------------------------
@@ -453,18 +638,40 @@ class Soledad(object):
def put_doc(self, doc):
"""
Update a document in the local encrypted database.
+
+ @param doc: the document to update
+ @type doc: LeapDocument
+
+ @return: the new revision identifier for the document
+ @rtype: str
"""
return self._db.put_doc(doc)
def delete_doc(self, doc):
"""
Delete a document from the local encrypted database.
+
+ @param doc: the document to delete
+ @type doc: LeapDocument
+
+ @return: the new revision identifier for the document
+ @rtype: str
"""
return self._db.delete_doc(doc)
def get_doc(self, doc_id, include_deleted=False):
"""
Retrieve a document from the local encrypted database.
+
+ @param doc_id: the unique document identifier
+ @type doc_id: str
+ @param include_deleted: if True, deleted documents will be
+ returned with empty content; otherwise asking for a deleted
+ document will return None
+ @type include_deleted: bool
+
+ @return: the document object or None
+ @rtype: LeapDocument
"""
return self._db.get_doc(doc_id, include_deleted=include_deleted)
@@ -472,6 +679,16 @@ class Soledad(object):
include_deleted=False):
"""
Get the content for many documents.
+
+ @param doc_ids: a list of document identifiers
+ @type doc_ids: list
+ @param check_for_conflicts: if set False, then the conflict check will
+ be skipped, and 'None' will be returned instead of True/False
+ @type check_for_conflicts: bool
+
+ @return: iterable giving the Document object for each document id
+ in matching doc_ids order.
+ @rtype: generator
"""
return self._db.get_docs(doc_ids,
check_for_conflicts=check_for_conflicts,
@@ -480,24 +697,52 @@ class Soledad(object):
def create_doc(self, content, doc_id=None):
"""
Create a new document in the local encrypted database.
+
+ @param content: the contents of the new document
+ @type content: dict
+ @param doc_id: an optional identifier specifying the document id
+ @type doc_id: str
+
+ @return: the new document
+ @rtype: LeapDocument
"""
return self._db.create_doc(content, doc_id=doc_id)
def get_doc_conflicts(self, doc_id):
"""
Get the list of conflicts for the given document.
+
+ @param doc_id: the document id
+ @type doc_id: str
+
+ @return: a list of the document entries that are conflicted
+ @rtype: list
+
"""
return self._db.get_doc_conflicts(doc_id)
def resolve_doc(self, doc, conflicted_doc_revs):
"""
Mark a document as no longer conflicted.
+
+ @param doc: a document with the new content to be inserted.
+ @type doc: LeapDocument
+ @param conflicted_doc_revs: a list of revisions that the new content
+ supersedes.
+ @type conflicted_doc_revs: list
"""
return self._db.resolve_doc(doc, conflicted_doc_revs)
def sync(self, url):
"""
- Synchronize the local encrypted database with LEAP server.
+ Synchronize the local encrypted replica with a remote replica.
+
+ @param url: the url of the target replica to sync with
+ @type url: str
+
+ @return: the local generation before the synchronisation was
+ performed.
+ @rtype: str
"""
# TODO: create authentication scheme for sync with server.
return self._db.sync(url, creds=None, autocreate=True)
@@ -506,7 +751,7 @@ class Soledad(object):
# Recovery document export and import
#-------------------------------------------------------------------------
- def export_recovery_document(self, passphrase):
+ def export_recovery_document(self, passphrase=None):
"""
Exports username, provider, private key and key for symmetric
encryption, optionally encrypted with a password.
@@ -525,9 +770,15 @@ class Soledad(object):
- provider
- private key.
- key for symmetric encryption
+
+ @param passphrase: an optional passphrase for encrypting the document
+ @type passphrase: str
+
+ @return: the recovery document json serialization
+ @rtype: str
"""
data = json.dumps({
- 'user_email': self._user_email,
+ 'user': self._user,
'privkey': self._gpg.export_keys(self._fingerprint, secret=True),
'symkey': self._symkey,
})
@@ -537,7 +788,16 @@ class Soledad(object):
symmetric=True))
return data
- def import_recovery_document(self, data, passphrase):
+ def import_recovery_document(self, data, passphrase=None):
+ """
+ Import username, provider, private key and key for symmetric
+ encryption from a recovery document.
+
+ @param data: the recovery document json serialization
+ @type data: str
+ @param passphrase: an optional passphrase for decrypting the document
+ @type passphrase: str
+ """
if self._has_keys():
raise KeyAlreadyExists("You tried to import a recovery document "
"but secret keys are already present.")
@@ -547,7 +807,7 @@ class Soledad(object):
if passphrase:
data = str(self._gpg.decrypt(data, passphrase=passphrase))
data = json.loads(data)
- self._user_email = data['user_email']
+ self._user = data['user']
self._gpg.import_keys(data['privkey'])
self._load_privkey()
self._symkey = data['symkey']