浏览代码

crypto: AEAD key classes

also:

cleanup class structure: less inheritance, more mixins.

define type bytes using the 4:4 split

upper 4 bits are ciphersuite:
0 == legacy AES-CTR based stuff
1+ == new AEAD stuff

lower 4 bits are keytype:
legacy: a bit mixed up, as it was...
new stuff: 0=keyfile 1=repokey, ...
Thomas Waldmann 3 年之前
父节点
当前提交
0f6f278b0f
共有 2 个文件被更改,包括 164 次插入16 次删除
  1. 12 0
      src/borg/constants.py
  2. 152 16
      src/borg/crypto/key.py

+ 12 - 0
src/borg/constants.py

@@ -111,6 +111,8 @@ class KeyBlobStorage:
 
 
 class KeyType:
+    # legacy crypto
+    # upper 4 bits are ciphersuite, 0 == legacy AES-CTR
     KEYFILE = 0x00
     # repos with PASSPHRASE mode could not be created any more since borg 1.0, see #97.
     # in borg 1.3 all of its code and also the "borg key migrate-to-repokey" command was removed.
@@ -123,6 +125,16 @@ class KeyType:
     BLAKE2REPO = 0x05
     BLAKE2AUTHENTICATED = 0x06
     AUTHENTICATED = 0x07
+    # new crypto
+    # upper 4 bits are ciphersuite, lower 4 bits are keytype
+    AESOCBKEYFILE = 0x10
+    AESOCBREPO = 0x11
+    CHPOKEYFILE = 0x20
+    CHPOREPO = 0x21
+    BLAKE2AESOCBKEYFILE = 0x30
+    BLAKE2AESOCBREPO = 0x31
+    BLAKE2CHPOKEYFILE = 0x40
+    BLAKE2CHPOREPO = 0x41
 
 
 REPOSITORY_README = """This is a Borg Backup repository.

+ 152 - 16
src/borg/crypto/key.py

@@ -23,7 +23,7 @@ from ..platform import SaveFile
 
 from .nonces import NonceManager
 from .low_level import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_cipher_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512
-from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b
+from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b, AES256_OCB, CHACHA20_POLY1305
 
 
 class UnsupportedPayloadError(Error):
@@ -336,7 +336,7 @@ class AESKeyBase(KeyBase):
 
     PAYLOAD_OVERHEAD = 1 + 32 + 8  # TYPE + HMAC + NONCE
 
-    CIPHERSUITE = AES256_CTR_HMAC_SHA256
+    CIPHERSUITE = None  # override in derived class
 
     logically_encrypted = True
 
@@ -383,7 +383,7 @@ class AESKeyBase(KeyBase):
         self.nonce_manager = NonceManager(self.repository, nonce)
 
 
-class FlexiKeyBase(AESKeyBase):
+class FlexiKeyBase:
     @classmethod
     def detect(cls, repository, manifest_data):
         key = cls(repository)
@@ -496,9 +496,7 @@ class FlexiKeyBase(AESKeyBase):
         raise NotImplementedError
 
 
-class FlexiKey(ID_HMAC_SHA_256, FlexiKeyBase):
-    TYPES_ACCEPTABLE = {KeyType.KEYFILE, KeyType.REPO, KeyType.PASSPHRASE}
-
+class FlexiKey(FlexiKeyBase):
     FILE_ID = 'BORG_KEY'
 
     def sanity_check(self, filename, id):
@@ -625,40 +623,43 @@ class FlexiKey(ID_HMAC_SHA_256, FlexiKeyBase):
             raise TypeError('Unsupported borg key storage type')
 
 
-class KeyfileKey(FlexiKey):
+class KeyfileKey(ID_HMAC_SHA_256, AESKeyBase, FlexiKey):
+    TYPES_ACCEPTABLE = {KeyType.KEYFILE, KeyType.REPO, KeyType.PASSPHRASE}
     TYPE = KeyType.KEYFILE
     NAME = 'key file'
     ARG_NAME = 'keyfile'
     STORAGE = KeyBlobStorage.KEYFILE
+    CIPHERSUITE = AES256_CTR_HMAC_SHA256
 
 
-class RepoKey(FlexiKey):
+class RepoKey(ID_HMAC_SHA_256, AESKeyBase, FlexiKey):
+    TYPES_ACCEPTABLE = {KeyType.KEYFILE, KeyType.REPO, KeyType.PASSPHRASE}
     TYPE = KeyType.REPO
     NAME = 'repokey'
     ARG_NAME = 'repokey'
     STORAGE = KeyBlobStorage.REPO
+    CIPHERSUITE = AES256_CTR_HMAC_SHA256
 
 
-class Blake2FlexiKey(ID_BLAKE2b_256, FlexiKey):
+class Blake2KeyfileKey(ID_BLAKE2b_256, AESKeyBase, FlexiKey):
     TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO}
-    CIPHERSUITE = AES256_CTR_BLAKE2b
-
-
-class Blake2KeyfileKey(Blake2FlexiKey):
     TYPE = KeyType.BLAKE2KEYFILE
     NAME = 'key file BLAKE2b'
     ARG_NAME = 'keyfile-blake2'
     STORAGE = KeyBlobStorage.KEYFILE
+    CIPHERSUITE = AES256_CTR_BLAKE2b
 
 
-class Blake2RepoKey(Blake2FlexiKey):
+class Blake2RepoKey(ID_BLAKE2b_256, AESKeyBase, FlexiKey):
+    TYPES_ACCEPTABLE = {KeyType.BLAKE2KEYFILE, KeyType.BLAKE2REPO}
     TYPE = KeyType.BLAKE2REPO
     NAME = 'repokey BLAKE2b'
     ARG_NAME = 'repokey-blake2'
     STORAGE = KeyBlobStorage.REPO
+    CIPHERSUITE = AES256_CTR_BLAKE2b
 
 
-class AuthenticatedKeyBase(FlexiKey):
+class AuthenticatedKeyBase(AESKeyBase, FlexiKey):
     STORAGE = KeyBlobStorage.REPO
 
     # It's only authenticated, not encrypted.
@@ -691,7 +692,7 @@ class AuthenticatedKeyBase(FlexiKey):
         return data
 
 
-class AuthenticatedKey(AuthenticatedKeyBase):
+class AuthenticatedKey(ID_HMAC_SHA_256, AuthenticatedKeyBase):
     TYPE = KeyType.AUTHENTICATED
     TYPES_ACCEPTABLE = {TYPE}
     NAME = 'authenticated'
@@ -705,8 +706,143 @@ class Blake2AuthenticatedKey(ID_BLAKE2b_256, AuthenticatedKeyBase):
     ARG_NAME = 'authenticated-blake2'
 
 
+# ------------ new crypto ------------
+
+
+class AEADKeyBase(KeyBase):
+    """
+    Chunks are encrypted and authenticated using some AEAD ciphersuite
+
+    Payload layout: TYPE(1) + SESSIONID(24) + NONCE(12) + MAC(16) + CIPHERTEXT
+                    ^------------- AAD ---------------^
+    """
+
+    PAYLOAD_OVERHEAD = 1 + 24 + 12 + 16  # TYPE + SESSIONID + NONCE + MAC
+
+    CIPHERSUITE = None  # override in subclass
+
+    logically_encrypted = True
+
+    def encrypt(self, chunk):
+        data = self.compressor.compress(chunk)
+        header = self.TYPE_STR + self.sessionid
+        iv = self.cipher.next_iv()
+        return self.cipher.encrypt(data, header=header, iv=iv)
+
+    def decrypt(self, id, data, decompress=True):
+        self.assert_type(data[0], id)
+        sessionid = data[1:13]  # XXX
+        self.init_ciphers(salt=salt, context=context, iv=iv)  # XXX
+        try:
+            payload = self.cipher.decrypt(data)
+        except IntegrityError as e:
+            raise IntegrityError(f"Chunk {bin_to_hex(id)}: Could not decrypt [{str(e)}]")
+        if not decompress:
+            return payload
+        data = self.decompress(payload)
+        self.assert_id(id, data)
+        return data
+
+    def init_from_random_data(self):
+        data = os.urandom(100)
+        self.enc_key = data[0:32]
+        self.enc_hmac_key = data[32:64]
+        self.id_key = data[64:96]
+        self.chunk_seed = bytes_to_int(data[96:100])
+        # Convert to signed int32
+        if self.chunk_seed & 0x80000000:
+            self.chunk_seed = self.chunk_seed - 0xffffffff - 1
+
+    def init_ciphers(self, salt=b'', context=b'', iv=0):
+        key = hkdf_hmac_sha512(
+            ikm=self.enc_key + self.enc_hmac_key,
+            salt=salt,
+            info=b'borg-crypto-' + context,  # XXX
+            output_length=32
+        )
+        self.cipher = self.CIPHERSUITE(key=key, header_len=1+24, aad_offset=0)  # XXX
+        self.cipher.set_iv(iv)
+
+
+class AESOCBKeyfileKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):
+    TYPES_ACCEPTABLE = {KeyType.AESOCBKEYFILE, KeyType.AESOCBREPO}
+    TYPE = KeyType.AESOCBKEYFILE
+    NAME = 'key file AES-OCB'
+    ARG_NAME = 'keyfile-aes-ocb'
+    STORAGE = KeyBlobStorage.KEYFILE
+    CIPHERSUITE = AES256_OCB
+
+
+class AESOCBRepoKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):
+    TYPES_ACCEPTABLE = {KeyType.AESOCBKEYFILE, KeyType.AESOCBREPO}
+    TYPE = KeyType.AESOCBREPO
+    NAME = 'repokey AES-OCB'
+    ARG_NAME = 'repokey-aes-ocb'
+    STORAGE = KeyBlobStorage.REPO
+    CIPHERSUITE = AES256_OCB
+
+
+class CHPOKeyfileKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):
+    TYPES_ACCEPTABLE = {KeyType.CHPOKEYFILE, KeyType.CHPOREPO}
+    TYPE = KeyType.CHPOKEYFILE
+    NAME = 'key file ChaCha20-Poly1305'
+    ARG_NAME = 'keyfile-chacha20-poly1305'
+    STORAGE = KeyBlobStorage.KEYFILE
+    CIPHERSUITE = CHACHA20_POLY1305
+
+
+class CHPORepoKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):
+    TYPES_ACCEPTABLE = {KeyType.CHPOKEYFILE, KeyType.CHPOREPO}
+    TYPE = KeyType.CHPOREPO
+    NAME = 'repokey ChaCha20-Poly1305'
+    ARG_NAME = 'repokey-chacha20-poly1305'
+    STORAGE = KeyBlobStorage.REPO
+    CIPHERSUITE = CHACHA20_POLY1305
+
+
+class Blake2AESOCBKeyfileKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
+    TYPES_ACCEPTABLE = {KeyType.BLAKE2AESOCBKEYFILE, KeyType.BLAKE2AESOCBREPO}
+    TYPE = KeyType.BLAKE2AESOCBKEYFILE
+    NAME = 'key file Blake2b AES-OCB'
+    ARG_NAME = 'keyfile-blake2-aes-ocb'
+    STORAGE = KeyBlobStorage.KEYFILE
+    CIPHERSUITE = AES256_OCB
+
+
+class Blake2AESOCBRepoKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
+    TYPES_ACCEPTABLE = {KeyType.BLAKE2AESOCBKEYFILE, KeyType.BLAKE2AESOCBREPO}
+    TYPE = KeyType.BLAKE2AESOCBREPO
+    NAME = 'repokey Blake2b AES-OCB'
+    ARG_NAME = 'repokey-blake2-aes-ocb'
+    STORAGE = KeyBlobStorage.REPO
+    CIPHERSUITE = AES256_OCB
+
+
+class Blake2CHPOKeyfileKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
+    TYPES_ACCEPTABLE = {KeyType.BLAKE2CHPOKEYFILE, KeyType.BLAKE2CHPOREPO}
+    TYPE = KeyType.BLAKE2CHPOKEYFILE
+    NAME = 'key file Blake2b ChaCha20-Poly1305'
+    ARG_NAME = 'keyfile-blake2-chacha20-poly1305'
+    STORAGE = KeyBlobStorage.KEYFILE
+    CIPHERSUITE = CHACHA20_POLY1305
+
+
+class Blake2CHPORepoKey(ID_BLAKE2b_256, AEADKeyBase, FlexiKey):
+    TYPES_ACCEPTABLE = {KeyType.BLAKE2CHPOKEYFILE, KeyType.BLAKE2CHPOREPO}
+    TYPE = KeyType.BLAKE2CHPOREPO
+    NAME = 'repokey Blake2b ChaCha20-Poly1305'
+    ARG_NAME = 'repokey-blake2-chacha20-poly1305'
+    STORAGE = KeyBlobStorage.REPO
+    CIPHERSUITE = CHACHA20_POLY1305
+
+
 AVAILABLE_KEY_TYPES = (
     PlaintextKey,
     KeyfileKey, RepoKey, AuthenticatedKey,
     Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey,
+    # new crypto
+    AESOCBKeyfileKey, AESOCBRepoKey,
+    CHPOKeyfileKey, CHPORepoKey,
+    Blake2AESOCBKeyfileKey, Blake2AESOCBRepoKey,
+    Blake2CHPOKeyfileKey, Blake2CHPORepoKey,
 )