Skip to content

Commit 8e85f90

Browse files
blake2b_256: fix key passing, fixes #8867
borg 1.x involved the key a bit unusually in the blake2b hash calculation (padding 64B of key material with another 64B of zeros to a total of 128B and then just prepending it to the data). since a while, we use blake2b from python stdlib and they support passing the key as a separate argument. up to 64B keys are allowed here. besides the way how the 64B of key material are passed, there are also other differences in how the key gets involved in the digest computation, thus the legacy blake2b hashes from borg 1.x are not compatible with the non-legacy ones - the hash is not the same for same key and data values.
1 parent 07183f1 commit 8e85f90

File tree

5 files changed

+91
-50
lines changed

5 files changed

+91
-50
lines changed

src/borg/archiver/benchmark_cmd.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def chunkit(ch):
180180
]:
181181
print(f"{spec:<24} {size:<10} {timeit(func, number=100):.3f}s")
182182

183-
from ..crypto.low_level import AES256_CTR_BLAKE2b, AES256_CTR_HMAC_SHA256
183+
from ..crypto.low_level import AES256_CTR_BLAKE2b_legacy, AES256_CTR_HMAC_SHA256
184184
from ..crypto.low_level import AES256_OCB, CHACHA20_POLY1305
185185

186186
print("Encryption =====================================================")
@@ -195,7 +195,7 @@ def chunkit(ch):
195195
),
196196
(
197197
"aes-256-ctr-blake2b",
198-
lambda: AES256_CTR_BLAKE2b(key_256 * 4, key_256, iv=key_128, header_len=1, aad_offset=1).encrypt(
198+
lambda: AES256_CTR_BLAKE2b_legacy(key_256 * 4, key_256, iv=key_128, header_len=1, aad_offset=1).encrypt(
199199
random_10M, header=b"X"
200200
),
201201
),

src/borg/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class KeyType:
179179
REPO = 0x03
180180
BLAKE2KEYFILE = 0x04
181181
BLAKE2REPO = 0x05
182-
BLAKE2AUTHENTICATED = 0x06
182+
BLAKE2AUTHENTICATEDLEGACY = 0x06
183183
AUTHENTICATED = 0x07
184184
# new crypto
185185
# upper 4 bits are ciphersuite, lower 4 bits are keytype
@@ -191,6 +191,7 @@ class KeyType:
191191
BLAKE2AESOCBREPO = 0x31
192192
BLAKE2CHPOKEYFILE = 0x40
193193
BLAKE2CHPOREPO = 0x41
194+
BLAKE2AUTHENTICATED = 0x51
194195

195196

196197
CACHE_TAG_NAME = "CACHEDIR.TAG"

src/borg/crypto/key.py

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030

3131
from .low_level import AES, bytes_to_int, num_cipher_blocks, hmac_sha256, blake2b_256
32-
from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b, AES256_OCB, CHACHA20_POLY1305
32+
from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b_legacy, AES256_OCB, CHACHA20_POLY1305
3333
from . import low_level
3434

3535
# workaround for lost passphrase or key in "authenticated" or "authenticated-blake2" mode
@@ -123,7 +123,7 @@ def uses_same_id_hash(other_key, key):
123123
new_sha256_ids = (PlaintextKey,)
124124
old_hmac_sha256_ids = (RepoKey, KeyfileKey, AuthenticatedKey)
125125
new_hmac_sha256_ids = (AESOCBRepoKey, AESOCBKeyfileKey, CHPORepoKey, CHPOKeyfileKey, AuthenticatedKey)
126-
old_blake2_ids = (Blake2RepoKey, Blake2KeyfileKey, Blake2AuthenticatedKey)
126+
old_blake2_ids = tuple() # empty tuple, old blake2 IDs are incompatible with new blake2 IDs
127127
new_blake2_ids = (
128128
Blake2AESOCBRepoKey,
129129
Blake2AESOCBKeyfileKey,
@@ -276,7 +276,7 @@ def decrypt(self, id, data):
276276
return memoryview(data)[1:]
277277

278278

279-
def random_blake2b_256_key():
279+
def random_blake2b_256_key_legacy(): # borg 1.x created the key this way
280280
# This might look a bit curious, but is the same construction used in the keyed mode of BLAKE2b.
281281
# Why limit the key to 64 bytes and pad it with 64 nulls nonetheless? The answer is that BLAKE2b
282282
# has a 128 byte block size, but only 64 bytes of internal state (this is also referred to as a
@@ -290,22 +290,36 @@ def random_blake2b_256_key():
290290
return os.urandom(64) + bytes(64)
291291

292292

293-
class ID_BLAKE2b_256:
293+
class ID_BLAKE2b_256_legacy: # borg 1.x
294294
"""
295295
Key mix-in class for using BLAKE2b-256 for the id key.
296+
"""
296297

297-
The id_key length must be 32 bytes.
298+
def id_hash(self, data):
299+
return blake2b_256(self.id_key, data, legacy=True)
300+
301+
def init_from_random_data(self):
302+
super().init_from_random_data()
303+
enc_key = os.urandom(32)
304+
enc_hmac_key = random_blake2b_256_key_legacy()
305+
self.crypt_key = enc_key + enc_hmac_key
306+
self.id_key = random_blake2b_256_key_legacy()
307+
308+
309+
class ID_BLAKE2b_256: # borg 2: either use the "fixed" blake2b or use blake3? see #8867
310+
"""
311+
Key mix-in class for using BLAKE2b-256 for the id key.
298312
"""
299313

300314
def id_hash(self, data):
301-
return blake2b_256(self.id_key, data)
315+
return blake2b_256(self.id_key, data, legacy=False)
302316

303317
def init_from_random_data(self):
304318
super().init_from_random_data()
305319
enc_key = os.urandom(32)
306-
enc_hmac_key = random_blake2b_256_key()
320+
enc_hmac_key = os.urandom(64)
307321
self.crypt_key = enc_key + enc_hmac_key
308-
self.id_key = random_blake2b_256_key()
322+
self.id_key = os.urandom(64)
309323

310324

311325
class ID_HMAC_SHA_256:
@@ -747,22 +761,22 @@ class RepoKey(ID_HMAC_SHA_256, AESKeyBase, FlexiKey):
747761
CIPHERSUITE = AES256_CTR_HMAC_SHA256
748762

749763

750-
class Blake2KeyfileKey(ID_BLAKE2b_256, AESKeyBase, FlexiKey):
751-
TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO}
752-
TYPE = KeyType.BLAKE2KEYFILE
753-
NAME = "key file BLAKE2b"
754-
ARG_NAME = "keyfile-blake2"
764+
class Blake2KeyfileKeyLegacy(ID_BLAKE2b_256_legacy, AESKeyBase, FlexiKey):
765+
TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO} # ???
766+
TYPE = KeyType.BLAKE2KEYFILE # ???
767+
NAME = "key file BLAKE2b (legacy)"
768+
ARG_NAME = "keyfile-blake2-legacy"
755769
STORAGE = KeyBlobStorage.KEYFILE
756-
CIPHERSUITE = AES256_CTR_BLAKE2b
770+
CIPHERSUITE = AES256_CTR_BLAKE2b_legacy
757771

758772

759-
class Blake2RepoKey(ID_BLAKE2b_256, AESKeyBase, FlexiKey):
760-
TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO}
761-
TYPE = KeyType.BLAKE2REPO
762-
NAME = "repokey BLAKE2b"
763-
ARG_NAME = "repokey-blake2"
773+
class Blake2RepoKeyLegacy(ID_BLAKE2b_256_legacy, AESKeyBase, FlexiKey):
774+
TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO} # ???
775+
TYPE = KeyType.BLAKE2REPO # ???
776+
NAME = "repokey BLAKE2b (legacy)"
777+
ARG_NAME = "repokey-blake2-legacy"
764778
STORAGE = KeyBlobStorage.REPO
765-
CIPHERSUITE = AES256_CTR_BLAKE2b
779+
CIPHERSUITE = AES256_CTR_BLAKE2b_legacy
766780

767781

768782
class AuthenticatedKeyBase(AESKeyBase, FlexiKey):
@@ -811,16 +825,23 @@ class AuthenticatedKey(ID_HMAC_SHA_256, AuthenticatedKeyBase):
811825
ARG_NAME = "authenticated"
812826

813827

828+
class Blake2AuthenticatedKeyLegacy(ID_BLAKE2b_256_legacy, AuthenticatedKeyBase):
829+
TYPE = KeyType.BLAKE2AUTHENTICATEDLEGACY
830+
TYPES_ACCEPTABLE = {TYPE}
831+
NAME = "authenticated BLAKE2b (legacy)"
832+
ARG_NAME = "authenticated-blake2-legacy"
833+
834+
835+
# ------------ new crypto ------------
836+
837+
814838
class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase):
815839
TYPE = KeyType.BLAKE2AUTHENTICATED
816840
TYPES_ACCEPTABLE = {TYPE}
817841
NAME = "authenticated BLAKE2b"
818842
ARG_NAME = "authenticated-blake2"
819843

820844

821-
# ------------ new crypto ------------
822-
823-
824845
class AEADKeyBase(KeyBase):
825846
"""
826847
Chunks are encrypted and authenticated using some AEAD ciphersuite
@@ -1003,24 +1024,25 @@ class Blake2CHPORepoKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
10031024

10041025

10051026
LEGACY_KEY_TYPES = (
1006-
# legacy (AES-CTR based) crypto
1027+
# legacy (AES-CTR or Blake2_legacy based) crypto
10071028
KeyfileKey,
10081029
RepoKey,
1009-
Blake2KeyfileKey,
1010-
Blake2RepoKey,
1030+
Blake2KeyfileKeyLegacy,
1031+
Blake2RepoKeyLegacy,
1032+
Blake2AuthenticatedKeyLegacy,
10111033
)
10121034

10131035
AVAILABLE_KEY_TYPES = (
10141036
# these are available encryption modes for new repositories
10151037
# not encrypted modes
10161038
PlaintextKey,
10171039
AuthenticatedKey,
1018-
Blake2AuthenticatedKey,
10191040
# new crypto
10201041
AESOCBKeyfileKey,
10211042
AESOCBRepoKey,
10221043
CHPOKeyfileKey,
10231044
CHPORepoKey,
1045+
Blake2AuthenticatedKey,
10241046
Blake2AESOCBKeyfileKey,
10251047
Blake2AESOCBRepoKey,
10261048
Blake2CHPOKeyfileKey,

src/borg/crypto/low_level.pyx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ cdef class AES256_CTR_HMAC_SHA256(AES256_CTR_BASE):
368368
raise IntegrityError('MAC Authentication failed')
369369

370370

371-
cdef class AES256_CTR_BLAKE2b(AES256_CTR_BASE):
371+
cdef class AES256_CTR_BLAKE2b_legacy(AES256_CTR_BASE):
372372
cdef unsigned char mac_key[128]
373373

374374
def __init__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):
@@ -712,8 +712,13 @@ def hmac_sha256(key, data):
712712
return hmac.digest(key, data, 'sha256')
713713

714714

715-
def blake2b_256(key, data):
716-
return hashlib.blake2b(key+data, digest_size=32).digest()
715+
def blake2b_256(key, data, legacy=False):
716+
if legacy:
717+
assert len(key) in (0, 128) # borg 1.x 64B key + 64B zero padding (b"" used by tests)
718+
return hashlib.blake2b(key+data, digest_size=32).digest()
719+
else:
720+
assert len(key) == 64 # borg 2.x 64B key
721+
return hashlib.blake2b(data, key=key, digest_size=32).digest()
717722

718723

719724
def blake2b_128(data):

src/borg/testsuite/crypto/key_test.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
import pytest
66

7-
from ...crypto.key import PlaintextKey, AuthenticatedKey, Blake2AuthenticatedKey
8-
from ...crypto.key import RepoKey, KeyfileKey, Blake2RepoKey, Blake2KeyfileKey
7+
from ...crypto.key import PlaintextKey, AuthenticatedKey, Blake2AuthenticatedKeyLegacy
8+
from ...crypto.key import RepoKey, KeyfileKey, Blake2RepoKeyLegacy, Blake2KeyfileKeyLegacy
99
from ...crypto.key import AEADKeyBase
1010
from ...crypto.key import AESOCBRepoKey, AESOCBKeyfileKey, CHPORepoKey, CHPOKeyfileKey
1111
from ...crypto.key import Blake2AESOCBRepoKey, Blake2AESOCBKeyfileKey, Blake2CHPORepoKey, Blake2CHPOKeyfileKey
12-
from ...crypto.key import ID_HMAC_SHA_256, ID_BLAKE2b_256
12+
from ...crypto.key import Blake2AuthenticatedKey
13+
from ...crypto.key import ID_HMAC_SHA_256, ID_BLAKE2b_256, ID_BLAKE2b_256_legacy
1314
from ...crypto.key import UnsupportedManifestError, UnsupportedKeyFormatError
1415
from ...crypto.key import identify_key
1516
from ...crypto.low_level import IntegrityError as IntegrityErrorBase
@@ -40,7 +41,7 @@ class MockArgs:
4041
)
4142
keyfile2_id = hex_to_bin("c3fbf14bc001ebcc3cd86e696c13482ed071740927cd7cbe1b01b4bfcee49314")
4243

43-
keyfile_blake2_key_file = """
44+
keyfile_blake2_key_file_legacy = """
4445
BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000
4546
hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaAZ7VCsTjbLhC1ipXOyhcGn7YnROEhP24UQvOCi
4647
Oar1G+JpwgO9BIYaiCODUpzPuDQEm6WxyTwEneJ3wsuyeqyh7ru2xo9FAUKRf6jcqqZnan
@@ -54,17 +55,17 @@ class MockArgs:
5455
UTHFJg343jqml0ZXJhdGlvbnPOAAGGoKRzYWx02gAgz3YaUZZ/s+UWywj97EY5b4KhtJYi
5556
qkPqtDDxs2j/T7+ndmVyc2lvbgE=""".strip()
5657

57-
keyfile_blake2_cdata = hex_to_bin(
58+
keyfile_blake2_cdata_legacy = hex_to_bin(
5859
"04d6040f5ef80e0a8ac92badcbe3dee83b7a6b53d5c9a58c4eed14964cb10ef591040404040404040d1e65cc1f435027"
5960
)
6061
# Verified against b2sum. Entire string passed to BLAKE2, including the padded 64 byte key contained in
61-
# keyfile_blake2_key_file above is
62+
# keyfile_blake2_key_file_legacy above is
6263
# 19280471de95185ec27ecb6fc9edbb4f4db26974c315ede1cd505fab4250ce7cd0d081ea66946c
6364
# 95f0db934d5f616921efbd869257e8ded2bd9bd93d7f07b1a30000000000000000000000000000
6465
# 000000000000000000000000000000000000000000000000000000000000000000000000000000
6566
# 00000000000000000000007061796c6f6164
6667
# p a y l o a d
67-
keyfile_blake2_id = hex_to_bin("d8bc68e961c79f99be39061589e5179b2113cd9226e07b08ddd4a1fef7ce93fb")
68+
keyfile_blake2_id_legacy = hex_to_bin("d8bc68e961c79f99be39061589e5179b2113cd9226e07b08ddd4a1fef7ce93fb")
6869

6970
@pytest.fixture
7071
def keys_dir(self, request, monkeypatch, tmpdir):
@@ -76,13 +77,14 @@ def keys_dir(self, request, monkeypatch, tmpdir):
7677
# not encrypted
7778
PlaintextKey,
7879
AuthenticatedKey,
79-
Blake2AuthenticatedKey,
8080
# legacy crypto
81+
Blake2AuthenticatedKeyLegacy,
8182
KeyfileKey,
82-
Blake2KeyfileKey,
83+
Blake2KeyfileKeyLegacy,
8384
RepoKey,
84-
Blake2RepoKey,
85+
Blake2RepoKeyLegacy,
8586
# new crypto
87+
Blake2AuthenticatedKey,
8688
AESOCBKeyfileKey,
8789
AESOCBRepoKey,
8890
Blake2AESOCBKeyfileKey,
@@ -176,12 +178,12 @@ def test_keyfile2_kfenv(self, tmpdir, monkeypatch):
176178
key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
177179
assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata) == b"payload"
178180

179-
def test_keyfile_blake2(self, monkeypatch, keys_dir):
181+
def test_keyfile_blake2_legacy(self, monkeypatch, keys_dir):
180182
with keys_dir.join("keyfile").open("w") as fd:
181-
fd.write(self.keyfile_blake2_key_file)
183+
fd.write(self.keyfile_blake2_key_file_legacy)
182184
monkeypatch.setenv("BORG_PASSPHRASE", "passphrase")
183-
key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata)
184-
assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata) == b"payload"
185+
key = Blake2KeyfileKeyLegacy.detect(self.MockRepository(), self.keyfile_blake2_cdata_legacy)
186+
assert key.decrypt(self.keyfile_blake2_id_legacy, self.keyfile_blake2_cdata_legacy) == b"payload"
185187

186188
def _corrupt_byte(self, key, data, offset):
187189
data = bytearray(data)
@@ -243,17 +245,28 @@ def test_authenticated_encrypt(self, monkeypatch):
243245
# 0x07 is the key TYPE.
244246
assert authenticated == b"\x07" + plaintext
245247

246-
def test_blake2_authenticated_encrypt(self, monkeypatch):
248+
def test_blake2_authenticated_encrypt_legacy(self, monkeypatch):
247249
monkeypatch.setenv("BORG_PASSPHRASE", "test")
248-
key = Blake2AuthenticatedKey.create(self.MockRepository(), self.MockArgs())
249-
assert Blake2AuthenticatedKey.id_hash is ID_BLAKE2b_256.id_hash
250+
key = Blake2AuthenticatedKeyLegacy.create(self.MockRepository(), self.MockArgs())
251+
assert Blake2AuthenticatedKeyLegacy.id_hash is ID_BLAKE2b_256_legacy.id_hash
250252
assert len(key.id_key) == 128
251253
plaintext = b"123456789"
252254
id = key.id_hash(plaintext)
253255
authenticated = key.encrypt(id, plaintext)
254256
# 0x06 is the key TYPE.
255257
assert authenticated == b"\x06" + plaintext
256258

259+
def test_blake2_authenticated_encrypt(self, monkeypatch):
260+
monkeypatch.setenv("BORG_PASSPHRASE", "test")
261+
key = Blake2AuthenticatedKey.create(self.MockRepository(), self.MockArgs())
262+
assert Blake2AuthenticatedKey.id_hash is ID_BLAKE2b_256.id_hash
263+
assert len(key.id_key) == 64
264+
plaintext = b"123456789"
265+
id = key.id_hash(plaintext)
266+
authenticated = key.encrypt(id, plaintext)
267+
# 0x51 is the key TYPE.
268+
assert authenticated == b"\x51" + plaintext
269+
257270

258271
class TestTAM:
259272
@pytest.fixture

0 commit comments

Comments
 (0)