1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
"""A U1DB backend that uses SQLCipher as its persistence layer."""
import os
from pysqlcipher import dbapi2
import time
# TODO: uncomment imports below after solving circular dependency issue
# between leap_client and soledad.
#from leap import util
from u1db.backends import sqlite_backend
#util.logger.debug(
# "Monkey-patching u1db.backends.sqlite_backend with pysqlcipher.dbapi2..."
#)
sqlite_backend.dbapi2 = dbapi2
from u1db import (
errors,
)
from leap.soledad.backends.leap_backend import LeapDocument
def open(path, password, create=True, document_factory=None, soledad=None):
"""Open a database at the given location.
Will raise u1db.errors.DatabaseDoesNotExist if create=False and the
database does not already exist.
:param path: The filesystem path for the database to open.
:param create: True/False, should the database be created if it doesn't
already exist?
:param document_factory: A function that will be called with the same
parameters as Document.__init__.
:return: An instance of Database.
"""
return SQLCipherDatabase.open_database(
path, password, create=create, document_factory=document_factory,
soledad=soledad)
class DatabaseIsNotEncrypted(Exception):
"""
Exception raised when trying to open non-encrypted databases.
"""
pass
class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase):
"""A U1DB implementation that uses SQLCipher as its persistence layer."""
_index_storage_value = 'expand referenced encrypted'
@classmethod
def set_pragma_key(cls, db_handle, key):
db_handle.cursor().execute("PRAGMA key = '%s'" % key)
def __init__(self, sqlite_file, password, document_factory=None,
soledad=None):
"""Create a new sqlcipher file."""
self._check_if_db_is_encrypted(sqlite_file)
self._db_handle = dbapi2.connect(sqlite_file)
SQLCipherDatabase.set_pragma_key(self._db_handle, password)
self._real_replica_uid = None
self._ensure_schema()
self._soledad = soledad
def factory(doc_id=None, rev=None, json='{}', has_conflicts=False,
encrypted_json=None, syncable=True):
return LeapDocument(doc_id=doc_id, rev=rev, json=json,
has_conflicts=has_conflicts,
encrypted_json=encrypted_json,
syncable=syncable, soledad=self._soledad)
self.set_document_factory(factory)
def _check_if_db_is_encrypted(self, sqlite_file):
if not os.path.exists(sqlite_file):
return
else:
try:
# try to open an encrypted database with the regular u1db
# backend should raise a DatabaseError exception.
sqlite_backend.SQLitePartialExpandDatabase(sqlite_file)
raise DatabaseIsNotEncrypted()
except dbapi2.DatabaseError:
pass
@classmethod
def _open_database(cls, sqlite_file, password, document_factory=None,
soledad=None):
if not os.path.isfile(sqlite_file):
raise errors.DatabaseDoesNotExist()
tries = 2
while True:
# Note: There seems to be a bug in sqlite 3.5.9 (with python2.6)
# where without re-opening the database on Windows, it
# doesn't see the transaction that was just committed
db_handle = dbapi2.connect(sqlite_file)
SQLCipherDatabase.set_pragma_key(db_handle, password)
c = db_handle.cursor()
v, err = cls._which_index_storage(c)
db_handle.close()
if v is not None:
break
# possibly another process is initializing it, wait for it to be
# done
if tries == 0:
raise err # go for the richest error?
tries -= 1
time.sleep(cls.WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL)
return SQLCipherDatabase._sqlite_registry[v](
sqlite_file, password, document_factory=document_factory,
soledad=soledad)
@classmethod
def open_database(cls, sqlite_file, password, create, backend_cls=None,
document_factory=None, soledad=None):
"""Open U1DB database using SQLCipher as backend."""
try:
return cls._open_database(sqlite_file, password,
document_factory=document_factory,
soledad=soledad)
except errors.DatabaseDoesNotExist:
if not create:
raise
if backend_cls is None:
# default is SQLCipherPartialExpandDatabase
backend_cls = SQLCipherDatabase
return backend_cls(sqlite_file, password,
document_factory=document_factory,
soledad=soledad)
def sync(self, url, creds=None, autocreate=True):
"""
Synchronize encrypted documents with remote replica exposed at url.
"""
from u1db.sync import Synchronizer
from leap.soledad.backends.leap_backend import LeapSyncTarget
return Synchronizer(
self,
LeapSyncTarget(url,
creds=creds,
soledad=self._soledad)).sync(autocreate=autocreate)
def _extra_schema_init(self, c):
c.execute(
'ALTER TABLE document '
'ADD COLUMN syncable BOOL NOT NULL DEFAULT TRUE')
def _put_and_update_indexes(self, old_doc, doc):
super(SQLCipherDatabase, self)._put_and_update_indexes(old_doc, doc)
c = self._db_handle.cursor()
c.execute('UPDATE document SET syncable=? WHERE doc_id=?',
(doc.syncable, doc.doc_id))
def _get_doc(self, doc_id, check_for_conflicts=False):
doc = super(SQLCipherDatabase, self)._get_doc(doc_id,
check_for_conflicts)
if doc:
c = self._db_handle.cursor()
c.execute('SELECT syncable FROM document WHERE doc_id=?',
(doc.doc_id,))
doc.syncable = bool(c.fetchone()[0])
return doc
sqlite_backend.SQLiteDatabase.register_implementation(SQLCipherDatabase)
|