blake2b key modes: use B2B as MAC; longer keys.

This commit is contained in:
Marian Beermann 2016-11-30 00:24:03 +01:00
parent f4c916e01f
commit 8949f2c758
No known key found for this signature in database
GPG Key ID: 9B8450B91D1362C1
2 changed files with 32 additions and 6 deletions

View File

@ -161,6 +161,20 @@ class PlaintextKey(KeyBase):
return Chunk(data) return Chunk(data)
def random_blake2b_256_key():
# This might look a bit curious, but is the same construction used in the keyed mode of BLAKE2b.
# Why limit the key to 64 bytes and pad it with 64 nulls nonetheless? The answer is that BLAKE2b
# has a 128 byte block size, but only 64 bytes of internal state (this is also referred to as a
# "local wide pipe" design, because the compression function transforms (block, state) => state,
# and len(block) >= len(state), hence wide.)
# In other words, a key longer than 64 bytes would have simply no advantage, since the function
# has no way of propagating more than 64 bytes of entropy internally.
# It's padded to a full block so that the key is never buffered internally by blake2b_update, ie.
# it remains in a single memory location that can be tracked and could be erased securely, if we
# wanted to.
return os.urandom(64) + bytes(64)
class ID_BLAKE2b_256: class ID_BLAKE2b_256:
""" """
Key mix-in class for using BLAKE2b-256 for the id key. Key mix-in class for using BLAKE2b-256 for the id key.
@ -171,6 +185,12 @@ class ID_BLAKE2b_256:
def id_hash(self, data): def id_hash(self, data):
return blake2b_256(self.id_key, data) return blake2b_256(self.id_key, data)
def init_from_random_data(self, data=None):
assert data is None # PassphraseKey is the only caller using *data*
super().init_from_random_data()
self.enc_hmac_key = random_blake2b_256_key()
self.id_key = random_blake2b_256_key()
class ID_HMAC_SHA_256: class ID_HMAC_SHA_256:
""" """
@ -198,12 +218,14 @@ class AESKeyBase(KeyBase):
PAYLOAD_OVERHEAD = 1 + 32 + 8 # TYPE + HMAC + NONCE PAYLOAD_OVERHEAD = 1 + 32 + 8 # TYPE + HMAC + NONCE
MAC = hmac_sha256
def encrypt(self, chunk): def encrypt(self, chunk):
chunk = self.compress(chunk) chunk = self.compress(chunk)
self.nonce_manager.ensure_reservation(num_aes_blocks(len(chunk.data))) self.nonce_manager.ensure_reservation(num_aes_blocks(len(chunk.data)))
self.enc_cipher.reset() self.enc_cipher.reset()
data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(chunk.data))) data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(chunk.data)))
hmac = hmac_sha256(self.enc_hmac_key, data) hmac = self.MAC(self.enc_hmac_key, data)
return b''.join((self.TYPE_STR, hmac, data)) return b''.join((self.TYPE_STR, hmac, data))
def decrypt(self, id, data, decompress=True): def decrypt(self, id, data, decompress=True):
@ -212,7 +234,7 @@ class AESKeyBase(KeyBase):
raise IntegrityError('Chunk %s: Invalid encryption envelope' % bin_to_hex(id)) raise IntegrityError('Chunk %s: Invalid encryption envelope' % bin_to_hex(id))
data_view = memoryview(data) data_view = memoryview(data)
hmac_given = data_view[1:33] hmac_given = data_view[1:33]
hmac_computed = memoryview(hmac_sha256(self.enc_hmac_key, data_view[33:])) hmac_computed = memoryview(self.MAC(self.enc_hmac_key, data_view[33:]))
if not compare_digest(hmac_computed, hmac_given): if not compare_digest(hmac_computed, hmac_given):
raise IntegrityError('Chunk %s: Encryption envelope checksum mismatch' % bin_to_hex(id)) raise IntegrityError('Chunk %s: Encryption envelope checksum mismatch' % bin_to_hex(id))
self.dec_cipher.reset(iv=PREFIX + data[33:41]) self.dec_cipher.reset(iv=PREFIX + data[33:41])
@ -230,7 +252,9 @@ class AESKeyBase(KeyBase):
nonce = bytes_to_long(payload[33:41]) nonce = bytes_to_long(payload[33:41])
return nonce return nonce
def init_from_random_data(self, data): def init_from_random_data(self, data=None):
if data is None:
data = os.urandom(100)
self.enc_key = data[0:32] self.enc_key = data[0:32]
self.enc_hmac_key = data[32:64] self.enc_hmac_key = data[32:64]
self.id_key = data[64:96] self.id_key = data[64:96]
@ -457,7 +481,7 @@ class KeyfileKeyBase(AESKeyBase):
passphrase = Passphrase.new(allow_empty=True) passphrase = Passphrase.new(allow_empty=True)
key = cls(repository) key = cls(repository)
key.repository_id = repository.id key.repository_id = repository.id
key.init_from_random_data(os.urandom(100)) key.init_from_random_data()
key.init_ciphers() key.init_ciphers()
target = key.get_new_target(args) target = key.get_new_target(args)
key.save(target, passphrase) key.save(target, passphrase)
@ -568,11 +592,13 @@ class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey):
TYPE = 0x04 TYPE = 0x04
NAME = 'key file BLAKE2b' NAME = 'key file BLAKE2b'
FILE_ID = 'BORG_KEY' FILE_ID = 'BORG_KEY'
MAC = blake2b_256
class Blake2RepoKey(ID_BLAKE2b_256, RepoKey): class Blake2RepoKey(ID_BLAKE2b_256, RepoKey):
TYPE = 0x05 TYPE = 0x05
NAME = 'repokey BLAKE2b' NAME = 'repokey BLAKE2b'
MAC = blake2b_256
class AuthenticatedKey(ID_BLAKE2b_256, RepoKey): class AuthenticatedKey(ID_BLAKE2b_256, RepoKey):

View File

@ -45,8 +45,8 @@ class TestKey:
zKvtzupDyTsfrJMqppdGVyYXRpb25zzgABhqCkc2FsdNoAIGTK3TR09UZqw1bPi17gyHOi zKvtzupDyTsfrJMqppdGVyYXRpb25zzgABhqCkc2FsdNoAIGTK3TR09UZqw1bPi17gyHOi
7YtSp4BVK7XptWeKh6Vip3ZlcnNpb24B""".strip() 7YtSp4BVK7XptWeKh6Vip3ZlcnNpb24B""".strip()
keyfile_blake2_cdata = bytes.fromhex('04dd21cc91140ef009bc9e4dd634d075e39d39025ccce1289c' keyfile_blake2_cdata = bytes.fromhex('045d225d745e07af9002d739391e4e7509ff82a04f98debd74'
'5536f9cb57f5f8130404040404040408ec852921309243b164') '012f09b82cc1d07e0404040404040408ec852921309243b164')
# Verified against b2sum. Entire string passed to BLAKE2, including the 32 byte key contained in # Verified against b2sum. Entire string passed to BLAKE2, including the 32 byte key contained in
# keyfile_blake2_key_file above is # keyfile_blake2_key_file above is
# 037fb9b75b20d623f1d5a568050fccde4a1b7c5f5047432925e941a17c7a2d0d7061796c6f6164 # 037fb9b75b20d623f1d5a568050fccde4a1b7c5f5047432925e941a17c7a2d0d7061796c6f6164