mirror of https://github.com/borgbackup/borg.git
Add BLAKE2b key types
This commit is contained in:
parent
09f470bd85
commit
00ac7b14be
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue