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
|
# -*- coding: utf-8 -*-
# _secrets/__init__.py
# Copyright (C) 2016 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/>.
import os
import scrypt
from leap.soledad.common.log import getLogger
from leap.soledad.client._secrets.storage import SecretsStorage
from leap.soledad.client._secrets.crypto import SecretsCrypto
from leap.soledad.client._secrets.util import emit, UserDataMixin
logger = getLogger(__name__)
class Secrets(UserDataMixin):
lengths = {
'remote_secret': 512, # remote_secret is used to encrypt remote data.
'local_salt': 64, # local_salt is used in conjunction with
'local_secret': 448, # local_secret to derive a local_key for storage
}
def __init__(self, soledad):
self._soledad = soledad
self._secrets = {}
self.crypto = SecretsCrypto(soledad)
self.storage = SecretsStorage(soledad)
self._bootstrap()
#
# bootstrap
#
def _bootstrap(self):
# attempt to load secrets from local storage
encrypted = self.storage.load_local()
if encrypted:
self._secrets = self.crypto.decrypt(encrypted)
# maybe update the format of storage of local secret.
if encrypted['version'] < self.crypto.VERSION:
self.store_secrets()
return
# no secret was found in local storage, so this is a first run of
# soledad for this user in this device. It is mandatory that we check
# if there's a secret stored in server.
encrypted = self.storage.load_remote()
if encrypted:
self._secrets = self.crypto.decrypt(encrypted)
self.store_secrets()
return
# we have *not* found a secret neither in local nor in remote storage,
# so we have to generate a new one, and then store it.
self._secrets = self._generate()
self.store_secrets()
#
# generation
#
@emit('creating')
def _generate(self):
logger.info("generating new set of secrets...")
secrets = {}
for name, length in self.lengths.iteritems():
secret = os.urandom(length)
secrets[name] = secret
logger.info("new set of secrets successfully generated")
return secrets
#
# crypto
#
def store_secrets(self):
# TODO: we have to improve the logic here, as we want to make sure that
# whatever is stored locally should only be used after remote storage
# is successful. Otherwise, this soledad could start encrypting with a
# secret while another soledad in another device could start encrypting
# with another secret, which would lead to decryption failures during
# sync.
encrypted = self.crypto.encrypt(self._secrets)
self.storage.save_local(encrypted)
self.storage.save_remote(encrypted)
#
# secrets
#
@property
def remote_secret(self):
return self._secrets.get('remote_secret')
@property
def local_salt(self):
return self._secrets.get('local_salt')
@property
def local_secret(self):
return self._secrets.get('local_secret')
@property
def local_key(self):
# local storage key is scrypt-derived from `local_secret` and
# `local_salt` above
secret = scrypt.hash(
password=self.local_secret,
salt=self.local_salt,
buflen=32, # we need a key with 256 bits (32 bytes)
)
return secret
|