Repository: borgbackup/borg
Pull request: Move the key derivation code from helpers.Passphrase to crypto.FlexiKey
Closes #6449

- [x] pbkdf2
- [x] argon2

@ThomasWaldmann, is this what you had in mind? If so, I'll migrate argon2 the same way.

Problem statement:
move kdf code
`borg.helpers.passphrase` module has a `Passphrase.kdf` method.

- that's crypto code, so it should rather be below `borg.crypto` package
- all the other code there is just rather simple input / output / env var processing

@hexagonrecursion ^^^

(master branch only)

Hints from issue discussion:

Commit message: Passphrase.kdf -> FlexiKey.pbkdf2

Patch to review:
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
index 318a8f20ef..54215799d8 100644
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -46,7 +46,7 @@
     from .constants import *  # NOQA
     from .compress import CompressionSpec
     from .crypto.key import key_creator, key_argument_names, tam_required_file, tam_required
-    from .crypto.key import RepoKey, KeyfileKey, Blake2RepoKey, Blake2KeyfileKey
+    from .crypto.key import RepoKey, KeyfileKey, Blake2RepoKey, Blake2KeyfileKey, FlexiKey
     from .crypto.keymanager import KeyManager
     from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE
     from .helpers import Error, NoManifestError, set_ec
@@ -626,7 +626,7 @@ def chunkit(chunker_name, *args, **kwargs):
         print("KDFs (slow is GOOD, use argon2!) ===============================")
         count = 5
         for spec, func in [
-            ("pbkdf2", lambda: Passphrase('mypassphrase').kdf(b'salt'*8, PBKDF2_ITERATIONS, 32)),
+            ("pbkdf2", lambda: FlexiKey.pbkdf2(Passphrase('mypassphrase'), b'salt'*8, PBKDF2_ITERATIONS, 32)),
             ("argon2", lambda: Passphrase('mypassphrase').argon2(64, b'S' * ARGON2_SALT_BYTES, **ARGON2_ARGS)),
         ]:
             print(f"{spec:<24} {count:<10} {timeit(func, number=count):.3f}s")
diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py
index d117b65832..bf5feb4499 100644
--- a/src/borg/crypto/key.py
+++ b/src/borg/crypto/key.py
@@ -3,7 +3,7 @@
 import os
 import textwrap
 from binascii import a2b_base64, b2a_base64, hexlify
-from hashlib import sha256
+from hashlib import sha256, pbkdf2_hmac
 
 from ..logger import create_logger
 
@@ -447,8 +447,14 @@ def decrypt_key_file(self, data, passphrase):
             else:
                 raise UnsupportedKeyFormatError()
 
+    @staticmethod
+    def pbkdf2(passphrase, salt, iterations, output_len_in_bytes):
+        if os.environ.get("BORG_TESTONLY_WEAKEN_KDF") == "1":
+            iterations = 1
+        return pbkdf2_hmac('sha256', passphrase.encode('utf-8'), salt, iterations, output_len_in_bytes)
+
     def decrypt_key_file_pbkdf2(self, encrypted_key, passphrase):
-        key = passphrase.kdf(encrypted_key.salt, encrypted_key.iterations, 32)
+        key = self.pbkdf2(passphrase, encrypted_key.salt, encrypted_key.iterations, 32)
         data = AES(key, b'\0'*16).decrypt(encrypted_key.data)
         if hmac.compare_digest(hmac_sha256(key, data), encrypted_key.hash):
             return data
@@ -485,7 +491,7 @@ def encrypt_key_file(self, data, passphrase, algorithm):
     def encrypt_key_file_pbkdf2(self, data, passphrase):
         salt = os.urandom(32)
         iterations = PBKDF2_ITERATIONS
-        key = passphrase.kdf(salt, iterations, 32)
+        key = self.pbkdf2(passphrase, salt, iterations, 32)
         hash = hmac_sha256(key, data)
         cdata = AES(key, b'\0'*16).encrypt(data)
         enc_key = EncryptedKey(
diff --git a/src/borg/helpers/passphrase.py b/src/borg/helpers/passphrase.py
index 52ecc11bb5..704d67cc66 100644
--- a/src/borg/helpers/passphrase.py
+++ b/src/borg/helpers/passphrase.py
@@ -3,7 +3,6 @@
 import shlex
 import subprocess
 import sys
-from hashlib import pbkdf2_hmac
 from typing import Literal
 
 from . import bin_to_hex
@@ -140,11 +139,6 @@ def new(cls, allow_empty=False):
     def __repr__(self):
         return '<Passphrase "***hidden***">'
 
-    def kdf(self, salt, iterations, length):
-        if os.environ.get("BORG_TESTONLY_WEAKEN_KDF") == "1":
-            iterations = 1
-        return pbkdf2_hmac('sha256', self.encode('utf-8'), salt, iterations, length)
-
     def argon2(
         self,
         output_len_in_bytes: int,
diff --git a/src/borg/testsuite/crypto.py b/src/borg/testsuite/crypto.py
index 3465bb8ff8..a907c910c9 100644
--- a/src/borg/testsuite/crypto.py
+++ b/src/borg/testsuite/crypto.py
@@ -10,7 +10,7 @@
 from ..crypto.low_level import bytes_to_long, bytes_to_int, long_to_bytes
 from ..crypto.low_level import hkdf_hmac_sha512
 from ..crypto.low_level import AES, hmac_sha256
-from ..crypto.key import KeyfileKey, UnsupportedKeyFormatError, RepoKey
+from ..crypto.key import KeyfileKey, UnsupportedKeyFormatError, RepoKey, FlexiKey
 from ..helpers.passphrase import Passphrase
 from ..helpers import msgpack
 from ..constants import KEY_ALGORITHMS
@@ -294,9 +294,8 @@ def test_decrypt_key_file_argon2_aes256_ctr_hmac_sha256(monkeypatch):
 def test_decrypt_key_file_pbkdf2_sha256_aes256_ctr_hmac_sha256(monkeypatch):
     plain = b'hello'
     salt = b'salt'*4
-    monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
-    passphrase = Passphrase.new()
-    key = passphrase.kdf(salt, iterations=1, length=32)
+    passphrase = Passphrase("hello, pass phrase")
+    key = FlexiKey.pbkdf2(passphrase, salt, 1, 32)
     hash = hmac_sha256(key, plain)
     data = AES(key, b'\0'*16).encrypt(plain)
     encrypted = msgpack.packb({


Code review comments:
diff_hunk: @@ -626,7 +626,7 @@ def chunkit(chunker_name, *args, **kwargs):
         print("KDFs (slow is GOOD, use argon2!) ===============================")
         count = 5
         for spec, func in [
-            ("pbkdf2", lambda: Passphrase('mypassphrase').kdf(b'salt'*8, PBKDF2_ITERATIONS, 32)),
+            ("pbkdf2", lambda: FlexiKey.pbkdf2(Passphrase('mypassphrase'), b'salt'*8, PBKDF2_ITERATIONS, 32)),
path: src/borg/archiver.py
text: @user1:
As Passphrase is just a subclass of `str` with some additional functionality (not relevant here), you can just give 'mypassphrase' directly.