Skip to content

Commit

Permalink
Key: crypt_key instead of enc_key + enc_hmac_key, fixes borgbackup#6611
Browse files Browse the repository at this point in the history
  • Loading branch information
ThomasWaldmann committed Aug 1, 2022
1 parent 7fde480 commit 23ae35e
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 53 deletions.
11 changes: 1 addition & 10 deletions src/borg/archiver/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,7 @@ def do_change_location(self, args, repository, manifest, key, cache):
print("Change not needed or not supported.")
return EXIT_WARNING

for name in (
"repository_id",
"enc_key",
"enc_hmac_key",
"id_key",
"chunk_seed",
"tam_required",
"sessionid",
"cipher",
):
for name in ("repository_id", "crypt_key", "id_key", "chunk_seed", "tam_required", "sessionid", "cipher"):
value = getattr(key, name)
setattr(key_new, name, value)

Expand Down
56 changes: 22 additions & 34 deletions src/borg/crypto/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def assert_type(self, type_byte, id=None):

def _tam_key(self, salt, context):
return hkdf_hmac_sha512(
ikm=self.id_key + self.enc_key + self.enc_hmac_key,
ikm=self.id_key + self.crypt_key,
salt=salt,
info=b"borg-metadata-authentication-" + context,
output_length=64,
Expand Down Expand Up @@ -345,7 +345,9 @@ def id_hash(self, data):

def init_from_random_data(self):
super().init_from_random_data()
self.enc_hmac_key = random_blake2b_256_key()
enc_key = os.urandom(32)
enc_hmac_key = random_blake2b_256_key()
self.crypt_key = enc_key + enc_hmac_key
self.id_key = random_blake2b_256_key()


Expand Down Expand Up @@ -396,13 +398,11 @@ def decrypt(self, id, data, decompress=True):
self.assert_id(id, data)
return data

def init_from_given_data(self, *, enc_key, enc_hmac_key, id_key, chunk_seed):
assert len(enc_key) >= 32
assert len(enc_hmac_key) >= 32
assert len(id_key) >= 32
def init_from_given_data(self, *, crypt_key, id_key, chunk_seed):
assert len(crypt_key) in (32 + 32, 32 + 128)
assert len(id_key) in (32, 128)
assert isinstance(chunk_seed, int)
self.enc_key = enc_key
self.enc_hmac_key = enc_hmac_key
self.crypt_key = crypt_key
self.id_key = id_key
self.chunk_seed = chunk_seed

Expand All @@ -412,12 +412,11 @@ def init_from_random_data(self):
# Convert to signed int32
if chunk_seed & 0x80000000:
chunk_seed = chunk_seed - 0xFFFFFFFF - 1
self.init_from_given_data(
enc_key=data[0:32], enc_hmac_key=data[32:64], id_key=data[64:96], chunk_seed=chunk_seed
)
self.init_from_given_data(crypt_key=data[0:64], id_key=data[64:96], chunk_seed=chunk_seed)

def init_ciphers(self, manifest_data=None):
self.cipher = self.CIPHERSUITE(mac_key=self.enc_hmac_key, enc_key=self.enc_key, header_len=1, aad_offset=1)
enc_key, enc_hmac_key = self.crypt_key[0:32], self.crypt_key[32:]
self.cipher = self.CIPHERSUITE(mac_key=enc_hmac_key, enc_key=enc_key, header_len=1, aad_offset=1)
if manifest_data is None:
nonce = 0
else:
Expand Down Expand Up @@ -465,8 +464,7 @@ def _load(self, key_data, passphrase):
if key.version != 1:
raise IntegrityError("Invalid key file header")
self.repository_id = key.repository_id
self.enc_key = key.enc_key
self.enc_hmac_key = key.enc_hmac_key
self.crypt_key = key.crypt_key
self.id_key = key.id_key
self.chunk_seed = key.chunk_seed
self.tam_required = key.get("tam_required", tam_required(self.repository))
Expand Down Expand Up @@ -579,8 +577,7 @@ def _save(self, passphrase, algorithm):
key = Key(
version=1,
repository_id=self.repository_id,
enc_key=self.enc_key,
enc_hmac_key=self.enc_hmac_key,
crypt_key=self.crypt_key,
id_key=self.id_key,
chunk_seed=self.chunk_seed,
tam_required=self.tam_required,
Expand Down Expand Up @@ -608,16 +605,11 @@ def create(cls, repository, args, *, other_key=None):
raise Error("You must keep the same ID hash (HMAC-SHA256 or BLAKE2b) or deduplication will break.")
if other_key.copy_ae_key:
# give the user the option to use the same authenticated encryption (AE) key
enc_key = other_key.enc_key
enc_hmac_key = other_key.enc_hmac_key
crypt_key = other_key.crypt_key
else:
# borg transfer re-encrypts all data anyway, thus we can default to a new, random AE key
data = os.urandom(64)
enc_key = data[0:32]
enc_hmac_key = data[32:64]
key.init_from_given_data(
enc_key=enc_key, enc_hmac_key=enc_hmac_key, id_key=other_key.id_key, chunk_seed=other_key.chunk_seed
)
crypt_key = os.urandom(64)
key.init_from_given_data(crypt_key=crypt_key, id_key=other_key.id_key, chunk_seed=other_key.chunk_seed)
passphrase = other_key._passphrase
else:
key.init_from_random_data()
Expand Down Expand Up @@ -901,13 +893,11 @@ def decrypt(self, id, data, decompress=True):
# decrypting only succeeds if we got the ciphertext we wrote **for that chunk id**.
return data

def init_from_given_data(self, *, enc_key, enc_hmac_key, id_key, chunk_seed):
assert len(enc_key) >= 32
assert len(enc_hmac_key) >= 32
assert len(id_key) >= 32
def init_from_given_data(self, *, crypt_key, id_key, chunk_seed):
assert len(crypt_key) in (32 + 32, 32 + 128)
assert len(id_key) in (32, 128)
assert isinstance(chunk_seed, int)
self.enc_key = enc_key
self.enc_hmac_key = enc_hmac_key
self.crypt_key = crypt_key
self.id_key = id_key
self.chunk_seed = chunk_seed

Expand All @@ -917,14 +907,12 @@ def init_from_random_data(self):
# Convert to signed int32
if chunk_seed & 0x80000000:
chunk_seed = chunk_seed - 0xFFFFFFFF - 1
self.init_from_given_data(
enc_key=data[0:32], enc_hmac_key=data[32:64], id_key=data[64:96], chunk_seed=chunk_seed
)
self.init_from_given_data(crypt_key=data[0:64], id_key=data[64:96], chunk_seed=chunk_seed)

def _get_session_key(self, sessionid):
assert len(sessionid) == 24 # 192bit
key = hkdf_hmac_sha512(
ikm=self.enc_key + self.enc_hmac_key,
ikm=self.crypt_key,
salt=sessionid,
info=b"borg-session-key-" + self.CIPHERSUITE.__name__.encode(),
output_length=32,
Expand Down
25 changes: 20 additions & 5 deletions src/borg/item.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -439,28 +439,43 @@ class Key(PropDict):
If a Key shall be serialized, give as_dict() method output to msgpack packer.
"""

VALID_KEYS = {'version', 'repository_id', 'enc_key', 'enc_hmac_key', 'id_key', 'chunk_seed', 'tam_required'} # str-typed keys
VALID_KEYS = {'version', 'repository_id', 'crypt_key', 'id_key', 'chunk_seed', 'tam_required'} # str-typed keys

__slots__ = ("_dict", ) # avoid setting attributes not supported by properties

version = PropDict._make_property('version', int)
repository_id = PropDict._make_property('repository_id', bytes)
enc_key = PropDict._make_property('enc_key', bytes)
enc_hmac_key = PropDict._make_property('enc_hmac_key', bytes)
crypt_key = PropDict._make_property('crypt_key', bytes)
id_key = PropDict._make_property('id_key', bytes)
chunk_seed = PropDict._make_property('chunk_seed', int)
tam_required = PropDict._make_property('tam_required', bool)

@property
def enc_key(self):
k = self.crypt_key[0:32] # for legacy code, 256bit AES key
assert len(k) == 32
return k

@property
def enc_hmac_key(self):
k = self.crypt_key[32:] # for legacy code, 256bit HMAC-SHA256 or 1024bit blake2b key
assert len(k) in (32, 128)
return k

def update_internal(self, d):
# legacy support for migration (data from old msgpacks comes in as bytes always, but sometimes we want str)
for k, v in list(d.items()):
k = fix_key(d, k)
if k == 'version':
assert isinstance(v, int)
if k in ('repository_id', 'enc_key', 'enc_hmac_key', 'id_key'):
if k in ('repository_id', 'crypt_key', 'id_key'):
v = fix_bytes_value(d, k)
self._dict[k] = v

if 'crypt_key' not in self._dict: # legacy, we're loading an old key
k = fix_bytes_value(d, 'enc_key') + fix_bytes_value(d, 'enc_hmac_key')
assert isinstance(k, bytes), "k == %r" % k
assert len(k) >= 32 + 32 # 256+256 or 256+1024 bits
self._dict['crypt_key'] = k

class ArchiveItem(PropDict):
"""
Expand Down
4 changes: 2 additions & 2 deletions src/borg/testsuite/archiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -3347,7 +3347,7 @@ def test_key_export_repokey(self):
backup_key = AESOCBKeyfileKey(key.TestKey.MockRepository())
backup_key.load(export_file, Passphrase.env_passphrase())

assert repo_key.enc_key == backup_key.enc_key
assert repo_key.crypt_key == backup_key.crypt_key

with Repository(self.repository_path) as repository:
repository.save_key(b"")
Expand All @@ -3358,7 +3358,7 @@ def test_key_export_repokey(self):
repo_key2 = AESOCBRepoKey(repository)
repo_key2.load(None, Passphrase.env_passphrase())

assert repo_key2.enc_key == repo_key2.enc_key
assert repo_key2.crypt_key == repo_key2.crypt_key

def test_key_export_qr(self):
export_file = self.output_path + "/exported.html"
Expand Down
4 changes: 2 additions & 2 deletions src/borg/testsuite/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def test_keyfile(self, monkeypatch, keys_dir):
key2 = KeyfileKey.detect(self.MockRepository(), manifest)
assert key2.cipher.next_iv() >= iv + key2.cipher.block_count(len(manifest) - KeyfileKey.PAYLOAD_OVERHEAD)
# Key data sanity check
assert len({key2.id_key, key2.enc_key, key2.enc_hmac_key}) == 3
assert len({key2.id_key, key2.crypt_key}) == 2
assert key2.chunk_seed != 0
chunk = b"foo"
id = key.id_hash(chunk)
Expand Down Expand Up @@ -414,7 +414,7 @@ def test_decrypt_key_file_v2_is_unsupported():

def test_key_file_roundtrip(monkeypatch):
def to_dict(key):
extract = "repository_id", "enc_key", "enc_hmac_key", "id_key", "chunk_seed"
extract = "repository_id", "crypt_key", "id_key", "chunk_seed"
return {a: getattr(key, a) for a in extract}

repository = MagicMock(id=b"repository_id")
Expand Down

0 comments on commit 23ae35e

Please sign in to comment.