summaryrefslogtreecommitdiff
path: root/src/leap/soledad/client/_secrets/crypto.py
blob: 8148151d3d012909fe4300428d526adb73e2d76d (plain)
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