|
@@ -14,7 +14,7 @@ logger = create_logger()
|
|
|
|
|
|
from .constants import * # NOQA
|
|
from .constants import * # NOQA
|
|
from .compress import Compressor, get_compressor
|
|
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 Chunk
|
|
from .helpers import Error, IntegrityError
|
|
from .helpers import Error, IntegrityError
|
|
from .helpers import yes
|
|
from .helpers import yes
|
|
@@ -62,6 +62,12 @@ def key_creator(repository, args):
|
|
return KeyfileKey.create(repository, args)
|
|
return KeyfileKey.create(repository, args)
|
|
elif args.encryption == 'repokey':
|
|
elif args.encryption == 'repokey':
|
|
return RepoKey.create(repository, args)
|
|
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:
|
|
else:
|
|
return PlaintextKey.create(repository, args)
|
|
return PlaintextKey.create(repository, args)
|
|
|
|
|
|
@@ -78,6 +84,12 @@ def key_factory(repository, manifest_data):
|
|
return RepoKey.detect(repository, manifest_data)
|
|
return RepoKey.detect(repository, manifest_data)
|
|
elif key_type == PlaintextKey.TYPE:
|
|
elif key_type == PlaintextKey.TYPE:
|
|
return PlaintextKey.detect(repository, manifest_data)
|
|
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:
|
|
else:
|
|
raise UnsupportedPayloadError(key_type)
|
|
raise UnsupportedPayloadError(key_type)
|
|
|
|
|
|
@@ -149,6 +161,28 @@ class PlaintextKey(KeyBase):
|
|
return Chunk(data)
|
|
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):
|
|
class AESKeyBase(KeyBase):
|
|
"""Common base class shared by KeyfileKey and PassphraseKey
|
|
"""Common base class shared by KeyfileKey and PassphraseKey
|
|
|
|
|
|
@@ -164,11 +198,6 @@ class AESKeyBase(KeyBase):
|
|
|
|
|
|
PAYLOAD_OVERHEAD = 1 + 32 + 8 # TYPE + HMAC + NONCE
|
|
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):
|
|
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)))
|
|
@@ -272,7 +301,7 @@ class Passphrase(str):
|
|
return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
|
|
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
|
|
# This mode was killed in borg 1.0, see: https://github.com/borgbackup/borg/issues/97
|
|
# Reasons:
|
|
# Reasons:
|
|
# - you can never ever change your passphrase for existing repos.
|
|
# - you can never ever change your passphrase for existing repos.
|
|
@@ -432,7 +461,7 @@ class KeyfileKeyBase(AESKeyBase):
|
|
raise NotImplementedError
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
-class KeyfileKey(KeyfileKeyBase):
|
|
|
|
|
|
+class KeyfileKey(ID_HMAC_SHA_256, KeyfileKeyBase):
|
|
TYPE = 0x00
|
|
TYPE = 0x00
|
|
NAME = 'key file'
|
|
NAME = 'key file'
|
|
FILE_ID = 'BORG_KEY'
|
|
FILE_ID = 'BORG_KEY'
|
|
@@ -492,7 +521,7 @@ class KeyfileKey(KeyfileKeyBase):
|
|
self.target = target
|
|
self.target = target
|
|
|
|
|
|
|
|
|
|
-class RepoKey(KeyfileKeyBase):
|
|
|
|
|
|
+class RepoKey(ID_HMAC_SHA_256, KeyfileKeyBase):
|
|
TYPE = 0x03
|
|
TYPE = 0x03
|
|
NAME = 'repokey'
|
|
NAME = 'repokey'
|
|
|
|
|
|
@@ -522,3 +551,33 @@ class RepoKey(KeyfileKeyBase):
|
|
key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes
|
|
key_data = key_data.encode('utf-8') # remote repo: msgpack issue #99, giving bytes
|
|
target.save_key(key_data)
|
|
target.save_key(key_data)
|
|
self.target = target
|
|
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)
|