Disclosure, per this repository's AGENTS.md: I am an LLM (Claude), reporting on behalf of the account owner. I am available to discuss this report and to revise the linked code in response to review feedback.
Summary
scrypt enc secret.txt secret.txt destroys secret.txt and exits 0. The file
is replaced by a 128-byte scrypt file whose plaintext is empty; the original
contents are gone, and nothing is printed to say so.
This is not a security issue, so I am reporting it here rather than by email,
per https://www.tarsnap.com/bugbounty.html.
What happens
scrypt_mode_enc_dec() in main.c opens the input, reads the passphrase, and
only then opens the output:
/* If the input isn't stdin, open the file. */
if (infilename != NULL) {
if ((infile = fopen(infilename, "rb")) == NULL) {
...
/* If we have an output filename, open it. */
if (outfilename != NULL) {
if ((outfile = fopen(outfilename, "wb")) == NULL) {
When the two names refer to the same file, fopen(outfilename, "wb")
truncates it to zero length. Nothing has been read from infile yet — the
first fread() in scryptenc_file() happens afterwards — so that fread()
returns 0 immediately:
do {
if ((readlen = fread(buf, 1, ENCBLOCK, infile)) == 0)
break;
ferror(infile) is false, so this is indistinguishable from a legitimately
empty input. scryptenc_file() writes the 96-byte header and the 32-byte
HMAC, returns SCRYPT_OK, and scrypt exits 0.
Reproducer
$ echo "important data" > secret.txt
$ echo "hunter2" | scrypt enc --passphrase dev:stdin-once secret.txt secret.txt
$ echo $?
0
$ ls -l secret.txt
-rw-r--r-- 1 user user 128 ... secret.txt
$ echo "hunter2" | scrypt dec --passphrase dev:stdin-once secret.txt
$ echo $?
0
The final decryption succeeds and prints nothing: the file now correctly
contains an encryption of zero bytes. The original 15 bytes are unrecoverable.
scrypt dec f f loses the ciphertext the same way — the header is read into
the cookie by scryptdec_file_prep(), then the file is truncated, then
scryptdec_file_copy() fails on the empty remainder — leaving f as a
zero-length file.
Both are also reachable with the input on stdin:
echo "hunter2" | scrypt enc --passphrase dev:stdin-once - f < f
Why I think this is worth fixing rather than "don't do that"
scrypt opens both files itself; the shell is not involved, so this is not the
sort f > f situation where the shell truncates before the program runs. A
user asking to encrypt a file in place is a natural thing to try, scrypt(1)
does not say it is unsupported, and the failure is silent and total —
cp, mv and install all diagnose the same mistake instead.
Fix
PR to follow: compare the open input file and the output path with fstat()
and stat() before opening the output, and exit with an error if they are the
same regular file. Devices are excluded, since opening /dev/null for writing
discards nothing. The check runs before the passphrase is read, so the user is
not prompted for a passphrase that cannot be used.
It adds tests/12-same-file.sh and a note in scrypt.1.
How this was found
Source review of main.c and lib/scryptenc/scryptenc.c at a71ae82,
following the ordering of the fopen() calls against the first fread().
Summary
scrypt enc secret.txt secret.txtdestroyssecret.txtand exits 0. The fileis replaced by a 128-byte scrypt file whose plaintext is empty; the original
contents are gone, and nothing is printed to say so.
This is not a security issue, so I am reporting it here rather than by email,
per https://www.tarsnap.com/bugbounty.html.
What happens
scrypt_mode_enc_dec()inmain.copens the input, reads the passphrase, andonly then opens the output:
When the two names refer to the same file,
fopen(outfilename, "wb")truncates it to zero length. Nothing has been read from
infileyet — thefirst
fread()inscryptenc_file()happens afterwards — so thatfread()returns 0 immediately:
ferror(infile)is false, so this is indistinguishable from a legitimatelyempty input.
scryptenc_file()writes the 96-byte header and the 32-byteHMAC, returns
SCRYPT_OK, andscryptexits 0.Reproducer
The final decryption succeeds and prints nothing: the file now correctly
contains an encryption of zero bytes. The original 15 bytes are unrecoverable.
scrypt dec f floses the ciphertext the same way — the header is read intothe cookie by
scryptdec_file_prep(), then the file is truncated, thenscryptdec_file_copy()fails on the empty remainder — leavingfas azero-length file.
Both are also reachable with the input on stdin:
Why I think this is worth fixing rather than "don't do that"
scryptopens both files itself; the shell is not involved, so this is not thesort f > fsituation where the shell truncates before the program runs. Auser asking to encrypt a file in place is a natural thing to try,
scrypt(1)does not say it is unsupported, and the failure is silent and total —
cp,mvandinstallall diagnose the same mistake instead.Fix
PR to follow: compare the open input file and the output path with
fstat()and
stat()before opening the output, and exit with an error if they are thesame regular file. Devices are excluded, since opening
/dev/nullfor writingdiscards nothing. The check runs before the passphrase is read, so the user is
not prompted for a passphrase that cannot be used.
It adds
tests/12-same-file.shand a note inscrypt.1.How this was found
Source review of
main.candlib/scryptenc/scryptenc.cata71ae82,following the ordering of the
fopen()calls against the firstfread().