Skip to content

Commit b92a620

Browse files
committed
Crypto code cleanup and test improvements.
1 parent ee77ce8 commit b92a620

File tree

4 files changed

+50
-15
lines changed

4 files changed

+50
-15
lines changed

attic/crypto.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""A thin ctypes based wrapper for OpenSSL 1.0
2+
"""
13
import sys
24
from ctypes import cdll, c_char_p, c_int, c_uint, c_void_p, POINTER, create_string_buffer
35
from ctypes.util import find_library
@@ -7,8 +9,10 @@
79
# Default libcrypto on OS X is too old, try the brew version
810
if not hasattr(libcrypto, 'PKCS5_PBKDF2_HMAC') and sys.platform == 'darwin':
911
libcrypto = cdll.LoadLibrary('/usr/local/opt/openssl/lib/libcrypto.dylib')
12+
# Default libcrypto on FreeBSD is too old, try the ports version
1013
if not hasattr(libcrypto, 'PKCS5_PBKDF2_HMAC') and sys.platform.startswith('freebsd'):
1114
libcrypto = cdll.LoadLibrary('/usr/local/lib/libcrypto.so')
15+
1216
libcrypto.PKCS5_PBKDF2_HMAC.argtypes = (c_char_p, c_int, c_char_p, c_int, c_int, c_void_p, c_int, c_char_p)
1317
libcrypto.EVP_sha256.restype = c_void_p
1418
libcrypto.AES_set_encrypt_key.argtypes = (c_char_p, c_int, c_char_p)
@@ -29,6 +33,8 @@ def num_aes_blocks(length):
2933

3034

3135
def pbkdf2_sha256(password, salt, iterations, size):
36+
"""Password based key derivation function 2 (RFC2898)
37+
"""
3238
key = create_string_buffer(size)
3339
rv = libcrypto.PKCS5_PBKDF2_HMAC(password, len(password), salt, len(salt), iterations, libcrypto.EVP_sha256(), size, key)
3440
if not rv:
@@ -46,6 +52,8 @@ def get_random_bytes(n):
4652

4753

4854
class AES:
55+
"""A thin wrapper around the OpenSSL AES CTR_MODE cipher
56+
"""
4957
def __init__(self, key, iv=None):
5058
self.key = create_string_buffer(2000)
5159
self.iv = create_string_buffer(16)

attic/key.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
from hashlib import sha256
88
import zlib
99

10-
from .crypto import pbkdf2_sha256, get_random_bytes, AES, bytes_to_long, long_to_bytes, bytes_to_int
11-
from .helpers import IntegrityError, get_keys_dir
10+
from attic.crypto import pbkdf2_sha256, get_random_bytes, AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks
11+
from attic.helpers import IntegrityError, get_keys_dir
1212

1313
PREFIX = b'\0' * 8
1414

1515

1616
class HMAC(hmac.HMAC):
17-
17+
"""Workaround a bug in Python < 3.4 Where HMAC does not accept memoryviews
18+
"""
1819
def update(self, msg):
1920
self.inner.update(msg)
2021

@@ -85,6 +86,19 @@ def decrypt(self, id, data):
8586

8687

8788
class AESKeyBase(KeyBase):
89+
"""Common base class shared by KeyfileKey and PassphraseKey
90+
91+
Chunks are encrypted using 256bit AES in Counter Mode (CTR)
92+
93+
Payload layout: TYPE(1) + HMAC(32) + NONCE(8) + CIPHERTEXT
94+
95+
To reduce payload size only 8 bytes of the 16 bytes nonce is saved
96+
in the payload, the first 8 bytes are always zeros. This does not
97+
affect security but limits the maximum repository capacity to
98+
only 295 exabytes!
99+
"""
100+
101+
PAYLOAD_OVERHEAD = 1 + 32 + 8 # TYPE + HMAC + NONCE
88102

89103
def id_hash(self, data):
90104
"""Return HMAC hash using the "id" HMAC key
@@ -110,7 +124,7 @@ def decrypt(self, id, data):
110124
raise IntegrityError('Chunk id verification failed')
111125
return data
112126

113-
def extract_iv(self, payload):
127+
def extract_nonce(self, payload):
114128
if payload[0] != self.TYPE:
115129
raise IntegrityError('Invalid encryption envelope')
116130
nonce = bytes_to_long(payload[33:41])
@@ -166,7 +180,8 @@ def detect(cls, repository, manifest_data):
166180
key.init(repository, passphrase)
167181
try:
168182
key.decrypt(None, manifest_data)
169-
key.init_ciphers(PREFIX + long_to_bytes(key.extract_iv(manifest_data) + 1000))
183+
num_blocks = num_aes_blocks(len(manifest_data) - 41)
184+
key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks))
170185
return key
171186
except IntegrityError:
172187
passphrase = getpass(prompt)
@@ -188,7 +203,8 @@ def detect(cls, repository, manifest_data):
188203
passphrase = os.environ.get('ATTIC_PASSPHRASE', '')
189204
while not key.load(path, passphrase):
190205
passphrase = getpass(prompt)
191-
key.init_ciphers(PREFIX + long_to_bytes(key.extract_iv(manifest_data) + 1000))
206+
num_blocks = num_aes_blocks(len(manifest_data) - 41)
207+
key.init_ciphers(PREFIX + long_to_bytes(key.extract_nonce(manifest_data) + num_blocks))
192208
return key
193209

194210
@classmethod

attic/testsuite/archiver.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,8 @@ def verify_uniqueness():
267267
if not hash in seen:
268268
seen.add(hash)
269269
num_blocks = num_aes_blocks(len(data) - 41)
270-
start = bytes_to_long(data[33:41])
271-
for counter in range(start, start + num_blocks):
270+
nonce = bytes_to_long(data[33:41])
271+
for counter in range(nonce, nonce + num_blocks):
272272
self.assert_not_in(counter, used)
273273
used.add(counter)
274274

@@ -282,6 +282,7 @@ def verify_uniqueness():
282282
verify_uniqueness()
283283
self.attic('delete', self.repository_location + '::test.2')
284284
verify_uniqueness()
285+
self.assert_equal(used, set(range(len(used))))
285286

286287
def test_aes_counter_uniqueness_keyfile(self):
287288
self.verify_aes_counter_uniqueness('keyfile')

attic/testsuite/key.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import shutil
44
import tempfile
55
from binascii import hexlify
6-
from attic.crypto import bytes_to_long
6+
from attic.crypto import bytes_to_long, num_aes_blocks
77
from attic.testsuite import AtticTestCase
88
from attic.key import PlaintextKey, PassphraseKey, KeyfileKey
99
from attic.helpers import Location, unhexlify
@@ -54,10 +54,15 @@ def test_keyfile(self):
5454
os.environ['ATTIC_PASSPHRASE'] = 'test'
5555
key = KeyfileKey.create(self.MockRepository(), self.MockArgs())
5656
self.assert_equal(bytes_to_long(key.enc_cipher.iv, 8), 0)
57-
manifest = key.encrypt(b'')
58-
iv = key.extract_iv(manifest)
57+
manifest = key.encrypt(b'XXX')
58+
self.assert_equal(key.extract_nonce(manifest), 0)
59+
manifest2 = key.encrypt(b'XXX')
60+
self.assert_not_equal(manifest, manifest2)
61+
self.assert_equal(key.decrypt(None, manifest), key.decrypt(None, manifest2))
62+
self.assert_equal(key.extract_nonce(manifest2), 1)
63+
iv = key.extract_nonce(manifest)
5964
key2 = KeyfileKey.detect(self.MockRepository(), manifest)
60-
self.assert_equal(bytes_to_long(key2.enc_cipher.iv, 8), iv + 1000)
65+
self.assert_equal(bytes_to_long(key2.enc_cipher.iv, 8), iv + num_aes_blocks(len(manifest) - KeyfileKey.PAYLOAD_OVERHEAD))
6166
# Key data sanity check
6267
self.assert_equal(len(set([key2.id_key, key2.enc_key, key2.enc_hmac_key])), 3)
6368
self.assert_equal(key2.chunk_seed == 0, False)
@@ -79,10 +84,15 @@ def test_passphrase(self):
7984
self.assert_equal(hexlify(key.enc_hmac_key), b'b885a05d329a086627412a6142aaeb9f6c54ab7950f996dd65587251f6bc0901')
8085
self.assert_equal(hexlify(key.enc_key), b'2ff3654c6daf7381dbbe718d2b20b4f1ea1e34caa6cc65f6bb3ac376b93fed2a')
8186
self.assert_equal(key.chunk_seed, -775740477)
82-
manifest = key.encrypt(b'')
83-
iv = key.extract_iv(manifest)
87+
manifest = key.encrypt(b'XXX')
88+
self.assert_equal(key.extract_nonce(manifest), 0)
89+
manifest2 = key.encrypt(b'XXX')
90+
self.assert_not_equal(manifest, manifest2)
91+
self.assert_equal(key.decrypt(None, manifest), key.decrypt(None, manifest2))
92+
self.assert_equal(key.extract_nonce(manifest2), 1)
93+
iv = key.extract_nonce(manifest)
8494
key2 = PassphraseKey.detect(self.MockRepository(), manifest)
85-
self.assert_equal(bytes_to_long(key2.enc_cipher.iv, 8), iv + 1000)
95+
self.assert_equal(bytes_to_long(key2.enc_cipher.iv, 8), iv + num_aes_blocks(len(manifest) - PassphraseKey.PAYLOAD_OVERHEAD))
8696
self.assert_equal(key.id_key, key2.id_key)
8797
self.assert_equal(key.enc_hmac_key, key2.enc_hmac_key)
8898
self.assert_equal(key.enc_key, key2.enc_key)

0 commit comments

Comments
 (0)