diff --git a/src/borg/constants.py b/src/borg/constants.py index 6b9544a31..5fb6b9a65 100644 --- a/src/borg/constants.py +++ b/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. diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 89674e5ed..f17ed5b74 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -23,7 +23,7 @@ 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 @@ def init_ciphers(self, manifest_data=None): 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 @@ def get_new_target(self, args): 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 @@ def remove(self, target): 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 @@ def decrypt(self, id, data, decompress=True): 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, )