diff --git a/.gitignore b/.gitignore index 666fe16b..ca3e70ea 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ bin/staticcheck bin/gocovmerge bin/misspell bin/config +cli-tests/*.out.actual *coverage.out .vscode tags diff --git a/.travis.yml b/.travis.yml index 0de7c6ea..1a1686df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,8 @@ jobs: - stage: presubmits name: Generate, Format, and Lint + before_install: + - sudo apt-get -y install shellcheck install: - make tools script: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ddd456cb..d5be7215 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,11 @@ On every pull request, [Travis CI](https://travis-ci.org/google/fscrypt) runs unit tests, integration tests, code formatters, and linters. To pass these checks you should make sure that in your submission: - `make` properly builds `fscrypt` and `pam_fscrypt.so`. -- All tests, including [integration tests](#running-integration-tests), should pass. +- All tests, including [integration tests](#running-integration-tests) and + [command-line interface (CLI) + tests](https://github.com/google/fscrypt/blob/master/cli-tests/README.md), + should pass. If the CLI tests fail due to an expected change in output, you + can use `make cli-test-update`. - `make format` has been run. - If you made any changes to files ending in `.proto`, the corresponding `.pb.go` files should be regenerated with `make gen`. @@ -74,17 +78,27 @@ Essentially, if you run: make test-setup make all make test-teardown +make cli-test go mod tidy ``` and everything succeeds, and no files are changed, you're good to submit. -The `Makefile` should automatically download and build whatever it needs. -The only exceptions to this rule are: +The `Makefile` will automatically download and build any needed Go dependencies. +However, you'll also need to install some non-Go dependencies: - `make format` requires [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html). - - `make test-setup` requires - [`e2fsprogs`](https://en.wikipedia.org/wiki/E2fsprogs) version 1.43 - or later (or any patched version that supports `-O encrypt`). + - `make lint` requires [`shellcheck`](https://github.com/koalaman/shellcheck). + - `make test-setup` and `make cli-test` require + [`e2fsprogs`](https://en.wikipedia.org/wiki/E2fsprogs) version 1.43 or + later. + - `make cli-test` requires [`expect`](https://en.wikipedia.org/wiki/Expect) + and + [`keyutils`](https://manpages.debian.org/testing/keyutils/keyctl.1.en.html). + +On Ubuntu, the following command installs the needed packages: +``` +sudo apt-get install clang-format shellcheck e2fsprogs expect keyutils +``` ### Running Integration Tests diff --git a/Makefile b/Makefile index d110dd84..54c381d9 100644 --- a/Makefile +++ b/Makefile @@ -110,11 +110,12 @@ lint: $(BIN)/golint $(BIN)/staticcheck $(BIN)/misspell go list ./... | xargs -L1 golint -set_exit_status staticcheck ./... misspell -source=text $(FILES) + ( cd cli-tests && shellcheck -x *.sh) clean: rm -f $(BIN)/$(NAME) $(PAM_MODULE) $(TOOLS) coverage.out $(COVERAGE_FILES) $(PAM_CONFIG) -###### Testing Commands (setup/teardown require sudo) ###### +###### Go tests ###### .PHONY: test test-setup test-teardown # If MOUNT exists signal that we should run integration tests. @@ -139,6 +140,15 @@ test-teardown: rmdir $(MOUNT) rm -f $(IMAGE) +###### Command-line interface tests ###### +.PHONY: cli-test cli-test-update + +cli-test: $(BIN)/$(NAME) + sudo cli-tests/run.sh + +cli-test-update: $(BIN)/$(NAME) + sudo cli-tests/run.sh --update-output + # Runs tests and generates coverage COVERAGE_FILES := $(addsuffix coverage.out,$(GO_DIRS)) coverage.out: $(BIN)/gocovmerge $(COVERAGE_FILES) diff --git a/actions/protector.go b/actions/protector.go index 4bd7c153..dab9c274 100644 --- a/actions/protector.go +++ b/actions/protector.go @@ -30,6 +30,10 @@ import ( "github.com/google/fscrypt/util" ) +// LoginProtectorMountpoint is the mountpoint where login protectors are stored. +// This can be overridden by the user of this package. +var LoginProtectorMountpoint = "/" + // Errors relating to Protectors var ( ErrProtectorName = errors.New("login protectors do not need a name") diff --git a/cli-tests/README.md b/cli-tests/README.md new file mode 100644 index 00000000..dfcc1d09 --- /dev/null +++ b/cli-tests/README.md @@ -0,0 +1,67 @@ +# fscrypt command-line interface tests + +## Usage + +To run the command-line interface (CLI) tests for `fscrypt`, ensure +that your kernel is v5.4 or later and has `CONFIG_FS_ENCRYPTION=y`. +Also ensure that you have the following packages installed: + +* e2fsprogs +* expect +* keyutils + +Then, run: + +```shell +make cli-test +``` + +You'll need to enter your `sudo` password, as the tests require root. + +If you only want to run specific tests, run a command like: + +```shell +make && sudo cli-tests/run.sh t_encrypt t_unlock +``` + +## Updating the expected output + +When the output of `fscrypt` has intentionally changed, the test +`.out` files need to be updated. This can be done automatically by +the following command, but be sure to review the changes: + +```shell +make cli-test-update +``` + +## Writing CLI tests + +The fscrypt CLI tests are `bash` scripts named like `t_*.sh`. + +The test scripts must be executable and begin by sourcing `common.sh`. +They all run in bash "extra-strict mode" (`-e -u -o pipefail`). They +run as root and have access to the following environment: + +* `$DEV`, `$DEV_ROOT`: ext4 filesystem images with encryption enabled + +* `$MNT`, `$MNT_ROOT`: the mountpoints of the above filesystems. + Initially all filesystems are mounted and are setup for fscrypt. + Login protectors will be stored on `$MNT_ROOT`. + +* `$TMPDIR`: a temporary directory that the test may use + +* `$FSCRYPT_CONF`: location of the fscrypt.conf file. Initially this + file exists and specifies to use v2 policies with the default + settings, except password hashing is configured to be extra fast. + +* `$TEST_USER`: a non-root user that the test may use. Their password + is `TEST_USER_PASS`. + +Any output (stdout and stderr) the test prints is compared to the +corresponding `.out` file. If a difference is detected then the test +is considered to have failed. The output is first sent through some +standard filters; see `run.sh`. + +The test is also failed if it exits with nonzero status. + +See `common.sh` for utility functions the tests may use. diff --git a/cli-tests/common.sh b/cli-tests/common.sh new file mode 100644 index 00000000..fcebfd65 --- /dev/null +++ b/cli-tests/common.sh @@ -0,0 +1,154 @@ +#!/bin/bash +# +# common.sh - helper functions for fscrypt command-line interface tests +# +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# + +# Use extra-strict mode. +set -e -u -o pipefail + +# Don't allow running the test scripts directly. They need to be run via +# run.sh, to set up everything correctly. +if [ -z "${MNT:-}" ] || [ -z "${MNT_ROOT:-}" ]; then + echo 1>&2 "ERROR: This script can only be run via run.sh, not on its own." + exit 1 +fi + +# Prints an error message, then fails the test by exiting with failure status. +_fail() +{ + [ $# -ne 1 ] && _fail "wrong argument count to ${FUNCNAME[0]}" + echo 1>&2 "ERROR: $1" + exit 1 +} + +# Runs a shell command and expects that it fails. +_expect_failure() +{ + [ $# -ne 1 ] && _fail "wrong argument count to ${FUNCNAME[0]}" + if eval "$1"; then + _fail "command unexpectedly succeeded: \"$1\"" + fi +} + +# Prints a message to mark the beginning of the next part of the test. +_print_header() +{ + [ $# -ne 1 ] && _fail "wrong argument count to ${FUNCNAME[0]}" + echo + echo "# $1" +} + +# Deletes all files on the test filesystems, including all policies and +# protectors. Leaves the fscrypt metadata directories themselves. +_reset_filesystems() +{ + local mnt + + [ $# -ne 0 ] && _fail "wrong argument count to ${FUNCNAME[0]}" + + for mnt in "$MNT" "$MNT_ROOT"; do + rm -rf "${mnt:?}"/* "${mnt:?}"/.fscrypt/{policies,protectors}/* + done +} + +# Prints the number of filesystems that have encryption support enabled. +_get_enabled_fs_count() +{ + local count + + [ $# -ne 0 ] && _fail "wrong argument count to ${FUNCNAME[0]}" + + count=$(fscrypt status | awk '/filesystems supporting encryption/ { print $4 }') + if [ -z "$count" ]; then + _fail "encryption support status line not found" + fi + echo "$count" +} + +# Prints the number of filesystems that have fscrypt metadata. +_get_setup_fs_count() +{ + local count + + [ $# -ne 0 ] && _fail "wrong argument count to ${FUNCNAME[0]}" + + count=$(fscrypt status | awk '/filesystems with fscrypt metadata/ { print $5 }') + if [ -z "$count" ]; then + _fail "fscrypt metadata status line not found" + fi + echo "$count" +} + +# Removes all fscrypt metadata from the given filesystem. +_rm_metadata() +{ + [ $# -ne 1 ] && _fail "wrong argument count to ${FUNCNAME[0]}" + + rm -r "${1:?}/.fscrypt" +} + +# Runs a shell command, ignoring its output (stdout and stderr) if it succeeds. +# If the command fails, prints its output and fails the test. +_run_noisy_command() +{ + [ $# -ne 1 ] && _fail "wrong argument count to ${FUNCNAME[0]}" + + if ! eval "$1" &> "$TMPDIR/out"; then + _fail "Command failed: '$1'. Output was: $(cat "$TMPDIR/out")" + fi +} + +# Runs the given shell command as the test user. +_user_do() +{ + [ $# -ne 1 ] && _fail "wrong argument count to ${FUNCNAME[0]}" + + su "$TEST_USER" --command="$1" +} + +# Runs the given shell command as the test user and expects it to fail. +_user_do_and_expect_failure() +{ + [ $# -ne 1 ] && _fail "wrong argument count to ${FUNCNAME[0]}" + + _expect_failure "_user_do '$1'" +} + +# Gives the test a new session keyring which contains the test user's keyring +# but not root's keyring. Also clears the test user's keyring. This must be +# called at the beginning of the test script as it may re-execute the script. +_setup_session_keyring() +{ + [ $# -ne 0 ] && _fail "wrong argument count to ${FUNCNAME[0]}" + + # This *should* just use 'keyctl new_session', but that doesn't work if + # the session keyring is owned by a user other than root. So instead we + # have to use 'keyctl session' and re-execute the script. + if [ -z "${FSCRYPT_SESSION_KEYRING_SET:-}" ]; then + export FSCRYPT_SESSION_KEYRING_SET=1 + set +e + keyctl session - "$0" |& grep -v '^Joined session keyring' + exit "${PIPESTATUS[0]}" + fi + + # Link the test user's keyring into the new session keyring. + keyctl setperm @s 0x3f000000 # all possessor permissions + _user_do "keyctl link @u @s" + + # Clear the test user's keyring. + _user_do "keyctl clear @u" +} diff --git a/cli-tests/run.sh b/cli-tests/run.sh new file mode 100755 index 00000000..909b6451 --- /dev/null +++ b/cli-tests/run.sh @@ -0,0 +1,299 @@ +#!/bin/bash +# +# run.sh - run the fscrypt command-line interface tests +# +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# + +# Use extra-strict mode. +set -e -u -o pipefail + +# Ensure we're in the cli-tests/ directory. +cd "$(dirname "$0")" + +# Names of the test devices. +# Variables with these names are exported to the tests. +DEVICES=(DEV DEV_ROOT) + +# Names of the mountpoint of each test device. +# Variables with these names are exported to the tests. +MOUNTS=(MNT MNT_ROOT) + +# Name of the test user. This user will be created and deleted by this script. +# This variable is exported to the tests. +TEST_USER=fscrypt-test-user + +# The temporary directory to use. +# This variable is exported to the tests. +TMPDIR=$(mktemp -d /tmp/fscrypt.XXXXXX) + +# The loopback devices that correspond to each test device. +LOOPS=() + +# Update the expected output files to match the actual output? +UPDATE_OUTPUT=false + +LONGOPTS_ARRAY=( +'help' +'update-output' +) +LONGOPTS=$(echo "${LONGOPTS_ARRAY[*]}" | tr ' ' ,) + +cleanup() +{ + local mnt loop + + # Unmount all the test filesystems. + for mnt in "${MOUNTS[@]}"; do + mnt="$TMPDIR/$mnt" + if mountpoint "$mnt" &> /dev/null; then + umount "$mnt" + fi + done + + # Delete the loopback device of each test device. + for loop in "${LOOPS[@]}"; do + losetup -d "$loop" + done + + # Delete all temporary files. + rm -rf "${TMPDIR:?}"/* +} + +cleanup_full() +{ + cleanup + rm -rf "$TMPDIR" + userdel "$TEST_USER" || true +} + +# Filters the output of the test script to make the output consistent on every +# run of the test. For example, references to the mountpoint like +# /tmp/fscrypt.4OTb6y/MNT will be replaced with simply MNT, since the name of +# the temporary directory is different every time. +filter_test_output() +{ + local sedscript="" + local raw_output=$TMPDIR/raw-test-output + local i + + cat > "$raw_output" + + # Filter mountpoint and device names. + for i in "${!DEVICES[@]}"; do + sedscript+="s@$TMPDIR/${MOUNTS[$i]}@${MOUNTS[$i]}@g;" + sedscript+="s@${LOOPS[$i]}@${DEVICES[$i]}@g;" + done + + # Filter the path to fscrypt.conf. + sedscript+="s@$FSCRYPT_CONF@FSCRYPT_CONF@g;" + + # Filter policy and protector descriptors. + sedscript+=$(grep -E -o '\<([a-f0-9]{16})|([a-f0-9]{32})\>' \ + "$raw_output" \ + | awk '{ printf "s@\\<" $1 "\\>@desc" NR "@g;" }') + + # Filter any other paths in TMPDIR. + sedscript+="s@$TMPDIR@TMPDIR@g;" + + sed -e "$sedscript" "$raw_output" +} + +# Prepares to run a test script. +setup_for_test() +{ + local i dev_var mnt_var img mnt loop + + # Start with a clean state. + cleanup + + # ../bin/fscrypt might not be accessible to $TEST_USER. Copy it into + # $TMPDIR so that $TEST_USER is guaranteed to have access to it. + mkdir "$TMPDIR/bin" + cp ../bin/fscrypt "$TMPDIR/bin/" + chmod 755 "$TMPDIR" "$TMPDIR/bin" "$TMPDIR/bin/fscrypt" + + # Create the test filesystems and mountpoints. + LOOPS=() + for i in "${!DEVICES[@]}"; do + dev_var=${DEVICES[$i]} + mnt_var=${MOUNTS[$i]} + img="$TMPDIR/$dev_var" + if ! mkfs.ext4 -O encrypt -F -b 4096 -I 256 "$img" $((1<<15)) \ + &> "$TMPDIR/mkfs.out" + then + cat 1>&2 "$TMPDIR/mkfs.out" + exit 1 + fi + loop=$(losetup --find --show "$img") + LOOPS+=("$loop") + export "$dev_var=$loop" + mnt="$TMPDIR/$mnt_var" + export "$mnt_var=$mnt" + mkdir -p "$mnt" + mount "$loop" "$mnt" + done + + # Give the tests their own "root" mount for storing login protectors, so + # they don't use the real "/". + export FSCRYPT_ROOT_MNT="$MNT_ROOT" + + # Enable consistent output mode. + export FSCRYPT_CONSISTENT_OUTPUT="1" + + # Give the tests their own fscrypt.conf. + export FSCRYPT_CONF="$TMPDIR/fscrypt.conf" + fscrypt setup --time=1ms > /dev/null + + # The tests assume kernel support for v2 policies. + if ! grep -q '"policy_version": "2"' "$FSCRYPT_CONF"; then + cat 1>&2 << EOF +ERROR: Can't run these tests because your kernel doesn't support v2 policies. +You need kernel v5.4 or later. +EOF + exit 1 + fi + + # Set up the test filesystems that aren't already set up. + fscrypt setup "$MNT" > /dev/null +} + +run_test() +{ + local t=$1 + + # Run the test script. + set +e + "./$1.sh" |& filter_test_output > "$t.out.actual" + status=${PIPESTATUS[0]} + set -e + + # Check for failure status. + if [ "$status" != 0 ]; then + echo 1>&2 "FAILED: $t [exited with failure status $status]" + if [ -s "$t.out.actual" ]; then + if (( $(wc -l "$t.out.actual" | cut -f1 -d' ') > 10 )); then + echo 1>&2 "Last 10 lines of test output:" + tail -n10 "$t.out.actual" | sed 1>&2 's/^/ /' + echo 1>&2 + echo 1>&2 "See $t.out.actual for the full output." + else + echo 1>&2 "Test output:" + sed 1>&2 's/^/ /' < "$t.out.actual" + fi + fi + exit 1 + fi + + # Check for output mismatch. + if ! cmp "$t.out" "$t.out.actual" &> /dev/null; then + if $UPDATE_OUTPUT; then + cp "$t.out.actual" "$t.out" + echo "Updated $t.out" + else + echo 1>&2 "FAILED: $t [output mismatch]" + echo 1>&2 "Differences between $t.out and $t.out.actual:" + echo 1>&2 + diff 1>&2 "$t.out" "$t.out.actual" + exit 1 + fi + fi + rm -f "$t.out.actual" +} + +usage() +{ + cat << EOF +Usage: run.sh [--update-output] [TEST_SCRIPT_NAME]..." +EOF + exit 1 +} + +if ! options=$(getopt -o "" -l "$LONGOPTS" -- "$@"); then + usage +fi +eval set -- "$options" +while (( $# >= 1 )); do + case "$1" in + --update-output) + UPDATE_OUTPUT=true + ;; + --) + shift + break + ;; + --help|*) + usage + ;; + esac + shift +done + +if [ "$(id -u)" != 0 ]; then + echo 1>&2 "ERROR: You must be root to run these tests." + exit 1 +fi + +# Check for prerequisites. +PREREQ_CMDS=(mkfs.ext4 expect keyctl) +PREREQ_PKGS=(e2fsprogs expect keyutils) +for i in ${!PREREQ_CMDS[*]}; do + if ! type -P "${PREREQ_CMDS[$i]}" > /dev/null; then + cat 1>&2 << EOF +ERROR: You must install the '${PREREQ_PKGS[$i]}' package to run these tests. + Try a command like 'sudo apt-get install ${PREREQ_PKGS[$i]}'. +EOF + exit 1 + fi +done + +# Use a consistent umask. +umask 022 + +# Use a consistent locale, to prevent output mismatches. +export LANG=C +export LC_ALL=C + +# Always cleanup fully on exit. +trap cleanup_full EXIT + +# Create a test user, so that we can test non-root use of fscrypt. Give them a +# password, so that we can test creating login passphrase protected directories. +userdel "$TEST_USER" &> /dev/null || true +useradd "$TEST_USER" +echo "$TEST_USER:TEST_USER_PASS" | chpasswd +export TEST_USER + +# Let the tests use $TMPDIR if they need it. +export TMPDIR + +# Make it so that running 'fscrypt' in the tests runs the correct binary. +export PATH="$TMPDIR/bin:$PATH" + +if (( $# >= 1 )); then + # Tests specified on command line. + tests=("$@") +else + # No tests specified on command line. Just run everything. + tests=(t_*.sh) +fi +for t in "${tests[@]}"; do + t=${t%.sh} + echo "Running $t" + setup_for_test + run_test "$t" +done + +echo "All tests passed!" diff --git a/cli-tests/t_change_passphrase.out b/cli-tests/t_change_passphrase.out new file mode 100644 index 00000000..747ed899 --- /dev/null +++ b/cli-tests/t_change_passphrase.out @@ -0,0 +1,32 @@ + +# Create encrypted directory + +# Try to unlock with wrong passphrase +[ERROR] fscrypt unlock: incorrect key provided +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# Change passphrase + +# Try to unlock with old passphrase +[ERROR] fscrypt unlock: incorrect key provided +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# Unlock with new passphrase + +# Try to change passphrase (interactively, mismatch) +spawn fscrypt metadata change-passphrase --protector=MNT:desc1 +Enter old custom passphrase for protector "prot": +Enter new custom passphrase for protector "prot": +Confirm passphrase: +[ERROR] fscrypt metadata change-passphrase: entered passphrases do not match + +# Change passphrase (interactively) +spawn fscrypt metadata change-passphrase --protector=MNT:desc1 +Enter old custom passphrase for protector "prot": +Enter new custom passphrase for protector "prot": +Confirm passphrase: +Passphrase for protector desc1 successfully changed. + +# Lock, then unlock with new passphrase +"MNT/dir" is now locked. +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available diff --git a/cli-tests/t_change_passphrase.sh b/cli-tests/t_change_passphrase.sh new file mode 100755 index 00000000..204512d1 --- /dev/null +++ b/cli-tests/t_change_passphrase.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Test changing the passphrase of a custom_passphrase protector. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" + +_print_header "Create encrypted directory" +mkdir "$dir" +echo pass1 | fscrypt encrypt --quiet --name=prot --skip-unlock "$dir" + +_print_header "Try to unlock with wrong passphrase" +_expect_failure "echo pass2 | fscrypt unlock --quiet '$dir'" +_expect_failure "mkdir '$dir/subdir'" +protector=$(fscrypt status "$dir" | awk '/custom protector/{print $1}') + +_print_header "Change passphrase" +echo $'pass1\npass2' | \ + fscrypt metadata change-passphrase --protector="$MNT:$protector" --quiet + +_print_header "Try to unlock with old passphrase" +_expect_failure "echo pass1 | fscrypt unlock --quiet '$dir'" +_expect_failure "mkdir '$dir/subdir'" + +_print_header "Unlock with new passphrase" +echo pass2 | fscrypt unlock --quiet "$dir" +mkdir "$dir/subdir" +rmdir "$dir/subdir" + +_print_header "Try to change passphrase (interactively, mismatch)" +expect << EOF +spawn fscrypt metadata change-passphrase --protector=$MNT:$protector +expect "Enter old custom passphrase" +send "pass2\r" +expect "Enter new custom passphrase" +send "pass3\r" +expect "Confirm passphrase" +send "bad\r" +expect eof +EOF + +_print_header "Change passphrase (interactively)" +expect << EOF +spawn fscrypt metadata change-passphrase --protector=$MNT:$protector +expect "Enter old custom passphrase" +send "pass2\r" +expect "Enter new custom passphrase" +send "pass3\r" +expect "Confirm passphrase" +send "pass3\r" +expect eof +EOF + +_print_header "Lock, then unlock with new passphrase" +fscrypt lock "$dir" +_expect_failure "mkdir '$dir/subdir'" +echo pass3 | fscrypt unlock --quiet "$dir" +mkdir "$dir/subdir" diff --git a/cli-tests/t_encrypt.out b/cli-tests/t_encrypt.out new file mode 100644 index 00000000..af38299b --- /dev/null +++ b/cli-tests/t_encrypt.out @@ -0,0 +1,67 @@ + +# Try to encrypt a nonexistent directory +[ERROR] fscrypt encrypt: no such file or directory +ext4 filesystem "MNT" has 0 protectors and 0 policies + +[ERROR] fscrypt status: get encryption policy MNT/dir: file + or directory not encrypted + +# Try to encrypt a nonempty directory +[ERROR] fscrypt encrypt: MNT/dir: not an empty directory + +Encryption can only be setup on empty directories; files cannot be encrypted +in-place. Instead, encrypt an empty directory, copy the files into that +encrypted directory, and securely delete the originals with "shred". +ext4 filesystem "MNT" has 0 protectors and 0 policies + +[ERROR] fscrypt status: get encryption policy MNT/dir: file + or directory not encrypted + +# Encrypt a directory as non-root user +ext4 filesystem "MNT" has 1 protector and 1 policy + +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" + +POLICY UNLOCKED PROTECTORS +desc2 Yes desc1 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc2 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" +ext4 filesystem "MNT" has 1 protector and 1 policy + +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" + +POLICY UNLOCKED PROTECTORS +desc2 Yes desc1 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc2 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" + +# Try to encrypt an already-encrypted directory +[ERROR] fscrypt encrypt: MNT/dir: file or directory already + encrypted + +# Try to encrypt another user's directory as a non-root user +[ERROR] fscrypt encrypt: MNT/dir: you do not own this + directory + +Encryption can only be setup on directories you own, even if you have write +permission for the directory. +ext4 filesystem "MNT" has 0 protectors and 0 policies + +[ERROR] fscrypt status: get encryption policy MNT/dir: file + or directory not encrypted diff --git a/cli-tests/t_encrypt.sh b/cli-tests/t_encrypt.sh new file mode 100755 index 00000000..9f19f5db --- /dev/null +++ b/cli-tests/t_encrypt.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# General tests for 'fscrypt encrypt'. For protector-specific tests, see +# t_encrypt_custom, t_encrypt_login, and t_encrypt_raw_key. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" + +begin() +{ + _reset_filesystems + mkdir "$dir" + _print_header "$@" +} + +show_status() +{ + local encrypted=$1 + + fscrypt status "$MNT" + if $encrypted; then + fscrypt status "$dir" + else + _expect_failure "fscrypt status '$dir'" + fi +} + +begin "Try to encrypt a nonexistent directory" +_expect_failure "echo hunter2 | fscrypt encrypt --quiet '$MNT/nonexistent'" +show_status false + +begin "Try to encrypt a nonempty directory" +touch "$dir/file" +_expect_failure "echo hunter2 | fscrypt encrypt --quiet '$dir'" +show_status false + +begin "Encrypt a directory as non-root user" +chown "$TEST_USER" "$dir" +_user_do "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" +show_status true +_user_do "fscrypt status '$MNT'" +_user_do "fscrypt status '$dir'" + +_print_header "Try to encrypt an already-encrypted directory" +_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" + +begin "Try to encrypt another user's directory as a non-root user" +_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" +show_status false diff --git a/cli-tests/t_encrypt_custom.out b/cli-tests/t_encrypt_custom.out new file mode 100644 index 00000000..572529a2 --- /dev/null +++ b/cli-tests/t_encrypt_custom.out @@ -0,0 +1,55 @@ + +# Encrypt with custom passphrase protector +ext4 filesystem "MNT" has 1 protector and 1 policy + +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" + +POLICY UNLOCKED PROTECTORS +desc2 Yes desc1 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc2 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" + +# Encrypt with custom passphrase protector, interactively +spawn fscrypt encrypt MNT/dir +The following protector sources are available: +1 - Your login passphrase (pam_passphrase) +2 - A custom passphrase (custom_passphrase) +3 - A raw 256-bit key (raw_key) +Enter the source number for the new protector [2 - custom_passphrase]: 2 +Enter a name for the new protector: prot +Enter custom passphrase for protector "prot": +Confirm passphrase: +"MNT/dir" is now encrypted, unlocked, and ready for use. +ext4 filesystem "MNT" has 1 protector and 1 policy + +PROTECTOR LINKED DESCRIPTION +desc6 No custom protector "prot" + +POLICY UNLOCKED PROTECTORS +desc7 Yes desc6 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc7 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc6 No custom protector "prot" + +# Try to use a custom protector without a name +[ERROR] fscrypt encrypt: custom protectors must have a name + +Use --name=PROTECTOR_NAME to specify a protector name. +ext4 filesystem "MNT" has 0 protectors and 0 policies + +[ERROR] fscrypt status: get encryption policy MNT/dir: file + or directory not encrypted diff --git a/cli-tests/t_encrypt_custom.sh b/cli-tests/t_encrypt_custom.sh new file mode 100755 index 00000000..48cbe25e --- /dev/null +++ b/cli-tests/t_encrypt_custom.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# Test encrypting a directory using a custom_passphrase protector. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" + +begin() +{ + _reset_filesystems + mkdir "$dir" + _print_header "$1" +} + +show_status() +{ + local encrypted=$1 + + fscrypt status "$MNT" + if $encrypted; then + fscrypt status "$dir" + else + _expect_failure "fscrypt status '$dir'" + fi +} + +begin "Encrypt with custom passphrase protector" +echo hunter2 | fscrypt encrypt --quiet --name=prot "$dir" +show_status true + +begin "Encrypt with custom passphrase protector, interactively" +expect << EOF +spawn fscrypt encrypt "$dir" +expect "Enter the source number for the new protector" +send "2\r" +expect "Enter a name for the new protector:" +send "prot\r" +expect "Enter custom passphrase" +send "hunter2\r" +expect "Confirm passphrase" +send "hunter2\r" +expect eof +EOF +show_status true + +begin "Try to use a custom protector without a name" +_expect_failure "echo hunter2 | fscrypt encrypt --quiet '$dir'" +show_status false diff --git a/cli-tests/t_encrypt_login.out b/cli-tests/t_encrypt_login.out new file mode 100644 index 00000000..c6eb4630 --- /dev/null +++ b/cli-tests/t_encrypt_login.out @@ -0,0 +1,148 @@ + +# Encrypt with login protector +See "MNT/dir/fscrypt_recovery_readme.txt" for important recovery instructions! +ext4 filesystem "MNT" has 2 protectors and 1 policy + +PROTECTOR LINKED DESCRIPTION +desc1 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc2 No custom protector "Recovery passphrase for dir" + +POLICY UNLOCKED PROTECTORS +desc3 Yes desc1, desc2 +ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies + +PROTECTOR LINKED DESCRIPTION +desc1 No login protector for fscrypt-test-user +"MNT/dir" is encrypted with fscrypt. + +Policy: desc3 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 2 protectors: +PROTECTOR LINKED DESCRIPTION +desc1 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc2 No custom protector "Recovery passphrase for dir" + +# => Lock, then unlock with login passphrase +"MNT/dir" is now locked. + +# => Lock, then unlock with recovery passphrase +"MNT/dir" is now locked. + +# Encrypt with login protector, interactively +spawn fscrypt encrypt MNT/dir +The following protector sources are available: +1 - Your login passphrase (pam_passphrase) +2 - A custom passphrase (custom_passphrase) +3 - A raw 256-bit key (raw_key) +Enter the source number for the new protector [2 - custom_passphrase]: 1 +Enter login passphrase for fscrypt-test-user: +Protector is on a different filesystem! Generate a recovery passphrase (recommended)? [Y/n] y +See "MNT/dir/fscrypt_recovery_readme.txt" for important recovery instructions! +"MNT/dir" is now encrypted, unlocked, and ready for use. +ext4 filesystem "MNT" has 2 protectors and 1 policy + +PROTECTOR LINKED DESCRIPTION +desc10 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc11 No custom protector "Recovery passphrase for dir" + +POLICY UNLOCKED PROTECTORS +desc12 Yes desc10, desc11 +ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies + +PROTECTOR LINKED DESCRIPTION +desc10 No login protector for fscrypt-test-user +"MNT/dir" is encrypted with fscrypt. + +Policy: desc12 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 2 protectors: +PROTECTOR LINKED DESCRIPTION +desc10 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc11 No custom protector "Recovery passphrase for dir" + +# Encrypt with login protector as root +See "MNT/dir/fscrypt_recovery_readme.txt" for important recovery instructions! +ext4 filesystem "MNT" has 2 protectors and 1 policy + +PROTECTOR LINKED DESCRIPTION +desc19 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc20 No custom protector "Recovery passphrase for dir" + +POLICY UNLOCKED PROTECTORS +desc21 Yes desc19, desc20 +ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies + +PROTECTOR LINKED DESCRIPTION +desc19 No login protector for fscrypt-test-user +"MNT/dir" is encrypted with fscrypt. + +Policy: desc21 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 2 protectors: +PROTECTOR LINKED DESCRIPTION +desc19 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc20 No custom protector "Recovery passphrase for dir" + +# Encrypt with login protector with --no-recovery +ext4 filesystem "MNT" has 1 protector and 1 policy + +PROTECTOR LINKED DESCRIPTION +desc28 Yes (MNT_ROOT) login protector for fscrypt-test-user + +POLICY UNLOCKED PROTECTORS +desc29 Yes desc28 +ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies + +PROTECTOR LINKED DESCRIPTION +desc28 No login protector for fscrypt-test-user +"MNT/dir" is encrypted with fscrypt. + +Policy: desc29 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc28 Yes (MNT_ROOT) login protector for fscrypt-test-user + +# Encrypt with login protector on root fs (shouldn't generate a recovery passphrase) +"MNT_ROOT/dir" is encrypted with fscrypt. + +Policy: desc34 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc35 No login protector for fscrypt-test-user +ext4 filesystem "MNT_ROOT" has 1 protector and 1 policy + +PROTECTOR LINKED DESCRIPTION +desc35 No login protector for fscrypt-test-user + +POLICY UNLOCKED PROTECTORS +desc34 Yes desc35 + +# Try to give a login protector a name +[ERROR] fscrypt encrypt: login protectors do not need a name +ext4 filesystem "MNT" has 0 protectors and 0 policies + +ext4 filesystem "MNT_ROOT" has 0 protectors and 0 policies + +[ERROR] fscrypt status: get encryption policy MNT/dir: file + or directory not encrypted + +# Try to use the wrong login passphrase +[ERROR] fscrypt encrypt: incorrect login passphrase +ext4 filesystem "MNT" has 0 protectors and 0 policies + +ext4 filesystem "MNT_ROOT" has 0 protectors and 0 policies + +[ERROR] fscrypt status: get encryption policy MNT/dir: file + or directory not encrypted diff --git a/cli-tests/t_encrypt_login.sh b/cli-tests/t_encrypt_login.sh new file mode 100755 index 00000000..11a62f1b --- /dev/null +++ b/cli-tests/t_encrypt_login.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +# Test encrypting a directory using a login (pam_passphrase) protector. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" + +begin() +{ + _reset_filesystems + mkdir "$dir" + _print_header "$1" +} + +show_status() +{ + local encrypted=$1 + + fscrypt status "$MNT" + fscrypt status "$MNT_ROOT" + if $encrypted; then + fscrypt status "$dir" + else + _expect_failure "fscrypt status '$dir'" + fi +} + +begin "Encrypt with login protector" +chown "$TEST_USER" "$dir" +_user_do "echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase '$dir'" +show_status true +recovery_passphrase=$(grep -E '^ +[a-z]{20}$' "$dir/fscrypt_recovery_readme.txt" | sed 's/^ +//') +recovery_protector=$(fscrypt status "$dir" | awk '/Recovery passphrase/{print $1}') +login_protector=$(fscrypt status "$dir" | awk '/login protector/{print $1}') +_print_header "=> Lock, then unlock with login passphrase" +_user_do "fscrypt lock '$dir'" +# FIXME: should we be able to use $MNT:$login_protector here? +_user_do "echo TEST_USER_PASS | fscrypt unlock --quiet --unlock-with=$MNT_ROOT:$login_protector '$dir'" +_print_header "=> Lock, then unlock with recovery passphrase" +_user_do "fscrypt lock '$dir'" +_user_do "echo $recovery_passphrase | fscrypt unlock --quiet --unlock-with=$MNT:$recovery_protector '$dir'" + +begin "Encrypt with login protector, interactively" +chown "$TEST_USER" "$dir" +_user_do expect << EOF +spawn fscrypt encrypt "$dir" +expect "Enter the source number for the new protector" +send "1\r" +expect "Enter login passphrase" +send "TEST_USER_PASS\r" +expect "Protector is on a different filesystem! Generate a recovery passphrase (recommended)?" +send "y\r" +expect eof +EOF +show_status true + +begin "Encrypt with login protector as root" +echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --user="$TEST_USER" "$dir" +show_status true + +begin "Encrypt with login protector with --no-recovery" +chown "$TEST_USER" "$dir" +_user_do "echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --no-recovery '$dir'" +show_status true + +begin "Encrypt with login protector on root fs (shouldn't generate a recovery passphrase)" +mkdir "$MNT_ROOT/dir" +chown "$TEST_USER" "$MNT_ROOT/dir" +_user_do "echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --no-recovery '$MNT_ROOT/dir'" +fscrypt status "$MNT_ROOT/dir" +fscrypt status "$MNT_ROOT" +rmdir "$MNT_ROOT/dir" + +begin "Try to give a login protector a name" +chown "$TEST_USER" "$dir" +_user_do_and_expect_failure \ + "echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --name=prot '$dir'" +show_status false + +begin "Try to use the wrong login passphrase" +chown "$TEST_USER" "$dir" +_user_do_and_expect_failure \ + "echo wrong_passphrase | fscrypt encrypt --quiet --source=pam_passphrase '$dir'" +show_status false diff --git a/cli-tests/t_encrypt_raw_key.out b/cli-tests/t_encrypt_raw_key.out new file mode 100644 index 00000000..c7c46eb9 --- /dev/null +++ b/cli-tests/t_encrypt_raw_key.out @@ -0,0 +1,25 @@ + +# Encrypt with raw_key protector +ext4 filesystem "MNT" has 1 protector and 1 policy + +PROTECTOR LINKED DESCRIPTION +desc1 No raw key protector "prot" + +POLICY UNLOCKED PROTECTORS +desc2 Yes desc1 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc2 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc1 No raw key protector "prot" + +# Try to encrypt with raw_key protector, using wrong key length +[ERROR] fscrypt encrypt: TMPDIR/raw_key: key file must be 32 bytes +ext4 filesystem "MNT" has 0 protectors and 0 policies + +[ERROR] fscrypt status: get encryption policy MNT/dir: file + or directory not encrypted diff --git a/cli-tests/t_encrypt_raw_key.sh b/cli-tests/t_encrypt_raw_key.sh new file mode 100755 index 00000000..260b094e --- /dev/null +++ b/cli-tests/t_encrypt_raw_key.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Test encrypting a directory using a raw_key protector. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" +raw_key_file="$TMPDIR/raw_key" + +begin() +{ + _reset_filesystems + mkdir "$dir" + _print_header "$1" +} + +show_status() +{ + local encrypted=$1 + + fscrypt status "$MNT" + if $encrypted; then + fscrypt status "$dir" + else + _expect_failure "fscrypt status '$dir'" + fi +} + +begin "Encrypt with raw_key protector" +head -c 32 /dev/urandom > "$raw_key_file" +fscrypt encrypt --quiet --name=prot --source=raw_key --key="$raw_key_file" "$dir" +show_status true + +begin "Try to encrypt with raw_key protector, using wrong key length" +head -c 16 /dev/urandom > "$raw_key_file" +_expect_failure "fscrypt encrypt --quiet --name=prot --source=raw_key --key='$raw_key_file' '$dir'" +show_status false diff --git a/cli-tests/t_lock.out b/cli-tests/t_lock.out new file mode 100644 index 00000000..c0f92791 --- /dev/null +++ b/cli-tests/t_lock.out @@ -0,0 +1,82 @@ + +# Encrypt directory +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Lock directory +"MNT/dir" is now locked. + +# => filenames should be in encrypted form +cat: MNT/dir/file: No such file or directory + +# => shouldn't be able to create a subdirectory +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# Unlock directory +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +contents + +# Try to lock directory while files busy +[ERROR] fscrypt lock: some files using the key are still open + +Directory was incompletely locked because some files are still open. These files +remain accessible. Try killing any processes using files in the directory, then +re-running 'fscrypt lock'. + +# => status should be incompletely locked +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Partially (incompletely locked) + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# => open file should still be readable +contents + +# => shouldn't be able to create a new file +bash: MNT/dir/file2: Required key not available + +# Finish locking directory +"MNT/dir" is now locked. +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +cat: MNT/dir/file: No such file or directory +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# Try to lock directory while other user has unlocked +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. +[ERROR] fscrypt lock: other users have added the key too + +Directory couldn't be fully locked because other user(s) have unlocked it. If +you want to force the directory to be locked, use 'sudo fscrypt lock --all-users +DIR'. +contents +"MNT/dir" is now locked. +cat: MNT/dir/file: No such file or directory diff --git a/cli-tests/t_lock.sh b/cli-tests/t_lock.sh new file mode 100755 index 00000000..7ac17274 --- /dev/null +++ b/cli-tests/t_lock.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Test locking a directory. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" +mkdir "$dir" + +_print_header "Encrypt directory" +echo hunter2 | fscrypt encrypt --quiet --name=prot "$dir" +fscrypt status "$dir" +echo contents > "$dir/file" + +_print_header "Lock directory" +fscrypt lock "$dir" +_print_header "=> filenames should be in encrypted form" +_expect_failure "cat '$dir/file'" +_print_header "=> shouldn't be able to create a subdirectory" +_expect_failure "mkdir '$dir/subdir'" + +_print_header "Unlock directory" +echo hunter2 | fscrypt unlock "$dir" +fscrypt status "$dir" +cat "$dir/file" + +_print_header "Try to lock directory while files busy" +exec 3<"$dir/file" +_expect_failure "fscrypt lock '$dir'" +_print_header "=> status should be incompletely locked" +fscrypt status "$dir" +_print_header "=> open file should still be readable" +cat "$dir/file" +_print_header "=> shouldn't be able to create a new file" +_expect_failure "bash -c \"echo contents > '$dir/file2'\"" + +_print_header "Finish locking directory" +exec 3<&- +fscrypt lock "$dir" +fscrypt status "$dir" +_expect_failure "cat '$dir/file'" +_expect_failure "mkdir '$dir/subdir'" + +_print_header "Try to lock directory while other user has unlocked" +chown "$TEST_USER" "$dir" +_user_do "echo hunter2 | fscrypt unlock '$dir'" +_expect_failure "fscrypt lock '$dir'" +cat "$dir/file" +fscrypt lock --all-users "$dir" +_expect_failure "cat '$dir/file'" diff --git a/cli-tests/t_not_enabled.out b/cli-tests/t_not_enabled.out new file mode 100644 index 00000000..7d74bcfe --- /dev/null +++ b/cli-tests/t_not_enabled.out @@ -0,0 +1,39 @@ + +# Disable encryption on DEV + +# Try to encrypt a directory when encryption is disabled +[ERROR] fscrypt encrypt: get encryption policy MNT/dir: + encryption not enabled + +Encryption is either disabled in the kernel config, or needs to be enabled for +this filesystem. See the documentation on how to enable encryption on ext4 +systems (and the risks of doing so). + +# Try to unlock a directory when encryption is disabled +[ERROR] fscrypt unlock: get encryption policy MNT/dir: + encryption not enabled + +Encryption is either disabled in the kernel config, or needs to be enabled for +this filesystem. See the documentation on how to enable encryption on ext4 +systems (and the risks of doing so). + +# Try to lock a directory when encryption is disabled +[ERROR] fscrypt lock: get encryption policy MNT/dir: + encryption not enabled + +Encryption is either disabled in the kernel config, or needs to be enabled for +this filesystem. See the documentation on how to enable encryption on ext4 +systems (and the risks of doing so). + +# Enable encryption on DEV + +# Encrypt a directory when encryption was just enabled +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" diff --git a/cli-tests/t_not_enabled.sh b/cli-tests/t_not_enabled.sh new file mode 100755 index 00000000..3c7d22cc --- /dev/null +++ b/cli-tests/t_not_enabled.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Test that fscrypt fails when the filesystem doesn't have the encrypt feature +# enabled. Then test enabling it. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" +mkdir "$dir" + +_print_header "Disable encryption on $DEV" +count_before=$(_get_enabled_fs_count) +umount "$MNT" +_run_noisy_command "debugfs -w -R 'feature -encrypt' '$DEV'" +mount "$DEV" "$MNT" +count_after=$(_get_enabled_fs_count) +(( count_after == count_before - 1 )) || _fail "wrong enabled count" + +_print_header "Try to encrypt a directory when encryption is disabled" +_expect_failure "fscrypt encrypt '$dir'" + +_print_header "Try to unlock a directory when encryption is disabled" +_expect_failure "fscrypt unlock '$dir'" + +_print_header "Try to lock a directory when encryption is disabled" +_expect_failure "fscrypt lock '$dir'" + +_print_header "Enable encryption on $DEV" +_run_noisy_command "tune2fs -O encrypt '$DEV'" + +_print_header "Encrypt a directory when encryption was just enabled" +echo hunter2 | fscrypt encrypt --quiet --source=custom_passphrase --name=prot "$dir" +fscrypt status "$dir" diff --git a/cli-tests/t_not_supported.out b/cli-tests/t_not_supported.out new file mode 100644 index 00000000..8af840c2 --- /dev/null +++ b/cli-tests/t_not_supported.out @@ -0,0 +1,11 @@ + +# Mount tmpfs + +# Create fscrypt metadata on tmpfs +Metadata directories created at "MNT/.fscrypt". + +# Try to encrypt a directory on tmpfs +[ERROR] fscrypt encrypt: get encryption policy MNT/dir: + encryption not supported + +Encryption for this type of filesystem is not supported on this kernel version. diff --git a/cli-tests/t_not_supported.sh b/cli-tests/t_not_supported.sh new file mode 100755 index 00000000..53a096af --- /dev/null +++ b/cli-tests/t_not_supported.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Test that fscrypt fails when the filesystem doesn't support encryption. + +cd "$(dirname "$0")" +. common.sh + +_print_header "Mount tmpfs" +umount "$MNT" +mount tmpfs -t tmpfs -o size=128m "$MNT" + +_print_header "Create fscrypt metadata on tmpfs" +fscrypt setup "$MNT" + +_print_header "Try to encrypt a directory on tmpfs" +mkdir "$MNT/dir" +_expect_failure "fscrypt encrypt '$MNT/dir'" diff --git a/cli-tests/t_passphrase_hashing.out b/cli-tests/t_passphrase_hashing.out new file mode 100644 index 00000000..e69de29b diff --git a/cli-tests/t_passphrase_hashing.sh b/cli-tests/t_passphrase_hashing.sh new file mode 100755 index 00000000..a67dd7c1 --- /dev/null +++ b/cli-tests/t_passphrase_hashing.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Test that the passphrase hashing seems to take long enough. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" + +# Test encrypting 5 dirs with default of 1s. +fscrypt setup --force --quiet +start_time=$(date +%s) +for i in $(seq 5); do + rm -rf "$dir" + mkdir "$dir" + echo hunter2 | fscrypt encrypt --quiet --name="prot$i" "$dir" +done +end_time=$(date +%s) +elapsed=$((end_time - start_time)) +if (( elapsed <= 3 )); then + _fail "Passphrase hashing was much faster than expected! (expected about 5 x 1 == 5s, got ${elapsed}s)" +fi + +# Test encrypting 1 dir with difficulty overridden to 5s. +fscrypt setup --force --quiet --time=5s +start_time=$(date +%s) +rm -rf "$dir" +mkdir "$dir" +echo hunter2 | fscrypt encrypt --quiet --name=prot6 "$dir" +end_time=$(date +%s) +elapsed=$((end_time - start_time)) +if (( elapsed <= 3 )); then + _fail "Passphrase hashing was much faster than expected! (expected about 5s, got ${elapsed}s)" +fi diff --git a/cli-tests/t_setup.out b/cli-tests/t_setup.out new file mode 100644 index 00000000..e1606ba6 --- /dev/null +++ b/cli-tests/t_setup.out @@ -0,0 +1,49 @@ + +# fscrypt setup creates fscrypt.conf +Defaulting to policy_version 2 because kernel supports it. +Customizing passphrase hashing difficulty for this system... +Created global config file at "FSCRYPT_CONF". +Skipping creating MNT_ROOT/.fscrypt because it already exists. + +# fscrypt setup creates fscrypt.conf and /.fscrypt +Defaulting to policy_version 2 because kernel supports it. +Customizing passphrase hashing difficulty for this system... +Created global config file at "FSCRYPT_CONF". +Metadata directories created at "MNT_ROOT/.fscrypt". + +# fscrypt setup when fscrypt.conf already exists (cancel) +Replace "FSCRYPT_CONF"? [y/N] [ERROR] fscrypt setup: operation canceled + +# fscrypt setup when fscrypt.conf already exists (cancel 2) +Replace "FSCRYPT_CONF"? [y/N] [ERROR] fscrypt setup: operation canceled + +# fscrypt setup when fscrypt.conf already exists (accept) +Replace "FSCRYPT_CONF"? [y/N] Defaulting to policy_version 2 because kernel supports it. +Customizing passphrase hashing difficulty for this system... +Created global config file at "FSCRYPT_CONF". +Skipping creating MNT_ROOT/.fscrypt because it already exists. + +# fscrypt setup --quiet when fscrypt.conf already exists +[ERROR] fscrypt setup: operation would be destructive + +Use --force to automatically run destructive operations. + +# fscrypt setup --quiet --force when fscrypt.conf already exists + +# fscrypt setup filesystem +Metadata directories created at "MNT/.fscrypt". + +# fscrypt setup filesystem (already set up) +[ERROR] fscrypt setup: filesystem MNT: already setup for use + with fscrypt + +# no config file +[ERROR] fscrypt setup: global config file does not exist + +Run "sudo fscrypt setup" to create the file. + +# bad config file +[ERROR] fscrypt setup: invalid character 'b' looking for beginning of value: + global config file has invalid data + +Run "sudo fscrypt setup" to recreate the file. diff --git a/cli-tests/t_setup.sh b/cli-tests/t_setup.sh new file mode 100755 index 00000000..a8a62a37 --- /dev/null +++ b/cli-tests/t_setup.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Test 'fscrypt setup'. + +cd "$(dirname "$0")" +. common.sh + +# global setup + +_print_header "fscrypt setup creates fscrypt.conf" +rm -f "$FSCRYPT_CONF" +fscrypt setup --time=1ms + +_print_header "fscrypt setup creates fscrypt.conf and /.fscrypt" +_rm_metadata "$MNT_ROOT" +rm -f "$FSCRYPT_CONF" +fscrypt setup --time=1ms +[ -e "$MNT_ROOT/.fscrypt" ] + +_print_header "fscrypt setup when fscrypt.conf already exists (cancel)" +_expect_failure "echo | fscrypt setup --time=1ms" + +_print_header "fscrypt setup when fscrypt.conf already exists (cancel 2)" +_expect_failure "echo N | fscrypt setup --time=1ms" + +_print_header "fscrypt setup when fscrypt.conf already exists (accept)" +echo y | fscrypt setup --time=1ms + +_print_header "fscrypt setup --quiet when fscrypt.conf already exists" +_expect_failure "fscrypt setup --quiet --time=1ms" + +_print_header "fscrypt setup --quiet --force when fscrypt.conf already exists" +fscrypt setup --quiet --force --time=1ms + + +# filesystem setup + +_print_header "fscrypt setup filesystem" +_rm_metadata "$MNT" +fscrypt setup "$MNT" +[ -e "$MNT/.fscrypt" ] + +_print_header "fscrypt setup filesystem (already set up)" +_expect_failure "fscrypt setup '$MNT'" + +_print_header "no config file" +rm -f "$FSCRYPT_CONF" +_expect_failure "fscrypt setup '$MNT'" + +_print_header "bad config file" +echo bad > "$FSCRYPT_CONF" +_expect_failure "fscrypt setup '$MNT'" diff --git a/cli-tests/t_status.out b/cli-tests/t_status.out new file mode 100644 index 00000000..b0367129 --- /dev/null +++ b/cli-tests/t_status.out @@ -0,0 +1,44 @@ + +# Get status of setup mountpoint via global status +ext4 supported Yes +ext4 supported Yes + +# Get status of setup mountpoint +ext4 filesystem "MNT" has 0 protectors and 0 policies + +ext4 filesystem "MNT" has 0 protectors and 0 policies + + +# Get status of unencrypted directory on setup mountpoint +[ERROR] fscrypt status: get encryption policy MNT/dir: file + or directory not encrypted +[ERROR] fscrypt status: get encryption policy MNT/dir: file + or directory not encrypted + +# Remove fscrypt metadata from MNT + +# Check enabled / setup count again + +# Get status of not-setup mounntpoint via global status +ext4 supported No +ext4 supported No + +# Get status of not-setup mountpoint +[ERROR] fscrypt status: filesystem MNT: not setup for use + with fscrypt + +Run "fscrypt setup MOUNTPOINT" to use fscrypt on this filesystem. +[ERROR] fscrypt status: filesystem MNT: not setup for use + with fscrypt + +Run "fscrypt setup MOUNTPOINT" to use fscrypt on this filesystem. + +# Get status of unencrypted directory on not-setup mountpoint +[ERROR] fscrypt status: filesystem MNT: not setup for use + with fscrypt + +Run "fscrypt setup MOUNTPOINT" to use fscrypt on this filesystem. +[ERROR] fscrypt status: filesystem MNT: not setup for use + with fscrypt + +Run "fscrypt setup MOUNTPOINT" to use fscrypt on this filesystem. diff --git a/cli-tests/t_status.sh b/cli-tests/t_status.sh new file mode 100755 index 00000000..cfc36168 --- /dev/null +++ b/cli-tests/t_status.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Test getting global, filesystem, and unencrypted directory status +# when the filesystem is or isn't set up for fscrypt. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" +mkdir "$dir" + +filter_mnt_status() +{ + awk '$1 == "'"$MNT"'" { print $3, $4, $5 }' +} + +# Initially, $MNT has encryption enabled and fscrypt setup. + +enabled_count1=$(_get_enabled_fs_count) +setup_count1=$(_get_setup_fs_count) + + +_print_header "Get status of setup mountpoint via global status" +fscrypt status | filter_mnt_status +_user_do "fscrypt status" | filter_mnt_status + +_print_header "Get status of setup mountpoint" +fscrypt status "$MNT" +_user_do "fscrypt status '$MNT'" + +_print_header "Get status of unencrypted directory on setup mountpoint" +_expect_failure "fscrypt status '$dir'" +_user_do_and_expect_failure "fscrypt status '$dir'" + +_print_header "Remove fscrypt metadata from $MNT" +_rm_metadata "$MNT" + +# Now, $MNT has encryption enabled but fscrypt *not* setup. + +_print_header "Check enabled / setup count again" +enabled_count2=$(_get_enabled_fs_count) +setup_count2=$(_get_setup_fs_count) +(( enabled_count2 == enabled_count1 )) || _fail "wrong enabled count" +(( setup_count2 == setup_count1 - 1 )) || _fail "wrong setup count" + +_print_header "Get status of not-setup mounntpoint via global status" +fscrypt status | filter_mnt_status +_user_do "fscrypt status" | filter_mnt_status + +_print_header "Get status of not-setup mountpoint" +_expect_failure "fscrypt status '$MNT'" +_user_do_and_expect_failure "fscrypt status '$MNT'" + +_print_header "Get status of unencrypted directory on not-setup mountpoint" +_expect_failure "fscrypt status '$dir'" +_user_do_and_expect_failure "fscrypt status '$dir'" diff --git a/cli-tests/t_unlock.out b/cli-tests/t_unlock.out new file mode 100644 index 00000000..29a10dd8 --- /dev/null +++ b/cli-tests/t_unlock.out @@ -0,0 +1,101 @@ + +# Encrypt directory with --skip-unlock + +# => Check dir status +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +touch: cannot touch 'MNT/dir/file': Required key not available + +# => Get policy status via mount: +desc1 No desc2 + +# Unlock directory +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. + +# => Check dir status +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# => Get policy status via mount: +desc1 Yes desc2 + +# Lock by cycling mount + +# => Check dir status +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# => Get policy status via mount: +desc1 No desc2 + +# Try to unlock with wrong passphrase +[ERROR] fscrypt unlock: incorrect key provided +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Unlock directory +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. + +# => Check dir status +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +contents + +# => Get policy status via mount: +desc1 Yes desc2 + +# Try to unlock with corrupt policy metadata +[ERROR] fscrypt unlock: MNT/dir: system error: missing + policy metadata for encrypted directory + +This file or directory has either been encrypted with another tool (such as +e4crypt) or the corresponding filesystem metadata has been deleted. + +# Try to unlock with missing policy metadata +[ERROR] fscrypt unlock: MNT/dir: system error: missing + policy metadata for encrypted directory + +This file or directory has either been encrypted with another tool (such as +e4crypt) or the corresponding filesystem metadata has been deleted. + +# Try to unlock with missing protector metadata +[ERROR] fscrypt unlock: could not load any protectors + +You may need to mount a linked filesystem. Run with --verbose for more +information. diff --git a/cli-tests/t_unlock.sh b/cli-tests/t_unlock.sh new file mode 100755 index 00000000..3dfba411 --- /dev/null +++ b/cli-tests/t_unlock.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Test unlocking a directory. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" +mkdir "$dir" + +_print_header "Encrypt directory with --skip-unlock" +echo hunter2 | fscrypt encrypt --quiet --name=prot --skip-unlock "$dir" +_print_header "=> Check dir status" +fscrypt status "$dir" +_expect_failure "touch '$dir/file'" +policy=$(fscrypt status "$dir" | awk '/Policy:/{print $2}') +_print_header "=> Get policy status via mount:" +fscrypt status "$MNT" | grep "^$policy" + +_print_header "Unlock directory" +echo hunter2 | fscrypt unlock "$dir" +_print_header "=> Check dir status" +fscrypt status "$dir" +echo contents > "$dir/file" +_print_header "=> Get policy status via mount:" +fscrypt status "$MNT" | grep "^$policy" + +_print_header "Lock by cycling mount" +umount "$MNT" +mount "$DEV" "$MNT" +_print_header "=> Check dir status" +fscrypt status "$dir" +_expect_failure "mkdir '$dir/subdir'" +_print_header "=> Get policy status via mount:" +fscrypt status "$MNT" | grep "^$policy" + +_print_header "Try to unlock with wrong passphrase" +_expect_failure "echo bad | fscrypt unlock --quiet '$dir'" +fscrypt status "$dir" + +_print_header "Unlock directory" +echo hunter2 | fscrypt unlock "$dir" +_print_header "=> Check dir status" +fscrypt status "$dir" +cat "$dir/file" +_print_header "=> Get policy status via mount:" +fscrypt status "$MNT" | grep "^$policy" + +_print_header "Try to unlock with corrupt policy metadata" +umount "$MNT" +mount "$DEV" "$MNT" +echo bad > "$MNT/.fscrypt/policies/$policy" +_expect_failure "echo hunter2 | fscrypt unlock '$dir'" + +_reset_filesystems + +_print_header "Try to unlock with missing policy metadata" +mkdir "$dir" +echo hunter2 | fscrypt encrypt --quiet --name=prot --skip-unlock "$dir" +rm "$MNT"/.fscrypt/policies/* +_expect_failure "echo hunter2 | fscrypt unlock '$dir'" + +_reset_filesystems + +_print_header "Try to unlock with missing protector metadata" +mkdir "$dir" +echo hunter2 | fscrypt encrypt --quiet --name=prot --skip-unlock "$dir" +rm "$MNT"/.fscrypt/protectors/* +_expect_failure "echo hunter2 | fscrypt unlock '$dir'" diff --git a/cli-tests/t_v1_policy.out b/cli-tests/t_v1_policy.out new file mode 100644 index 00000000..747cf81d --- /dev/null +++ b/cli-tests/t_v1_policy.out @@ -0,0 +1,98 @@ + +# Set policy_version 1 + +# Try to encrypt as root +[ERROR] fscrypt encrypt: user must be specified when run as root + +When running this command as root, you usually still want to provision/remove +keys for a normal user's keyring and use a normal user's login passphrase as a +protector (so the corresponding files will be accessible for that user). This +can be done with --user=USERNAME. To use the root user's keyring or passphrase, +use --user=root. + +# Try to use --user=root as user +[ERROR] fscrypt encrypt: setting uids: operation not permitted: could not access + user keyring + +You can only use --user=USERNAME to access the user keyring of another user if +you are running as root. + +# Try to encrypt without user keyring in session keyring +[ERROR] fscrypt encrypt: user keyring not linked into session keyring + +This is usually the result of a bad PAM configuration. Either correct the +problem in your PAM stack, enable pam_keyinit.so, or run "keyctl link @u @s". + +# Encrypt a directory + +# Get dir status as user +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Get dir status as root +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Create files in v1-encrypted directory + +# Try to lock v1-encrypted directory as user +[ERROR] fscrypt lock: inode cache can only be dropped as root + +Either this command should be run as root to properly clear the inode cache, or +it should be run with --drop-caches=false (this may leave encrypted files and +directories in an accessible state). +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Try to lock v1-encrypted directory as root without --user +[ERROR] fscrypt lock: user must be specified when run as root + +When running this command as root, you usually still want to provision/remove +keys for a normal user's keyring and use a normal user's login passphrase as a +protector (so the corresponding files will be accessible for that user). This +can be done with --user=USERNAME. To use the root user's keyring or passphrase, +use --user=root. +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Lock v1-encrypted directory +Encrypted data removed from filesystem cache. +"MNT/dir" is now locked. +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +cat: MNT/dir/file: No such file or directory diff --git a/cli-tests/t_v1_policy.sh b/cli-tests/t_v1_policy.sh new file mode 100755 index 00000000..1ebfae55 --- /dev/null +++ b/cli-tests/t_v1_policy.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Test using v1 encryption policies (deprecated). + +cd "$(dirname "$0")" +. common.sh + +_setup_session_keyring + +dir="$MNT/dir" +mkdir "$dir" +chown "$TEST_USER" "$dir" + +_print_header "Set policy_version 1" +sed -i 's/"policy_version": "2"/"policy_version": "1"/' "$FSCRYPT_CONF" + +_print_header "Try to encrypt as root" +_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" + +_print_header "Try to use --user=root as user" +_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot --user=root '$dir'" + +_print_header "Try to encrypt without user keyring in session keyring" +_user_do "keyctl unlink @u @s" +_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" +_user_do "keyctl link @u @s" + +_print_header "Encrypt a directory" +_user_do "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" + +_print_header "Get dir status as user" +_user_do "fscrypt status '$dir'" + +_print_header "Get dir status as root" +fscrypt status "$dir" + +_print_header "Create files in v1-encrypted directory" +echo contents > "$dir/file" +mkdir "$dir/subdir" +ln -s target "$dir/symlink" + +# Due to the limitations of the v1 key management mechanism, 'fscrypt lock' only +# works when run as root and with the --user argument. + +_print_header "Try to lock v1-encrypted directory as user" +_user_do_and_expect_failure "fscrypt lock '$dir'" +_user_do "fscrypt status '$dir'" + +_print_header "Try to lock v1-encrypted directory as root without --user" +_expect_failure "fscrypt lock '$dir'" +_user_do "fscrypt status '$dir'" + +_print_header "Lock v1-encrypted directory" +fscrypt lock "$dir" --user="$TEST_USER" +_user_do "fscrypt status '$dir'" +_expect_failure "cat '$dir/file'" diff --git a/cli-tests/t_v1_policy_fs_keyring.out b/cli-tests/t_v1_policy_fs_keyring.out new file mode 100644 index 00000000..ca32ec10 --- /dev/null +++ b/cli-tests/t_v1_policy_fs_keyring.out @@ -0,0 +1,75 @@ + +# Enable v1 policies with fs keyring + +# Try to encrypt directory as user +[ERROR] fscrypt encrypt: root is required to add/remove v1 encryption policy + keys to/from filesystem + +Either this command should be run as root, or you should set +'"use_fs_keyring_for_v1_policies": false' in /etc/fscrypt.conf, or you should +re-create your encrypted directories using v2 encryption policies rather than v1 +(this requires setting '"policy_version": "2"' in the "options" section of +/etc/fscrypt.conf). +[ERROR] fscrypt status: get encryption policy MNT/dir: file + or directory not encrypted + +# Encrypt directory as user with --skip-unlock +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# Try to unlock directory as user +[ERROR] fscrypt unlock: root is required to add/remove v1 encryption policy keys + to/from filesystem + +Either this command should be run as root, or you should set +'"use_fs_keyring_for_v1_policies": false' in /etc/fscrypt.conf, or you should +re-create your encrypted directories using v2 encryption policies rather than v1 +(this requires setting '"policy_version": "2"' in the "options" section of +/etc/fscrypt.conf). + +# Unlock directory as root +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Try to lock directory as user +[ERROR] fscrypt lock: root is required to add/remove v1 encryption policy keys + to/from filesystem + +Either this command should be run as root, or you should set +'"use_fs_keyring_for_v1_policies": false' in /etc/fscrypt.conf, or you should +re-create your encrypted directories using v2 encryption policies rather than v1 +(this requires setting '"policy_version": "2"' in the "options" section of +/etc/fscrypt.conf). + +# Lock directory as root +"MNT/dir" is now locked. +cat: MNT/dir/file: No such file or directory +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Check that user can access file when directory is unlocked by root +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. +contents diff --git a/cli-tests/t_v1_policy_fs_keyring.sh b/cli-tests/t_v1_policy_fs_keyring.sh new file mode 100755 index 00000000..bf1191a2 --- /dev/null +++ b/cli-tests/t_v1_policy_fs_keyring.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Test using v1 encryption policies (deprecated) with +# use_fs_keyring_for_v1_policies = true. + +# This works similar to v2 policies, except locking and unlocking (including +# 'fscrypt encrypt' without --skip-unlock) will only work as root. + +cd "$(dirname "$0")" +. common.sh + +_print_header "Enable v1 policies with fs keyring" +sed -e 's/"use_fs_keyring_for_v1_policies": false/"use_fs_keyring_for_v1_policies": true/' \ + -e 's/"policy_version": "2"/"policy_version": "1"/' \ + -i "$FSCRYPT_CONF" + +dir="$MNT/dir" +mkdir "$dir" +chown "$TEST_USER" "$dir" + +_print_header "Try to encrypt directory as user" +_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" +_expect_failure "fscrypt status '$dir'" + +_print_header "Encrypt directory as user with --skip-unlock" +_user_do "echo hunter2 | fscrypt encrypt --quiet --name=prot --skip-unlock '$dir'" +fscrypt status "$dir" +_expect_failure "mkdir '$dir/subdir'" + +_print_header "Try to unlock directory as user" +_user_do_and_expect_failure "echo hunter2 | fscrypt unlock '$dir'" + +_print_header "Unlock directory as root" +echo hunter2 | fscrypt unlock "$dir" +mkdir "$dir/subdir" +echo contents > "$dir/file" +fscrypt status "$dir" + +_print_header "Try to lock directory as user" +_user_do_and_expect_failure "fscrypt lock '$dir'" + +_print_header "Lock directory as root" +fscrypt lock "$dir" +_expect_failure "cat '$dir/file'" +fscrypt status "$dir" + +_print_header "Check that user can access file when directory is unlocked by root" +echo hunter2 | fscrypt unlock "$dir" +_user_do "cat '$dir/file'" diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go index f84102e9..ec755846 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -73,12 +73,13 @@ func setupAction(c *cli.Context) error { if err := createGlobalConfig(c.App.Writer, actions.ConfigFileLocation); err != nil { return newExitError(c, err) } - if err := setupFilesystem(c.App.Writer, "/"); err != nil { + if err := setupFilesystem(c.App.Writer, actions.LoginProtectorMountpoint); err != nil { if errors.Cause(err) != filesystem.ErrAlreadySetup { return newExitError(c, err) } fmt.Fprintf(c.App.Writer, - "Skipping creating /.fscrypt because it already exists.\n") + "Skipping creating %s because it already exists.\n", + filepath.Join(actions.LoginProtectorMountpoint, ".fscrypt")) } case 1: // Case (2) - filesystem setup diff --git a/cmd/fscrypt/fscrypt.go b/cmd/fscrypt/fscrypt.go index e260f7f6..aa5b6f42 100644 --- a/cmd/fscrypt/fscrypt.go +++ b/cmd/fscrypt/fscrypt.go @@ -31,6 +31,9 @@ import ( "os" "github.com/urfave/cli" + + "github.com/google/fscrypt/actions" + "github.com/google/fscrypt/filesystem" ) // Current version of the program (set by Makefile) @@ -41,6 +44,16 @@ func main() { cli.CommandHelpTemplate = commandHelpTemplate cli.SubcommandHelpTemplate = subcommandHelpTemplate + if conffile := os.Getenv("FSCRYPT_CONF"); conffile != "" { + actions.ConfigFileLocation = conffile + } + if rootmnt := os.Getenv("FSCRYPT_ROOT_MNT"); rootmnt != "" { + actions.LoginProtectorMountpoint = rootmnt + } + if consistent := os.Getenv("FSCRYPT_CONSISTENT_OUTPUT"); consistent == "1" { + filesystem.SortDescriptorsByLastMtime = true + } + // Create our command line application app := cli.NewApp() app.Usage = shortUsage diff --git a/cmd/fscrypt/protector.go b/cmd/fscrypt/protector.go index 25f19849..6d35d9e2 100644 --- a/cmd/fscrypt/protector.go +++ b/cmd/fscrypt/protector.go @@ -51,8 +51,10 @@ func createProtectorFromContext(ctx *actions.Context) (*actions.Protector, error // We only want to create new login protectors on the root filesystem. // So we make a new context if necessary. - if ctx.Config.Source == metadata.SourceType_pam_passphrase && ctx.Mount.Path != "/" { - log.Printf("creating login protector on %q instead of %q", "/", ctx.Mount.Path) + if ctx.Config.Source == metadata.SourceType_pam_passphrase && + ctx.Mount.Path != actions.LoginProtectorMountpoint { + log.Printf("creating login protector on %q instead of %q", + actions.LoginProtectorMountpoint, ctx.Mount.Path) if ctx, err = modifiedContext(ctx); err != nil { return nil, err } @@ -84,7 +86,7 @@ func expandedProtectorOptions(ctx *actions.Context) ([]*actions.ProtectorOption, } // Do nothing different if we are at the root, or cannot load the root. - if ctx.Mount.Path == "/" { + if ctx.Mount.Path == actions.LoginProtectorMountpoint { return options, nil } if ctx, err = modifiedContext(ctx); err != nil { @@ -117,10 +119,10 @@ func expandedProtectorOptions(ctx *actions.Context) ([]*actions.ProtectorOption, return options, nil } -// modifiedContext returns a copy of ctx with the mountpoint replaced by that of -// the root filesystem. +// modifiedContext returns a copy of ctx with the mountpoint replaced by +// LoginProtectorMountpoint. func modifiedContext(ctx *actions.Context) (*actions.Context, error) { - mnt, err := filesystem.GetMount("/") + mnt, err := filesystem.GetMount(actions.LoginProtectorMountpoint) if err != nil { return nil, err } diff --git a/filesystem/filesystem.go b/filesystem/filesystem.go index ecdeae1f..e01f9ff9 100644 --- a/filesystem/filesystem.go +++ b/filesystem/filesystem.go @@ -38,7 +38,9 @@ import ( "log" "os" "path/filepath" + "sort" "strings" + "time" "github.com/golang/protobuf/proto" "github.com/pkg/errors" @@ -63,6 +65,11 @@ var ( ErrCorruptMetadata = util.SystemError("on-disk metadata is corrupt") ) +// SortDescriptorsByLastMtime indicates whether descriptors are sorted by last +// modification time when being listed. This can be set to true to get +// consistent output for testing. +var SortDescriptorsByLastMtime = false + // Mount contains information for a specific mounted filesystem. // Path - Absolute path where the directory is mounted // FilesystemType - Type of the mounted filesystem, e.g. "ext4" @@ -534,6 +541,37 @@ func (m *Mount) ListPolicies() ([]string, error) { return policies, m.err(err) } +type namesAndTimes struct { + names []string + times []time.Time +} + +func (c namesAndTimes) Len() int { + return len(c.names) +} + +func (c namesAndTimes) Less(i, j int) bool { + return c.times[i].Before(c.times[j]) +} + +func (c namesAndTimes) Swap(i, j int) { + c.names[i], c.names[j] = c.names[j], c.names[i] + c.times[i], c.times[j] = c.times[j], c.times[i] +} + +func sortFileListByLastMtime(directoryPath string, names []string) error { + c := namesAndTimes{names: names, times: make([]time.Time, len(names))} + for i, name := range names { + fi, err := os.Lstat(filepath.Join(directoryPath, name)) + if err != nil { + return err + } + c.times[i] = fi.ModTime() + } + sort.Sort(c) + return nil +} + // listDirectory returns a list of descriptors for a metadata directory, // including files which are links to other filesystem's metadata. func (m *Mount) listDirectory(directoryPath string) ([]string, error) { @@ -549,6 +587,12 @@ func (m *Mount) listDirectory(directoryPath string) ([]string, error) { return nil, err } + if SortDescriptorsByLastMtime { + if err := sortFileListByLastMtime(directoryPath, names); err != nil { + return nil, err + } + } + descriptors := make([]string, 0, len(names)) for _, name := range names { // Be sure to include links as well diff --git a/pam_fscrypt/run_fscrypt.go b/pam_fscrypt/run_fscrypt.go index 3d0acb13..ef7ff924 100644 --- a/pam_fscrypt/run_fscrypt.go +++ b/pam_fscrypt/run_fscrypt.go @@ -132,7 +132,8 @@ func setupLogging(args map[string]bool) io.Writer { // one exists. This protector descriptor (if found) will be cached in the pam // data, under descriptorLabel. func loginProtector(handle *pam.Handle) (*actions.Protector, error) { - ctx, err := actions.NewContextFromMountpoint("/", handle.PamUser) + ctx, err := actions.NewContextFromMountpoint(actions.LoginProtectorMountpoint, + handle.PamUser) if err != nil { return nil, err }