summaryrefslogtreecommitdiff
path: root/soledad/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'soledad/__init__.py')
-rw-r--r--soledad/__init__.py451
1 files changed, 0 insertions, 451 deletions
diff --git a/soledad/__init__.py b/soledad/__init__.py
deleted file mode 100644
index 86eb762e..00000000
--- a/soledad/__init__.py
+++ /dev/null
@@ -1,451 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Soledad - Synchronization Of Locally Encrypted Data Among Devices.
-
-Soledad is the part of LEAP that manages storage and synchronization of
-application data. It is built on top of U1DB reference Python API and
-implements (1) a SQLCipher backend for local storage in the client, (2) a
-SyncTarget that encrypts data to the user's private OpenPGP key before
-syncing, and (3) a CouchDB backend for remote storage in the server side.
-"""
-
-import os
-import string
-import random
-import hmac
-import configparser
-import re
-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 (
- LeapDocument,
- DocumentNotEncrypted,
-)
-from leap.soledad.shared_db import SoledadSharedDatabase
-
-
-class KeyDoesNotExist(Exception):
- """
- Soledad attempted to find a key that does not exist locally.
- """
-
-
-class KeyAlreadyExists(Exception):
- """
- Soledad attempted to create a key that already exists locally.
- """
-
-
-#-----------------------------------------------------------------------------
-# Soledad: local encrypted storage and remote encrypted sync.
-#-----------------------------------------------------------------------------
-
-class Soledad(object):
- """
- Soledad provides encrypted data storage and sync.
-
- A Soledad instance is used to store and retrieve data in a local encrypted
- database and synchronize this database with Soledad server.
-
- This class is also responsible for bootstrapping users' account by
- creating OpenPGP keys and other cryptographic secrets and/or
- storing/fetching them on Soledad server.
- """
-
- # other configs
- SECRET_LENGTH = 50
- DEFAULT_CONF = {
- 'gnupg_home': '%s/gnupg',
- 'secret_path': '%s/secret.gpg',
- 'local_db_path': '%s/soledad.u1db',
- 'config_file': '%s/soledad.ini',
- 'shared_db_url': '',
- }
-
- 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.
- """
- self._user_email = user_email
- 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,
- }
- )
- 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:
- 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.
-
- 3. Key material has already been generated and uploaded, and is
- also stored locally, so we just need to load it from disk.
-
- This method decides which bootstrap stage has to be performed and
- performs it.
- """
- # TODO: make sure key storage always happens (even if this method is
- # interrupted).
- # TODO: write tests for bootstrap stages.
- 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
- self._init_keys()
- # TODO: change key below
- self._send_keys(self._secret)
- # stage 3 bootstrap
- self._load_keys()
- self._send_keys(self._secret)
- self._init_db()
-
- def _init_config(self, param_conf):
- """
- Initialize configuration, with precedence order give by: instance
- parameters > config file > default values.
- """
- # TODO: write tests for _init_config()
- self.prefix = param_conf['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
- if m.match(val):
- val = val % self.prefix
- setattr(self, key, val)
- # get config from file
- # TODO: sanitize options from config file.
- config = configparser.ConfigParser()
- config.read(self.config_file)
- if 'soledad-client' in config:
- for key in default_conf:
- if key in config['soledad-client'] and not param_conf[key]:
- setattr(self, key, config['soledad-client'][key])
-
- def _init_dirs(self):
- """
- Create work directories.
- """
- if not os.path.isdir(self.prefix):
- os.makedirs(self.prefix)
-
- def _init_keys(self):
- """
- Generate (if needed) and load OpenPGP keypair and secret for symmetric
- encryption.
- """
- # TODO: write tests for methods below.
- # load/generate OpenPGP keypair
- if not self._has_openpgp_keypair():
- self._gen_openpgp_keypair()
- self._load_openpgp_keypair()
- # load/generate secret
- if not self._has_secret():
- self._gen_secret()
- self._load_secret()
-
- def _init_db(self):
- """
- Initialize the database for local storage .
- """
- # instantiate u1db
- # TODO: verify if secret for sqlcipher should be the same as the
- # one for symmetric encryption.
- self._db = sqlcipher.open(
- self.local_db_path,
- self._secret,
- create=True,
- document_factory=LeapDocument,
- soledad=self)
-
- def close(self):
- """
- Close underlying U1DB database.
- """
- self._db.close()
-
- #-------------------------------------------------------------------------
- # Management of secret for symmetric encryption
- #-------------------------------------------------------------------------
-
- # TODO: refactor the following methods to somewhere out of here
- # (SoledadCrypto, maybe?)
-
- def _has_secret(self):
- """
- Verify if secret for symmetric encryption exists in a local encrypted
- file.
- """
- # does the file exist in disk?
- if not os.path.isfile(self.secret_path):
- return False
- # is it asymmetrically encrypted?
- f = open(self.secret_path, 'r')
- content = f.read()
- if not self.is_encrypted_asym(content):
- raise DocumentNotEncrypted(
- "File %s is not encrypted!" % self.secret_path)
- # can we decrypt it?
- fp = self._gpg.encrypted_to(content)['fingerprint']
- if fp != self._fingerprint:
- raise KeyDoesNotExist("Secret for symmetric encryption is "
- "encrypted to key with fingerprint '%s' "
- "which we don't have." % fp)
- return True
-
- def _load_secret(self):
- """
- Load secret for symmetric encryption from local encrypted file.
- """
- if not self._has_secret():
- 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()))
- except IOError:
- raise IOError('Failed to open secret file %s.' % self.secret_path)
-
- def _gen_secret(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(
- random.choice(
- string.ascii_letters +
- string.digits) for x in range(self.SECRET_LENGTH))
- ciphertext = self._gpg.encrypt(self._secret, self._fingerprint,
- self._fingerprint)
- f = open(self.secret_path, 'w')
- f.write(str(ciphertext))
- f.close()
-
- #-------------------------------------------------------------------------
- # Management of OpenPGP keypair
- #-------------------------------------------------------------------------
-
- def _has_openpgp_keypair(self):
- """
- Verify if there exists an OpenPGP keypair for this user.
- """
- try:
- self._load_openpgp_keypair()
- return True
- except:
- return False
-
- def _gen_openpgp_keypair(self):
- """
- Generate an OpenPGP keypair for this user.
- """
- if self._has_openpgp_keypair():
- raise KeyAlreadyExists("Tried to generate OpenPGP keypair but it "
- "already exists on disk.")
- params = self._gpg.gen_key_input(
- key_type='RSA',
- key_length=4096,
- name_real=self._user_email,
- name_email=self._user_email,
- name_comment='Generated by LEAP Soledad.')
- self._gpg.gen_key(params)
-
- def _load_openpgp_keypair(self):
- """
- Find fingerprint for this user's OpenPGP keypair.
- """
- # TODO: verify if we have the corresponding private key.
- try:
- 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 "
- "not exist on disk.")
-
- def publish_pubkey(self, keyserver):
- """
- Publish OpenPGP public key to a keyserver.
- """
- # TODO: this has to talk to LEAP's Nickserver.
- pass
-
- #-------------------------------------------------------------------------
- # General crypto utility methods.
- #-------------------------------------------------------------------------
-
- def _has_keys(self):
- return self._has_openpgp_keypair() and self._has_secret()
-
- def _load_keys(self):
- self._load_openpgp_keypair()
- self._load_secret()
-
- def _gen_keys(self):
- self._gen_openpgp_keypair()
- self._gen_secret()
-
- def _user_hash(self):
- return hmac.new(self._user_email, 'user').hexdigest()
-
- def _retrieve_keys(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:
- doc = LeapDocument(doc_id=self._user_hash(), soledad=self)
- doc.content = content
- self._shared_db.put_doc(doc)
-
- #-------------------------------------------------------------------------
- # Data encryption and decryption
- #-------------------------------------------------------------------------
-
- def encrypt(self, data, sign=None, passphrase=None, symmetric=False):
- """
- Encrypt data.
- """
- return str(self._gpg.encrypt(data, self._fingerprint, sign=sign,
- passphrase=passphrase,
- symmetric=symmetric))
-
- def encrypt_symmetric(self, doc_id, data, sign=None):
- """
- Encrypt data using symmetric secret.
- """
- return self.encrypt(data, sign=sign,
- passphrase=self._hmac_passphrase(doc_id),
- symmetric=True)
-
- def decrypt(self, data, passphrase=None, symmetric=False):
- """
- Decrypt data.
- """
- return str(self._gpg.decrypt(data, passphrase=passphrase))
-
- def decrypt_symmetric(self, doc_id, data):
- """
- Decrypt data using symmetric secret.
- """
- return self.decrypt(data, passphrase=self._hmac_passphrase(doc_id))
-
- def _hmac_passphrase(self, doc_id):
- return hmac.new(self._secret, doc_id).hexdigest()
-
- def is_encrypted(self, data):
- return self._gpg.is_encrypted(data)
-
- def is_encrypted_sym(self, data):
- return self._gpg.is_encrypted_sym(data)
-
- def is_encrypted_asym(self, data):
- return self._gpg.is_encrypted_asym(data)
-
- #-------------------------------------------------------------------------
- # Document storage, retrieval and sync
- #-------------------------------------------------------------------------
-
- # TODO: refactor the following methods to somewhere out of here
- # (SoledadLocalDatabase, maybe?)
-
- def put_doc(self, doc):
- """
- Update a document in the local encrypted database.
- """
- return self._db.put_doc(doc)
-
- def delete_doc(self, doc):
- """
- Delete a document from the local encrypted database.
- """
- return self._db.delete_doc(doc)
-
- def get_doc(self, doc_id, include_deleted=False):
- """
- Retrieve a document from the local encrypted database.
- """
- return self._db.get_doc(doc_id, include_deleted=include_deleted)
-
- def get_docs(self, doc_ids, check_for_conflicts=True,
- include_deleted=False):
- """
- Get the content for many documents.
- """
- return self._db.get_docs(doc_ids,
- check_for_conflicts=check_for_conflicts,
- include_deleted=include_deleted)
-
- def create_doc(self, content, doc_id=None):
- """
- Create a new document in the local encrypted database.
- """
- 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.
- """
- return self._db.get_doc_conflicts(doc_id)
-
- def resolve_doc(self, doc, conflicted_doc_revs):
- """
- Mark a document as no longer conflicted.
- """
- return self._db.resolve_doc(doc, conflicted_doc_revs)
-
- def sync(self, url):
- """
- Synchronize the local encrypted database with LEAP server.
- """
- # TODO: create authentication scheme for sync with server.
- return self._db.sync(url, creds=None, autocreate=True)
-
-
-__all__ = ['util', 'server', 'shared_db']