summaryrefslogtreecommitdiff
path: root/client/src/leap/soledad/client/target.py
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/leap/soledad/client/target.py')
-rw-r--r--client/src/leap/soledad/client/target.py239
1 files changed, 175 insertions, 64 deletions
diff --git a/client/src/leap/soledad/client/target.py b/client/src/leap/soledad/client/target.py
index 3b3d6870..56652b0b 100644
--- a/client/src/leap/soledad/client/target.py
+++ b/client/src/leap/soledad/client/target.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# target.py
-# Copyright (C) 2013 LEAP
+# Copyright (C) 2013, 2014 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
@@ -24,6 +24,8 @@ import gzip
import hashlib
import hmac
import logging
+import os
+import sqlite3
import urllib
import simplejson as json
@@ -56,6 +58,9 @@ from leap.soledad.client.crypto import (
EncryptionMethods,
UnknownEncryptionMethod,
)
+from leap.soledad.client.crypto import encrypt_sym, doc_mac_key
+
+from leap.common.check import leap_check
logger = logging.getLogger(__name__)
@@ -76,7 +81,7 @@ class DocumentNotEncrypted(Exception):
#
-def mac_doc(crypto, doc_id, doc_rev, ciphertext, mac_method):
+def mac_doc(doc_id, doc_rev, ciphertext, mac_method, secret):
"""
Calculate a MAC for C{doc} using C{ciphertext}.
@@ -86,8 +91,6 @@ def mac_doc(crypto, doc_id, doc_rev, ciphertext, mac_method):
* msg: doc_id + doc_rev + ciphertext
* digestmod: sha256
- :param crypto: A SoledadCryto instance used to perform the encryption.
- :type crypto: leap.soledad.crypto.SoledadCrypto
:param doc_id: The id of the document.
:type doc_id: str
:param doc_rev: The revision of the document.
@@ -96,20 +99,22 @@ def mac_doc(crypto, doc_id, doc_rev, ciphertext, mac_method):
:type ciphertext: str
:param mac_method: The MAC method to use.
:type mac_method: str
+ :param secret: soledad secret
+ :type secret: Soledad.secret_storage
:return: The calculated MAC.
:rtype: str
"""
if mac_method == MacMethods.HMAC:
return hmac.new(
- crypto.doc_mac_key(doc_id),
+ doc_mac_key(doc_id, secret),
str(doc_id) + str(doc_rev) + ciphertext,
hashlib.sha256).digest()
# raise if we do not know how to handle this MAC method
raise UnknownMacMethod('Unknown MAC method: %s.' % mac_method)
-def encrypt_doc(crypto, doc):
+def encrypt_docstr(docstr, doc_id, doc_rev, key, secret):
"""
Encrypt C{doc}'s content.
@@ -125,21 +130,29 @@ def encrypt_doc(crypto, doc):
MAC_METHOD_KEY: 'hmac'
}
- :param crypto: A SoledadCryto instance used to perform the encryption.
- :type crypto: leap.soledad.crypto.SoledadCrypto
- :param doc: The document with contents to be encrypted.
- :type doc: SoledadDocument
+ :param docstr: A representation of the document to be encrypted.
+ :type docstr: str or unicode.
+
+ :param doc_id: The document id.
+ :type doc_id: str
+
+ :param doc_rev: The document revision.
+ :type doc_rev: str
+
+ :param key: The key used to encrypt ``data`` (must be 256 bits long).
+ :type key: str
+
+ :param secret:
+ :type secret:
:return: The JSON serialization of the dict representing the encrypted
content.
:rtype: str
"""
- soledad_assert(doc.is_tombstone() is False)
# encrypt content using AES-256 CTR mode
- iv, ciphertext = crypto.encrypt_sym(
- str(doc.get_json()), # encryption/decryption routines expect str
- crypto.doc_passphrase(doc.doc_id),
- method=EncryptionMethods.AES_256_CTR)
+ iv, ciphertext = encrypt_sym(
+ str(docstr), # encryption/decryption routines expect str
+ key, method=EncryptionMethods.AES_256_CTR)
# Return a representation for the encrypted content. In the following, we
# convert binary data to hexadecimal representation so the JSON
# serialization does not complain about what it tries to serialize.
@@ -150,9 +163,8 @@ def encrypt_doc(crypto, doc):
ENC_METHOD_KEY: EncryptionMethods.AES_256_CTR,
ENC_IV_KEY: iv,
MAC_KEY: binascii.b2a_hex(mac_doc( # store the mac as hex.
- crypto, doc.doc_id, doc.rev,
- ciphertext,
- MacMethods.HMAC)),
+ doc_id, doc_rev, ciphertext,
+ MacMethods.HMAC, secret)),
MAC_METHOD_KEY: MacMethods.HMAC,
})
@@ -197,9 +209,9 @@ def decrypt_doc(crypto, doc):
ciphertext = binascii.a2b_hex( # content is stored as hex.
doc.content[ENC_JSON_KEY])
mac = mac_doc(
- crypto, doc.doc_id, doc.rev,
+ doc.doc_id, doc.rev,
ciphertext,
- doc.content[MAC_METHOD_KEY])
+ doc.content[MAC_METHOD_KEY], crypto.secret)
# we compare mac's hashes to avoid possible timing attacks that might
# exploit python's builtin comparison operator behaviour, which fails
# immediatelly when non-matching bytes are found.
@@ -254,63 +266,50 @@ class SoledadSyncTarget(HTTPSyncTarget, TokenBasedAuth):
"""
A SyncTarget that encrypts data before sending and decrypts data after
receiving.
- """
-
- #
- # Token auth methods.
- #
-
- def set_token_credentials(self, uuid, token):
- """
- Store given credentials so we can sign the request later.
-
- :param uuid: The user's uuid.
- :type uuid: str
- :param token: The authentication token.
- :type token: str
- """
- TokenBasedAuth.set_token_credentials(self, uuid, token)
- def _sign_request(self, method, url_query, params):
- """
- Return an authorization header to be included in the HTTP request.
-
- :param method: The HTTP method.
- :type method: str
- :param url_query: The URL query string.
- :type url_query: str
- :param params: A list with encoded query parameters.
- :type param: list
-
- :return: The Authorization header.
- :rtype: list of tuple
- """
- return TokenBasedAuth._sign_request(self, method, url_query, params)
+ Normally encryption will have been written to the sync database upon
+ document modification. The sync database is also used to write temporarily
+ the parsed documents that the remote send us, before being decrypted and
+ written to the main database.
+ """
#
# Modified HTTPSyncTarget methods.
#
- @staticmethod
- def connect(url, crypto=None):
- return SoledadSyncTarget(url, crypto=crypto)
-
- def __init__(self, url, creds=None, crypto=None):
+ def __init__(self, url, creds=None, crypto=None, sync_db_path=None):
"""
Initialize the SoledadSyncTarget.
:param url: The url of the target replica to sync with.
:type url: str
+
:param creds: optional dictionary giving credentials.
- to authorize the operation with the server.
+ to authorize the operation with the server.
:type creds: dict
+
:param soledad: An instance of Soledad so we can encrypt/decrypt
- document contents when syncing.
+ document contents when syncing.
:type soledad: soledad.Soledad
+
+ :param sync_db_path: Optional. Path to the db with the symmetric
+ encryption of the syncing documents. If
+ None, encryption will be done in-place,
+ instead of retreiving it from the dedicated
+ database.
+ :type sync_db_path: str
"""
HTTPSyncTarget.__init__(self, url, creds)
self._crypto = crypto
+ self._sync_db = None
+ if sync_db_path is not None:
+ self._init_sync_db(sync_db_path)
+
+ @staticmethod
+ def connect(url, crypto=None):
+ return SoledadSyncTarget(url, crypto=crypto)
+
def _parse_sync_stream(self, data, return_doc_cb, ensure_callback=None):
"""
Parse incoming synchronization stream and insert documents in the
@@ -322,17 +321,19 @@ class SoledadSyncTarget(HTTPSyncTarget, TokenBasedAuth):
:param data: The body of the HTTP response.
:type data: str
+
:param return_doc_cb: A callback to insert docs from target.
:type return_doc_cb: function
+
:param ensure_callback: A callback to ensure we have the correct
- target_replica_uid, if it was just created.
+ target_replica_uid, if it was just created.
:type ensure_callback: function
:raise BrokenSyncStream: If C{data} is malformed.
:return: A dictionary representing the first line of the response got
- from remote replica.
- :rtype: list of str
+ from remote replica.
+ :rtype: dict
"""
parts = data.splitlines() # one at a time
if not parts or parts[0] != '[':
@@ -475,10 +476,11 @@ class SoledadSyncTarget(HTTPSyncTarget, TokenBasedAuth):
:param last_known_trans_id: Target's last known transaction id.
:type last_known_trans_id: str
:param return_doc_cb: A callback for inserting received documents from
- target.
+ target.
:type return_doc_cb: function
:param ensure_callback: A callback that ensures we know the target
- replica uid if the target replica was just created.
+ replica uid if the target replica was just
+ created.
:type ensure_callback: function
:return: The new generation and transaction id of the target replica.
@@ -507,6 +509,8 @@ class SoledadSyncTarget(HTTPSyncTarget, TokenBasedAuth):
last_known_trans_id=last_known_trans_id,
ensure=ensure_callback is not None)
comma = ','
+
+ synced = []
for doc, gen, trans_id in docs_by_generations:
# skip non-syncable docs
if isinstance(doc, SoledadDocument) and not doc.syncable:
@@ -516,13 +520,31 @@ class SoledadSyncTarget(HTTPSyncTarget, TokenBasedAuth):
#-------------------------------------------------------------
doc_json = doc.get_json()
if not doc.is_tombstone():
- doc_json = encrypt_doc(self._crypto, doc)
+ if self._sync_db is None:
+ # fallback case, for tests
+ doc_json = encrypt_docstr(
+ json.dumps(doc.get_json()),
+ doc.doc_id, doc.rev, self._crypto.secret)
+ else:
+ try:
+ doc_json = self.get_encrypted_doc_from_db(
+ doc.doc_id, doc.rev)
+ except Exception as exc:
+ logger.error("Error while getting "
+ "encrypted doc from db")
+ logger.exception(exc)
+ continue
+ if doc_json is None:
+ # Not marked as tombstone, but we got nothing
+ # from the sync db. Maybe not encrypted yet.
+ continue
#-------------------------------------------------------------
# end of symmetric encryption
#-------------------------------------------------------------
size += prepare(id=doc.doc_id, rev=doc.rev,
content=doc_json,
gen=gen, trans_id=trans_id)
+ synced.append((doc.doc_id, doc.rev))
entries.append('\r\n]')
size += len(entries[-1])
self._conn.putheader('content-length', str(size))
@@ -533,5 +555,94 @@ class SoledadSyncTarget(HTTPSyncTarget, TokenBasedAuth):
data, headers = self._response()
res = self._parse_sync_stream(data, return_doc_cb, ensure_callback)
+
+ # delete documents from the sync queue
+ self.delete_encrypted_docs_from_db(synced)
+
data = None
return res['new_generation'], res['new_transaction_id']
+
+ #
+ # Token auth methods.
+ #
+
+ def set_token_credentials(self, uuid, token):
+ """
+ Store given credentials so we can sign the request later.
+
+ :param uuid: The user's uuid.
+ :type uuid: str
+ :param token: The authentication token.
+ :type token: str
+ """
+ TokenBasedAuth.set_token_credentials(self, uuid, token)
+
+ def _sign_request(self, method, url_query, params):
+ """
+ Return an authorization header to be included in the HTTP request.
+
+ :param method: The HTTP method.
+ :type method: str
+ :param url_query: The URL query string.
+ :type url_query: str
+ :param params: A list with encoded query parameters.
+ :type param: list
+
+ :return: The Authorization header.
+ :rtype: list of tuple
+ """
+ return TokenBasedAuth._sign_request(self, method, url_query, params)
+
+ #
+ # Syncing db
+ #
+
+ def _init_sync_db(self, path):
+ """
+ Open a connection to the local db of encrypted docs for sync.
+
+ :param path: The path to the local db.
+ :type path: str
+ """
+ leap_check(path is not None, "Need a path to initialize db")
+ if not os.path.isfile(path):
+ logger.warning("Cannot open db: non-existent file!")
+ return
+ self._sync_db = sqlite3.connect(path, check_same_thread=False)
+
+ def get_encrypted_doc_from_db(self, doc_id, doc_rev):
+ """
+ Retrieve encrypted document from the database of encrypted docs for
+ sync.
+
+ :param doc_id: The Document id.
+ :type doc_id: str
+
+ :param doc_rev: The document revision
+ :type doc_rev: str
+ """
+ c = self._sync_db.cursor()
+ # XXX interpolate table name
+ sql = ("SELECT content FROM docs_tosync "
+ "WHERE doc_id=? and rev=?")
+ c.execute(sql, (doc_id, doc_rev))
+ res = c.fetchall()
+ if len(res) != 0:
+ return res[0][0]
+
+ def delete_encrypted_docs_from_db(self, docs_ids):
+ """
+ Delete several encrypted documents from the database of symmetrically
+ encrypted docs to sync.
+
+ :param docs_ids: an iterable with (doc_id, doc_rev) for all documents
+ to be deleted.
+ :type docs_ids: any iterable of tuples of str
+ """
+ c = self._sync_db.cursor()
+ for doc_id, doc_rev in docs_ids:
+ # XXX interpolate table name
+ sql = ("DELETE FROM docs_tosync "
+ "WHERE doc_id=? and rev=?")
+ c.execute(sql, (doc_id, doc_rev))
+ self._sync_db.commit()