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
|
# -*- coding: utf-8 -*-
# _secrets/crypto.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 binascii
import json
import os
import scrypt
from leap.soledad.common import soledad_assert
from leap.soledad.common.log import getLogger
from leap.soledad.client._crypto import encrypt_sym, decrypt_sym, ENC_METHOD
from leap.soledad.client._secrets.util import SecretsError
logger = getLogger(__name__)
class SecretsCrypto(object):
VERSION = 2
def __init__(self, soledad):
self._soledad = soledad
def _get_key(self, salt):
passphrase = self._soledad.passphrase.encode('utf8')
key = scrypt.hash(passphrase, salt, buflen=32)
return key
#
# encryption
#
def encrypt(self, secrets):
encoded = {}
for name, value in secrets.iteritems():
encoded[name] = binascii.b2a_base64(value)
plaintext = json.dumps(encoded)
salt = os.urandom(64) # TODO: get salt length from somewhere else
key = self._get_key(salt)
iv, ciphertext = encrypt_sym(plaintext, key,
method=ENC_METHOD.aes_256_gcm)
encrypted = {
'version': self.VERSION,
'kdf': 'scrypt',
'kdf_salt': binascii.b2a_base64(salt),
'kdf_length': len(key),
'cipher': ENC_METHOD.aes_256_gcm,
'length': len(plaintext),
'iv': str(iv),
'secrets': binascii.b2a_base64(ciphertext),
}
return encrypted
#
# decryption
#
def decrypt(self, data):
version = data.setdefault('version', 1)
method = getattr(self, '_decrypt_v%d' % version)
try:
return method(data)
except Exception as e:
logger.error('error decrypting secrets: %r' % e)
raise SecretsError(e)
def _decrypt_v1(self, data):
# get encrypted secret from dictionary: the old format allowed for
# storage of more than one secret, but this feature was never used and
# soledad has been using only one secret so far. As there is a corner
# case where the old 'active_secret' key might not be set, we just
# ignore it and pop the only secret found in the 'storage_secrets' key.
secret_id = data['storage_secrets'].keys().pop()
encrypted = data['storage_secrets'][secret_id]
# assert that we know how to decrypt the secret
soledad_assert('cipher' in encrypted)
cipher = encrypted['cipher']
if cipher == 'aes256':
cipher = ENC_METHOD.aes_256_ctr
soledad_assert(cipher in ENC_METHOD)
# decrypt
salt = binascii.a2b_base64(encrypted['kdf_salt'])
key = self._get_key(salt)
separator = ':'
iv, ciphertext = encrypted['secret'].split(separator, 1)
ciphertext = binascii.a2b_base64(ciphertext)
plaintext = self._decrypt(key, iv, ciphertext, encrypted, cipher)
# create secrets dictionary
secrets = {
'remote_secret': plaintext[0:512],
'local_salt': plaintext[512:576],
'local_secret': plaintext[576:1024],
}
return secrets
def _decrypt_v2(self, encrypted):
cipher = encrypted['cipher']
soledad_assert(cipher in ENC_METHOD)
salt = binascii.a2b_base64(encrypted['kdf_salt'])
key = self._get_key(salt)
iv = encrypted['iv']
ciphertext = binascii.a2b_base64(encrypted['secrets'])
plaintext = self._decrypt(
key, iv, ciphertext, encrypted, cipher)
encoded = json.loads(plaintext)
secrets = {}
for name, value in encoded.iteritems():
secrets[name] = binascii.a2b_base64(value)
return secrets
def _decrypt(self, key, iv, ciphertext, encrypted, method):
# assert some properties of the stored secret
soledad_assert(encrypted['kdf'] == 'scrypt')
soledad_assert(encrypted['kdf_length'] == len(key))
# decrypt
plaintext = decrypt_sym(ciphertext, key, iv, method)
soledad_assert(encrypted['length'] == len(plaintext))
return plaintext
|