Procházet zdrojové kódy

Merge pull request #8905 from ThomasWaldmann/derive-key

key: add derive_key to derive new keys from existing key material
TW před 6 dny
rodič
revize
3cf8d7cf2f
2 změnil soubory, kde provedl 82 přidání a 4 odebrání
  1. 25 3
      src/borg/crypto/key.py
  2. 57 1
      src/borg/testsuite/crypto_test.py

+ 25 - 3
src/borg/crypto/key.py

@@ -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)

+ 57 - 1
src/borg/testsuite/crypto_test.py

@@ -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