Repository: borgbackup/borg

Issue:
add passphrase debugging option?
Sometimes people have issues when trying to access a borg repo from a new or newly installed host with borg telling them that the passphrase is incorrect.

Besides the obvious root cause of the passphrase really being incorrect, there are also:
- keyboard issues (kbd mapping, deadkey vs nodeadkey with accents in passphrases, ...)
- differences in unicode normalisation between platforms
- troubles correctly setting `BORG_PASSPHRASE`, `BORG_PASSCOMMAND`, ... env vars

`borg init` (borg 1.x)  and `borg repo-create` (borg 2) show the utf-8 hex bytes sequence of the encoded passphrase **IF** the passphrase is non-ascii.

We could:
- init: display the hex bytes in any case (the deadkey issue can also happen within ascii, e.g. if the original passphrase has `^a` and the wrong one happens to have `â`).
- add an option to display the password and/or hex bytes when the password is **USED** and is determined to be incorrect.
- show which env vars were used

Discussion:
alighazi288: @ThomasWaldmann Any suggestions for another issue I could work on next?
ThomasWaldmann: The docs CI failure seems unrelated to your PR changes.

Maybe some stuff in sphinx changed, so it is triggering new warnings.

Guess I need to change so that code unit tests do not depend on docs test.

alighazi288: @ThomasWaldmann  if this is ready to close, do you have any suggestions for another issue I could work on next?
ThomasWaldmann: I've fixed the docs issue in master.

Please update your local master branch, then rebase the PR branch onto master, then force push to github.

In the issue tracker, some stuff is labelled as easy, good first issue or help wanted - choose from these if you feel it is a good fit for you.

alighazi288: I realized I completely forgot to update the tests after changing my implementation—apologies for that oversight! I've reviewed and adjusted everything now, so the tests should align correctly with the updated implementation. Let me know if there's anything else you'd like me to refine.

Patch:
diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py
index 82ac416c..9f8f99f0 100644
--- a/src/borg/crypto/key.py
+++ b/src/borg/crypto/key.py
@@ -371,10 +371,12 @@ def detect(cls, repository, manifest_data):
                     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
diff --git a/src/borg/helpers/passphrase.py b/src/borg/helpers/passphrase.py
index 963e1836..cb12ce53 100644
--- a/src/borg/helpers/passphrase.py
+++ b/src/borg/helpers/passphrase.py
@@ -3,6 +3,7 @@
 import shlex
 import subprocess
 import sys
+import textwrap
 
 from . import bin_to_hex
 from . import Error
@@ -109,20 +110,39 @@ def verification(cls, passphrase):
             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):


Test patch:
diff --git a/src/borg/testsuite/helpers_test.py b/src/borg/testsuite/helpers_test.py
index 102ead54..0c0c91de 100644
--- a/src/borg/testsuite/helpers_test.py
+++ b/src/borg/testsuite/helpers_test.py
@@ -1408,6 +1408,45 @@ def test_passphrase_new_retries(self, monkeypatch):
     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",