Skip to content

readpass_file: reject a passphrase file with a stray carriage return - #432

Open
woahwhattheheck wants to merge 2 commits into
Tarsnap:masterfrom
woahwhattheheck:scrypt-passphrase-file-cr
Open

readpass_file: reject a passphrase file with a stray carriage return#432
woahwhattheheck wants to merge 2 commits into
Tarsnap:masterfrom
woahwhattheheck:scrypt-passphrase-file-cr

Conversation

@woahwhattheheck

@woahwhattheheck woahwhattheheck commented Sep 6, 2026

Copy link
Copy Markdown

Disclosure, per this repository's AGENTS.md: I am an LLM (Claude), submitting on behalf of the account owner. I am available to discuss this change and to revise it in response to review feedback.

Fixes #431.

readpass_file() ends with

	/* Truncate at any newline character. */
	passbuf[strcspn(passbuf, "\r\n")] = '\0';

fgets() stops after a \n, and the fgetc(f) != EOF check above rejects
anything following it, so a \n in the buffer is always the last character.
A \r is not: fgets() reads past it, so a \r anywhere in the line reaches
strcspn() and silently cuts the passphrase off there.

--passphrase file:pw with pw containing hunter2\rextra therefore uses
hunter2. It round-trips, because encryption truncates the same way, so
nothing shows until the passphrase is supplied by another route. The header
in libcperciva/util/readpass.h already says a file like that should be
rejected:

Print an error and fail if the file is 2048 characters or more, or if it
contains any newline \n or \r\n characters other than at the end of the
file.

The change

Strip a trailing \n or \r\n explicitly, then reject the file if any \r
or \n is left. I did not make the leftover character part of the
passphrase: both that and the current truncation change the passphrase an
existing file yields, and neither is safe to choose on the user's behalf
without telling them. No documentation change — this makes the code match
what readpass.h already describes.

Files whose only newline is a \n or a trailing \r\n are unaffected, so
ordinary Unix and Windows passphrase files keep working.

Test

tests/08-passphrase-file.sh gains four checks rather than a new test file:

  • a passphrase file containing hunter2\rextra must be rejected, with the
    error message, and must not create an output file. The passphrase begins
    with the correct one, so before this change that decryption succeeds and
    the check fails — it is a real regression test, not a restatement.
  • a passphrase file with CRLF line endings must still decrypt the reference
    file correctly.

Note

This is libcperciva code, so it also lives in tarsnap and kivaloo; I have
only touched the copy in this repository.

readpass_file() finished with

	passbuf[strcspn(passbuf, "\r\n")] = '\0';

fgets() stops after a "\n", and the fgetc() check above rejects anything
which follows it, so a "\n" in the buffer is always the last character.
A "\r" is not: fgets() reads past it, so a "\r" anywhere in the line
reaches strcspn() and silently cuts the passphrase off there.

"--passphrase file:pw" with pw holding "hunter2\rextra" therefore used
"hunter2".  It round-trips, since encryption truncates the same way, so
nothing showed until the passphrase was supplied by another route.

Strip a trailing "\n" or "\r\n" explicitly, then reject the file if any
"\r" or "\n" is left.  Keeping the character as passphrase data would
also change the passphrase an existing file yields, so neither answer is
safe to pick silently.  readpass.h already documented this behaviour.

Extend tests/08-passphrase-file.sh: a file holding "hunter2\rextra" must
be rejected -- it begins with the correct passphrase, so before this
change that decryption succeeded -- and a CRLF file must still work.
Use distinct output paths for the missing-file and incorrect-passphrase cases, and check the path used by each command. Preserve the existing implementation and upstream submission.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[bug bounty] readpass_file() silently truncates the passphrase at a carriage return

2 participants