Add BLAKE2b key types

This commit is contained in:
Marian Beermann 2016-11-10 00:41:27 +01:00
parent 09f470bd85
commit 00ac7b14be
5 changed files with 138 additions and 15 deletions

View File

@ -959,7 +959,7 @@ class Archiver:
else:
encrypted = 'Yes (%s)' % key.NAME
print('Encrypted: %s' % encrypted)
if key.NAME == 'key file':
if key.NAME.startswith('key file'):
print('Key file: %s' % key.find_key())
print('Cache: %s' % cache.path)
print(DASHES)
@ -1556,6 +1556,7 @@ class Archiver:
'Access to all sub-directories is granted implicitly; PATH doesn\'t need to directly point to a repository.')
subparser.add_argument('--append-only', dest='append_only', action='store_true',
help='only allow appending to repository segment files')
init_epilog = textwrap.dedent("""
This command initializes an empty repository. A repository is a filesystem
directory containing the deduplicated data from zero or more archives.
@ -1599,8 +1600,21 @@ class Archiver:
You can change your passphrase for existing repos at any time, it won't affect
the encryption/decryption key or other secrets.
When encrypting, AES-CTR-256 is used for encryption, and HMAC-SHA256 for
authentication. Hardware acceleration will be used automatically.
Encryption modes
++++++++++++++++
repokey and keyfile use AES-CTR-256 for encryption and HMAC-SHA256 for
authentication in an encrypt-then-MAC (EtM) construction. The chunk ID hash
is HMAC-SHA256 as well (with a separate key).
repokey-blake2 and keyfile-blake2 use the same authenticated encryption, but
use a keyed BLAKE2b-256 hash for the chunk ID hash.
"authenticated" mode uses no encryption, but authenticates repository contents
through the same keyed BLAKE2b-256 hash as the other blake2 modes.
The key is stored like repokey.
Hardware acceleration will be used automatically.
""")
subparser = subparsers.add_parser('init', parents=[common_parser], add_help=False,
description=self.do_init.__doc__, epilog=init_epilog,
@ -1611,7 +1625,8 @@ class Archiver:
type=location_validator(archive=False),
help='repository to create')
subparser.add_argument('-e', '--encryption', dest='encryption',
choices=('none', 'keyfile', 'repokey'), default='repokey',
choices=('none', 'keyfile', 'repokey', 'keyfile-blake2', 'repokey-blake2', 'authenticated'),
default='repokey',
help='select encryption key mode (default: "%(default)s")')
subparser.add_argument('-a', '--append-only', dest='append_only', action='store_true',
help='create an append-only mode repository')

View File

View File

@ -231,6 +231,11 @@ def blake2b_256(key, data):
md = bytes(32)
cdef unsigned char *md_ptr = md
# This is secure, because BLAKE2 is not vulnerable to length-extension attacks (unlike SHA-1/2, MD-5 and others).
# See the BLAKE2 paper section 2.9 "Keyed hashing (MAC and PRF)" for details.
# A nice benefit is that this simpler prefix-MAC mode has less overhead than the more complex HMAC mode.
# We don't use the BLAKE2 parameter block (via blake2s_init_key) for this to
# avoid incompatibility with the limited API of OpenSSL.
blake2b_update_from_buffer(&state, key)
blake2b_update_from_buffer(&state, data)

View File

@ -14,7 +14,7 @@ logger = create_logger()
from .constants import * # NOQA
from .compress import Compressor, get_compressor
from .crypto import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks, hmac_sha256
from .crypto import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256
from .helpers import Chunk
from .helpers import Error, IntegrityError
from .helpers import yes
@ -62,6 +62,12 @@ def key_creator(repository, args):
return KeyfileKey.create(repository, args)
elif args.encryption == 'repokey':
return RepoKey.create(repository, args)
elif args.encryption == 'keyfile-blake2':
return Blake2KeyfileKey.create(repository, args)
elif args.encryption == 'repokey-blake2':
return Blake2RepoKey.create(repository, args)
elif args.encryption == 'authenticated':
return AuthenticatedKey.create(repository, args)
else:
return PlaintextKey.create(repository, args)
@ -78,6 +84,12 @@ def key_factory(repository, manifest_data):
return RepoKey.detect(repository, manifest_data)
elif key_type == PlaintextKey.TYPE:
return PlaintextKey.detect(repository, manifest_data)
elif key_type == Blake2KeyfileKey.TYPE:
return Blake2KeyfileKey.detect(repository, manifest_data)
elif key_type == Blake2RepoKey.TYPE:
return Blake2RepoKey.detect(repository, manifest_data)
elif key_type == AuthenticatedKey.TYPE:
return AuthenticatedKey.detect(repository, manifest_data)
else:
raise UnsupportedPayloadError(key_type)
@ -149,6 +161,28 @@ class PlaintextKey(KeyBase):
return Chunk(data)
class ID_BLAKE2b_256:
"""
Key mix-in class for using BLAKE2b-256 for the id key.
The id_key length must be 32 bytes.
"""
def id_hash(self, data):
return blake2b_256(self.id_key, data)
class ID_HMAC_SHA_256:
"""
Key mix-in class for using HMAC-SHA-256 for the id key.
The id_key length must be 32 bytes.
"""
def id_hash(self, data):
return hmac_sha256(self.id_key, data)
class AESKeyBase(KeyBase):
"""Common base class shared by KeyfileKey and PassphraseKey
@ -164,11 +198,6 @@ class AESKeyBase(KeyBase):
PAYLOAD_OVERHEAD = 1 + 32 + 8 # TYPE + HMAC + NONCE
def id_hash(self, data):
"""Return HMAC hash using the "id" HMAC key
"""
return hmac_sha256(self.id_key, data)
def encrypt(self, chunk):
chunk = self.compress(chunk)
self.nonce_manager.ensure_reservation(num_aes_blocks(len(chunk.data)))
@ -272,7 +301,7 @@ class Passphrase(str):
return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
class PassphraseKey(AESKeyBase):
class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase):
# This mode was killed in borg 1.0, see: https://github.com/borgbackup/borg/issues/97
# Reasons:
# - you can never ever change your passphrase for existing repos.
@ -432,7 +461,7 @@ class KeyfileKeyBase(AESKeyBase):
raise NotImplementedError
class KeyfileKey(KeyfileKeyBase):
class KeyfileKey(ID_HMAC_SHA_256, KeyfileKeyBase):
TYPE = 0x00
NAME = 'key file'
FILE_ID = 'BORG_KEY'
@ -492,7 +521,7 @@ class KeyfileKey(KeyfileKeyBase):
self.target = target
class RepoKey(KeyfileKeyBase):
class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase):
TYPE = 0x03
NAME = 'repokey'
@ -522,3 +551,33 @@ class RepoKey(KeyfileKeyBase):
key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes
target.save_key(key_data)
self.target = target
class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey):
TYPE = 0x04
NAME = 'key file BLAKE2b'
FILE_ID = 'BORG_KEY'
class Blake2RepoKey(ID_BLAKE2b_256, RepoKey):
TYPE = 0x05
NAME = 'repokey BLAKE2b'
class AuthenticatedKey(ID_BLAKE2b_256, RepoKey):
TYPE = 0x06
NAME = 'authenticated BLAKE2b'
def encrypt(self, chunk):
chunk = self.compress(chunk)
return b''.join([self.TYPE_STR, chunk.data])
def decrypt(self, id, data, decompress=True):
if data[0] != self.TYPE:
raise IntegrityError('Chunk %s: Invalid envelope' % bin_to_hex(id))
payload = memoryview(data)[1:]
if not decompress:
return Chunk(payload)
data = self.compressor.decompress(payload)
self.assert_id(id, data)
return Chunk(data)

View File

@ -11,7 +11,8 @@ from ..helpers import Location
from ..helpers import Chunk
from ..helpers import IntegrityError
from ..helpers import get_nonces_dir
from ..key import PlaintextKey, PassphraseKey, KeyfileKey, Passphrase, PasswordRetriesExceeded, bin_to_hex
from ..key import PlaintextKey, PassphraseKey, KeyfileKey, RepoKey, Blake2KeyfileKey, Blake2RepoKey, AuthenticatedKey
from ..key import Passphrase, PasswordRetriesExceeded, bin_to_hex
class TestKey:
@ -34,6 +35,24 @@ class TestKey:
"""))
keyfile2_id = unhexlify('c3fbf14bc001ebcc3cd86e696c13482ed071740927cd7cbe1b01b4bfcee49314')
keyfile_blake2_key_file = """
BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000
hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaANAwo4EbUPF/kLQXhQnT4LxRc1advS8lUiegDa
q2Q6oOkP1Jc7MwBa7ZVMgoBG1sBeKYO6Sn6W6BBrHbMR8Dxv7xquaQIh8jIpnjLWpzyFIk
JlijFiTWI58Sxj+2D19b2ayFolnGkF9PJSARgfaieo0GkryqjcIgcXuKHO/H9NfaUDk5YJ
UqrJ9TUMohXSQzwF1pO4ak2BHPZKnbeJ7XL/8fFN8VFQZl27R0et4WlTFRBI1qQYyQaTiL
+/1ICMUpVsQM0mvyW6dc8/zGMsAlmZVApGhhc2jaACDdRF7uPv90UN3zsZy5Be89728RBl
zKvtzupDyTsfrJMqppdGVyYXRpb25zzgABhqCkc2FsdNoAIGTK3TR09UZqw1bPi17gyHOi
7YtSp4BVK7XptWeKh6Vip3ZlcnNpb24B""".strip()
keyfile_blake2_cdata = bytes.fromhex('04dd21cc91140ef009bc9e4dd634d075e39d39025ccce1289c'
'5536f9cb57f5f8130404040404040408ec852921309243b164')
# Verified against b2sum. Entire string passed to BLAKE2, including the 32 byte key contained in
# keyfile_blake2_key_file above is
# 037fb9b75b20d623f1d5a568050fccde4a1b7c5f5047432925e941a17c7a2d0d7061796c6f6164
# p a y l o a d
keyfile_blake2_id = bytes.fromhex('a22d4fc81bb61c3846c334a09eaf28d22dd7df08c9a7a41e713ef28d80eebd45')
@pytest.fixture
def keys_dir(self, request, monkeypatch, tmpdir):
monkeypatch.setenv('BORG_KEYS_DIR', tmpdir)
@ -41,7 +60,11 @@ class TestKey:
@pytest.fixture(params=(
KeyfileKey,
PlaintextKey
PlaintextKey,
RepoKey,
Blake2KeyfileKey,
Blake2RepoKey,
AuthenticatedKey,
))
def key(self, request, monkeypatch):
monkeypatch.setenv('BORG_PASSPHRASE', 'test')
@ -61,6 +84,12 @@ class TestKey:
def commit_nonce_reservation(self, next_unreserved, start_nonce):
pass
def save_key(self, data):
self.key_data = data
def load_key(self):
return self.key_data
def test_plaintext(self):
key = PlaintextKey.create(None, None)
chunk = Chunk(b'foo')
@ -128,6 +157,13 @@ class TestKey:
key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata).data == b'payload'
def test_keyfile_blake2(self, monkeypatch, keys_dir):
with keys_dir.join('keyfile').open('w') as fd:
fd.write(self.keyfile_blake2_key_file)
monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase')
key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata)
assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata).data == b'payload'
def test_passphrase(self, keys_dir, monkeypatch):
monkeypatch.setenv('BORG_PASSPHRASE', 'test')
key = PassphraseKey.create(self.MockRepository(), None)
@ -193,6 +229,14 @@ class TestKey:
with pytest.raises(IntegrityError):
key.assert_id(id, plaintext_changed)
def test_authenticated_encrypt(self, monkeypatch):
monkeypatch.setenv('BORG_PASSPHRASE', 'test')
key = AuthenticatedKey.create(self.MockRepository(), self.MockArgs())
plaintext = Chunk(b'123456789')
authenticated = key.encrypt(plaintext)
# 0x06 is the key TYPE, 0x0000 identifies CNONE compression
assert authenticated == b'\x06\x00\x00' + plaintext.data
class TestPassphrase:
def test_passphrase_new_verification(self, capsys, monkeypatch):