Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/chapters/troubleshooting.texi
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ their root causes, and how to fix them.
* Safety Rejection:: A plan step is blocked by the safety engine.
* Invalid Plan:: Steps miss arguments their tools need.
* Index Built With Another Model:: EMBED_MODEL changed since indexing.
* Key Refused on a Slow Moment:: A right passphrase, refused while busy.
* Web Search Returns No Results:: The DuckDuckGo scraper finds nothing.
* Tests Cannot Import codebud:: The package is not on the path.
@end menu
Expand Down Expand Up @@ -316,6 +317,35 @@ codebud index --rebuild
@end example


@node Key Refused on a Slow Moment
@section Key Refused on a Slow Moment
@cindex passphrase refused
@cindex excessive work parameter

@example
codebud: this machine was slower just now than when the key was sealed,
and age refuses a passphrase it estimates would take too long to check
here; the passphrase may well be right -- try again, ideally with the
machine less busy
@end example

@noindent
@strong{What happened:} age, which seals your encryption key, sets the
passphrase's work factor (how long checking it takes) from the
machine's speed when the key was made, and on every unlock measures
the speed again and refuses a work factor far above it. On a busy
or throttled moment --- a heavy build, a laptop on battery --- a correct
passphrase can be refused. Codebud tries three times before saying
so, and never reports it as a wrong passphrase.

@strong{Fix:} try again when the machine is less busy. A key sealed on
a much faster machine and moved to a slow one may be refused every
time, and @command{codebud key passwd} cannot help, since it seals the
key again for the speed of the machine it runs on. Use the key on a
machine at least as fast as the one it was made on; or create a new key
on the slow machine with @command{codebud key init} --- data encrypted
to the old key stays readable only with the old key.

@node Web Search Returns No Results
@section Web Search Returns No Results
@cindex web search, no results
Expand Down
24 changes: 20 additions & 4 deletions src/codebud/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,31 @@ def recipient() -> str:
return (key_dir() / RECIPIENT_FILE).read_text().strip()


_SLOW_RETRIES = 3


def _open_identity(passphrase: str):
pyrage = _pyrage()
_require_key()
_harden_process() # the passphrase and the secret key are about to be in memory
sealed = (key_dir() / IDENTITY_FILE).read_bytes()
try:
secret = pyrage.passphrase.decrypt(sealed, passphrase)
except pyrage.DecryptError as exc:
raise CryptoError("wrong passphrase (or a damaged key file)") from exc
# age sets the passphrase's work factor from this machine's speed when
# sealing, and refuses one far above its speed when opening, measured
# again each time: on a busy or throttled moment a correct passphrase is
# refused. Try again a few times, and never call that a wrong passphrase.
for _attempt in range(_SLOW_RETRIES):
try:
secret = pyrage.passphrase.decrypt(sealed, passphrase)
break
except pyrage.DecryptError as exc:
if "excessive work" not in str(exc).lower():
raise CryptoError("wrong passphrase (or a damaged key file)") from exc
else:
raise CryptoError(
"this machine was slower just now than when the key was sealed, and age "
"refuses a passphrase it estimates would take too long to check here; the "
"passphrase may well be right -- try again, ideally with the machine less busy"
)
return pyrage.x25519.Identity.from_str(secret.decode())


Expand Down
61 changes: 61 additions & 0 deletions tests/test_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,64 @@ def test_while_unlocked_writes_use_the_key_not_the_file(unlocked):
(crypto.key_dir() / crypto.RECIPIENT_FILE).write_text(str(stranger.to_public()) + "\n")
sealed = crypto.encrypt(b"mine")
assert crypto.decrypt(sealed) == b"mine" # encrypted to my key, not the stranger's


# --- age refusing a correct passphrase on a slow moment ---------------------------


def _excessive_then(real, times):
"""pyrage.passphrase.decrypt that refuses ``times`` times the way age
does when the machine is slower than when the key was sealed, then
works."""
import pyrage

calls = []

def decrypt(data, passphrase):
calls.append(1)
if len(calls) <= times:
raise pyrage.DecryptError("Excessive work parameter for passphrase.")
return real(data, passphrase)

return decrypt, calls


def test_a_slow_moment_is_retried(monkeypatch):
"""age calibrates its passphrase work factor to the machine's speed when
sealing, and refuses one far above the current speed when opening: on a
loaded machine a correct passphrase was reported as wrong (seen on
macOS CI)."""
import pyrage

crypto.create_key("correct horse")
crypto.lock()
decrypt, calls = _excessive_then(pyrage.passphrase.decrypt, 2)
monkeypatch.setattr(pyrage.passphrase, "decrypt", decrypt)
crypto.unlock("correct horse")
assert len(calls) == 3


def test_a_persistent_refusal_is_not_called_a_wrong_passphrase(monkeypatch):
import pyrage

crypto.create_key("correct horse")
crypto.lock()
decrypt, _ = _excessive_then(pyrage.passphrase.decrypt, 99)
monkeypatch.setattr(pyrage.passphrase, "decrypt", decrypt)
with pytest.raises(crypto.CryptoError) as exc:
crypto.unlock("correct horse")
assert "wrong passphrase" not in str(exc.value)
assert "slower" in str(exc.value)


def test_a_wrong_passphrase_is_still_wrong_and_not_retried(monkeypatch):
import pyrage

crypto.create_key("correct horse")
crypto.lock()
calls = []
real = pyrage.passphrase.decrypt
monkeypatch.setattr(pyrage.passphrase, "decrypt", lambda d, p: calls.append(1) or real(d, p))
with pytest.raises(crypto.CryptoError, match="wrong passphrase"):
crypto.unlock("wrong horse")
assert len(calls) == 1
Loading