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.
This commit is contained in:
Syed Ali Ghazi Ejaz 2025-01-08 11:58:35 -05:00 committed by GitHub
parent 5b141e2077
commit 40df2f3c49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 75 additions and 14 deletions

View file

@ -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

View file

@ -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):

View file

@ -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",