Browse Source

Enhance passphrase handling (Fixes #8496) (#8605)

Improve handling when defining a passphrase or debugging passphrase issues, fixes #8496

Setting `BORG_DEBUG_PASSPHRASE=YES` enables passphrase debug logging to stderr, showing passphrase, hex utf-8 byte sequence and related env vars if a wrong passphrase was encountered.

Setting `BORG_DISPLAY_PASSHRASE=YES` now always shows passphrase and its hex utf-8 byte sequence.
Syed Ali Ghazi Ejaz 4 months ago
parent
commit
40df2f3c49
3 changed files with 75 additions and 14 deletions
  1. 2 0
      src/borg/crypto/key.py
  2. 34 14
      src/borg/helpers/passphrase.py
  3. 39 0
      src/borg/testsuite/helpers_test.py

+ 2 - 0
src/borg/crypto/key.py

@@ -371,10 +371,12 @@ class FlexiKey:
                     passphrase = Passphrase.getpass(prompt)
                     if key.load(target, passphrase):
                         break
+                    Passphrase.display_debug_info(passphrase)
                 else:
                     raise PasswordRetriesExceeded
         else:
             if not key.load(target, passphrase):
+                Passphrase.display_debug_info(passphrase)
                 raise PassphraseWrong
         key.init_ciphers(manifest_data)
         key._passphrase = passphrase

+ 34 - 14
src/borg/helpers/passphrase.py

@@ -3,6 +3,7 @@ import os
 import shlex
 import subprocess
 import sys
+import textwrap
 
 from . import bin_to_hex
 from . import Error
@@ -109,20 +110,39 @@ class Passphrase(str):
             retry=True,
             env_var_override="BORG_DISPLAY_PASSPHRASE",
         ):
-            print('Your passphrase (between double-quotes): "%s"' % passphrase, file=sys.stderr)
-            print("Make sure the passphrase displayed above is exactly what you wanted.", file=sys.stderr)
-            try:
-                passphrase.encode("ascii")
-            except UnicodeEncodeError:
-                print(
-                    "Your passphrase (UTF-8 encoding in hex): %s" % bin_to_hex(passphrase.encode("utf-8")),
-                    file=sys.stderr,
-                )
-                print(
-                    "As you have a non-ASCII passphrase, it is recommended to keep the "
-                    "UTF-8 encoding in hex together with the passphrase at a safe place.",
-                    file=sys.stderr,
-                )
+            pw_msg = textwrap.dedent(
+                f"""\
+            Your passphrase (between double-quotes): "{passphrase}"
+            Make sure the passphrase displayed above is exactly what you wanted.
+            Your passphrase (UTF-8 encoding in hex): {bin_to_hex(passphrase.encode("utf-8"))}
+            It is recommended to keep the UTF-8 encoding in hex together with the passphrase at a safe place.
+            In case you should ever run into passphrase issues, it could sometimes help debugging them.
+            """
+            )
+            print(pw_msg, file=sys.stderr)
+
+    @staticmethod
+    def display_debug_info(passphrase):
+        def fmt_var(env_var):
+            env_var_value = os.environ.get(env_var)
+            if env_var_value is not None:
+                return f'{env_var} = "{env_var_value}"'
+            else:
+                return f"# {env_var} is not set"
+
+        if os.environ.get("BORG_DEBUG_PASSPHRASE") == "YES":
+            passphrase_info = textwrap.dedent(
+                f"""\
+                Incorrect passphrase!
+                Passphrase used (between double-quotes): "{passphrase}"
+                Same, UTF-8 encoded, in hex: {bin_to_hex(passphrase.encode('utf-8'))}
+                Relevant Environment Variables:
+                {fmt_var("BORG_PASSPHRASE")}
+                {fmt_var("BORG_PASSCOMMAND")}
+                {fmt_var("BORG_PASSPHRASE_FD")}
+                """
+            )
+            print(passphrase_info, file=sys.stderr)
 
     @classmethod
     def new(cls, allow_empty=False):

+ 39 - 0
src/borg/testsuite/helpers_test.py

@@ -1408,6 +1408,45 @@ class TestPassphrase:
     def test_passphrase_repr(self):
         assert "secret" not in repr(Passphrase("secret"))
 
+    def test_passphrase_wrong_debug(self, capsys, monkeypatch):
+        passphrase = "wrong_passphrase"
+        monkeypatch.setenv("BORG_DEBUG_PASSPHRASE", "YES")
+        monkeypatch.setenv("BORG_PASSPHRASE", "env_passphrase")
+        monkeypatch.setenv("BORG_PASSCOMMAND", "command")
+        monkeypatch.setenv("BORG_PASSPHRASE_FD", "fd_value")
+
+        Passphrase.display_debug_info(passphrase)
+
+        out, err = capsys.readouterr()
+        assert "Incorrect passphrase!" in err
+        assert passphrase in err
+        assert bin_to_hex(passphrase.encode("utf-8")) in err
+        assert 'BORG_PASSPHRASE = "env_passphrase"' in err
+        assert 'BORG_PASSCOMMAND = "command"' in err
+        assert 'BORG_PASSPHRASE_FD = "fd_value"' in err
+
+        monkeypatch.delenv("BORG_DEBUG_PASSPHRASE", raising=False)
+        Passphrase.display_debug_info(passphrase)
+        out, err = capsys.readouterr()
+
+        assert "Incorrect passphrase!" not in err
+        assert passphrase not in err
+
+    def test_verification(self, capsys, monkeypatch):
+        passphrase = "test_passphrase"
+        hex_value = passphrase.encode("utf-8").hex()
+
+        monkeypatch.setenv("BORG_DISPLAY_PASSPHRASE", "no")
+        Passphrase.verification(passphrase)
+        out, err = capsys.readouterr()
+        assert passphrase not in err
+
+        monkeypatch.setenv("BORG_DISPLAY_PASSPHRASE", "yes")
+        Passphrase.verification(passphrase)
+        out, err = capsys.readouterr()
+        assert passphrase in err
+        assert hex_value in err
+
 
 @pytest.mark.parametrize(
     "ec_range,ec_class",