diff --git a/Makefile.am b/Makefile.am index f3212b4a..88c64b30 100644 --- a/Makefile.am +++ b/Makefile.am @@ -309,6 +309,7 @@ EXTRA_DIST= \ tests/08-passphrase-file.sh \ tests/09-explicit-params.sh \ tests/10-check-params.sh \ + tests/12-same-file.sh \ tests/shared_test_functions.sh \ tests/shared_valgrind_functions.sh \ tests/test_scrypt.sh \ diff --git a/main.c b/main.c index 2b05e252..150d7fca 100644 --- a/main.c +++ b/main.c @@ -25,6 +25,9 @@ */ #include "platform.h" +#include +#include + #include #include #include @@ -96,6 +99,37 @@ scrypt_mode_info(const char * infilename) return (-1); } +/** + * same_file(infile, outfilename): + * Return non-zero if the already-open ${infile} and the path ${outfilename} + * refer to the same regular file. If we cannot tell -- most importantly if + * ${outfilename} does not exist yet -- return zero. + */ +static int +same_file(FILE * infile, const char * outfilename) +{ + struct stat sb_in; + struct stat sb_out; + + /* If we can't stat either file, assume that they're different. */ + if (fstat(fileno(infile), &sb_in)) + return (0); + if (stat(outfilename, &sb_out)) + return (0); + + /* + * Only a regular file can be destroyed this way; opening a character + * device such as /dev/null for writing does not discard the data we + * are about to read. + */ + if (!S_ISREG(sb_in.st_mode) || !S_ISREG(sb_out.st_mode)) + return (0); + + /* The same device and inode is the same file. */ + return ((sb_in.st_dev == sb_out.st_dev) && + (sb_in.st_ino == sb_out.st_ino)); +} + /** * scrypt_mode_enc_dec(params, passphrase_entry, passphrase_arg, dec, verbose, * force_resources, infilename, outfilename): @@ -129,6 +163,17 @@ scrypt_mode_enc_dec(struct scryptenc_params params, infile = stdin; } + /* + * Refuse to write the output into the file we are reading. Opening + * it for writing truncates it, so we would destroy the data we were + * asked to encrypt or decrypt and then "succeed" in writing an + * encryption of nothing. + */ + if ((outfilename != NULL) && same_file(infile, outfilename)) { + warn0("Input and output files are the same: %s", outfilename); + goto err1; + } + /* Get the password. */ if (passphrase_entry_readpass(&passwd, passphrase_entry, passphrase_arg, "Please enter passphrase", diff --git a/scrypt.1 b/scrypt.1 index b81bb365..8b22ed4a 100644 --- a/scrypt.1 +++ b/scrypt.1 @@ -63,6 +63,16 @@ if specified, or the standard output otherwise. The user will be prompted to enter the passphrase used at encryption time to generate the derived encryption key. .Pp +.Ar outfile +cannot be the same file as +.Ar infile : +.Nm +truncates +.Ar outfile +before it reads +.Ar infile , +so it exits with an error instead of destroying the input. +.Pp .Nm Cm info provides information about the encryption parameters used for .Ar infile . diff --git a/tests/12-same-file.sh b/tests/12-same-file.sh new file mode 100644 index 00000000..d23a5658 --- /dev/null +++ b/tests/12-same-file.sh @@ -0,0 +1,153 @@ +#!/bin/sh + +### Constants +c_valgrind_min=1 +reference_file="${scriptdir}/verify-strings/test_scrypt.good" +encrypted_reference_file="${scriptdir}/verify-strings/test_scrypt_good.enc" +plaintext_target="${s_basename}-plaintext.txt" +encrypted_target="${s_basename}-encrypted.enc" +normal_output="${s_basename}-normal.enc" +stderr_enc="${s_basename}-enc.stderr" +stderr_dec="${s_basename}-dec.stderr" + +scenario_cmd() { + # "scrypt enc" must not write into the file it is reading; opening + # the output file truncates it. + cp "${reference_file}" "${plaintext_target}" + + setup_check "scrypt enc same file" + ( + echo "${password}" | ${c_valgrind_cmd} "${bindir}/scrypt" \ + enc --passphrase dev:stdin-once \ + "${plaintext_target}" "${plaintext_target}" \ + 2> "${stderr_enc}" + expected_exitcode 1 $? > "${c_exitfile}" + ) + + setup_check "scrypt enc same file error" + grep -q "scrypt: Input and output files are the same" \ + "${stderr_enc}" + echo $? > "${c_exitfile}" + + setup_check "scrypt enc same file leaves input alone" + cmp -s "${plaintext_target}" "${reference_file}" + echo $? > "${c_exitfile}" + + # The same applies to "scrypt dec". + cp "${encrypted_reference_file}" "${encrypted_target}" + + setup_check "scrypt dec same file" + ( + echo "${password}" | ${c_valgrind_cmd} "${bindir}/scrypt" \ + dec --passphrase dev:stdin-once \ + "${encrypted_target}" "${encrypted_target}" \ + 2> "${stderr_dec}" + expected_exitcode 1 $? > "${c_exitfile}" + ) + + setup_check "scrypt dec same file error" + grep -q "scrypt: Input and output files are the same" \ + "${stderr_dec}" + echo $? > "${c_exitfile}" + + setup_check "scrypt dec same file leaves input alone" + cmp -s "${encrypted_target}" "${encrypted_reference_file}" + echo $? > "${c_exitfile}" + + # Writing to a different file must still work. + setup_check "scrypt enc different file" + echo "${password}" | ${c_valgrind_cmd} "${bindir}/scrypt" \ + enc --passphrase dev:stdin-once \ + "${plaintext_target}" "${normal_output}" + echo $? > "${c_exitfile}" + + # Extended static-alias coverage. This block belongs inside scenario_cmd. + alias_password="${s_basename}-alias-password.txt" + printf '%s\n' "${password}" > "${alias_password}" + for alias_mode in enc dec; do + alias_params="" + alias_reference="${encrypted_reference_file}" + if [ "${alias_mode}" = enc ]; then + alias_params="--logN 10 -r 1 -p 1" + alias_reference="${reference_file}" + fi + for alias_kind in hardlink output-symlink input-symlink stdin dotdot; do + alias_input="${s_basename}-${alias_mode}-${alias_kind}-input" + alias_link="${s_basename}-${alias_mode}-${alias_kind}-link" + alias_stderr="${s_basename}-${alias_mode}-${alias_kind}.stderr" + cp "${alias_reference}" "${alias_input}" + alias_source="${alias_input}" + alias_dest="${alias_link}" + case "${alias_kind}" in + hardlink) + ln "${alias_input}" "${alias_link}" + ;; + output-symlink) + ln -s "${alias_input}" "${alias_link}" + ;; + input-symlink) + ln -s "${alias_input}" "${alias_link}" + alias_source="${alias_link}" + alias_dest="${alias_input}" + ;; + stdin) + alias_source="-" + alias_dest="${alias_input}" + ;; + dotdot) + alias_dir="${s_basename}-${alias_mode}-${alias_kind}-dir" + mkdir "${alias_dir}" + alias_dest="${alias_dir}/../$(basename "${alias_input}")" + ;; + esac + + setup_check "scrypt ${alias_mode} ${alias_kind} rejects alias" + ${c_valgrind_cmd} "${bindir}/scrypt" "${alias_mode}" \ + ${alias_params} --passphrase file:"${alias_password}" \ + "${alias_source}" "${alias_dest}" \ + < "${alias_input}" 2> "${alias_stderr}" + expected_exitcode 1 $? > "${c_exitfile}" + + setup_check "scrypt ${alias_mode} ${alias_kind} error" + grep -q "scrypt: Input and output files are the same" \ + "${alias_stderr}" + echo $? > "${c_exitfile}" + + setup_check "scrypt ${alias_mode} ${alias_kind} preserves input" + cmp -s "${alias_input}" "${alias_reference}" + echo $? > "${c_exitfile}" + done + done + + # A character device is not a destructive same-regular-file alias. + setup_check "scrypt enc permits same null device" + ${c_valgrind_cmd} "${bindir}/scrypt" enc --logN 10 -r 1 -p 1 \ + --passphrase file:"${alias_password}" /dev/null /dev/null + echo $? > "${c_exitfile}" + + # Equal contents are not the same inode; preserve and roundtrip the input. + independent_input="${s_basename}-independent-input" + independent_output="${s_basename}-independent-output" + independent_plain="${s_basename}-independent-plain" + cp "${reference_file}" "${independent_input}" + cp "${reference_file}" "${independent_output}" + setup_check "scrypt enc permits identical contents in independent files" + ${c_valgrind_cmd} "${bindir}/scrypt" enc --logN 10 -r 1 -p 1 \ + --passphrase file:"${alias_password}" \ + "${independent_input}" "${independent_output}" + echo $? > "${c_exitfile}" + + setup_check "scrypt enc independent files preserves input" + cmp -s "${independent_input}" "${reference_file}" + echo $? > "${c_exitfile}" + + setup_check "scrypt dec independent output roundtrip" + ${c_valgrind_cmd} "${bindir}/scrypt" dec \ + --passphrase file:"${alias_password}" \ + "${independent_output}" "${independent_plain}" + echo $? > "${c_exitfile}" + + setup_check "scrypt dec independent output matches input" + cmp -s "${independent_plain}" "${reference_file}" + echo $? > "${c_exitfile}" +}