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.sh187
-rwxr-xr-xcli-tests/run.sh307
-rw-r--r--cli-tests/t_change_passphrase.out32
-rwxr-xr-xcli-tests/t_change_passphrase.sh60
-rw-r--r--cli-tests/t_encrypt.out106
-rwxr-xr-xcli-tests/t_encrypt.sh54
-rw-r--r--cli-tests/t_encrypt_custom.out58
-rwxr-xr-xcli-tests/t_encrypt_custom.sh50
-rw-r--r--cli-tests/t_encrypt_login.out209
-rwxr-xr-xcli-tests/t_encrypt_login.sh104
-rw-r--r--cli-tests/t_encrypt_raw_key.out74
-rwxr-xr-xcli-tests/t_encrypt_raw_key.sh53
-rw-r--r--cli-tests/t_lock.out102
-rwxr-xr-xcli-tests/t_lock.sh65
-rw-r--r--cli-tests/t_metadata.out19
-rwxr-xr-xcli-tests/t_metadata.sh36
-rw-r--r--cli-tests/t_not_enabled.out63
-rwxr-xr-xcli-tests/t_not_enabled.sh39
-rw-r--r--cli-tests/t_not_supported.out9
-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.out51
-rwxr-xr-xcli-tests/t_setup.sh52
-rw-r--r--cli-tests/t_single_user.out30
-rwxr-xr-xcli-tests/t_single_user.sh55
-rw-r--r--cli-tests/t_status.out50
-rwxr-xr-xcli-tests/t_status.sh56
-rw-r--r--cli-tests/t_unlock.out116
-rwxr-xr-xcli-tests/t_unlock.sh82
-rw-r--r--cli-tests/t_v1_policy.out144
-rwxr-xr-xcli-tests/t_v1_policy.sh72
-rw-r--r--cli-tests/t_v1_policy_fs_keyring.out75
-rwxr-xr-xcli-tests/t_v1_policy_fs_keyring.sh49
35 files changed, 2577 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..1d7b17b
--- /dev/null
+++ b/cli-tests/common.sh
@@ -0,0 +1,187 @@
+#!/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()
+{
+ echo 1>&2 "ERROR: $1"
+ exit 1
+}
+
+# Runs a shell command and expects that it fails.
+_expect_failure()
+{
+ 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()
+{
+ 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
+
+ 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
+
+ count=$(fscrypt status | awk '/filesystems supporting encryption/ { print $4 }')
+ if [ -z "$count" ]; then
+ _fail "encryption support status line not found"
+ fi
+ echo "$count"
+}
+
+# Gets the descriptor of the given protector.
+_get_protector_descriptor()
+{
+ local mnt=$1
+ local source=$2
+
+ case $source in
+ custom)
+ local name=$3
+ local description="custom protector \\\"$name\\\""
+ ;;
+ login)
+ local user=$3
+ local description="login protector for $user"
+ ;;
+ *)
+ _fail "Unknown protector source $source"
+ esac
+
+ local descriptor
+ descriptor=$(fscrypt status "$mnt" |
+ awk -F ' *' '{ if ($3 == "'"$description"'") print $1 }')
+ if [ -z "$descriptor" ]; then
+ _fail "Can't find $description on $mnt"
+ fi
+ echo "$descriptor"
+}
+
+# Gets the descriptor of the login protector for $TEST_USER.
+_get_login_descriptor()
+{
+ _get_protector_descriptor "$MNT_ROOT" login "$TEST_USER"
+}
+
+# Prints the number of filesystems that have fscrypt metadata.
+_get_setup_fs_count()
+{
+ local count
+
+ 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()
+{
+ 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()
+{
+ 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()
+{
+ su "$TEST_USER" --shell=/bin/bash --command="export PATH='$PATH'; $1"
+}
+
+# Runs the given shell command as the test user and expects it to fail.
+_user_do_and_expect_failure()
+{
+ _expect_failure "_user_do '$1'"
+}
+
+# Clear the test user's user keyring and unlink it from root's user keyring, if
+# it is linked into it.
+_cleanup_user_keyrings()
+{
+ local ringid
+
+ ringid=$(_user_do "keyctl show @u" | awk '/keyring: _uid/{print $1}')
+
+ _user_do "keyctl clear $ringid"
+ keyctl unlink "$ringid" @u &> /dev/null || true
+}
+
+# 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()
+{
+ # 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"
+}
+
+# Wraps the 'expect' command to force subprocesses to have 80-column output.
+expect()
+{
+ command expect -c 'set stty_init "cols 80"' -f -
+}
diff --git a/cli-tests/run.sh b/cli-tests/run.sh
new file mode 100755
index 0000000..9ab5b78
--- /dev/null
+++ b/cli-tests/run.sh
@@ -0,0 +1,307 @@
+#!/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;"
+
+ # At some point, 'bash -c COMMAND' started showing error messages as
+ # "bash: line 1: " instead of just "bash: ". Filter out the "line 1: ".
+ sedscript+="s@^bash: line 1: @bash: @;"
+
+ # Work around protojson whitespace randomization.
+ sedscript+="/^Options: /s@ @ @g;"
+ sedscript+="s@^Options: @Options: @;"
+
+ 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 --quiet --all-users > /dev/null
+
+ # The tests assume kernel support for v2 policies.
+ if ! grep -E -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 --quiet --all-users "$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..1360bc2
--- /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=$(_get_protector_descriptor "$dir" custom prot)
+
+_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..4de05e4
--- /dev/null
+++ b/cli-tests/t_encrypt.out
@@ -0,0 +1,106 @@
+
+# Try to encrypt a nonexistent directory
+[ERROR] fscrypt encrypt: no such file or directory
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
+
+# Try to encrypt a nonempty directory
+[ERROR] fscrypt encrypt: Directory "MNT/dir" cannot be
+ encrypted because it is non-empty.
+
+Files cannot be encrypted in-place. Instead, encrypt a new directory, copy the
+files into it, and securely delete the original directory. For example:
+
+ mkdir "MNT/dir.new"
+ fscrypt encrypt "MNT/dir.new"
+ cp -a -T "MNT/dir" "MNT/dir.new"
+ find "MNT/dir" -type f -print0 | xargs -0 shred -n1 --remove=unlink
+ rm -rf "MNT/dir"
+ mv "MNT/dir.new" "MNT/dir"
+
+Caution: due to the nature of modern storage devices and filesystems, the
+original data may still be recoverable from disk. It's much better to encrypt
+your files from the start.
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
+
+# => with trailing slash
+[ERROR] fscrypt encrypt: Directory "MNT/dir/" cannot be
+ encrypted because it is non-empty.
+
+Files cannot be encrypted in-place. Instead, encrypt a new directory, copy the
+files into it, and securely delete the original directory. For example:
+
+ mkdir "MNT/dir.new"
+ fscrypt encrypt "MNT/dir.new"
+ cp -a -T "MNT/dir" "MNT/dir.new"
+ find "MNT/dir" -type f -print0 | xargs -0 shred -n1 --remove=unlink
+ rm -rf "MNT/dir"
+ mv "MNT/dir.new" "MNT/dir"
+
+Caution: due to the nature of modern storage devices and filesystems, the
+original data may still be recoverable from disk. It's much better to encrypt
+your files from the start.
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
+
+# Encrypt a directory as non-root user
+ext4 filesystem "MNT" has 1 protector and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+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 (only including ones owned by fscrypt-test-user or root).
+All users can create fscrypt metadata on this filesystem.
+
+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: file or directory "MNT/dir" is
+ already encrypted
+
+# Try to encrypt another user's directory as a non-root user
+[ERROR] fscrypt encrypt: cannot encrypt "MNT/dir" because
+ it's owned by another user (root).
+
+ Encryption can only be enabled on a directory you own,
+ even if you have write access to the directory.
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
diff --git a/cli-tests/t_encrypt.sh b/cli-tests/t_encrypt.sh
new file mode 100755
index 0000000..ffd6165
--- /dev/null
+++ b/cli-tests/t_encrypt.sh
@@ -0,0 +1,54 @@
+#!/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
+_print_header "=> with trailing slash"
+_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..2f1c03c
--- /dev/null
+++ b/cli-tests/t_encrypt_custom.out
@@ -0,0 +1,58 @@
+
+# Encrypt with custom passphrase protector
+ext4 filesystem "MNT" has 1 protector and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+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.
+All users can create fscrypt metadata on this filesystem.
+
+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_passphrase protectors must be named
+
+Use --name=PROTECTOR_NAME to specify a protector name.
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is 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..b1f6c82
--- /dev/null
+++ b/cli-tests/t_encrypt_login.out
@@ -0,0 +1,209 @@
+
+# Encrypt with login protector
+
+IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for
+ important recovery instructions. It is *strongly recommended* to
+ record the recovery passphrase in a secure location; otherwise you
+ will lose access to this directory if you reinstall the operating
+ system or move this filesystem to another system.
+
+ext4 filesystem "MNT" has 2 protectors and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+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.
+All users can create fscrypt metadata on this filesystem.
+
+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
+
+IMPORTANT: Before continuing, ensure you have properly set up your system for
+ login protectors. See
+ https://github.com/google/fscrypt#setting-up-for-login-protectors
+
+Enter login passphrase for fscrypt-test-user:
+
+IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for
+ important recovery instructions. It is *strongly recommended* to
+ record the recovery passphrase in a secure location; otherwise you
+ will lose access to this directory if you reinstall the operating
+ system or move this filesystem to another system.
+
+"MNT/dir" is now encrypted, unlocked, and ready for use.
+ext4 filesystem "MNT" has 2 protectors and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+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.
+All users can create fscrypt metadata on this filesystem.
+
+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
+
+IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for
+ important recovery instructions. It is *strongly recommended* to
+ record the recovery passphrase in a secure location; otherwise you
+ will lose access to this directory if you reinstall the operating
+ system or move this filesystem to another system.
+
+ext4 filesystem "MNT" has 2 protectors and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+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.
+All users can create fscrypt metadata on this filesystem.
+
+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"
+
+Protector is owned by fscrypt-test-user:fscrypt-test-user
+"MNT/dir" is now locked.
+"MNT/dir" is now locked.
+
+# Encrypt with login protector with --no-recovery
+ext4 filesystem "MNT" has 1 protector and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+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.
+All users can create fscrypt metadata on this filesystem.
+
+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.
+All users can create fscrypt metadata on this filesystem.
+
+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: cannot assign name "prot" to new login protector for
+ user "fscrypt-test-user" because login protectors are
+ identified by user, not by name.
+
+To fix this, don't specify the --name=PROTECTOR_NAME option.
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+ext4 filesystem "MNT_ROOT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
+
+# Try to use the wrong login passphrase
+[ERROR] fscrypt encrypt: incorrect login passphrase
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+ext4 filesystem "MNT_ROOT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
+
+# Test that linked protector works even if UUID link is broken
+
+IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for
+ important recovery instructions. It is *strongly recommended* to
+ record the recovery passphrase in a secure location; otherwise you
+ will lose access to this directory if you reinstall the operating
+ system or move this filesystem to another system.
+
+ext4 filesystem "MNT" has 2 protectors and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc39 No custom protector "Recovery passphrase for dir"
+desc40 Yes (MNT_ROOT) login protector for fscrypt-test-user
+
+POLICY UNLOCKED PROTECTORS
+desc41 Yes desc40, desc39
diff --git a/cli-tests/t_encrypt_login.sh b/cli-tests/t_encrypt_login.sh
new file mode 100755
index 0000000..b6ae2d8
--- /dev/null
+++ b/cli-tests/t_encrypt_login.sh
@@ -0,0 +1,104 @@
+#!/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=$(_get_protector_descriptor "$MNT" custom 'Recovery passphrase for dir')
+login_protector=$(_get_login_descriptor)
+_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 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
+# The newly-created login protector should be owned by the user, not root.
+# This is partly redundant with the below check, but we might as well test both.
+login_protector=$(_get_login_descriptor)
+owner=$(stat -c "%U:%G" "$MNT_ROOT/.fscrypt/protectors/$login_protector")
+echo -e "\nProtector is owned by $owner"
+# The user should be able to lock and unlock the directory themselves. This
+# tests that the fscrypt metadata file permissions got set appropriately when
+# root set up the encryption on the user's behalf.
+chown "$TEST_USER" "$dir"
+_user_do "fscrypt lock $dir"
+_user_do "echo TEST_USER_PASS | fscrypt unlock $dir --quiet --unlock-with=$MNT_ROOT:$login_protector"
+_user_do "fscrypt lock $dir"
+
+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
+
+begin "Test that linked protector works even if UUID link is broken"
+echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --user="$TEST_USER" "$dir"
+protector=$(_get_login_descriptor)
+link_file=$MNT/.fscrypt/protectors/$protector.link
+[ -e "$link_file" ] || _fail "$link_file does not exist"
+sed -i 's/UUID=.*/UUID=00000000-0000-0000-0000-000000000000/' "$link_file"
+fscrypt status "$MNT"
diff --git a/cli-tests/t_encrypt_raw_key.out b/cli-tests/t_encrypt_raw_key.out
new file mode 100644
index 0000000..78aa0b7
--- /dev/null
+++ b/cli-tests/t_encrypt_raw_key.out
@@ -0,0 +1,74 @@
+
+# Encrypt with raw_key protector from file
+ext4 filesystem "MNT" has 1 protector and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+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"
+
+# Encrypt with raw_key protector from stdin
+ext4 filesystem "MNT" has 1 protector and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc6 No raw key 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 raw key protector "prot"
+
+# Try to encrypt with raw_key protector from file, 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.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
+
+# Try to encrypt with raw_key protector from stdin, using wrong key length
+[ERROR] fscrypt encrypt: unexpected EOF
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
+
+# Encrypt with raw_key protector from file, unlock from stdin
+"MNT/dir" is now locked.
+ext4 filesystem "MNT" has 1 protector and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc11 No raw key protector "prot"
+
+POLICY UNLOCKED PROTECTORS
+desc12 Yes desc11
+"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 1 protector:
+PROTECTOR LINKED DESCRIPTION
+desc11 No raw key protector "prot"
diff --git a/cli-tests/t_encrypt_raw_key.sh b/cli-tests/t_encrypt_raw_key.sh
new file mode 100755
index 0000000..e5c6d20
--- /dev/null
+++ b/cli-tests/t_encrypt_raw_key.sh
@@ -0,0 +1,53 @@
+#!/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 from file"
+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 "Encrypt with raw_key protector from stdin"
+head -c 32 /dev/urandom | fscrypt encrypt --quiet --name=prot --source=raw_key "$dir"
+show_status true
+
+begin "Try to encrypt with raw_key protector from file, 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
+
+begin "Try to encrypt with raw_key protector from stdin, using wrong key length"
+_expect_failure "head -c 16 /dev/urandom | fscrypt encrypt --quiet --name=prot --source=raw_key '$dir'"
+show_status false
+
+begin "Encrypt with raw_key protector from file, unlock from stdin"
+head -c 32 /dev/urandom > "$raw_key_file"
+fscrypt encrypt --quiet --name=prot --source=raw_key --key="$raw_key_file" "$dir"
+fscrypt lock "$dir"
+fscrypt unlock --quiet "$dir" < "$raw_key_file"
+show_status true
diff --git a/cli-tests/t_lock.out b/cli-tests/t_lock.out
new file mode 100644
index 0000000..ce27713
--- /dev/null
+++ b/cli-tests/t_lock.out
@@ -0,0 +1,102 @@
+
+# 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: Directory was incompletely locked because some files are
+ still open. These files remain accessible.
+
+Try killing any processes using files in the directory, for example using:
+
+ find "MNT/dir" -print0 | xargs -0 fuser -k
+
+Then re-run:
+
+ fscrypt lock "MNT/dir"
+
+# => 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
+[ERROR] fscrypt lock: Directory "MNT/dir" 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 "MNT/dir"
+contents
+"MNT/dir" is now locked.
+cat: MNT/dir/file: No such file or directory
+
+# Try to operate on locked regular file
+"MNT/dir" is now locked.
+[ERROR] fscrypt status: cannot operate on locked regular file
+ "MNT/file"
+
+It is not possible to operate directly on a locked regular file, since the
+kernel does not support this. Specify the parent directory instead. (For loose
+files, any directory with the file's policy works.)
+[ERROR] fscrypt unlock: cannot operate on locked regular file
+ "MNT/file"
+
+It is not possible to operate directly on a locked regular file, since the
+kernel does not support this. Specify the parent directory instead. (For loose
+files, any directory with the file's policy works.)
diff --git a/cli-tests/t_lock.sh b/cli-tests/t_lock.sh
new file mode 100755
index 0000000..e5df4df
--- /dev/null
+++ b/cli-tests/t_lock.sh
@@ -0,0 +1,65 @@
+#!/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"
+rm -rf "$dir"
+mkdir "$dir"
+chown "$TEST_USER" "$dir"
+_user_do "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'"
+_user_do "echo contents > $dir/file"
+_expect_failure "fscrypt lock '$dir'"
+cat "$dir/file"
+fscrypt lock --all-users "$dir"
+_expect_failure "cat '$dir/file'"
+
+_print_header "Try to operate on locked regular file"
+_reset_filesystems
+rm -rf "$dir"
+mkdir "$dir"
+echo hunter2 | fscrypt encrypt --quiet --name=prot "$dir"
+echo contents > "$dir/file"
+mv "$dir/file" "$MNT/file" # Make it a loose encrypted file.
+fscrypt lock "$dir"
+_expect_failure "fscrypt status '$MNT/file'"
+_expect_failure "fscrypt unlock '$MNT/file'"
diff --git a/cli-tests/t_metadata.out b/cli-tests/t_metadata.out
new file mode 100644
index 0000000..bbcc0f2
--- /dev/null
+++ b/cli-tests/t_metadata.out
@@ -0,0 +1,19 @@
+ext4 filesystem "MNT" has 3 protectors and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc1 No custom protector "foo"
+desc2 No custom protector "bar"
+desc3 No custom protector "baz"
+
+POLICY UNLOCKED PROTECTORS
+desc4 No desc1, desc2, desc3
+ext4 filesystem "MNT" has 2 protectors and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc1 No custom protector "foo"
+desc2 No custom protector "bar"
+
+POLICY UNLOCKED PROTECTORS
+desc4 No desc1
diff --git a/cli-tests/t_metadata.sh b/cli-tests/t_metadata.sh
new file mode 100755
index 0000000..e688eda
--- /dev/null
+++ b/cli-tests/t_metadata.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# Test 'fscrypt metadata'.
+
+cd "$(dirname "$0")"
+. common.sh
+
+# Create three protectors, and a policy protected by them.
+echo foo | fscrypt metadata create protector "$MNT" \
+ --quiet --name=foo --source=custom_passphrase
+echo bar | fscrypt metadata create protector "$MNT" \
+ --quiet --name=bar --source=custom_passphrase
+echo baz | fscrypt metadata create protector "$MNT" \
+ --quiet --name=baz --source=custom_passphrase
+prot_foo=$MNT:$(_get_protector_descriptor "$MNT" custom foo)
+prot_bar=$MNT:$(_get_protector_descriptor "$MNT" custom bar)
+desc_baz=$(_get_protector_descriptor "$MNT" custom baz)
+prot_baz=$MNT:$desc_baz
+echo foo | fscrypt metadata create policy "$MNT" --quiet \
+ --protector="$prot_foo"
+policy=$MNT:$(fscrypt status "$MNT" | grep -A10 "^POLICY" | \
+ tail -1 | awk '{print $1}')
+echo -e "bar\nfoo" | fscrypt metadata add-protector-to-policy --quiet \
+ --policy="$policy" --protector="$prot_bar"
+echo -e "baz\nfoo" | fscrypt metadata add-protector-to-policy --quiet \
+ --policy="$policy" --protector="$prot_baz" --unlock-with="$prot_foo"
+fscrypt status "$MNT"
+
+# Remove two of the protectors from the policy.
+# Make sure that this works even if the protector was already deleted.
+fscrypt metadata remove-protector-from-policy --quiet --force \
+ --policy="$policy" --protector="$prot_bar"
+rm "$MNT/.fscrypt/protectors/$desc_baz"
+fscrypt metadata remove-protector-from-policy --quiet --force \
+ --policy="$policy" --protector="$prot_baz"
+fscrypt status "$MNT"
diff --git a/cli-tests/t_not_enabled.out b/cli-tests/t_not_enabled.out
new file mode 100644
index 0000000..07c9aa3
--- /dev/null
+++ b/cli-tests/t_not_enabled.out
@@ -0,0 +1,63 @@
+
+# Disable encryption on DEV
+
+# Try to encrypt a directory when encryption is disabled
+[ERROR] fscrypt encrypt: encryption not enabled on filesystem
+ MNT (DEV).
+
+To enable encryption support on this filesystem, run:
+
+ sudo tune2fs -O encrypt "DEV"
+
+Also ensure that your kernel has CONFIG_FS_ENCRYPTION=y. See the documentation
+for more details.
+
+# Try to unlock a directory when encryption is disabled
+[ERROR] fscrypt unlock: encryption not enabled on filesystem
+ MNT (DEV).
+
+To enable encryption support on this filesystem, run:
+
+ sudo tune2fs -O encrypt "DEV"
+
+Also ensure that your kernel has CONFIG_FS_ENCRYPTION=y. See the documentation
+for more details.
+
+# Try to lock a directory when encryption is disabled
+[ERROR] fscrypt lock: encryption not enabled on filesystem
+ MNT (DEV).
+
+To enable encryption support on this filesystem, run:
+
+ sudo tune2fs -O encrypt "DEV"
+
+Also ensure that your kernel has CONFIG_FS_ENCRYPTION=y. See the documentation
+for more details.
+
+# Check for additional message when GRUB appears to be installed
+[ERROR] fscrypt encrypt: encryption not enabled on filesystem
+ MNT (DEV).
+
+To enable encryption support on this filesystem, run:
+
+ sudo tune2fs -O encrypt "DEV"
+
+WARNING: you seem to have GRUB installed on this filesystem. Before doing the
+above, make sure you are using GRUB v2.04 or later; otherwise your system will
+become unbootable.
+
+Also ensure that your kernel has CONFIG_FS_ENCRYPTION=y. See the documentation
+for more details.
+
+# 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..fae1094
--- /dev/null
+++ b/cli-tests/t_not_enabled.sh
@@ -0,0 +1,39 @@
+#!/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 "Check for additional message when GRUB appears to be installed"
+mkdir -p "$MNT/boot/grub"
+_expect_failure "fscrypt encrypt '$dir'"
+rm -r "${MNT:?}/boot"
+
+_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..68e0897
--- /dev/null
+++ b/cli-tests/t_not_supported.out
@@ -0,0 +1,9 @@
+
+# Mount tmpfs
+
+# Try to create fscrypt metadata on tmpfs
+[ERROR] fscrypt setup: filesystem type tmpfs is not supported for fscrypt setup
+
+# Try to encrypt a directory on tmpfs
+[ERROR] fscrypt encrypt: This kernel doesn't support encryption on tmpfs
+ filesystems.
diff --git a/cli-tests/t_not_supported.sh b/cli-tests/t_not_supported.sh
new file mode 100755
index 0000000..8b52392
--- /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 "Try to create fscrypt metadata on tmpfs"
+_expect_failure "fscrypt setup --quiet '$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..ab0052c
--- /dev/null
+++ b/cli-tests/t_setup.out
@@ -0,0 +1,51 @@
+
+# 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".
+Allow users other than root to create fscrypt metadata on this filesystem? (See
+https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem) [y/N] Metadata directories created at "MNT_ROOT/.fscrypt", writable by everyone.
+
+# 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
+
+If desired, use --force to automatically run destructive operations.
+
+# fscrypt setup --quiet --force when fscrypt.conf already exists
+
+# fscrypt setup filesystem
+Allow users other than root to create fscrypt metadata on this filesystem? (See
+https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem) [y/N] Metadata directories created at "MNT/.fscrypt", writable by everyone.
+
+# fscrypt setup filesystem (already set up)
+[ERROR] fscrypt setup: filesystem MNT is already setup for
+ use with fscrypt
+
+# no config file
+[ERROR] fscrypt setup: "FSCRYPT_CONF" doesn't exist
+
+Run "sudo fscrypt setup" to create this file.
+
+# bad config file
+[ERROR] fscrypt setup: "FSCRYPT_CONF" is invalid: proto:
+ syntax error (line 1:1): invalid value bad
+
+Either fix this file manually, or run "sudo fscrypt setup" to recreate it.
diff --git a/cli-tests/t_setup.sh b/cli-tests/t_setup.sh
new file mode 100755
index 0000000..f7e302d
--- /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"
+echo y | 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"
+echo y | 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_single_user.out b/cli-tests/t_single_user.out
new file mode 100644
index 0000000..d038d52
--- /dev/null
+++ b/cli-tests/t_single_user.out
@@ -0,0 +1,30 @@
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+Only root can create fscrypt metadata on this filesystem.
+
+ext4 filesystem "MNT" has 0 protectors and 0 policies (only including ones owned by fscrypt-test-user or root).
+Only root can create fscrypt metadata on this filesystem.
+
+
+# Encrypt, lock, and unlock as root
+"MNT/dir" is now locked.
+
+# Encrypt as root with user's login protector
+
+IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for
+ important recovery instructions. It is *strongly recommended* to
+ record the recovery passphrase in a secure location; otherwise you
+ will lose access to this directory if you reinstall the operating
+ system or move this filesystem to another system.
+
+Protector desc1 no longer protecting policy desc2.
+"MNT/dir" is now locked.
+Enter login passphrase for fscrypt-test-user: "MNT/dir" is now unlocked and ready for use.
+
+# Encrypt as user (should fail)
+[ERROR] fscrypt encrypt: user lacks permission to create fscrypt metadata on
+ MNT
+
+For how to allow users to create fscrypt metadata on a filesystem, refer to
+https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem
+
+# Encrypt as user if they set up filesystem (should succeed)
diff --git a/cli-tests/t_single_user.sh b/cli-tests/t_single_user.sh
new file mode 100755
index 0000000..c569f20
--- /dev/null
+++ b/cli-tests/t_single_user.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+# Test 'fscrypt setup' without --all-users.
+
+cd "$(dirname "$0")"
+. common.sh
+
+_rm_metadata "$MNT_ROOT"
+_rm_metadata "$MNT"
+rm "$FSCRYPT_CONF"
+fscrypt setup --time=1ms --quiet
+fscrypt setup --time=1ms --quiet "$MNT"
+fscrypt status "$MNT"
+_user_do "fscrypt status \"$MNT\""
+
+dir=$MNT/dir
+
+begin()
+{
+ _reset_filesystems
+ mkdir "$dir"
+ _print_header "$1"
+}
+
+begin "Encrypt, lock, and unlock as root"
+echo hunter2 | fscrypt encrypt --quiet --name=dir --skip-unlock "$dir"
+echo hunter2 | fscrypt unlock --quiet "$dir"
+fscrypt lock "$dir"
+
+begin "Encrypt as root with user's login protector"
+echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --user="$TEST_USER" "$dir"
+# The user should be able to update the policy and protectors created by the
+# above command themselves. The easiest way to test this is by updating the
+# policy to remove the auto-generated recovery protector. This verifies that
+# (a) the policy was made owned by the user, and that (b) policy updates fall
+# back to overwrites when the process cannot write to the containing directory.
+# (It would be better to test updating the protectors too, but this is the
+# easiest test to do here.)
+policy=$(fscrypt status "$dir" | awk '/Policy/{print $2}')
+recovery_protector=$(_get_protector_descriptor "$MNT" custom 'Recovery passphrase for dir')
+_user_do "fscrypt metadata remove-protector-from-policy --force --protector=$MNT:$recovery_protector --policy=$MNT:$policy"
+chown "$TEST_USER" "$dir"
+_user_do "fscrypt lock $dir"
+_user_do "echo TEST_USER_PASS | fscrypt unlock $dir"
+
+begin "Encrypt as user (should fail)"
+chown "$TEST_USER" "$dir"
+_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=dir --skip-unlock \"$dir\""
+
+begin "Encrypt as user if they set up filesystem (should succeed)"
+_rm_metadata "$MNT"
+chown "$TEST_USER" "$MNT"
+chown "$TEST_USER" "$dir"
+_user_do "fscrypt setup --time=1ms --quiet $MNT"
+_user_do "echo hunter2 | fscrypt encrypt --quiet --name=dir3 --skip-unlock \"$dir\""
diff --git a/cli-tests/t_status.out b/cli-tests/t_status.out
new file mode 100644
index 0000000..058c62c
--- /dev/null
+++ b/cli-tests/t_status.out
@@ -0,0 +1,50 @@
+
+# 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.
+All users can create fscrypt metadata on this filesystem.
+
+ext4 filesystem "MNT" has 0 protectors and 0 policies (only including ones owned by fscrypt-test-user or root).
+All users can create fscrypt metadata on this filesystem.
+
+
+# Get status of unencrypted directory on setup mountpoint
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
+[ERROR] fscrypt status: file or directory "MNT/dir" is 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 is not setup for use
+ with fscrypt
+
+Run "sudo fscrypt setup MNT" to use fscrypt on this
+filesystem.
+[ERROR] fscrypt status: filesystem MNT is not setup for use
+ with fscrypt
+
+Run "sudo fscrypt setup MNT" to use fscrypt on this
+filesystem.
+
+# Get status of unencrypted directory on not-setup mountpoint
+[ERROR] fscrypt status: filesystem MNT is not setup for use
+ with fscrypt
+
+Run "sudo fscrypt setup MNT" to use fscrypt on this
+filesystem.
+[ERROR] fscrypt status: filesystem MNT is not setup for use
+ with fscrypt
+
+Run "sudo fscrypt setup MNT" 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..b3c9b2a
--- /dev/null
+++ b/cli-tests/t_unlock.out
@@ -0,0 +1,116 @@
+
+# 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: fscrypt metadata file at
+ "MNT/.fscrypt/policies/desc1"
+ is corrupt: proto: cannot parse invalid wire-format data
+
+# Try to unlock with missing policy metadata
+[ERROR] fscrypt unlock: filesystem "MNT" does not contain
+ the policy metadata for "MNT/dir".
+ This directory has either been encrypted with another
+ tool (such as e4crypt), or the file
+ "MNT/.fscrypt/policies/desc20"
+ 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.
+
+# Try to unlock with wrong policy metadata
+[ERROR] fscrypt unlock: inconsistent metadata between encrypted directory
+ "MNT/dir1" and its corresponding
+ metadata file
+ "MNT/.fscrypt/policies/desc21".
+
+ Directory has
+ descriptor:desc21 padding:32
+ contents:AES_256_XTS filenames:AES_256_CTS
+ policy_version:2
+
+ Metadata file has
+ descriptor:desc23 padding:32
+ contents:AES_256_XTS filenames:AES_256_CTS
+ policy_version:2
diff --git a/cli-tests/t_unlock.sh b/cli-tests/t_unlock.sh
new file mode 100755
index 0000000..e32b0f7
--- /dev/null
+++ b/cli-tests/t_unlock.sh
@@ -0,0 +1,82 @@
+#!/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'"
+
+_print_header "Try to unlock with wrong policy metadata"
+_reset_filesystems
+mkdir "$MNT/dir1"
+mkdir "$MNT/dir2"
+echo hunter2 | fscrypt encrypt --quiet --name=dir1 --skip-unlock "$MNT/dir1"
+echo hunter2 | fscrypt encrypt --quiet --name=dir2 --skip-unlock "$MNT/dir2"
+policy1=$(find "$MNT/.fscrypt/policies/" -type f | head -1)
+policy2=$(find "$MNT/.fscrypt/policies/" -type f | tail -1)
+mv "$policy1" "$TMPDIR/policy"
+mv "$policy2" "$policy1"
+mv "$TMPDIR/policy" "$policy2"
+_expect_failure "echo hunter2 | fscrypt unlock '$MNT/dir1'"
diff --git a/cli-tests/t_v1_policy.out b/cli-tests/t_v1_policy.out
new file mode 100644
index 0000000..f14f357
--- /dev/null
+++ b/cli-tests/t_v1_policy.out
@@ -0,0 +1,144 @@
+
+# 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: could not access user keyring for "root": setting uids:
+ operation not permitted
+
+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 for "fscrypt-test-user" is not linked into
+ the 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: Partially (incompletely locked, or unlocked by another user)
+
+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
+
+# Testing incompletely locking v1-encrypted directory
+Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use.
+Encrypted data removed from filesystem cache.
+[ERROR] fscrypt lock: Directory was incompletely locked because some files are
+ still open. These files remain accessible.
+
+Try killing any processes using files in the directory, for example using:
+
+ find "MNT/dir" -print0 | xargs -0 fuser -k
+
+Then re-run:
+
+ fscrypt lock "MNT/dir"
+"MNT/dir" is encrypted with fscrypt.
+
+Policy: desc1
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1
+Unlocked: Partially (incompletely locked, or unlocked by another user)
+
+Protected with 1 protector:
+PROTECTOR LINKED DESCRIPTION
+desc2 No custom protector "prot"
+ext4 filesystem "MNT" has 1 protector and 1 policy (only including ones owned by fscrypt-test-user or root).
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc2 No custom protector "prot"
+
+POLICY UNLOCKED PROTECTORS
+desc1 No desc2
+
+# Finishing locking 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..46ccdaf
--- /dev/null
+++ b/cli-tests/t_v1_policy.sh
@@ -0,0 +1,72 @@
+#!/bin/bash
+
+# Test using v1 encryption policies (deprecated).
+
+cd "$(dirname "$0")"
+. common.sh
+
+_setup_session_keyring
+trap _cleanup_user_keyrings EXIT
+
+dir="$MNT/dir"
+mkdir "$dir"
+chown "$TEST_USER" "$dir"
+
+_print_header "Set policy_version 1"
+sed -E -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'"
+
+# 'fscrypt lock' and 'fscrypt status' implement a heuristic that should detect
+# the "files busy" case with v1.
+_print_header "Testing incompletely locking v1-encrypted directory"
+_user_do "echo hunter2 | fscrypt unlock '$dir'"
+exec 3<"$dir/file"
+_expect_failure "fscrypt lock '$dir' --user='$TEST_USER'"
+_user_do "fscrypt status '$dir'"
+# ... except in this case, because we can't detect it without a directory path.
+_user_do "fscrypt status '$MNT'"
+exec 3<&-
+_print_header "Finishing locking 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..9f0f0ab
--- /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: file or directory "MNT/dir" is 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..a8fd333
--- /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 -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'"