Repository: borgbackup/borg

Issue:
switch argon2 keys to chacha20-poly1305 cipher
recently we had chosen to use aes-ctr-hmac-sha256 ciphersuite for the new argon2 keys.

the reasoning for this was:

1. be able to get rid of the low_level.AES class long term
2. we could not use AES-OCB or CHACHA20-POLY1305 because LibreSSL does not have that and we needed LibreSSL on OpenBSD.

now, as we moved to building borg with OpenSSL even on OpenBSD, we better do:

1. still applies
2. with OpenSSL, we can use CHACHA20-POLY1305 now! this is better than the AES-CTR based stuff and also we need it long-term anyway because we'll encrypt backup data chunks with it.
3. if we use CH-PO, we do not need AES-CTR any more for the borg key (and even better: we maybe could get completely rid of it long term)


Discussion:

Patch:
diff --git a/src/borg/constants.py b/src/borg/constants.py
index 208991ee..0b2ef16a 100644
--- a/src/borg/constants.py
+++ b/src/borg/constants.py
@@ -112,8 +112,8 @@
 KEY_ALGORITHMS = {
     # encrypt-and-MAC, kdf: PBKDF2(HMAC−SHA256), encryption: AES256-CTR, authentication: HMAC-SHA256
     'pbkdf2': 'sha256',
-    # encrypt-then-MAC, kdf: argon2, encryption: AES256-CTR, authentication: HMAC-SHA256
-    'argon2': 'argon2 aes256-ctr hmac-sha256',
+    # encrypt-then-MAC, kdf: argon2, encryption: chacha20, authentication: poly1305
+    'argon2': 'argon2 chacha20-poly1305',
 }
 
 
diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py
index 879f85c8..ad4cab9d 100644
--- a/src/borg/crypto/key.py
+++ b/src/borg/crypto/key.py
@@ -445,7 +445,7 @@ def decrypt_key_file(self, data, passphrase):
             self._encrypted_key_algorithm = encrypted_key.algorithm
             if encrypted_key.algorithm == 'sha256':
                 return self.decrypt_key_file_pbkdf2(encrypted_key, passphrase)
-            elif encrypted_key.algorithm == 'argon2 aes256-ctr hmac-sha256':
+            elif encrypted_key.algorithm == 'argon2 chacha20-poly1305':
                 return self.decrypt_key_file_argon2(encrypted_key, passphrase)
             else:
                 raise UnsupportedKeyFormatError()
@@ -497,19 +497,14 @@ def decrypt_key_file_pbkdf2(self, encrypted_key, passphrase):
     def decrypt_key_file_argon2(self, encrypted_key, passphrase):
         key = self.argon2(
             passphrase,
-            output_len_in_bytes=64,
+            output_len_in_bytes=32,
             salt=encrypted_key.salt,
             time_cost=encrypted_key.argon2_time_cost,
             memory_cost=encrypted_key.argon2_memory_cost,
             parallelism=encrypted_key.argon2_parallelism,
             type=encrypted_key.argon2_type,
         )
-        enc_key, mac_key = key[:32], key[32:]
-        ae_cipher = AES256_CTR_HMAC_SHA256(
-            iv=0, header_len=0, aad_offset=0,
-            enc_key=enc_key,
-            mac_key=mac_key,
-        )
+        ae_cipher = CHACHA20_POLY1305(key=key, iv=0, header_len=0, aad_offset=0)
         try:
             return ae_cipher.decrypt(encrypted_key.data)
         except low_level.IntegrityError:
@@ -518,7 +513,7 @@ def decrypt_key_file_argon2(self, encrypted_key, passphrase):
     def encrypt_key_file(self, data, passphrase, algorithm):
         if algorithm == 'sha256':
             return self.encrypt_key_file_pbkdf2(data, passphrase)
-        elif algorithm == 'argon2 aes256-ctr hmac-sha256':
+        elif algorithm == 'argon2 chacha20-poly1305':
             return self.encrypt_key_file_argon2(data, passphrase)
         else:
             raise ValueError(f'Unexpected algorithm: {algorithm}')
@@ -543,19 +538,14 @@ def encrypt_key_file_argon2(self, data, passphrase):
         salt = os.urandom(ARGON2_SALT_BYTES)
         key = self.argon2(
             passphrase,
-            output_len_in_bytes=64,
+            output_len_in_bytes=32,
             salt=salt,
             **ARGON2_ARGS,
         )
-        enc_key, mac_key = key[:32], key[32:]
-        ae_cipher = AES256_CTR_HMAC_SHA256(
-            iv=0, header_len=0, aad_offset=0,
-            enc_key=enc_key,
-            mac_key=mac_key,
-        )
+        ae_cipher = CHACHA20_POLY1305(key=key, iv=0, header_len=0, aad_offset=0)
         encrypted_key = EncryptedKey(
             version=1,
-            algorithm='argon2 aes256-ctr hmac-sha256',
+            algorithm='argon2 chacha20-poly1305',
             salt=salt,
             data=ae_cipher.encrypt(data),
             **{'argon2_' + k: v for k, v in ARGON2_ARGS.items()},


Test patch:
diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py
index 76d615f6..445ebac2 100644
--- a/src/borg/testsuite/archiver.py
+++ b/src/borg/testsuite/archiver.py
@@ -3600,7 +3600,7 @@ def test_init_defaults_to_argon2(self):
         self.cmd('init', '--encryption=repokey', self.repository_location)
         with Repository(self.repository_path) as repository:
             key = msgpack.unpackb(a2b_base64(repository.load_key()))
-        assert key[b'algorithm'] == b'argon2 aes256-ctr hmac-sha256'
+        assert key[b'algorithm'] == b'argon2 chacha20-poly1305'
 
     def test_init_with_explicit_key_algorithm(self):
         """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401"""
@@ -3620,7 +3620,7 @@ def verify_change_passphrase_does_not_change_algorithm(self, given_algorithm, ex
             assert key[b'algorithm'] == expected_algorithm
 
     def test_change_passphrase_does_not_change_algorithm_argon2(self):
-        self.verify_change_passphrase_does_not_change_algorithm('argon2', b'argon2 aes256-ctr hmac-sha256')
+        self.verify_change_passphrase_does_not_change_algorithm('argon2', b'argon2 chacha20-poly1305')
 
     def test_change_passphrase_does_not_change_algorithm_pbkdf2(self):
         self.verify_change_passphrase_does_not_change_algorithm('pbkdf2', b'sha256')
@@ -3635,7 +3635,7 @@ def verify_change_location_does_not_change_algorithm(self, given_algorithm, expe
             assert key[b'algorithm'] == expected_algorithm
 
     def test_change_location_does_not_change_algorithm_argon2(self):
-        self.verify_change_location_does_not_change_algorithm('argon2', b'argon2 aes256-ctr hmac-sha256')
+        self.verify_change_location_does_not_change_algorithm('argon2', b'argon2 chacha20-poly1305')
 
     def test_change_location_does_not_change_algorithm_pbkdf2(self):
         self.verify_change_location_does_not_change_algorithm('pbkdf2', b'sha256')
@@ -3647,7 +3647,7 @@ def test_key_change_algorithm(self):
 
         with Repository(self.repository_path) as repository:
             _, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)
-        assert key._encrypted_key_algorithm == 'argon2 aes256-ctr hmac-sha256'
+        assert key._encrypted_key_algorithm == 'argon2 chacha20-poly1305'
         self.cmd('info', self.repository_location)
 
 
diff --git a/src/borg/testsuite/crypto.py b/src/borg/testsuite/crypto.py
index 570afbc1..dac3efeb 100644
--- a/src/borg/testsuite/crypto.py
+++ b/src/borg/testsuite/crypto.py
@@ -247,15 +247,11 @@ def test_hkdf_hmac_sha512_5(self):
         assert okm == bytes.fromhex('1407d46013d98bc6decefcfee55f0f90b0c7f63d68eb1a80eaf07e953cfc0a3a5240a155d6e4daa965bb')
 
 
-def test_decrypt_key_file_argon2_aes256_ctr_hmac_sha256():
+def test_decrypt_key_file_argon2_chacha20_poly1305():
     plain = b'hello'
-    # echo -n "hello, pass phrase" | argon2 saltsaltsaltsalt -id -t 1 -k 8 -p 1 -l 64 -r
-    key = bytes.fromhex('d07cc7f9cfb483303e0b9fec176b2a9c559bb70c3a9fb0d5f9c0c23527cd09570212449f09f8cd28c1a41b73fa0098e889c3f2642e87c392e51f95d70d248d9d')
-    ae_cipher = AES256_CTR_HMAC_SHA256(
-        iv=0, header_len=0, aad_offset=0,
-        enc_key=key[:32],
-        mac_key=key[32:],
-    )
+    # echo -n "hello, pass phrase" | argon2 saltsaltsaltsalt -id -t 1 -k 8 -p 1 -l 32 -r
+    key = bytes.fromhex('a1b0cba145c154fbd8960996c5ce3428e9920cfe53c84ef08b4102a70832bcec')
+    ae_cipher = CHACHA20_POLY1305(key=key, iv=0, header_len=0, aad_offset=0)
 
     envelope = ae_cipher.encrypt(plain)
 
@@ -266,7 +262,7 @@ def test_decrypt_key_file_argon2_aes256_ctr_hmac_sha256():
         'argon2_memory_cost': 8,
         'argon2_parallelism': 1,
         'argon2_type': b'id',
-        'algorithm': 'argon2 aes256-ctr hmac-sha256',
+        'algorithm': 'argon2 chacha20-poly1305',
         'data': envelope,
     })
     key = KeyfileKey(None)