aboutsummaryrefslogtreecommitdiff
path: root/cli-tests
diff options
context:
space:
mode:
Diffstat (limited to 'cli-tests')
-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
31 files changed, 2049 insertions, 0 deletions
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'"