Projet

Général

Profil

0001-misc-move-authentic2.crypto-to-authentic2.utils.cryp.patch

Benjamin Dauvergne, 27 janvier 2022 08:40

Télécharger (15,8 ko)

Voir les différences:

Subject: [PATCH 1/3] misc: move authentic2.crypto to authentic2.utils.crypto
 (#60129)

 src/authentic2/crypto.py                      | 226 +-----------------
 src/authentic2/utils/crypto.py                | 223 +++++++++++++++++
 .../{test_crypto.py => test_utils_crypto.py}  |   0
 3 files changed, 226 insertions(+), 223 deletions(-)
 create mode 100644 src/authentic2/utils/crypto.py
 rename tests/{test_crypto.py => test_utils_crypto.py} (100%)
src/authentic2/crypto.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import base64
18
import hashlib
19
import hmac
20
import struct
21
from binascii import Error as Base64Error
22

  
23
from Cryptodome import Random
24
from Cryptodome.Cipher import AES
25
from Cryptodome.Hash import HMAC, SHA256
26
from Cryptodome.Protocol.KDF import PBKDF2
27
from django.conf import settings
28
from django.utils.crypto import constant_time_compare
29
from django.utils.encoding import force_bytes
30

  
31

  
32
class DecryptionError(Exception):
33
    pass
34

  
35

  
36
def base64url_decode(raw):
37
    rem = len(raw) % 4
38
    if rem > 0:
39
        raw += b'=' * (4 - rem)
40
    return base64.urlsafe_b64decode(raw)
41

  
42

  
43
def base64url_encode(raw):
44
    return base64.urlsafe_b64encode(raw).rstrip(b'=')
45

  
46

  
47
def get_hashclass(name):
48
    if name in ['md5', 'sha1', 'sha256', 'sha384', 'sha512']:
49
        return getattr(hashlib, name)
50
    return None
51

  
52

  
53
def aes_base64_encrypt(key, data):
54
    """Generate an AES key from any key material using PBKDF2, and encrypt data using CFB mode. A
55
    new IV is generated each time, the IV is also used as salt for PBKDF2.
56
    """
57
    iv = Random.get_random_bytes(16)
58
    aes_key = PBKDF2(key, iv)
59
    aes = AES.new(aes_key, AES.MODE_CFB, iv=iv)
60
    crypted = aes.encrypt(data)
61
    return b'%s$%s' % (base64.b64encode(iv), base64.b64encode(crypted))
62

  
63

  
64
def aes_base64_decrypt(key, payload, raise_on_error=True):
65
    '''Decrypt data encrypted with aes_base64_encrypt'''
66
    if not isinstance(payload, bytes):
67
        try:
68
            payload = payload.encode('ascii')
69
        except Exception:
70
            raise DecryptionError('payload is not an ASCII string')
71
    try:
72
        iv, crypted = payload.split(b'$')
73
    except (ValueError, TypeError):
74
        if raise_on_error:
75
            raise DecryptionError('bad payload')
76
        return None
77
    try:
78
        iv = base64.b64decode(iv)
79
        crypted = base64.b64decode(crypted)
80
    except Base64Error:
81
        if raise_on_error:
82
            raise DecryptionError('incorrect base64 encoding')
83
        return None
84
    aes_key = PBKDF2(key, iv)
85
    aes = AES.new(aes_key, AES.MODE_CFB, iv=iv)
86
    return aes.decrypt(crypted)
87

  
88

  
89
def add_padding(msg, block_size):
90
    '''Pad message with zero bytes to match block_size'''
91
    pad_length = block_size - (len(msg) + 2) % block_size
92
    padded = struct.pack('<h%ds%ds' % (len(msg), pad_length), len(msg), msg, b'\0' * pad_length)
93
    assert len(padded) % block_size == 0
94
    return padded
95

  
96

  
97
def remove_padding(msg, block_size):
98
    '''Ignore padded zero bytes'''
99
    try:
100
        (msg_length,) = struct.unpack('<h', msg[:2])
101
    except struct.error:
102
        raise DecryptionError('wrong padding')
103
    if len(msg) % block_size != 0:
104
        raise DecryptionError('message length is not a multiple of block size', len(msg), block_size)
105
    unpadded = msg[2 : 2 + msg_length]
106
    if msg_length > len(msg) - 2:
107
        raise DecryptionError('wrong padding')
108
    if len(msg[2 + msg_length :].strip(force_bytes('\0'))):
109
        raise DecryptionError('padding is not all zero')
110
    if len(unpadded) != msg_length:
111
        raise DecryptionError('wrong padding')
112
    return unpadded
113

  
114

  
115
def aes_base64url_deterministic_encrypt(key, data, salt, hash_name='sha256', count=1):
116
    """Encrypt using AES-128 and sign using HMAC-SHA256 shortened to 64 bits.
117

  
118
    Count and algorithm are encoded in the final string for future evolution.
119

  
120
    """
121
    mode = 1  # AES128-SHA256
122
    hashmod = SHA256
123
    key_size = 16
124
    hmac_size = key_size
125

  
126
    if isinstance(salt, str):
127
        salt = force_bytes(salt)
128
    iv = hashmod.new(salt).digest()
129

  
130
    def prf(secret, salt):
131
        return HMAC.new(secret, salt, hashmod).digest()
132

  
133
    aes_key = PBKDF2(key, iv, dkLen=key_size, count=count, prf=prf)
134

  
135
    key_size = len(aes_key)
136

  
137
    aes = AES.new(aes_key, AES.MODE_CBC, iv[:key_size])
138

  
139
    crypted = aes.encrypt(add_padding(data, key_size))
140

  
141
    hmac = prf(key, crypted)[:hmac_size]
142

  
143
    raw = struct.pack('<2sBH', b'a2', mode, count) + crypted + hmac
144
    return base64url_encode(raw)
145

  
146

  
147
def aes_base64url_deterministic_decrypt(key, urlencoded, salt, raise_on_error=True, max_count=1):
148
    mode = 1  # AES128-SHA256
149
    hashmod = SHA256
150
    key_size = 16
151
    hmac_size = key_size
152

  
153
    def prf(secret, salt):
154
        return HMAC.new(secret, salt, hashmod).digest()
155

  
156
    try:
157
        try:
158
            raw = base64url_decode(urlencoded)
159
        except Exception as e:
160
            raise DecryptionError('base64 decoding failed', e)
161
        try:
162
            magic, mode, count = struct.unpack('<2sBH', raw[:5])
163
        except struct.error as e:
164
            raise DecryptionError('invalid packing', e)
165
        if magic != b'a2':
166
            raise DecryptionError('invalid magic string', magic)
167
        if mode != 1:
168
            raise DecryptionError('mode is not AES128-SHA256', mode)
169
        if count > max_count:
170
            raise DecryptionError('count is too big', count)
171

  
172
        crypted, hmac = raw[5:-hmac_size], raw[-hmac_size:]
173

  
174
        if not crypted or not hmac or prf(key, crypted)[:hmac_size] != hmac:
175
            raise DecryptionError('invalid HMAC')
176

  
177
        if isinstance(salt, str):
178
            salt = force_bytes(salt)
179
        iv = hashmod.new(salt).digest()
180

  
181
        aes_key = PBKDF2(key, iv, dkLen=key_size, count=count, prf=prf)
182

  
183
        aes = AES.new(aes_key, AES.MODE_CBC, iv[:key_size])
184

  
185
        data = remove_padding(aes.decrypt(crypted), key_size)
186

  
187
        return data
188
    except DecryptionError:
189
        if not raise_on_error:
190
            return None
191
        raise
192

  
193

  
194
def hmac_url(key, url):
195
    if hasattr(key, 'encode'):
196
        key = key.encode()
197
    if hasattr(url, 'encode'):
198
        url = url.encode()
199
    return (
200
        base64.b32encode(hmac.HMAC(key=key, msg=url, digestmod=hashlib.sha256).digest())
201
        .decode('ascii')
202
        .strip('=')
203
    )
204

  
205

  
206
def check_hmac_url(key, url, signature):
207
    if hasattr(signature, 'decode'):
208
        signature = signature.decode()
209
    return constant_time_compare(signature, hmac_url(key, url).encode('ascii'))
210

  
211

  
212
def hash_chain(n, seed=None, encoded_seed=None):
213
    '''Generate a chain of hashes'''
214
    if encoded_seed:
215
        seed = base64url_decode(encoded_seed.encode())
216
    if hasattr(seed, 'encode'):
217
        seed = seed.encode()
218
    if seed is None:
219
        seed = Random.get_random_bytes(16)
220
    chain = [seed]
221
    for dummy in range(n - 1):
222
        chain.append(hashlib.sha256(chain[-1] + settings.SECRET_KEY.encode()).digest())
223
    return [base64url_encode(x).decode('ascii') for x in chain]
1
# authentic2.crypto was moved to authentic2.utils.cryptor, use wildcard import to prevent
2
# breakage of import in other modules
3
from .utils.crypto import *  # pylint: disable=unused-wildcard-import,wildcard-import
src/authentic2/utils/crypto.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import base64
18
import hashlib
19
import hmac
20
import struct
21
from binascii import Error as Base64Error
22

  
23
from Cryptodome import Random
24
from Cryptodome.Cipher import AES
25
from Cryptodome.Hash import HMAC, SHA256
26
from Cryptodome.Protocol.KDF import PBKDF2
27
from django.conf import settings
28
from django.utils.crypto import constant_time_compare
29
from django.utils.encoding import force_bytes
30

  
31

  
32
class DecryptionError(Exception):
33
    pass
34

  
35

  
36
def base64url_decode(raw):
37
    rem = len(raw) % 4
38
    if rem > 0:
39
        raw += b'=' * (4 - rem)
40
    return base64.urlsafe_b64decode(raw)
41

  
42

  
43
def base64url_encode(raw):
44
    return base64.urlsafe_b64encode(raw).rstrip(b'=')
45

  
46

  
47
def get_hashclass(name):
48
    if name in ['md5', 'sha1', 'sha256', 'sha384', 'sha512']:
49
        return getattr(hashlib, name)
50
    return None
51

  
52

  
53
def aes_base64_encrypt(key, data):
54
    """Generate an AES key from any key material using PBKDF2, and encrypt data using CFB mode. A
55
    new IV is generated each time, the IV is also used as salt for PBKDF2.
56
    """
57
    iv = Random.get_random_bytes(16)
58
    aes_key = PBKDF2(key, iv)
59
    aes = AES.new(aes_key, AES.MODE_CFB, iv=iv)
60
    crypted = aes.encrypt(data)
61
    return b'%s$%s' % (base64.b64encode(iv), base64.b64encode(crypted))
62

  
63

  
64
def aes_base64_decrypt(key, payload, raise_on_error=True):
65
    '''Decrypt data encrypted with aes_base64_encrypt'''
66
    if not isinstance(payload, bytes):
67
        try:
68
            payload = payload.encode('ascii')
69
        except Exception:
70
            raise DecryptionError('payload is not an ASCII string')
71
    try:
72
        iv, crypted = payload.split(b'$')
73
    except (ValueError, TypeError):
74
        if raise_on_error:
75
            raise DecryptionError('bad payload')
76
        return None
77
    try:
78
        iv = base64.b64decode(iv)
79
        crypted = base64.b64decode(crypted)
80
    except Base64Error:
81
        if raise_on_error:
82
            raise DecryptionError('incorrect base64 encoding')
83
        return None
84
    aes_key = PBKDF2(key, iv)
85
    aes = AES.new(aes_key, AES.MODE_CFB, iv=iv)
86
    return aes.decrypt(crypted)
87

  
88

  
89
def add_padding(msg, block_size):
90
    '''Pad message with zero bytes to match block_size'''
91
    pad_length = block_size - (len(msg) + 2) % block_size
92
    padded = struct.pack('<h%ds%ds' % (len(msg), pad_length), len(msg), msg, b'\0' * pad_length)
93
    assert len(padded) % block_size == 0
94
    return padded
95

  
96

  
97
def remove_padding(msg, block_size):
98
    '''Ignore padded zero bytes'''
99
    try:
100
        (msg_length,) = struct.unpack('<h', msg[:2])
101
    except struct.error:
102
        raise DecryptionError('wrong padding')
103
    if len(msg) % block_size != 0:
104
        raise DecryptionError('message length is not a multiple of block size', len(msg), block_size)
105
    unpadded = msg[2 : 2 + msg_length]
106
    if msg_length > len(msg) - 2:
107
        raise DecryptionError('wrong padding')
108
    if len(msg[2 + msg_length :].strip(force_bytes('\0'))):
109
        raise DecryptionError('padding is not all zero')
110
    if len(unpadded) != msg_length:
111
        raise DecryptionError('wrong padding')
112
    return unpadded
113

  
114

  
115
def aes_base64url_deterministic_encrypt(key, data, salt, hash_name='sha256', count=1):
116
    """Encrypt using AES-128 and sign using HMAC-SHA256 shortened to 64 bits.
117

  
118
    Count and algorithm are encoded in the final string for future evolution.
119

  
120
    """
121
    mode = 1  # AES128-SHA256
122
    hashmod = SHA256
123
    key_size = 16
124
    hmac_size = key_size
125

  
126
    if isinstance(salt, str):
127
        salt = force_bytes(salt)
128
    iv = hashmod.new(salt).digest()
129

  
130
    def prf(secret, salt):
131
        return HMAC.new(secret, salt, hashmod).digest()
132

  
133
    aes_key = PBKDF2(key, iv, dkLen=key_size, count=count, prf=prf)
134

  
135
    key_size = len(aes_key)
136

  
137
    aes = AES.new(aes_key, AES.MODE_CBC, iv[:key_size])
138

  
139
    crypted = aes.encrypt(add_padding(data, key_size))
140

  
141
    hmac = prf(key, crypted)[:hmac_size]
142

  
143
    raw = struct.pack('<2sBH', b'a2', mode, count) + crypted + hmac
144
    return base64url_encode(raw)
145

  
146

  
147
def aes_base64url_deterministic_decrypt(key, urlencoded, salt, raise_on_error=True, max_count=1):
148
    mode = 1  # AES128-SHA256
149
    hashmod = SHA256
150
    key_size = 16
151
    hmac_size = key_size
152

  
153
    def prf(secret, salt):
154
        return HMAC.new(secret, salt, hashmod).digest()
155

  
156
    try:
157
        try:
158
            raw = base64url_decode(urlencoded)
159
        except Exception as e:
160
            raise DecryptionError('base64 decoding failed', e)
161
        try:
162
            magic, mode, count = struct.unpack('<2sBH', raw[:5])
163
        except struct.error as e:
164
            raise DecryptionError('invalid packing', e)
165
        if magic != b'a2':
166
            raise DecryptionError('invalid magic string', magic)
167
        if mode != 1:
168
            raise DecryptionError('mode is not AES128-SHA256', mode)
169
        if count > max_count:
170
            raise DecryptionError('count is too big', count)
171

  
172
        crypted, hmac = raw[5:-hmac_size], raw[-hmac_size:]
173

  
174
        if not crypted or not hmac or prf(key, crypted)[:hmac_size] != hmac:
175
            raise DecryptionError('invalid HMAC')
176

  
177
        if isinstance(salt, str):
178
            salt = force_bytes(salt)
179
        iv = hashmod.new(salt).digest()
180

  
181
        aes_key = PBKDF2(key, iv, dkLen=key_size, count=count, prf=prf)
182

  
183
        aes = AES.new(aes_key, AES.MODE_CBC, iv[:key_size])
184

  
185
        data = remove_padding(aes.decrypt(crypted), key_size)
186

  
187
        return data
188
    except DecryptionError:
189
        if not raise_on_error:
190
            return None
191
        raise
192

  
193

  
194
def hmac_url(key, url):
195
    if hasattr(key, 'encode'):
196
        key = key.encode()
197
    if hasattr(url, 'encode'):
198
        url = url.encode()
199
    return (
200
        base64.b32encode(hmac.HMAC(key=key, msg=url, digestmod=hashlib.sha256).digest())
201
        .decode('ascii')
202
        .strip('=')
203
    )
204

  
205

  
206
def check_hmac_url(key, url, signature):
207
    if hasattr(signature, 'decode'):
208
        signature = signature.decode()
209
    return constant_time_compare(signature, hmac_url(key, url).encode('ascii'))
210

  
211

  
212
def hash_chain(n, seed=None, encoded_seed=None):
213
    '''Generate a chain of hashes'''
214
    if encoded_seed:
215
        seed = base64url_decode(encoded_seed.encode())
216
    if hasattr(seed, 'encode'):
217
        seed = seed.encode()
218
    if seed is None:
219
        seed = Random.get_random_bytes(16)
220
    chain = [seed]
221
    for dummy in range(n - 1):
222
        chain.append(hashlib.sha256(chain[-1] + settings.SECRET_KEY.encode()).digest())
223
    return [base64url_encode(x).decode('ascii') for x in chain]