Skip to content

Commit

Permalink
Enhance passphrase handling (Fixes #8496) (#8605)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alighazi288 authored Jan 8, 2025
1 parent 5b141e2 commit 40df2f3
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 14 deletions.
2 changes: 2 additions & 0 deletions src/borg/crypto/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 34 additions & 14 deletions src/borg/helpers/passphrase.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import shlex
import subprocess
import sys
import textwrap

from . import bin_to_hex
from . import Error
Expand Down Expand Up @@ -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):
Expand Down
39 changes: 39 additions & 0 deletions src/borg/testsuite/helpers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 40df2f3

Please sign in to comment.