Skip to content

A slow moment is not a wrong passphrase (age's excessive-work refusal) - #19

Merged
mkhellat merged 1 commit into
mainfrom
fix/crypto-excessive-work
Nov 12, 2026
Merged

mkhellat merged 1 commit into
mainfrom
fix/crypto-excessive-work

Conversation

@mkhellat

Copy link
Copy Markdown
Owner

A pre-existing defect found while PR #18's CI ran. It isn't caused by #18, which doesn't touch crypto.

The failure

macOS CI (Python 3.10) failed test_crypto.py::test_create_unlock_and_round_trip: unlocking a key created a moment earlier raised pyrage's "Excessive work parameter for passphrase", which codebud reported as "wrong passphrase".

Why

age sets the passphrase's scrypt work factor from the machine's speed when it seals the key. On every unlock it measures the speed again and refuses a work factor far above it. On a busy or throttled moment (a loaded CI runner, a laptop on battery), a correct passphrase is refused. Calling that a wrong passphrase sends the user looking for a typo, or giving their data up for lost.

Fix

pyrage exposes no work-factor control (decrypt(ciphertext, passphrase) only). So crypto._open_identity now:

  • retries that specific refusal three times, since age re-measures each time;
  • then says what happened: the machine was slower than when the key was sealed, and the passphrase may well be right.

A genuinely wrong passphrase is still reported as such, and isn't retried. The manual's Troubleshooting chapter gains "Key Refused on a Slow Moment", including the persistent case: a key moved to a much slower machine, which key passwd can't fix because it seals for the machine it runs on.

Testing

3 new tests: retried then opened; a persistent refusal not called a wrong passphrase; a wrong passphrase reported once, not retried. 1457 tests pass, ruff is clean, the manual builds without warnings, and make distcheck passes.

Found on macOS CI (PR #18's run, macOS Py 3.10): unlocking a key made a
moment earlier failed with pyrage's "Excessive work parameter for
passphrase", which codebud reported as "wrong passphrase".  age sets
the passphrase's scrypt work factor from the machine's speed when it
seals the key, and on every unlock measures the speed again and refuses
a work factor far above it.  On a busy or throttled moment -- a loaded
runner, a laptop on battery -- a correct passphrase is refused, and
codebud told the user it was wrong: a lockout that sends them looking
for a typo, or giving their data up for lost.

pyrage offers no control over the work factor, so codebud now retries
that specific refusal (age measures again each time) three times, then
says what happened -- the machine was slower than when the key was
sealed, the passphrase may well be right -- instead of "wrong
passphrase".  A genuinely wrong passphrase is still reported as such,
and not retried.  The manual's Troubleshooting chapter gains "Key
Refused on a Slow Moment", including the persistent case of a key
moved to a much slower machine.
@mkhellat

Copy link
Copy Markdown
Owner Author

Review summary: correct, with one documented limit

One full sweep over this PR's head: conditions → scenarios → code → De Morgan.

Condition (crypto._open_identity): opened ⇔ ∃ attempt ≤ 3: decrypt succeeds. A DecryptError whose text contains "excessive work" is retried. Any other DecryptError is reported at once as a wrong passphrase. Three excessive-work refusals end in the honest "slower just now" message.

Scenario Result Verified
Right passphrase, normal machine opens on the first try existing tests
Right passphrase, slow moment (refused twice) opens on the third try test_a_slow_moment_is_retried (3 calls)
Right passphrase, refused every time "slower just now… may well be right", not "wrong passphrase" test_a_persistent_refusal_is_not_called_a_wrong_passphrase
Wrong passphrase "wrong passphrase", one attempt only test_a_wrong_passphrase_is_still_wrong_and_not_retried

De Morgan: what's still reported as "wrong passphrase". age localises its error messages (Fluent), so on a system language other than English the excessive-work text may not contain "excessive work". That case falls back to today's behaviour: "wrong passphrase", no retry. That's no worse than before this PR, and pyrage gives no error code to match on instead. I'll record it as a known limit rather than block on it.

Cost: a persistent refusal now takes up to three scrypt runs (a few seconds) before the message. That's acceptable.

Verdict: correct. I'll merge with a merge commit once CI passes on all jobs, macOS included, and before #18, whose macOS failure this explains.

@mkhellat

Copy link
Copy Markdown
Owner Author

Merging. The review found the fix correct, with one documented limit: on a non-English system, age's localised message falls back to today's behaviour. CI passes on all five jobs at 3c3e82f: Ubuntu Py 3.10 and 3.14, macOS Py 3.10 and 3.14, Fedora. Locally, 1457 tests pass and make distcheck passes. Main hasn't moved since the branch was made. Merged before #18, whose macOS Py 3.10 failure this explains.

@mkhellat
mkhellat merged commit e2a900c into main Nov 12, 2026
5 checks passed
@mkhellat
mkhellat deleted the fix/crypto-excessive-work branch November 12, 2026 08:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant