From 40df2f3c49e98444364d96fde2c4c49bee81a998 Mon Sep 17 00:00:00 2001 From: Syed Ali Ghazi Ejaz <51366992+alighazi288@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:58:35 -0500 Subject: [PATCH] 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. --- src/borg/crypto/key.py | 2 ++ src/borg/helpers/passphrase.py | 48 +++++++++++++++++++++--------- src/borg/testsuite/helpers_test.py | 39 ++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 82ac416c7b..9f8f99f0b2 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 963e18369b..cb12ce5330 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): diff --git a/src/borg/testsuite/helpers_test.py b/src/borg/testsuite/helpers_test.py index 102ead54b5..0c0c91de7d 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",