-
Notifications
You must be signed in to change notification settings - Fork 15
/
simplecrypt.py
160 lines (117 loc) · 5.56 KB
/
simplecrypt.py
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
#locally stored to prevent an even of abandoned repository removal
from Cryptodome.Cipher import AES
from Cryptodome.Hash import SHA256, HMAC
from Cryptodome.Protocol.KDF import PBKDF2
from Cryptodome.Random.random import getrandbits
from Cryptodome.Util import Counter
# see: http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html
EXPANSION_COUNT = (10000, 10000, 100000)
AES_KEY_LEN = 256
SALT_LEN = (128, 256, 256)
HASH = SHA256
PREFIX = b'sc'
HEADER = (PREFIX + b'\x00\x00', PREFIX + b'\x00\x01', PREFIX + b'\x00\x02')
LATEST = 2 # index into SALT_LEN, EXPANSION_COUNT, HEADER
# lengths here are in bits, but pcrypto uses block size in bytes
HALF_BLOCK = AES.block_size * 8 // 2
for salt_len in SALT_LEN:
assert HALF_BLOCK <= salt_len # we use a subset of the salt as nonce
HEADER_LEN = 4
for header in HEADER:
assert len(header) == HEADER_LEN
def encrypt(password, data):
'''
Encrypt some data. Input can be bytes or a string (which will be encoded
using UTF-8).
@param password: The secret value used as the basis for a key.
This should be as long as varied as possible. Try to avoid common words.
@param data: The data to be encrypted.
@return: The encrypted data, as bytes.
'''
data = _str_to_bytes(data)
_assert_encrypt_length(data)
salt = bytes(_random_bytes(SALT_LEN[LATEST] // 8))
hmac_key, cipher_key = _expand_keys(password, salt, EXPANSION_COUNT[LATEST])
counter = Counter.new(HALF_BLOCK, prefix=salt[:HALF_BLOCK // 8])
cipher = AES.new(cipher_key, AES.MODE_CTR, counter=counter)
encrypted = cipher.encrypt(data)
hmac = _hmac(hmac_key, HEADER[LATEST] + salt + encrypted)
return HEADER[LATEST] + salt + encrypted + hmac
def decrypt(password, data):
'''
Decrypt some data. Input must be bytes.
@param password: The secret value used as the basis for a key.
This should be as long as varied as possible. Try to avoid common words.
@param data: The data to be decrypted, typically as bytes.
@return: The decrypted data, as bytes. If the original message was a
string you can re-create that using `result.decode('utf8')`.
'''
_assert_not_unicode(data)
_assert_header_prefix(data)
version = _assert_header_version(data)
_assert_decrypt_length(data, version)
raw = data[HEADER_LEN:]
salt = raw[:SALT_LEN[version] // 8]
hmac_key, cipher_key = _expand_keys(password, salt, EXPANSION_COUNT[version])
hmac = raw[-HASH.digest_size:]
hmac2 = _hmac(hmac_key, data[:-HASH.digest_size])
_assert_hmac(hmac_key, hmac, hmac2)
counter = Counter.new(HALF_BLOCK, prefix=salt[:HALF_BLOCK // 8])
cipher = AES.new(cipher_key, AES.MODE_CTR, counter=counter)
return cipher.decrypt(raw[SALT_LEN[version] // 8:-HASH.digest_size])
class DecryptionException(Exception): pass
class EncryptionException(Exception): pass
def _assert_not_unicode(data):
# warn confused users
u_type = type(b''.decode('utf8'))
if isinstance(data, u_type):
raise DecryptionException('Data to decrypt must be bytes; ' +
'you cannot use a string because no string encoding will accept all possible characters.')
def _assert_encrypt_length(data):
# for AES this is never going to fail
if len(data) > 2 ** HALF_BLOCK:
raise EncryptionException('Message too long.')
def _assert_decrypt_length(data, version):
if len(data) < HEADER_LEN + SALT_LEN[version] // 8 + HASH.digest_size:
raise DecryptionException('Missing data.')
def _assert_header_prefix(data):
if len(data) >= 2 and data[:2] != PREFIX:
raise DecryptionException('Data passed to decrypt were not generated by simple-crypt (bad header).')
def _assert_header_version(data):
if len(data) >= HEADER_LEN:
try:
return HEADER.index(data[:HEADER_LEN])
except:
raise DecryptionException(
'The data appear to be encrypted with a more recent version of simple-crypt (bad header). ' +
'Please update the library and try again.')
else:
raise DecryptionException('Missing header.')
def _assert_hmac(key, hmac, hmac2):
# https://www.isecpartners.com/news-events/news/2011/february/double-hmac-verification.aspx
if _hmac(key, hmac) != _hmac(key, hmac2):
raise DecryptionException('Bad password or corrupt / modified data.')
def _pbkdf2(password, salt, n_bytes, count):
# the form of the prf below is taken from the code for PBKDF2
return PBKDF2(password, salt, dkLen=n_bytes,
count=count, prf=lambda p, s: HMAC.new(p, s, HASH).digest())
def _expand_keys(password, salt, expansion_count):
if not salt: raise ValueError('Missing salt.')
if not password: raise ValueError('Missing password.')
key_len = AES_KEY_LEN // 8
keys = _pbkdf2(_str_to_bytes(password), salt, 2 * key_len, expansion_count)
return keys[:key_len], keys[key_len:]
def _hide(ranbytes):
# appelbaum recommends obscuring output from random number generators since it can reveal state.
# we can do this explicitly with a hash, but this is what a PBKDF does anyway, so use one.
# we don't care about the salt or work factor because there is a large space of values anyway.
return bytearray(_pbkdf2(bytes(ranbytes), b'', len(ranbytes), 1))
def _random_bytes(n):
return _hide(bytearray(getrandbits(8) for _ in range(n)))
def _hmac(key, data):
return HMAC.new(key, data, HASH).digest()
def _str_to_bytes(data):
u_type = type(b''.decode('utf8'))
if isinstance(data, u_type):
return data.encode('utf8')
return data