aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Biggers <ebiggers@google.com>2020-05-09 15:15:12 -0700
committerGitHub <noreply@github.com>2020-05-09 15:15:12 -0700
commit338347ac4766f899fdc471d57f293798ff0e6c29 (patch)
tree8f5c0969a49a396d60c33a324834d92d9911a240
parent1aef2541a434bd9e88ebd52be72f13d56c5ef748 (diff)
parente68d65c440125ff1e47627abf1fc5a97f700d38d (diff)
Merge pull request #218 from ebiggers/cli-tests
Add tests for command-line interface Add tests that directly test the fscrypt command-line tool. See cli-tests/README.md for information about the test framework. The following test scripts are included: * t_change_passphrase * t_encrypt_custom * t_encrypt_login * t_encrypt_raw_key * t_encrypt * t_lock * t_not_enabled * t_not_supported * t_passphrase_hashing * t_setup * t_status * t_unlock * t_v1_policy_fs_keyring * t_v1_policy Unfortunately, we can't actually make Travis CI run these tests yet because they need kernel v5.4 or later, and Travis CI doesn't support an Ubuntu version that has that yet. But for now, they can be run manually using make cli-test.
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml2
-rw-r--r--CONTRIBUTING.md26
-rw-r--r--Makefile12
-rw-r--r--actions/protector.go4
-rw-r--r--cli-tests/README.md67
-rw-r--r--cli-tests/common.sh154
-rwxr-xr-xcli-tests/run.sh299
-rw-r--r--cli-tests/t_change_passphrase.out32
-rwxr-xr-xcli-tests/t_change_passphrase.sh60
-rw-r--r--cli-tests/t_encrypt.out67
-rwxr-xr-xcli-tests/t_encrypt.sh51
-rw-r--r--cli-tests/t_encrypt_custom.out55
-rwxr-xr-xcli-tests/t_encrypt_custom.sh50
-rw-r--r--cli-tests/t_encrypt_login.out148
-rwxr-xr-xcli-tests/t_encrypt_login.sh86
-rw-r--r--cli-tests/t_encrypt_raw_key.out25
-rwxr-xr-xcli-tests/t_encrypt_raw_key.sh38
-rw-r--r--cli-tests/t_lock.out82
-rwxr-xr-xcli-tests/t_lock.sh51
-rw-r--r--cli-tests/t_not_enabled.out39
-rwxr-xr-xcli-tests/t_not_enabled.sh34
-rw-r--r--cli-tests/t_not_supported.out11
-rwxr-xr-xcli-tests/t_not_supported.sh17
-rw-r--r--cli-tests/t_passphrase_hashing.out0
-rwxr-xr-xcli-tests/t_passphrase_hashing.sh34
-rw-r--r--cli-tests/t_setup.out49
-rwxr-xr-xcli-tests/t_setup.sh52
-rw-r--r--cli-tests/t_status.out44
-rwxr-xr-xcli-tests/t_status.sh56
-rw-r--r--cli-tests/t_unlock.out101
-rwxr-xr-xcli-tests/t_unlock.sh69
-rw-r--r--cli-tests/t_v1_policy.out98
-rwxr-xr-xcli-tests/t_v1_policy.sh56
-rw-r--r--cli-tests/t_v1_policy_fs_keyring.out75
-rwxr-xr-xcli-tests/t_v1_policy_fs_keyring.sh49
-rw-r--r--cmd/fscrypt/commands.go5
-rw-r--r--cmd/fscrypt/fscrypt.go13
-rw-r--r--cmd/fscrypt/protector.go14
-rw-r--r--filesystem/filesystem.go44
-rw-r--r--pam_fscrypt/run_fscrypt.go3
41 files changed, 2157 insertions, 16 deletions
diff --git a/.gitignore b/.gitignore
index 666fe16..ca3e70e 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 0de7c6e..1a1686d 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 ddd456c..d5be721 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 d110dd8..54c381d 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 4bd7c15..dab9c27 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 0000000..dfcc1d0
--- /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 0000000..fcebfd6
--- /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 0000000..909b645
--- /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 0000000..747ed89
--- /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 0000000..204512d
--- /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 0000000..af38299
--- /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 0000000..9f19f5d
--- /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 0000000..572529a
--- /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 0000000..48cbe25
--- /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 0000000..c6eb463
--- /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 0000000..11a62f1
--- /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 0000000..c7c46eb
--- /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 0000000..260b094
--- /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 0000000..c0f9279
--- /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 0000000..7ac1727
--- /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 0000000..7d74bcf
--- /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 0000000..3c7d22c
--- /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 0000000..8af840c
--- /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 0000000..53a096a
--- /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 0000000..e69de29
--- /dev/null
+++ b/cli-tests/t_passphrase_hashing.out
diff --git a/cli-tests/t_passphrase_hashing.sh b/cli-tests/t_passphrase_hashing.sh
new file mode 100755
index 0000000..a67dd7c
--- /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 0000000..e1606ba
--- /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 0000000..a8a62a3
--- /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 0000000..b036712
--- /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 0000000..cfc3616
--- /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 0000000..29a10dd
--- /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 0000000..3dfba41
--- /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 0000000..747cf81
--- /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 0000000..1ebfae5
--- /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 0000000..ca32ec1
--- /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 0000000..bf1191a
--- /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 f84102e..ec75584 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 e260f7f..aa5b6f4 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 25f1984..6d35d9e 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 ecdeae1..e01f9ff 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 3d0acb1..ef7ff92 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
}