mirror of
https://github.com/borgbackup/borg.git
synced 2026-05-28 04:03:21 -04:00
key: add derive_key to derive new keys from existing key material.
Just a slight refactor of existing code to make it more useful for other key-generation purposes.
This commit is contained in:
parent
fb527051cb
commit
e86bae79c2
2 changed files with 82 additions and 4 deletions
|
|
@ -160,6 +160,12 @@ class KeyBase:
|
|||
# type is int
|
||||
chunk_seed: int = None
|
||||
|
||||
# crypt_key dummy, needs to be overwritten by subclass
|
||||
crypt_key: bytes = None
|
||||
|
||||
# id_key dummy, needs to be overwritten by subclass
|
||||
id_key: bytes = None
|
||||
|
||||
# Whether this *particular instance* is encrypted from a practical point of view,
|
||||
# i.e. when it's using encryption with a empty passphrase, then
|
||||
# that may be *technically* called encryption, but for all intents and purposes
|
||||
|
|
@ -196,6 +202,21 @@ class KeyBase:
|
|||
id_str = bin_to_hex(id) if id is not None else "(unknown)"
|
||||
raise IntegrityError(f"Chunk {id_str}: Invalid encryption envelope")
|
||||
|
||||
def derive_key(self, *, salt, domain, size, from_id_key=False):
|
||||
"""
|
||||
create a new crypto key (<size> bytes long) from existing key material, a given salt and domain.
|
||||
from_id_key == False: derive from self.crypt_key (default)
|
||||
from_id_key == True: derive from self.id_key (note: related repos have same ID key)
|
||||
"""
|
||||
from_key = self.id_key if from_id_key else self.crypt_key
|
||||
assert isinstance(from_key, bytes)
|
||||
assert isinstance(salt, bytes)
|
||||
assert isinstance(domain, bytes)
|
||||
assert size <= 32 # sha256 gives us 32 bytes
|
||||
# Because crypt_key is already a PRK, we do not need KDF security here, PRF security is good enough.
|
||||
# See https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Cr2.pdf section 4 "one-step KDF".
|
||||
return sha256(from_key + salt + domain).digest()[:size]
|
||||
|
||||
def pack_metadata(self, metadata_dict):
|
||||
metadata_dict = StableDict(metadata_dict)
|
||||
return msgpack.packb(metadata_dict)
|
||||
|
|
@ -229,6 +250,9 @@ class PlaintextKey(KeyBase):
|
|||
ARG_NAME = "none"
|
||||
|
||||
chunk_seed = 0
|
||||
crypt_key = b"" # makes .derive_key() work, nothing secret here
|
||||
id_key = b"" # makes .derive_key() work, nothing secret here
|
||||
|
||||
logically_encrypted = False
|
||||
|
||||
@classmethod
|
||||
|
|
@ -891,9 +915,7 @@ class AEADKeyBase(KeyBase):
|
|||
assert len(sessionid) == 24 # 192bit
|
||||
if domain is None:
|
||||
domain = b"borg-session-key-" + self.CIPHERSUITE.__name__.encode()
|
||||
# Because crypt_key is already a PRK, we do not need KDF security here, PRF security is good enough.
|
||||
# See https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Cr2.pdf section 4 "one-step KDF".
|
||||
return sha256(self.crypt_key + sessionid + domain).digest()
|
||||
return self.derive_key(salt=sessionid, domain=domain, size=32) # 256bit
|
||||
|
||||
def _get_cipher(self, sessionid, iv):
|
||||
assert isinstance(iv, int)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import unittest
|
|||
from ..crypto.low_level import AES256_CTR_HMAC_SHA256, AES256_OCB, CHACHA20_POLY1305, UNENCRYPTED, IntegrityError
|
||||
from ..crypto.low_level import bytes_to_long, bytes_to_int, long_to_bytes
|
||||
from ..crypto.low_level import AES, hmac_sha256
|
||||
from ..crypto.key import CHPOKeyfileKey, AESOCBRepoKey, FlexiKey
|
||||
from hashlib import sha256
|
||||
from ..crypto.key import CHPOKeyfileKey, AESOCBRepoKey, FlexiKey, KeyBase, PlaintextKey
|
||||
from ..helpers import msgpack, bin_to_hex
|
||||
|
||||
from . import BaseTestCase
|
||||
|
|
@ -276,3 +277,58 @@ def test_repo_key_detect_does_not_raise_integrity_error(getpass, monkeypatch):
|
|||
repository.load_key.return_value = repository.save_key.call_args.args[0]
|
||||
|
||||
AESOCBRepoKey.detect(repository, manifest_data=None)
|
||||
|
||||
|
||||
class TestDeriveKey(BaseTestCase):
|
||||
# Create a simple KeyBase subclass with a non-empty crypt_key
|
||||
class CustomKey(KeyBase):
|
||||
def __init__(self, crypt_key, id_key):
|
||||
self.crypt_key = crypt_key
|
||||
self.id_key = id_key
|
||||
|
||||
def test_derive_key_with_plaintext_key(self):
|
||||
"""Test derive_key with PlaintextKey (empty crypt_key)"""
|
||||
key = PlaintextKey(None)
|
||||
salt, domain, size = b"salt", b"domain", 16
|
||||
|
||||
# PlaintextKey has an empty crypt_key, so the derived key should be based on salt and domain only
|
||||
derived_key = key.derive_key(salt=salt, domain=domain, size=size)
|
||||
expected = sha256(b"" + salt + domain).digest()[:size]
|
||||
self.assert_equal(derived_key, expected)
|
||||
|
||||
def test_derive_key_with_custom_key(self):
|
||||
"""Test derive_key with a custom KeyBase subclass (non-empty crypt_key)"""
|
||||
crypt_key, id_key = b"test_crypt_key", b"test_id_key"
|
||||
key = self.CustomKey(crypt_key, id_key)
|
||||
salt, domain, size = b"salt", b"domain", 32
|
||||
|
||||
# derived key size and value as expected
|
||||
expected = sha256(crypt_key + salt + domain).digest()[:size]
|
||||
derived_key = key.derive_key(salt=salt, domain=domain, size=size)
|
||||
self.assert_equal(derived_key, expected)
|
||||
|
||||
# domain separation
|
||||
derived_key = key.derive_key(salt=salt, domain=b"other_domain", size=size)
|
||||
assert derived_key != expected
|
||||
assert len(derived_key) == size
|
||||
|
||||
# salt separation
|
||||
derived_key = key.derive_key(salt=b"other salt", domain=domain, size=size)
|
||||
assert derived_key != expected
|
||||
assert len(derived_key) == size
|
||||
|
||||
def test_derive_key_from_different_keys(self):
|
||||
"""Test derive_key with different key material"""
|
||||
crypt_key, id_key = b"test_crypt_key", b"test_id_key"
|
||||
key = self.CustomKey(crypt_key, id_key)
|
||||
salt, domain, size = b"salt", b"domain", 32
|
||||
|
||||
# derived key size and value as expected (using the ID key)
|
||||
expected = sha256(id_key + salt + domain).digest()[:size]
|
||||
derived_key = key.derive_key(salt=salt, domain=domain, size=size, from_id_key=True)
|
||||
self.assert_equal(derived_key, expected)
|
||||
|
||||
# generating different keys from crypt_key and id_key
|
||||
derived_key_from_id = key.derive_key(salt=salt, domain=domain, size=size, from_id_key=True)
|
||||
derived_key_from_crypt = key.derive_key(salt=salt, domain=domain, size=size, from_id_key=False)
|
||||
assert derived_key_from_id != derived_key_from_crypt
|
||||
|
|
|
|||
Loading…
Reference in a new issue