]> git-server-git.apps.pok.os.sepia.ceph.com Git - fscrypt.git/commitdiff
build(deps): bump golang.org/x/crypto from 0.13.0 to 0.17.0
authordependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Mon, 18 Dec 2023 23:20:51 +0000 (23:20 +0000)
committerEric Biggers <ebiggers3@gmail.com>
Wed, 27 Dec 2023 04:33:42 +0000 (20:33 -0800)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.13.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.13.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
111 files changed:
.github/workflows/ci.yml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
CODE_OF_CONDUCT.md [new file with mode: 0644]
CONTRIBUTING.md [new file with mode: 0644]
LICENSE [new file with mode: 0644]
Makefile [new file with mode: 0644]
NEWS.md [new file with mode: 0644]
README.md [new file with mode: 0644]
actions/callback.go [new file with mode: 0644]
actions/config.go [new file with mode: 0644]
actions/config_test.go [new file with mode: 0644]
actions/context.go [new file with mode: 0644]
actions/context_test.go [new file with mode: 0644]
actions/hashing_test.go [new file with mode: 0644]
actions/policy.go [new file with mode: 0644]
actions/policy_test.go [new file with mode: 0644]
actions/protector.go [new file with mode: 0644]
actions/protector_test.go [new file with mode: 0644]
actions/recovery.go [new file with mode: 0644]
actions/recovery_test.go [new file with mode: 0644]
bin/files-changed [new file with mode: 0755]
cli-tests/README.md [new file with mode: 0644]
cli-tests/common.sh [new file with mode: 0644]
cli-tests/run.sh [new file with mode: 0755]
cli-tests/t_change_passphrase.out [new file with mode: 0644]
cli-tests/t_change_passphrase.sh [new file with mode: 0755]
cli-tests/t_encrypt.out [new file with mode: 0644]
cli-tests/t_encrypt.sh [new file with mode: 0755]
cli-tests/t_encrypt_custom.out [new file with mode: 0644]
cli-tests/t_encrypt_custom.sh [new file with mode: 0755]
cli-tests/t_encrypt_login.out [new file with mode: 0644]
cli-tests/t_encrypt_login.sh [new file with mode: 0755]
cli-tests/t_encrypt_raw_key.out [new file with mode: 0644]
cli-tests/t_encrypt_raw_key.sh [new file with mode: 0755]
cli-tests/t_lock.out [new file with mode: 0644]
cli-tests/t_lock.sh [new file with mode: 0755]
cli-tests/t_metadata.out [new file with mode: 0644]
cli-tests/t_metadata.sh [new file with mode: 0755]
cli-tests/t_not_enabled.out [new file with mode: 0644]
cli-tests/t_not_enabled.sh [new file with mode: 0755]
cli-tests/t_not_supported.out [new file with mode: 0644]
cli-tests/t_not_supported.sh [new file with mode: 0755]
cli-tests/t_passphrase_hashing.out [new file with mode: 0644]
cli-tests/t_passphrase_hashing.sh [new file with mode: 0755]
cli-tests/t_setup.out [new file with mode: 0644]
cli-tests/t_setup.sh [new file with mode: 0755]
cli-tests/t_single_user.out [new file with mode: 0644]
cli-tests/t_single_user.sh [new file with mode: 0755]
cli-tests/t_status.out [new file with mode: 0644]
cli-tests/t_status.sh [new file with mode: 0755]
cli-tests/t_unlock.out [new file with mode: 0644]
cli-tests/t_unlock.sh [new file with mode: 0755]
cli-tests/t_v1_policy.out [new file with mode: 0644]
cli-tests/t_v1_policy.sh [new file with mode: 0755]
cli-tests/t_v1_policy_fs_keyring.out [new file with mode: 0644]
cli-tests/t_v1_policy_fs_keyring.sh [new file with mode: 0755]
cmd/fscrypt/commands.go [new file with mode: 0644]
cmd/fscrypt/errors.go [new file with mode: 0644]
cmd/fscrypt/flags.go [new file with mode: 0644]
cmd/fscrypt/format.go [new file with mode: 0644]
cmd/fscrypt/fscrypt.go [new file with mode: 0644]
cmd/fscrypt/fscrypt_bash_completion [new file with mode: 0644]
cmd/fscrypt/fscrypt_test.go [new file with mode: 0644]
cmd/fscrypt/keys.go [new file with mode: 0644]
cmd/fscrypt/prompt.go [new file with mode: 0644]
cmd/fscrypt/protector.go [new file with mode: 0644]
cmd/fscrypt/setup.go [new file with mode: 0644]
cmd/fscrypt/status.go [new file with mode: 0644]
cmd/fscrypt/strings.go [new file with mode: 0644]
crypto/crypto.go [new file with mode: 0644]
crypto/crypto_test.go [new file with mode: 0644]
crypto/key.go [new file with mode: 0644]
crypto/rand.go [new file with mode: 0644]
crypto/recovery_test.go [new file with mode: 0644]
filesystem/filesystem.go [new file with mode: 0644]
filesystem/filesystem_test.go [new file with mode: 0644]
filesystem/mountpoint.go [new file with mode: 0644]
filesystem/mountpoint_test.go [new file with mode: 0644]
filesystem/path.go [new file with mode: 0644]
filesystem/path_test.go [new file with mode: 0644]
go.mod [new file with mode: 0644]
go.sum [new file with mode: 0644]
keyring/fs_keyring.go [new file with mode: 0644]
keyring/keyring.go [new file with mode: 0644]
keyring/keyring_test.go [new file with mode: 0644]
keyring/user_keyring.go [new file with mode: 0644]
metadata/checks.go [new file with mode: 0644]
metadata/config.go [new file with mode: 0644]
metadata/config_test.go [new file with mode: 0644]
metadata/constants.go [new file with mode: 0644]
metadata/metadata.pb.go [new file with mode: 0644]
metadata/metadata.proto [new file with mode: 0644]
metadata/policy.go [new file with mode: 0644]
metadata/policy_test.go [new file with mode: 0644]
pam/constants.go [new file with mode: 0644]
pam/login.go [new file with mode: 0644]
pam/pam.c [new file with mode: 0644]
pam/pam.go [new file with mode: 0644]
pam/pam.h [new file with mode: 0644]
pam/pam_test.go [new file with mode: 0644]
pam_fscrypt/config [new file with mode: 0644]
pam_fscrypt/pam_fscrypt.go [new file with mode: 0644]
pam_fscrypt/run_fscrypt.go [new file with mode: 0644]
pam_fscrypt/run_test.go [new file with mode: 0644]
security/cache.go [new file with mode: 0644]
security/privileges.go [new file with mode: 0644]
security/security_test.go [new file with mode: 0644]
tools.go [new file with mode: 0644]
util/errors.go [new file with mode: 0644]
util/util.go [new file with mode: 0644]
util/util_test.go [new file with mode: 0644]

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644 (file)
index 0000000..1377df2
--- /dev/null
@@ -0,0 +1,143 @@
+#
+# 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.
+#
+
+name: CI
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
+
+jobs:
+  build:
+    strategy:
+      matrix:
+        go: ['1.18', '1.19', '1.20']
+    name: Build (Go ${{ matrix.go }})
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - uses: actions/setup-go@v2
+      with:
+        go-version: ${{ matrix.go }}
+    - name: Install dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y libpam0g-dev
+    - name: Build
+      run: make
+
+  build-32bit:
+    name: Build (32-bit)
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - uses: actions/setup-go@v2
+      with:
+        go-version: '1.20'
+    - name: Install dependencies
+      run: |
+        sudo dpkg --add-architecture i386
+        sudo apt-get update
+        sudo apt-get install -y libpam0g-dev:i386 gcc-multilib
+    - name: Build
+      run: CGO_ENABLED=1 GOARCH=386 make
+
+  run-integration-tests:
+    name: Run integration tests
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - uses: actions/setup-go@v2
+      with:
+        go-version: '1.20'
+    - name: Install dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y libpam0g-dev e2fsprogs keyutils
+    - name: Run integration tests
+      run: |
+        make test-setup
+        keyctl link @u @s
+        make test
+        make test-teardown
+
+  # This isn't working currently because qemu user-mode emulation doesn't
+  # support passing through the keyctl() system call and the fscrypt ioctls.
+  # Hopefully GitHub Actions will natively support other architectures soon...
+  #
+  # run-integration-tests-other-arch:
+  #   name: Run integration tests (${{ matrix.arch }})
+  #   strategy:
+  #     matrix:
+  #       arch: [armv7, aarch64, ppc64le]
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - uses: actions/checkout@v3
+  #     - uses: uraimo/run-on-arch-action@v2.0.5
+  #       with:
+  #         arch: ${{ matrix.arch }}
+  #         distro: buster
+  #         githubToken: ${{ github.token }}
+  #         # Needed for 'make test-setup' to mount the test filesystem.
+  #         dockerRunArgs: --privileged
+  #         install: |
+  #           apt-get update
+  #           apt-get install -y build-essential git sudo golang-go \
+  #                              libpam0g-dev e2fsprogs keyutils
+  #         run: |
+  #           make test-setup
+  #           keyctl link @u @s
+  #           make test
+  #           make test-teardown
+
+  run-cli-tests:
+    name: Run command-line interface tests
+    # The cli tests require kernel 5.4 or later, and thus Ubuntu 20.04 or later.
+    runs-on: ubuntu-20.04
+    steps:
+    - uses: actions/checkout@v3
+    - uses: actions/setup-go@v2
+      with:
+        go-version: '1.20'
+    - name: Install dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y libpam0g-dev e2fsprogs expect keyutils
+    - name: Run command-line interface tests
+      run: make cli-test
+
+  generate-format-and-lint:
+    name: Generate, format, and lint
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - uses: actions/setup-go@v2
+      with:
+        go-version: '1.20'
+    - name: Install dependencies
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y libpam0g-dev shellcheck
+        make tools
+    - name: Generate
+      run: make gen && bin/files-changed proto
+    - name: Format
+      run: make format && bin/files-changed format
+    - name: Lint
+      run: make lint
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..0d8b56f
--- /dev/null
@@ -0,0 +1,13 @@
+bin/fscrypt
+bin/pam_fscrypt.so
+bin/protoc
+bin/protoc-gen-go
+bin/goimports
+bin/staticcheck
+bin/gocovmerge
+bin/misspell
+bin/config
+cli-tests/*.out.actual
+*coverage.out
+.vscode
+tags
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644 (file)
index 0000000..8bca305
--- /dev/null
@@ -0,0 +1,93 @@
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of
+experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+*   Using welcoming and inclusive language
+*   Being respectful of differing viewpoints and experiences
+*   Gracefully accepting constructive criticism
+*   Focusing on what is best for the community
+*   Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+*   The use of sexualized language or imagery and unwelcome sexual attention or
+    advances
+*   Trolling, insulting/derogatory comments, and personal or political attacks
+*   Public or private harassment
+*   Publishing others' private information, such as a physical or electronic
+    address, without explicit permission
+*   Other conduct which could reasonably be considered inappropriate in a
+    professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, or to ban temporarily or permanently any
+contributor for other behaviors that they deem inappropriate, threatening,
+offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+This Code of Conduct also applies outside the project spaces when the Project
+Steward has a reasonable belief that an individual's behavior may have a
+negative impact on the project or its community.
+
+## Conflict Resolution
+
+We do not believe that all conflict is bad; healthy debate and disagreement
+often yield positive results. However, it is never okay to be disrespectful or
+to engage in behavior that violates the project’s code of conduct.
+
+If you see someone violating the code of conduct, you are encouraged to address
+the behavior directly with those involved. Many issues can be resolved quickly
+and easily, and this gives people more control over the outcome of their
+dispute. If you are unable to resolve the matter for any reason, or if the
+behavior is threatening or harassing, report it. We are dedicated to providing
+an environment where participants feel welcome and safe.
+
+Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the
+Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to
+receive and address reported violations of the code of conduct. They will then
+work with a committee consisting of representatives from the Open Source
+Programs Office and the Google Open Source Strategy team. If for any reason you
+are uncomfortable reaching out the Project Steward, please email
+opensource@google.com.
+
+We will investigate every complaint, but you may not receive a direct response.
+We will use our discretion in determining when and how to follow up on reported
+incidents, which may range from not taking action to permanent expulsion from
+the project and project-sponsored spaces. We will notify the accused of the
+report and provide them an opportunity to discuss it before any action is taken.
+The identity of the reporter will be omitted from the details of the report
+supplied to the accused. In potentially harmful situations, such as ongoing
+harassment or threats to anyone's safety, we may take action without notice.
+
+## Attribution
+
+This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
+available at
+https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644 (file)
index 0000000..a547427
--- /dev/null
@@ -0,0 +1,163 @@
+# How to Contribute to fscrypt
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines we ask you to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Reporting an Issue, Discussing Design, or Asking a Question
+
+__IMPORTANT__: Any significant security issues should __NOT__ be reported in
+the public issue tracker. Practice responsible disclosure by emailing
+<joerichey@google.com> and <ebiggers@google.com> directly.
+
+Any bugs, problems, or design discussion relating to fscrypt should be raised
+in the [Github Issue Tracker](https://github.com/google/fscrypt/issues/new).
+
+When reporting an issue or problem, be sure to give as much information as
+possible. Also, make sure you are running the `fscrypt` and `pam_fscrypt.so`
+built from the current `master` branch.
+
+If reporting an issue around the fscrypt command-line tool, post the
+relevant output from fscrypt, running with the `--verbose` flag. For the
+`pam_fscrypt` module, use the `debug` option with the module and post the
+relevant parts of the syslog (usually at `/var/log/syslog`).
+
+Be sure to correctly tag your issue. The usage for the tags is as follows:
+* `bug` - General problems with the program's behavior
+       * The program crashes or hangs
+       * Directories cannot be locked/unlocked
+       * Metadata corruption
+       * Data loss/corruption
+* `documentation`
+       * Typos or unclear explanations in `README.md` or man pages.
+       * Outdated example output
+       * Unclear or ambiguous error messages
+* `enhancement` - Things you want in fscrypt
+* `question` - You don't know how something works with fscrypt
+       * This usually turns into a `documentation` issue.
+* `testing` - Strange test failures or missing tests
+
+## Submitting a Change to fscrypt
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+On every pull request, [GitHub
+Actions](https://github.com/google/fscrypt/actions) runs tests, code formatters,
+and linters. To pass these checks you should make sure that in your submission:
+- `make` properly builds `fscrypt` and `pam_fscrypt.so`.
+- All tests, including [integration tests](#running-integration-tests) and
+  [command-line interface (CLI)
+  tests](https://github.com/google/fscrypt/blob/master/cli-tests/README.md),
+  should pass. If the CLI tests fail due to an expected change in output, you
+  can use `make cli-test-update`.
+- `make format` has been run.
+- If you made any changes to files ending in `.proto`, the corresponding
+  `.pb.go` files should be regenerated with `make gen`.
+- Any issues found by `make lint` have been addressed.
+- If any dependencies have changed, run `go mod tidy`.
+- `make coverage.out` can be used to generate a coverage report for all of the
+  tests, but isn't required for submission
+  (ideally most code would be tested, we are far from that ideal).
+
+Essentially, if you run:
+```
+make test-setup
+make all
+make test-teardown
+make cli-test
+go mod tidy
+```
+and everything succeeds, and no files are changed, you're good to submit.
+
+The `Makefile` will automatically download and build any needed Go dependencies.
+However, you'll also need to install some non-Go dependencies:
+  - `make format` requires
+    [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html).
+  - `make lint` requires [`shellcheck`](https://github.com/koalaman/shellcheck).
+  - `make test-setup` and `make cli-test` require
+    [`e2fsprogs`](https://en.wikipedia.org/wiki/E2fsprogs) version 1.43 or
+    later.
+  - `make cli-test` requires [`expect`](https://en.wikipedia.org/wiki/Expect)
+    and
+    [`keyutils`](https://manpages.debian.org/testing/keyutils/keyctl.1.en.html).
+
+On Ubuntu, the following command installs the needed packages:
+```
+sudo apt-get install clang-format shellcheck e2fsprogs expect keyutils
+```
+
+### Running Integration Tests
+
+Running `make test` will build each package and run the unit tests, but will
+skip the integration tests. To run the integration tests, you will need a
+filesystem that supports encryption. If you already have some empty filesystem
+at `/foo/bar` that supports filesystem encryption, just run:
+```bash
+make test MOUNT=/foo/bar
+```
+
+Otherwise, you can use the `make test-setup`/`make test-teardown` commands to
+create/destroy a test filesystem for running integration tests. By default, a
+filesystem will be created (then destroyed) at `/tmp/fscrypt-mount` (using an
+image file at `/tmp/fscrypt-image`). To create/test/destroy a filesystem at a
+custom mountpoint `/foo/bar`, run:
+```bash
+make test-setup MOUNT=/foo/bar
+make test MOUNT=/foo/bar
+make test-teardown MOUNT=/foo/bar
+```
+Running the commands without `MOUNT=/foo/bar` uses the default locations.
+
+Note that the setup/teardown commands require `sudo` to mount/unmount the
+test filesystem.
+
+### Changing dependencies
+
+fscrypt's dependencies are managed using the
+[Go module system](https://github.com/golang/go/wiki/Modules).
+If you add or remove a dependency, be sure to update `go.mod` and `go.sum`.
+
+Also, when adding a dependency, the license of the package must be compatible
+with [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). See the
+[FSF's article](https://www.gnu.org/licenses/license-list.html) for more
+information. This (unfortunately) means we cannot use external packages under
+the [GPL](https://choosealicense.com/licenses/gpl-3.0) or
+[LGPL](https://choosealicense.com/licenses/lgpl-3.0/). We also cannot use
+packages with missing, misleading, or joke licenses (e.g.
+[Unlicense](http://unlicense.org/), [WTFPL](http://www.wtfpl.net/),
+[CC0](https://creativecommons.org/publicdomain/zero/1.0/)).
+
+### Build System Details ###
+
+Under the hood, the Makefile uses many go tools to generate, format, and lint
+your code.
+
+`make gen`:
+  - Downloads [`protoc`](https://github.com/google/protobuf) to compile the
+  `.proto` files.
+  - Turns each `.proto` file into a matching `.pb.go` file using
+    [`protoc-gen-go`](https://go.googlesource.com/protobuf/+/refs/heads/master/cmd/protoc-gen-go).
+
+`make format` runs:
+  - [`goimports`](https://godoc.org/golang.org/x/tools/cmd/goimports)
+    on the `.go` files.
+  - [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html)
+    on the `.c` and `.h` files.
+
+`make lint` runs:
+  - [`go vet`](https://golang.org/cmd/vet/) 
+  - [`staticcheck`](https://github.com/dominikh/go-tools/tree/master/cmd/staticcheck)
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..aee8553
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,237 @@
+# Makefile for fscrypt
+#
+# Copyright 2017 Google Inc.
+# Author: Joe Richey (joerichey@google.com)
+#
+# 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.
+
+# Update this on each new release, along with the NEWS.md file.
+VERSION := v0.3.4
+
+NAME := fscrypt
+PAM_NAME := pam_$(NAME)
+
+###### Makefile Command Line Flags ######
+#
+# BIN: The location where binaries will be built.     Default: bin
+# DESTDIR: Installation destination directory.        Default: ""
+# PREFIX: Installation path prefix.                   Default: /usr/local
+# BINDIR: Where to install the fscrypt binary.        Default: $(PREFIX)/bin
+# PAM_MODULE_DIR: Where to install pam_fscrypt.so.    Default: $(PREFIX)/lib/security
+# PAM_CONFIG_DIR: Where to install Ubuntu PAM config. Default: $(PREFIX)/share/pam-configs
+#   If the empty string, then the Ubuntu PAM config will not be installed.
+#
+# MOUNT: The filesystem where our tests are run.    Default: /mnt/fscrypt_mount
+#   Ex: make test-setup MOUNT=/foo/bar
+#     Creates a test filesystem at that location.
+#   Ex: make test-teardown MOUNT=/foo/bar
+#     Cleans up a test filesystem created with "make test-setup".
+#   Ex: make test MOUNT=/foo/bar
+#     Run all integration tests on that filesystem. This can be an existing
+#     filesystem, or one created with "make test-setup" (this is the default).
+#
+# CFLAGS: The flags passed to the C compiler.       Default: -O2 -Wall
+#   Ex: make fscrypt "CFLAGS = -O3 -Werror"
+#     Builds fscrypt with the C code failing on warnings and highly optimized.
+#
+# LDFLAGS: The flags passed to the C linker.        Default empty
+#   Ex: make fscrypt "LDFLAGS = -static -ldl -laudit -lcap-ng"
+#     Builds fscrypt as a static binary.
+#
+# GO_FLAGS: The flags passed to "go build".         Default empty
+#   Ex: make fscrypt "GO_FLAGS = -race"
+#     Builds fscrypt with race detection for the go code.
+#
+# GO_LINK_FLAGS: The flags passed to the go linker. Default: -s -w
+#   Ex: make fscrypt GO_LINK_FLAGS=""
+#     Builds fscrypt without stripping the binary.
+
+BIN := bin
+export PATH := $(BIN):$(PATH)
+PAM_MODULE := $(BIN)/$(PAM_NAME).so
+
+###### Setup Build Flags #####
+CFLAGS := -O2 -Wall
+# Pass CFLAGS to each cgo invocation.
+export CGO_CFLAGS = $(CFLAGS)
+# By default, we strip the binary to reduce size.
+GO_LINK_FLAGS := -s -w
+
+# Flag to embed the version (pulled from tags) into the binary.
+TAG_VERSION := $(shell git describe --tags)
+VERSION_FLAG := -X "main.version=$(if $(TAG_VERSION),$(TAG_VERSION),$(VERSION))"
+
+override GO_LINK_FLAGS += $(VERSION_FLAG) -extldflags "$(LDFLAGS)"
+override GO_FLAGS += --ldflags '$(GO_LINK_FLAGS)'
+# Use -trimpath if available
+ifneq "" "$(shell go help build | grep trimpath)"
+override GO_FLAGS += -trimpath
+endif
+
+###### Find All Files and Directories ######
+FILES := $(shell find . -path '*/.git*' -prune -o -type f -printf "%P\n")
+GO_FILES := $(filter %.go,$(FILES))
+GO_NONGEN_FILES := $(filter-out %.pb.go,$(GO_FILES))
+GO_DIRS := $(sort $(dir $(GO_FILES)))
+C_FILES := $(filter %.c %.h,$(FILES))
+PROTO_FILES := $(filter %.proto,$(FILES))
+
+###### Build, Formatting, and Linting Commands ######
+.PHONY: default all gen format lint clean
+default: $(BIN)/$(NAME) $(PAM_MODULE)
+all: tools gen default format lint test
+
+$(BIN)/$(NAME): $(GO_FILES) $(C_FILES)
+       go build $(GO_FLAGS) -o $@ ./cmd/$(NAME)
+
+$(PAM_MODULE): $(GO_FILES) $(C_FILES)
+       go build $(GO_FLAGS) -buildmode=c-shared -o $@ ./$(PAM_NAME)
+       rm -f $(BIN)/$(PAM_NAME).h
+
+gen: $(BIN)/protoc $(BIN)/protoc-gen-go $(PROTO_FILES)
+       protoc --go_out=. --go_opt=paths=source_relative $(PROTO_FILES)
+
+format: $(BIN)/goimports
+       goimports -w $(GO_NONGEN_FILES)
+       clang-format -i -style=Google $(C_FILES)
+
+lint: $(BIN)/staticcheck $(BIN)/misspell
+       go vet ./...
+       staticcheck ./...
+       misspell -source=text $(FILES)
+       shellcheck -s bash cmd/fscrypt/fscrypt_bash_completion
+       ( cd cli-tests && shellcheck -x *.sh)
+
+clean:
+       rm -f $(BIN)/$(NAME) $(PAM_MODULE) $(TOOLS) coverage.out $(COVERAGE_FILES) $(PAM_CONFIG)
+
+###### Go tests ######
+.PHONY: test test-setup test-teardown
+
+# If MOUNT exists signal that we should run integration tests.
+MOUNT := /tmp/$(NAME)-mount
+IMAGE := /tmp/$(NAME)-image
+ifneq ("$(wildcard $(MOUNT))","")
+export TEST_FILESYSTEM_ROOT = $(MOUNT)
+endif
+
+test:
+       go test -p 1 ./...
+
+test-setup:
+       dd if=/dev/zero of=$(IMAGE) bs=1M count=20
+       mkfs.ext4 -b 4096 -O encrypt $(IMAGE) -F
+       mkdir -p $(MOUNT)
+       sudo mount -o rw,loop,user $(IMAGE) $(MOUNT)
+       sudo chmod +777 $(MOUNT)
+
+test-teardown:
+       sudo umount $(MOUNT)
+       rmdir $(MOUNT)
+       rm -f $(IMAGE)
+
+###### Command-line interface tests ######
+.PHONY: cli-test cli-test-update
+
+cli-test: $(BIN)/$(NAME)
+       sudo cli-tests/run.sh
+
+cli-test-update: $(BIN)/$(NAME)
+       sudo cli-tests/run.sh --update-output
+
+# Runs tests and generates coverage
+COVERAGE_FILES := $(addsuffix coverage.out,$(GO_DIRS))
+coverage.out: $(BIN)/gocovmerge $(COVERAGE_FILES)
+       @gocovmerge $(COVERAGE_FILES) > $@
+
+%/coverage.out: $(GO_FILES) $(C_FILES)
+       @go test -coverpkg=./... -covermode=count -coverprofile=$@ -p 1 ./$* 2> /dev/null
+
+###### Installation Commands (require sudo) #####
+.PHONY: install install-bin install-pam uninstall install-completion
+install: install-bin install-pam install-completion
+
+PREFIX := /usr/local
+BINDIR := $(PREFIX)/bin
+
+install-bin: $(BIN)/$(NAME)
+       install -d $(DESTDIR)$(BINDIR)
+       install $< $(DESTDIR)$(BINDIR)
+
+PAM_MODULE_DIR := $(PREFIX)/lib/security
+PAM_INSTALL_PATH := $(PAM_MODULE_DIR)/$(PAM_NAME).so
+PAM_CONFIG := $(BIN)/config
+PAM_CONFIG_DIR := $(PREFIX)/share/pam-configs
+
+install-pam: $(PAM_MODULE)
+       install -d $(DESTDIR)$(PAM_MODULE_DIR)
+       install $(PAM_MODULE) $(DESTDIR)$(PAM_MODULE_DIR)
+ifdef PAM_CONFIG_DIR
+       m4 --define=PAM_INSTALL_PATH=$(PAM_INSTALL_PATH) < $(PAM_NAME)/config > $(PAM_CONFIG)
+       install -d $(DESTDIR)$(PAM_CONFIG_DIR)
+       install $(PAM_CONFIG) $(DESTDIR)$(PAM_CONFIG_DIR)/$(NAME)
+endif
+
+COMPLETION_INSTALL_DIR := $(PREFIX)/share/bash-completion/completions
+
+install-completion: cmd/fscrypt/fscrypt_bash_completion
+       install -Dm644 $< $(DESTDIR)$(COMPLETION_INSTALL_DIR)/fscrypt
+
+uninstall:
+       rm -f $(DESTDIR)$(BINDIR)/$(NAME) \
+             $(DESTDIR)$(PAM_INSTALL_PATH) \
+             $(DESTDIR)$(COMPLETION_INSTALL_DIR)/fscrypt
+ifdef PAM_CONFIG_DIR
+       rm -f $(DESTDIR)$(PAM_CONFIG_DIR)/$(NAME)
+endif
+
+#### Tool Building Commands ####
+TOOLS := $(addprefix $(BIN)/,protoc protoc-gen-go goimports staticcheck gocovmerge misspell)
+.PHONY: tools
+tools: $(TOOLS)
+
+$(BIN)/protoc-gen-go:
+       go build -o $@ google.golang.org/protobuf/cmd/protoc-gen-go
+$(BIN)/goimports:
+       go build -o $@ golang.org/x/tools/cmd/goimports
+$(BIN)/staticcheck:
+       go build -o $@ honnef.co/go/tools/cmd/staticcheck
+$(BIN)/gocovmerge:
+       go build -o $@ github.com/wadey/gocovmerge
+$(BIN)/misspell:
+       go build -o $@ github.com/client9/misspell/cmd/misspell
+
+# Non-go tools downloaded from appropriate repository
+PROTOC_VERSION := 3.6.1
+ARCH := $(shell uname -m)
+ifeq (x86_64,$(ARCH))
+PROTOC_ARCH := x86_64
+else ifneq ($(filter i386 i686,$(ARCH)),)
+PROTOC_ARCH := x86_32
+else ifneq ($(filter aarch64 armv8l,$(ARCH)),)
+PROTOC_ARCH := aarch_64
+endif
+ifdef PROTOC_ARCH
+PROTOC_URL := https://github.com/google/protobuf/releases/download/v$(PROTOC_VERSION)/protoc-$(PROTOC_VERSION)-linux-$(PROTOC_ARCH).zip
+$(BIN)/protoc:
+       wget -q $(PROTOC_URL) -O /tmp/protoc.zip
+       unzip -q -j /tmp/protoc.zip bin/protoc -d $(BIN)
+else
+PROTOC_PATH := $(shell which protoc)
+$(BIN)/protoc: $(PROTOC_PATH)
+ifneq ($(wildcard $(PROTOC_PATH)),)
+       cp $< $@
+else
+       $(error Could not download protoc binary or locate it on the system. Please install it)
+endif
+endif
diff --git a/NEWS.md b/NEWS.md
new file mode 100644 (file)
index 0000000..b282b25
--- /dev/null
+++ b/NEWS.md
@@ -0,0 +1,295 @@
+# `fscrypt` release notes
+
+## Version 0.3.4
+
+* `fscrypt` now requires Go 1.16 or later to build.
+
+* `pam_fscrypt` now supports the option `unlock_only` to disable locking of
+  directories on logout.
+
+* Fixed a bug where the number of CPUs used in the passphrase hash would be
+  calculated incorrectly on systems with more than 255 CPUs.
+
+* Added support for AES-256-HCTR2 filenames encryption.
+
+* Directories are now synced immediately after an encryption policy is applied,
+  reducing the chance of an inconsistency after a sudden crash.
+
+* Added Lustre to the list of allowed filesystems.
+
+* Added a NEWS.md file that contains the release notes, and backfilled it from
+  the GitHub release notes.
+
+## Version 0.3.3
+
+This release contains fixes for three security vulnerabilities and related
+security hardening:
+
+* Correctly handle malicious mountpoint paths in the `fscrypt` bash completion
+  script (CVE-2022-25328, command injection).
+
+* Validate the size, type, and owner (for login protectors) of policy and
+  protector files (CVE-2022-25327, denial of service).
+
+* Make the `fscrypt` metadata directories non-world-writable by default
+  (CVE-2022-25326, denial of service).
+
+* When running as a non-root user, ignore policy and protector files that aren't
+  owned by the user or by root.
+
+* Also require that the metadata directories themselves and the mountpoint root
+  directory be owned by the user or by root.
+
+* Make policy and protector files mode `0600` rather than `0644`.
+
+* Make all relevant files owned by the user when `root` encrypts a directory
+  with a user's login protector, not just the login protector itself.
+
+* Make `pam_fscrypt` ignore system users completely.
+
+Thanks to Matthias Gerstner (SUSE) for reporting the above vulnerabilities and
+suggesting additional hardening.
+
+Note: none of these vulnerabilities or changes are related to the cryptography
+used.  The main issue was that it wasn't fully considered how `fscrypt`'s
+metadata storage method could lead to denial-of-service attacks if a local user
+is malicious.
+
+Although upgrading to v0.3.3 shouldn't break existing users, there may be some
+edge cases where users were relying on functionality in ways we didn't
+anticipate.  If you encounter any issues, please report them as soon as possible
+so that we can find a solution for you.
+
+## Version 0.3.2
+
+* Made linked protectors (e.g., login protectors used on a non-root filesystem)
+  more reliable when a filesystem UUID changes.
+
+* Made login protectors be owned by the user when they are created as root, so
+  that the user has permission to update them later.
+
+* Made `fscrypt` work when the root directory is on a btrfs filesystem.
+
+* Made `pam_fscrypt` start warning when a user's login protector is getting
+  de-synced due to their password being changed by root.
+
+* Support reading the key for raw key protectors from standard input.
+
+* Made `fscrypt metadata remove-protector-from-policy` work even if the
+  protector is no longer accessible.
+
+* Made `fscrypt` stop trying to access irrelevant filesystems.
+
+* Improved the documentation.
+
+## Version 0.3.1
+
+* Slightly decreased the amount of memory that `fscrypt` uses for password
+  hashing, to avoid out-of-memory situations.
+
+* Made recovery passphrase generation happen without a prompt by default, and
+  improved the explanation given.
+
+* Made many improvements to the README file.
+
+* Various other minor fixes
+
+## Version 0.3.0
+
+While this release includes some potentially breaking changes, we don't expect
+this to break users in practice.
+
+* Potentially breaking changes to `pam_fscrypt` module:
+    * Remove the `drop_caches` and `lock_policies` options.  The `lock_policies`
+      behavior is now unconditional, while the correct `drop_caches` setting is
+      now auto-detected.  Existing PAM files that specify these options will
+      continue to work, but these options will now be ignored.
+    * Prioritize over other session modules.  The `pam_fscrypt` session hook is
+      now inserted into the correct place in the PAM stack when `pam_fscrypt` is
+      configured using Debian's / Ubuntu's PAM configuration framework.
+
+* Non-breaking changes:
+    * Add Bash completions for `fscrypt`.
+    * Fix an error message.
+    * Correctly detect "incompletely locked" v1-encrypted directories on kernel
+      versions 5.10 and later.
+
+* Other:
+    * Improve Ubuntu installation instructions.
+    * Minor README updates
+    * CI updates, including switching from Travis CI to GitHub Actions
+
+## Version 0.2.9
+
+This release includes:
+
+* Fix 32-bit build.  This was supposed to be fixed in v0.2.8, but another
+  breakage was added in the same release.
+
+* Clarify output of `fscrypt status DIR` on v1-encrypted directories in some
+  cases.
+
+* [Developers]
+    * Add 32-bit build to presubmit checks.
+    * Fix `cli-tests/t_v1_policy` to not be flaky.
+
+## Version 0.2.8
+
+* Build fixes
+    * Fix build on 32-bit platforms.
+    * Fix build with gcc 10.
+
+* Allow `fscrypt` to work in containers.
+
+* Usability improvements
+    * Improve many error messages and suggestions.  For example, if the
+      `encrypt` feature flag needs to be enabled on an ext4 filesystem,
+      `fscrypt` will now show the `tune2fs` command to run.
+    * Document how to securely use login protectors, and link to that
+      documentation when creating a new login protector.
+    * Try to detect incomplete locking of v1-encrypted directory.
+    * Several other small improvements
+
+* [Developers] Added command-line interface tests.
+
+## Version 0.2.7
+
+The main addition in this release is that we now automatically detect support
+for V2 policies when running `fscrypt setup` and configure `/etc/fscrypt.conf`
+appropriately.  This allows users on newer kernels to automatically start using
+V2 policies without manually changing `/etc/fscrypt.conf`.  To use these new
+policies, simply run `sudo fscrypt setup` and your `/etc/fscrypt.conf` will be
+automatically updated.
+
+We also made changes to make the build of `fscrypt` reproducible:
+* Simplify `fscrypt --version` output.
+* Use `-trimpath`.
+
+Finally, we added improved documentation and fixed up the Makefile.
+
+## Version 0.2.6
+
+The big feature in this release is support for v2 kernel encryption policies.
+With the release of Linux 5.4, the kernel added a [new type of
+policy](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html) that
+makes `fscrypt` much easier to use.  For directories using these new policies:
+
+* `fscrypt unlock` makes the plaintext version of the directory visible to all
+  users (if they have permission).  This makes sharing encrypted folders between
+  users (or a user and root) much easier.
+
+* `fscrypt lock` (also new in this release) can be run as a non-root user.
+
+* The policies are no longer tied to the buggy kernel keyring API.
+    * This removes the need for users to run `keyctl link` or to reconfigure
+      `pam_keyinit`.
+    * Some systemd related bugs will no longer be an issue.
+
+* Denial-of-Service attacks possible with the v1 API can no longer be used.
+
+To use this new functionality, make sure you are on Linux 5.4 or later.  Then,
+add `"policy_version": "2"` to `"options"` in `/etc/fscrypt.conf`.  After this,
+all new directories will encrypted with v2 polices.  See the `README.md` for
+more information, including how to use some of the new kernel features with
+existing directories.
+
+Many thanks to @ebiggers for the herculean effort to get this code (and the
+kernel code) tested and merged.
+
+Other new features in this release:
+* The `.fscrypt` directory can now be a symlink.
+* When an encrypted directory and a protector reside on different
+  filesystems, we now automatically create a recovery password.
+
+Bug fixes in this release:
+* Bind mounts are now handled correctly.
+* Cleanup polices/protectors on failure.
+* Config file is created with the correct mode.
+* `fscrypt setup` now properly creates `/.fscrypt`.
+* Work around strange Go interaction with process keyrings.
+* Misc Optimizations
+* Build and CI improvements
+* Doc updates
+
+## Version 0.2.5
+
+A special thanks to @ebiggers for most of the changes in this release.
+
+With the release of 1.13 recently, the minimum supported version of Go for
+`fscrypt` is now 1.12.
+
+`fscrypt` now uses go modules (and no longer uses `dep`).
+
+New Features:
+* [Adiantum](https://github.com/google/adiantum) support
+* Display encryption options in `fscrypt status DIR`.
+
+Changes to improve stability of `fscrypt`:
+* Ensure `fscrypt` file updates are always atomic.
+* Use sane defaults for newly encrypted directories.
+* Install PAM modules/configs correctly.
+
+The remaining changes include numerous fixes to the Documentation and CI.
+
+## Version 0.2.4
+
+This release contains multiple bug fixes, including a fix for
+[CVE-2018-6558](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-6558),
+which allowed for privilege escalation.  Please update `fscrypt` as soon as
+possible.  Debian and Ubuntu updates should be available soon.
+
+## Version 0.2.3
+
+This small release makes `fscrypt` much easier to build and use.
+
+* `PasswordHash` has completely moved to
+  [`x/crypto/argon2`](https://godoc.org/golang.org/x/crypto/argon2), eliminating
+  the [`libargon2`](https://github.com/P-H-C/phc-winner-argon2) build and
+  runtime dependency.  Now the dependencies to build `fscrypt` are `go`, `make`,
+  `gcc`, and some system headers.  That's it!
+
+* `PasswordHash` will only use at most 128MiB.  This allows users to encrypt
+  files on removable media and rest assured that it will still work when plugged
+  into another system with less memory.
+
+* `fscrypt`'s build and CI systems have been greatly improved.  All dependencies
+  are now vendored with `dep` allowing for reproducible builds.  Building,
+  testing, and changing `fscrypt` is now much more straightforward.
+
+* Other minor fixes
+
+## Version 0.2.2
+
+This release improves the process of purging keyrings by:
+* Fixing a bug where keys would not be cleared on logout if the session
+  keyring was misconfigured
+* Always syncing the filesystem metadata when purging keys
+
+Minor features include:
+* Added cryptographic algorithms from the 4.13 kernel.
+* Improved our Travis CI processes.
+
+Features coming in 0.3:
+* Major Documentation rewrite
+* Commands to automatically handle ext4 feature flags
+* UI refactoring
+
+## Version 0.2.1
+
+See the Pull Requests and Closed Issues for more detailed information.
+
+* The PAM module now works without crashing the login process.
+* Keys work properly when switching between root and non-root users.
+* Finalized how the keys will be provisioned into the kernel keyring.
+
+## Version 0.2.0
+
+This release introduces the PAM Module and associated documentation.
+
+It also includes numerous bug fixes.
+
+## Version 0.1.0
+
+This is the version of `fscrypt` which was first made public on Github.
+
+The redacted commit history from internal development is maintained.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..044b0bf
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1282 @@
+# fscrypt [![GitHub version](https://badge.fury.io/go/github.com%2Fgoogle%2Ffscrypt.svg)](https://github.com/google/fscrypt/releases)
+
+[![Build Status](https://github.com/google/fscrypt/workflows/CI/badge.svg)](https://github.com/google/fscrypt/actions?query=workflow%3ACI+branch%3Amaster)
+[![GoDoc](https://godoc.org/github.com/google/fscrypt?status.svg)](https://godoc.org/github.com/google/fscrypt)
+[![Go Report Card](https://goreportcard.com/badge/github.com/google/fscrypt)](https://goreportcard.com/report/github.com/google/fscrypt)
+[![License](https://img.shields.io/badge/LICENSE-Apache2.0-ff69b4.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)
+
+`fscrypt` is a high-level tool for the management of [Linux native filesystem
+encryption](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html).
+`fscrypt` manages metadata, key generation, key wrapping, PAM integration, and
+provides a uniform interface for creating and modifying encrypted directories.
+For a small low-level tool that directly sets policies, see
+[`fscryptctl`](https://github.com/google/fscryptctl).
+
+To use `fscrypt`, you must have a filesystem that supports the Linux native
+filesystem encryption API (which is also sometimes called "fscrypt"; this
+documentation calls it "Linux native filesystem encryption" to avoid confusion).
+Only certain filesystems, such as [ext4](https://en.wikipedia.org/wiki/Ext4) and
+[f2fs](https://en.wikipedia.org/wiki/F2FS), support this API.  For a full list
+of supported filesystems and how to enable encryption support on each one, see
+[Runtime dependencies](#runtime-dependencies).
+
+For the release notes, see the [NEWS file](NEWS.md).
+
+## Table of contents
+
+- [Alternatives to consider](#alternatives-to-consider)
+- [Threat model](#threat-model)
+- [Features](#features)
+- [Building and installing](#building-and-installing)
+- [Runtime dependencies](#runtime-dependencies)
+- [Configuration file](#configuration-file)
+- [Setting up `fscrypt` on a filesystem](#setting-up-fscrypt-on-a-filesystem)
+- [Setting up for login protectors](#setting-up-for-login-protectors)
+  - [Securing your login passphrase](#securing-your-login-passphrase)
+  - [Enabling the PAM module](#enabling-the-pam-module)
+    - [Enabling the PAM module on Debian or Ubuntu](#enabling-the-pam-module-on-debian-or-ubuntu)
+       - [Enabling the PAM module on Arch Linux](#enabling-the-pam-module-on-arch-linux)
+       - [Enabling the PAM module on other Linux distros](#enabling-the-pam-module-on-other-linux-distros)
+  - [Allowing `fscrypt` to check your login passphrase](#allowing-fscrypt-to-check-your-login-passphrase)
+- [Backup, restore, and recovery](#backup-restore-and-recovery)
+- [Encrypting existing files](#encrypting-existing-files)
+- [Example usage](#example-usage)
+  - [Setting up fscrypt on a directory](#setting-up-fscrypt-on-a-directory)
+  - [Locking and unlocking a directory](#locking-and-unlocking-a-directory)
+  - [Protecting a directory with your login passphrase](#protecting-a-directory-with-your-login-passphrase)
+  - [Changing a custom passphrase](#changing-a-custom-passphrase)
+  - [Using a raw key protector](#using-a-raw-key-protector)
+  - [Using multiple protectors for a policy](#using-multiple-protectors-for-a-policy)
+- [Contributing](#contributing)
+- [Troubleshooting](#troubleshooting)
+  - [I changed my login passphrase, now all my directories are inaccessible](#i-changed-my-login-passphrase-now-all-my-directories-are-inaccessible)
+  - [Directories using my login passphrase are not automatically unlocking](#directories-using-my-login-passphrase-are-not-automatically-unlocking)
+  - [Getting "encryption not enabled" on an ext4 filesystem](#getting-encryption-not-enabled-on-an-ext4-filesystem)
+  - [Getting "user keyring not linked into session keyring"](#getting-user-keyring-not-linked-into-session-keyring)
+  - [Getting "Operation not permitted" when moving files into an encrypted directory](#getting-operation-not-permitted-when-moving-files-into-an-encrypted-directory)
+  - [Getting "Package not installed" when trying to use an encrypted directory](#getting-package-not-installed-when-trying-to-use-an-encrypted-directory)
+  - [Some processes can't access unlocked encrypted files](#some-processes-cant-access-unlocked-encrypted-files)
+  - [Users can access other users' unlocked encrypted files](#users-can-access-other-users-unlocked-encrypted-files)
+  - [Getting "Required key not available" when backing up locked encrypted files](#getting-required-key-not-available-when-backing-up-locked-encrypted-files)
+  - [The reported size of encrypted symlinks is wrong](#the-reported-size-of-encrypted-symlinks-is-wrong)
+- [Legal](#legal)
+
+## Alternatives to consider
+
+Operating-system level storage encryption solutions work at either the
+filesystem or block device level.  [Linux native filesystem
+encryption](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html)
+(the solution configured by `fscrypt`) is filesystem-level; it encrypts
+individual directories.  Only file contents and filenames are encrypted;
+non-filename metadata, such as timestamps, the sizes and number of files, and
+extended attributes, is **not** encrypted.  Users choose which directories will
+be encrypted, and with what keys.
+
+Before using `fscrypt`, you should consider other solutions:
+
+* [**dm-crypt/LUKS**](https://en.wikipedia.org/wiki/Dm-crypt) is block device
+  level encryption: it encrypts an entire block device (and hence an entire
+  filesystem) with one key.  Unlocking this key will unlock the entire block
+  device.  dm-crypt/LUKS is usually configured using
+  [cryptsetup](https://gitlab.com/cryptsetup/cryptsetup/-/wikis/home).
+
+* [**eCryptfs**](https://en.wikipedia.org/wiki/ECryptfs) is an alternative
+  filesystem-level encryption solution.  It is a stacked filesystem, which means
+  it sits on top of a real filesystem, rather than being directly integrated
+  into the real filesystem.  Stacked filesystems have a couple advantages (such
+  as working on almost any real filesystem), but also some significant
+  disadvantages.  eCryptfs is usually configured using
+  [ecryptfs-utils](https://packages.debian.org/stretch/ecryptfs-utils).
+
+* The [**ZFS**](https://en.wikipedia.org/wiki/ZFS) filesystem supports
+  encryption in its own way (not compatible with `fscrypt`).  ZFS encryption has
+  some advantages; however, ZFS isn't part of the upstream Linux kernel and is
+  less common than other filesystems, so this solution usually isn't an option.
+
+Which solution to use?  Here are our recommendations:
+
+* eCryptfs shouldn't be used, if at all possible.  eCryptfs's use of filesystem
+  stacking causes a number of issues, and eCryptfs is no longer actively
+  maintained.  The original author of eCryptfs recommends using Linux native
+  filesystem encryption instead.  The largest users of eCryptfs (Ubuntu and
+  Chrome OS) have switched to dm-crypt or Linux native filesystem encryption.
+
+* If you need fine-grained control of encryption within a filesystem, then use
+  `fscrypt`, or `fscrypt` together with dm-crypt/LUKS.  If you don't need this,
+  then use dm-crypt/LUKS.
+
+  To understand this recommendation: consider that the main advantage of
+  `fscrypt` is to allow different files on the same filesystem to be encrypted
+  by different keys, and thus be unlockable, lockable, and securely deletable
+  independently from each other.  Therefore, `fscrypt` is useful in cases such
+  as:
+
+    * Multi-user systems, since each user's files can be encrypted with their
+      own key that is unlocked by their own passphrase.
+
+    * Single-user systems where it's not possible for all files to have the
+      strongest level of protection.  For example, it might be necessary for the
+      system to boot up without user interaction.  Any files that are needed to
+      do so can only be encrypted by a hardware-protected (e.g. TPM-bound) key
+      at best.  If the user's personal files are located on the same filesystem,
+      then with dm-crypt/LUKS the user's personal files would be limited to this
+      weak level of protection.  With `fscrypt`, the user's personal files could
+      be fully protected using the user's passphrase.
+
+  `fscrypt` isn't very useful in the following cases:
+
+    * Single-user systems where the user is willing to enter a strong passphrase
+      at boot time to unlock the entire filesystem.  In this case, the main
+      advantage of `fscrypt` would go unused, so dm-crypt/LUKS would be better
+      as it would provide better security (due to ensuring that all files and
+      all filesystem metadata are encrypted).
+
+    * Any case where it is feasible to create a separate filesystem for every
+      encryption key you want to use.
+
+  Note: dm-crypt/LUKS and `fscrypt` aren't mutually exclusive; they can be used
+  together when the performance hit of double encryption is tolerable.  It only
+  makes sense to do this when the keys for each encryption layer are protected
+  in different ways, such that each layer serves a different purpose.  A
+  reasonable set-up would be to encrypt the whole filesystem with dm-crypt/LUKS
+  using a TPM-bound key that is automatically unlocked at boot time, and also
+  encrypt users' home directories with `fscrypt` using their login passphrases.
+
+## Threat model
+
+Like other storage encryption solutions (including dm-crypt/LUKS and eCryptfs),
+Linux native filesystem encryption is primarily intended to protect the
+confidentiality of data from a single point-in-time permanent offline compromise
+of the disk.  For a detailed description of the threat model, see the [kernel
+documentation](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html#threat-model).
+
+It's worth emphasizing that none of these encryption solutions protect unlocked
+encrypted files from other users on the same system (that's the job of OS-level
+access control, such as UNIX file permissions), or from the cloud provider you
+may be running a virtual machine on.  By themselves, they also do not protect
+from "evil maid" attacks, i.e. non-permanent offline compromises of the disk.
+
+## Features
+
+`fscrypt` is intended to improve upon the work in
+[e4crypt](http://man7.org/linux/man-pages/man8/e4crypt.8.html) by providing a
+more managed environment and handling more functionality in the background.
+`fscrypt` has a [design document](https://goo.gl/55cCrI) specifying its full
+architecture.  See also the [kernel documentation for Linux native filesystem
+encryption](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html).
+
+Briefly, `fscrypt` deals with protectors and policies. Protectors represent some
+secret or information used to protect the confidentiality of your data. The
+three currently supported protector types are:
+
+1. Your login passphrase, through [PAM](http://www.linux-pam.org/Linux-PAM-html).
+   The included PAM module (`pam_fscrypt.so`) can automatically unlock
+   directories protected by your login passphrase when you log in, and lock them
+   when you log out.  __IMPORTANT:__ before using a login protector, follow
+   [Setting up for login protectors](#setting-up-for-login-protectors).
+
+2. A custom passphrase.  This passphrase is hashed with
+   [Argon2id](https://en.wikipedia.org/wiki/Argon2), by default calibrated to
+   use all CPUs and take about 1 second.
+
+3. A raw key file.  See [Using a raw key protector](#using-a-raw-key-protector).
+
+These protectors are mutable, so the information can change without needing to
+update any of your encrypted directories.
+
+Policies represent the actual key passed to the kernel. This "policy key" is
+immutable and policies are (usually) applied to a single directory. Protectors
+then protect policies, so that having one of the protectors for a policy is
+enough to get the policy key and access the data. Which protectors protect a
+policy can also be changed. This allows a user to change how a directory is
+protected without needing to reencrypt the directory's contents.
+
+Concretely, `fscrypt` contains the following functionality:
+*   `fscrypt setup` - Creates `/etc/fscrypt.conf` and the `/.fscrypt` directory
+    * This is the only functionality which always requires root privileges
+*   `fscrypt setup MOUNTPOINT` - Gets a filesystem ready for use with fscrypt
+*   `fscrypt encrypt DIRECTORY` - Encrypts an empty directory
+*   `fscrypt unlock DIRECTORY` - Unlocks an encrypted directory
+*   `fscrypt lock DIRECTORY` - Locks an encrypted directory
+*   `fscrypt purge MOUNTPOINT` - Locks all encrypted directories on a filesystem
+*   `fscrypt status [PATH]` - Gets detailed info about filesystems or paths
+*   `fscrypt metadata` - Manages policies or protectors directly
+
+See the example usage section below or run `fscrypt COMMAND --help` for more
+information about each of the commands.
+
+## Building and installing
+
+`fscrypt` has a minimal set of build dependencies:
+*   [Go](https://golang.org/doc/install) 1.18 or higher. Older versions may work
+    but they are not tested or supported.
+*   A C compiler (`gcc` or `clang`)
+*   `make`
+*   Headers for [`libpam`](http://www.linux-pam.org/).
+    Install them with the appropriate package manager:
+    - Debian/Ubuntu: `sudo apt install libpam0g-dev`
+    - Red Hat: `sudo yum install pam-devel`
+    - Arch: [`pam`](https://www.archlinux.org/packages/core/x86_64/pam/)
+      package (usually installed by default)
+
+Once all the dependencies are installed, clone the repository by running:
+```shell
+git clone https://github.com/google/fscrypt
+```
+Running `make` builds the binary (`fscrypt`) and PAM module (`pam_fscrypt.so`)
+in the `bin/` directory.
+
+Running `sudo make install` installs `fscrypt` into `/usr/local/bin`,
+`pam_fscrypt.so` into `/usr/local/lib/security`, and `pam_fscrypt/config` into
+`/usr/local/share/pam-configs`.
+
+On Debian (and Debian derivatives such as Ubuntu), use `sudo make install
+PREFIX=/usr` to install into `/usr` instead of the default of `/usr/local`.
+Ordinarily you shouldn't manually install software into `/usr`, since `/usr` is
+reserved for Debian's own packages.  However, Debian's PAM configuration
+framework only recognizes configuration files in `/usr`, not in `/usr/local`.
+Therefore, the PAM module will only work if you install into `/usr`.  Note: if
+you later decide to switch to using the Debian package `libpam-fscrypt`, you'll
+have to first manually run `sudo make uninstall PREFIX=/usr`.
+
+It is also possible to use `make install-bin` to only install the `fscrypt`
+binary, or `make install-pam` to only install the PAM files.
+
+Alternatively, if you only want to install the `fscrypt` binary to
+`$GOPATH/bin`, simply run:
+```shell
+go install github.com/google/fscrypt/cmd/fscrypt@latest
+```
+
+See the `Makefile` for instructions on how to further customize the build.
+
+## Runtime dependencies
+
+To run, `fscrypt` needs the following libraries:
+*   `libpam.so` (almost certainly already on your system)
+
+In addition, `fscrypt` requires a filesystem that supports the Linux native
+filesystem encryption API.  Currently, the filesystems that support this are:
+
+* ext4, with upstream kernel v4.1 or later.  The kernel configuration must
+  contain `CONFIG_FS_ENCRYPTION=y` (for kernels v5.1+) or
+  `CONFIG_EXT4_ENCRYPTION=y` or `=m` (for older kernels).  The filesystem must
+  also have the `encrypt` feature flag enabled; to enable this flag, see
+  [here](#getting-encryption-not-enabled-on-an-ext4-filesystem).
+
+* f2fs, with upstream kernel v4.2 or later.  The kernel configuration must
+  contain `CONFIG_FS_ENCRYPTION=y` (for kernels v5.1+) or
+  `CONFIG_F2FS_FS_ENCRYPTION=y` (for older kernels).  The filesystem must also
+  have the `encrypt` feature flag enabled; this flag can be enabled at format
+  time by `mkfs.f2fs -O encrypt` or later by `fsck.f2fs -O encrypt`.
+
+* UBIFS, with upstream kernel v4.10 or later.  The kernel configuration must
+  contain `CONFIG_FS_ENCRYPTION=y` (for kernels v5.1+) or
+  `CONFIG_UBIFS_FS_ENCRYPTION=y` (for older kernels).
+
+* CephFS, with upstream kernel v6.6 or later.  The kernel configuration must
+  contain `CONFIG_FS_ENCRYPTION=y`.
+
+* [Lustre](https://www.lustre.org/), with Lustre v2.14.0 or later.  For details,
+  see the Lustre documentation.  Please note that Lustre is not part of the
+  upstream Linux kernel, and its encryption implementation has not been reviewed
+  by the authors of `fscrypt`.  Questions/issues about Lustre encryption should
+  be directed to the Lustre developers.  Lustre version 2.14 does not encrypt
+  filenames, even though it claims to, so v2.15.0 or later should be used.
+
+To check whether the needed option is enabled in your kernel, run:
+```shell
+zgrep -h ENCRYPTION /proc/config.gz /boot/config-$(uname -r) | sort | uniq
+```
+
+It is also recommended to use Linux kernel v5.4 or later, since this
+allows the use of v2 encryption policies.  v2 policies have several
+security and usability improvements over v1 policies.
+
+Be careful when using ext4 encryption on removable media, since ext4 filesystems
+with the `encrypt` feature cannot be mounted on systems with kernel versions
+older than the minimums listed above -- even to access unencrypted files!
+
+If you configure `fscrypt` to use non-default features, other kernel
+prerequisites may be needed too.  See [Configuration
+file](#configuration-file).
+
+## Configuration file
+
+Running `sudo fscrypt setup` will create the configuration file
+`/etc/fscrypt.conf` if it doesn't already exist.  It's a JSON file
+that looks like the following:
+
+```
+{
+       "source": "custom_passphrase",
+       "hash_costs": {
+               "time": "52",
+               "memory": "131072",
+               "parallelism": "32"
+       },
+       "options": {
+               "padding": "32",
+               "contents": "AES_256_XTS",
+               "filenames": "AES_256_CTS",
+               "policy_version": "2"
+       },
+       "use_fs_keyring_for_v1_policies": false,
+       "allow_cross_user_metadata": false
+}
+```
+
+The fields are:
+
+* "source" is the default source for new protectors.  The choices are
+  "pam\_passphrase", "custom\_passphrase", and "raw\_key".
+
+* "hash\_costs" describes how difficult the passphrase hashing is.
+  By default, `fscrypt setup` calibrates the hashing to use all CPUs
+  and take about 1 second.  The `--time` option to `fscrypt setup` can
+  be used to customize this time when creating the configuration file.
+
+* "options" are the encryption options to use for new encrypted
+  directories:
+
+    * "padding" is the number of bytes by which filenames are padded
+      before being encrypted.  The choices are "32", "16", "8", and
+      "4".  "32" is recommended.
+
+    * "contents" is the algorithm used to encrypt file contents.  The
+      choices are "AES_256_XTS", "AES_128_CBC", and "Adiantum".
+      Normally, "AES_256_XTS" is recommended.
+
+    * "filenames" is the algorithm used to encrypt file names.  The
+      choices are "AES_256_CTS", "AES_128_CTS", "Adiantum", and
+      "AES_256_HCTR2".  Normally, "AES_256_CTS" is recommended.
+
+      To use algorithms other than "AES_256_XTS" for contents and
+      "AES_256_CTS" for filenames, the needed algorithm(s) may need to
+      be enabled in the Linux kernel's cryptography API.  For example,
+      to use Adiantum, `CONFIG_CRYPTO_ADIANTUM` must be set.  Also,
+      not all combinations of algorithms are allowed; for example,
+      "Adiantum" for contents can only be paired with "Adiantum" for
+      filenames.  See the [kernel
+      documentation](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html#encryption-modes-and-usage)
+      for more details about the supported algorithms.
+
+    * "policy\_version" is the version of encryption policy to use.
+      The choices are "1" and "2".  If unset, "1" is assumed.
+      Directories created with policy version "2" are only usable on
+      kernel v5.4 or later, but are preferable to version "1" if you
+      don't mind this restriction.
+
+* "use\_fs\_keyring\_for\_v1\_policies" specifies whether to add keys for v1
+  encryption policies to the filesystem keyrings, rather than to user keyrings.
+  This can solve [issues with processes being unable to access unlocked
+  encrypted files](#some-processes-cant-access-unlocked-encrypted-files).
+  However, it requires kernel v5.4 or later, and it makes unlocking and locking
+  encrypted directories require root.  (The PAM module will still work.)
+
+  The purpose of this setting is to allow people to take advantage of some of
+  the improvements in Linux v5.4 on encrypted directories that are also
+  compatible with older kernels.  If you don't need compatibility with older
+  kernels, it's better to not use this setting and instead (re-)create your
+  encrypted directories with `"policy_version": "2"`.
+
+* "allow\_cross\_user\_metadata" specifies whether `fscrypt` will allow
+  protectors and policies from other non-root users to be read, e.g. to be
+  offered as options by `fscrypt encrypt`.  The default value is `false`, since
+  other users might be untrusted and could create malicious files.  This can be
+  set to `true` to restore the old behavior on systems where `fscrypt` metadata
+  needs to be shared between multiple users.  Note that this option is
+  independent from the permissions on the metadata files themselves, which are
+  set to 0600 by default; users who wish to share their metadata files with
+  other users would also need to explicitly change their mode to 0644.
+
+## Setting up `fscrypt` on a filesystem
+
+`fscrypt` needs some directories to exist on the filesystem on which encryption
+will be used:
+
+* `MOUNTPOINT/.fscrypt/policies`
+* `MOUNTPOINT/.fscrypt/protectors`
+
+(If login protectors are used, these must also exist on the root filesystem.)
+
+To create these directories, run `fscrypt setup MOUNTPOINT`.  If MOUNTPOINT is
+owned by root, as is usually the case, then this command will require root.
+
+There will be one decision you'll need to make: whether non-root users will be
+allowed to create `fscrypt` metadata (policies and protectors).
+
+If you say `y`, then these directories will be made world-writable, with the
+sticky bit set so that users can't delete each other's files -- just like
+`/tmp`.  If you say `N`, then these directories will be writable only by root.
+
+Saying `y` maximizes the usability of `fscrypt`, and on most systems it's fine
+to say `y`.  However, on some systems this may be inappropriate, as it will
+allow malicious users to fill the entire filesystem unless filesystem quotas
+have been configured -- similar to problems that have historically existed with
+other world-writable directories, e.g. `/tmp`.  If you are concerned about this,
+say `N`.  If you say `N`, then you'll only be able to run `fscrypt` as root to
+set up encryption on users' behalf, unless you manually set custom permissions
+on the metadata directories to grant write access to specific users or groups.
+
+If you chose the wrong mode at `fscrypt setup` time, you can change the
+directory permissions at any time.  To enable single-user writable mode, run:
+
+    sudo chmod 0755 MOUNTPOINT/.fscrypt/*
+
+To enable world-writable mode, run:
+
+    sudo chmod 1777 MOUNTPOINT/.fscrypt/*
+
+## Setting up for login protectors
+
+If you want any encrypted directories to be protected by your login passphrase,
+you'll need to:
+
+1. Secure your login passphrase (optional, but strongly recommended)
+2. Enable the PAM module (`pam_fscrypt.so`)
+
+If you installed `fscrypt` from source rather than from your distro's package
+manager, you may also need to allow `fscrypt` to check your login passphrase.
+
+### Securing your login passphrase
+
+Although `fscrypt` uses a strong passphrase hash algorithm, the security of
+login protectors is also limited by the strength of your system's passphrase
+hashing in `/etc/shadow`.  On most Linux distributions, `/etc/shadow` by default
+uses SHA-512 with 5000 rounds, which is much weaker than what `fscrypt` uses.
+
+To mitigate this, you should use a strong login passphrase.
+
+If using a strong login passphrase is annoying because it needs to be entered
+frequently to run `sudo`, consider increasing the `sudo` timeout.  That can be
+done by adding the following to `/etc/sudoers`:
+```
+Defaults timestamp_timeout=60
+```
+
+You should also increase the number of rounds that your system's passphrase
+hashing uses (though this doesn't increase security as much as choosing a strong
+passphrase).  To do this, find the line in `/etc/pam.d/passwd` that looks like:
+```
+password       required        pam_unix.so sha512 shadow nullok
+```
+
+Append `rounds=1000000` (or another number of your choice; the goal is to make
+the passphrase hashing take about 1 second, similar to `fscrypt`'s default):
+```
+password       required        pam_unix.so sha512 shadow nullok rounds=1000000
+```
+
+Then, change your login passphrase to a new, strong passphrase:
+```
+passwd
+```
+
+If you'd like to keep the same login passphrase (not recommended, as the old
+passphrase hash may still be recoverable from disk), then instead run
+`sudo passwd $USER` and enter your existing passphrase.  This re-hashes your
+existing passphrase with the new `rounds`.
+
+### Enabling the PAM module
+
+To enable the PAM module `pam_fscrypt.so`, follow the directions for your Linux
+distro below.  Enabling the PAM module is needed for login passphrase-protected
+directories to be automatically unlocked when you log in (and be automatically
+locked when you log out), and for login passphrase-protected directories to
+remain accessible when you change your login passphrase.
+
+#### Enabling the PAM module on Debian or Ubuntu
+
+The official `libpam-fscrypt` package for Debian (and Debian derivatives such as
+Ubuntu) will install a configuration file for [Debian's PAM configuration
+framework](https://wiki.ubuntu.com/PAMConfigFrameworkSpec) to
+`/usr/share/pam-configs/fscrypt`.  This file contains reasonable defaults for
+the PAM module.  To automatically apply these defaults, run
+`sudo pam-auth-update` and follow the on-screen instructions.
+
+This file also gets installed if you build and install `fscrypt` from source,
+but it is only installed to the correct location if you use `make install
+PREFIX=/usr` to install into `/usr` instead of the default of `/usr/local`.
+
+#### Enabling the PAM module on Arch Linux
+
+On Arch Linux, follow the recommendations at the [Arch Linux
+Wiki](https://wiki.archlinux.org/index.php/Fscrypt#Auto-unlocking_directories).
+
+We recommend using the Arch Linux package, either `fscrypt` (official) or
+`fscrypt-git` (AUR).  If you instead install `fscrypt` manually using `sudo make
+install`, then in addition to the steps on the Wiki you'll also need to [create
+`/etc/pam.d/fscrypt`](#allowing-fscrypt-to-check-your-login-passphrase).
+
+#### Enabling the PAM module on other Linux distros
+
+On all other Linux distros, follow the general guidance below to edit
+your PAM configuration files.
+
+The `fscrypt` PAM module implements the Auth, Session, and Password
+[types](http://www.linux-pam.org/Linux-PAM-html/sag-configuration-file.html).
+
+The Password functionality of `pam_fscrypt.so` is used to automatically rewrap
+a user's login protector when their unix passphrase changes. An easy way to get
+the working is to add the line:
+```
+password    optional    pam_fscrypt.so
+```
+after `pam_unix.so` in `/etc/pam.d/common-password` or similar.
+
+The Auth and Session functionality of `pam_fscrypt.so` are used to automatically
+unlock directories when logging in as a user, and lock them when logging out.
+An easy way to get this working is to add the line:
+```
+auth        optional    pam_fscrypt.so
+```
+after `pam_unix.so` in `/etc/pam.d/common-auth` or similar, and to add the
+line:
+```
+session     optional    pam_fscrypt.so
+```
+after `pam_unix.so` in `/etc/pam.d/common-session` or similar, but before
+`pam_systemd.so` or any other module that accesses the user's home directory or
+which starts processes that access the user's home directory during their
+session.
+
+`pam_fscrypt.so` accepts several options:
+
+* `debug`: print additional debug messages to the syslog.  All hook types accept
+  this option.
+
+* `unlock_only`: only unlock directories (at log-in); don't also lock them (at
+  log-out).  This is only relevant for the "session" hook.  Note that in
+  `fscrypt` v0.2.9 and earlier, unlock-only was the default behavior, and
+  `lock_policies` needed to be specified to enable locking.
+
+### Allowing `fscrypt` to check your login passphrase
+
+This step is only needed if you installed `fscrypt` from source code.
+
+Some Linux distros use restrictive settings in `/etc/pam.d/other` that prevent
+programs from checking your login passphrase unless a per-program PAM
+configuration file grants access.  This prevents `fscrypt` from creating any
+login passphrase-protected directories, even without auto-unlocking.  To ensure
+that `fscrypt` will work properly (if you didn't install an official `fscrypt`
+package from your distro, which should have already handled this), also create a
+file `/etc/pam.d/fscrypt` containing:
+```
+auth        required    pam_unix.so
+```
+
+## Backup, restore, and recovery
+
+Encrypted files and directories can't be backed up while they are "locked", i.e.
+while they appear in encrypted form.  They can only be backed up while they are
+unlocked, in which case they can be backed up like any other files.  Note that
+since the encryption is transparent, the files won't be encrypted in the backup
+(unless the backup applies its own encryption).
+
+For the same reason (and several others), an encrypted directory can't be
+directly "moved" to another filesystem.  However, it is possible to create a new
+encrypted directory on the destination filesystem using `fscrypt encrypt`, then
+copy the contents of the source directory into it.
+
+For directories protected by a `custom_passphrase` or `raw_key` protector, all
+metadata needed to unlock the directory (excluding the actual passphrase or raw
+key, of course) is located in the `.fscrypt` directory at the root of the
+filesystem that contains the encrypted directory.  For example, if you have an
+encrypted directory `/home/$USER/private` that is protected by a custom
+passphrase, all `fscrypt` metadata needed to unlock the directory with that
+custom passphrase will be located in `/home/.fscrypt` if you are using a
+dedicated `/home` filesystem or in `/.fscrypt` if you aren't.  If desired, you
+can back up the `fscrypt` metadata by making a copy of this directory, although
+this isn't too important since this metadata is located on the same filesystem
+as the encrypted directory(s).
+
+`pam_passphrase` (login passphrase) protectors are a bit different as they are
+always stored on the root filesystem, in `/.fscrypt`.  This ties them to the
+specific system and ensures that each user has only a single login protector.
+Therefore, encrypted directories on a non-root filesystem **can't be unlocked
+via a login protector if the operating system is reinstalled or if the disk is
+connected to another system** -- even if the new system uses the same login
+passphrase for the user.
+
+Because of this, `fscrypt encrypt` will automatically generate a recovery
+passphrase when creating a login passphrase-protected directory on a non-root
+filesystem.  The recovery passphrase is simply a `custom_passphrase` protector
+with a randomly generated high-entropy passphrase.  Initially, the recovery
+passphrase is stored in a file in the encrypted directory itself; therefore, to
+use it you **must** record it in another secure location.  It is strongly
+recommended to do this.  Then, if ever needed, you can use `fscrypt unlock` to
+unlock the directory with the recovery passphrase (by choosing the recovery
+protector instead of the login protector).
+
+If you really want to disable the generation of a recovery passphrase, use the
+`--no-recovery` option.  Only do this if you really know what you are doing and
+are prepared for potential data loss.
+
+Alternative approaches to supporting recovery of login passphrase-protected
+directories include the following:
+
+* Manually adding your own recovery protector, using
+  `fscrypt metadata add-protector-to-policy`.
+
+* Backing up and restoring the `/.fscrypt` directory on the root filesystem.
+  Note that after restoring the `/.fscrypt` directory, unlocking the login
+  protectors will require the passphrases they had at the time the backup was
+  made **even if they were changed later**, so make sure to remember these
+  passphrase(s) or record them in a secure location.  Also note that if the UUID
+  of the root filesystem changed, you will need to manually fix the UUID in any
+  `.fscrypt/protectors/*.link` files on other filesystems.
+
+The auto-generated recovery passphrases should be enough for most users, though.
+
+## Encrypting existing files
+
+`fscrypt` isn't designed to encrypt existing files, as this presents significant
+technical challenges and usually is impossible to do securely.  Therefore,
+`fscrypt encrypt` only works on empty directories.
+
+Of course, it is still possible to create an encrypted directory, copy files
+into it, and delete the original files.  The `mv` command will even work, as it
+will fall back to a copy and delete ([except on older
+kernels](#getting-operation-not-permitted-when-moving-files-into-an-encrypted-directory)).
+However, beware that due to the characteristics of filesystems and storage
+devices, this may not properly protect the files, as their original contents may
+still be forensically recoverable from disk even after being deleted.  It's
+**much** better to encrypt files from the very beginning.
+
+There are only a few cases where copying files into an encrypted directory can
+really make sense, such as:
+
+* The source files are located on an in-memory filesystem such as `tmpfs`.
+
+* The confidentiality of the source files isn't important, e.g. they are
+  system default files and the user hasn't added any personal files yet.
+
+* The source files are protected by a different `fscrypt` policy, the old and
+  new policies are protected by only the same protector(s), and the old policy
+  uses similar strength encryption.
+
+If one of the above doesn't apply, then it's probably too late to securely
+encrypt your existing files.
+
+As a best-effort attempt, you can use the `shred` program to try to erase the
+original files.  Here are the recommended commands for "best-effort" encryption
+of an existing directory named "dir":
+
+```bash
+mkdir dir.new
+fscrypt encrypt dir.new
+cp -a -T dir dir.new
+find dir -type f -print0 | xargs -0 shred -n1 --remove=unlink
+rm -rf dir
+mv dir.new dir
+```
+
+However, beware that `shred` isn't guaranteed to be effective on all storage
+devices and filesystems.  For example, if you're using an SSD, "overwrites" of
+data typically go to new flash blocks, so they aren't really overwrites.
+
+Note: for reasons similar to the above, changed or removed `fscrypt` protectors
+aren't guaranteed to be forensically unrecoverable from disk either.  Thus, the
+use of weak or default passphrases should be avoided, even if changed later.
+
+## Example usage
+
+All these examples assume there is an ext4 filesystem which supports
+encryption mounted at `/mnt/disk`.  See
+[here](#getting-encryption-not-enabled-on-an-ext4-filesystem) for how
+to enable encryption support on an ext4 filesystem.
+
+### Setting up fscrypt on a directory
+
+```bash
+# Check which directories on our system support encryption
+>>>>> fscrypt status
+filesystems supporting encryption: 1
+filesystems with fscrypt metadata: 0
+
+MOUNTPOINT  DEVICE     FILESYSTEM  ENCRYPTION   FSCRYPT
+/           /dev/sda1  ext4        not enabled  No
+/mnt/disk   /dev/sdb   ext4        supported    No
+
+# Create the global configuration file. Nothing else necessarily needs root.
+>>>>> sudo fscrypt setup
+Defaulting to policy_version 2 because kernel supports it.
+Customizing passphrase hashing difficulty for this system...
+Created global config file at "/etc/fscrypt.conf".
+Allow users other than root to create fscrypt metadata on the root filesystem?
+(See https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem) [y/N] y
+Metadata directories created at "/.fscrypt", writable by everyone.
+
+# Start using fscrypt with our filesystem
+>>>>> sudo fscrypt setup /mnt/disk
+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] y
+Metadata directories created at "/mnt/disk/.fscrypt", writable by everyone.
+
+# Initialize encryption on a new empty directory
+>>>>> mkdir /mnt/disk/dir1
+>>>>> fscrypt encrypt /mnt/disk/dir1
+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: Super Secret
+Enter custom passphrase for protector "Super Secret":
+Confirm passphrase:
+"/mnt/disk/dir1" is now encrypted, unlocked, and ready for use.
+
+# We can see this created one policy and one protector for this directory
+>>>>> fscrypt status /mnt/disk
+ext4 filesystem "/mnt/disk" has 1 protector and 1 policy
+
+PROTECTOR         LINKED  DESCRIPTION
+7626382168311a9d  No      custom protector "Super Secret"
+
+POLICY                            UNLOCKED  PROTECTORS
+16382f282d7b29ee27e6460151d03382  Yes       7626382168311a9d
+```
+
+#### Quiet version
+```bash
+>>>>> sudo fscrypt setup --quiet --force --all-users
+>>>>> sudo fscrypt setup /mnt/disk --quiet --all-users
+>>>>> echo "hunter2" | fscrypt encrypt /mnt/disk/dir1 --quiet --source=custom_passphrase  --name="Super Secret"
+```
+
+### Locking and unlocking a directory
+
+```bash
+# Write a file to our encrypted directory.
+>>>>> echo "Hello World" > /mnt/disk/dir1/secret.txt
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy:   16382f282d7b29ee27e6460151d03382
+Options:  padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR         LINKED  DESCRIPTION
+7626382168311a9d  No      custom protector "Super Secret"
+
+# Lock the directory.  Note: if using a v1 encryption policy instead
+# of v2, you'll need 'sudo fscrypt lock /mnt/disk/dir1 --user=$USER'.
+>>>>> fscrypt lock /mnt/disk/dir1
+"/mnt/disk/dir1" is now locked.
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy:   16382f282d7b29ee27e6460151d03382
+Options:  padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: No
+
+Protected with 1 protector:
+PROTECTOR         LINKED  DESCRIPTION
+7626382168311a9d  No      custom protector "Super Secret"
+
+# Now the filenames and file contents are inaccessible
+>>>>> ls /mnt/disk/dir1
+u,k20l9HrtrizDjh0zGkw2dTfBkX4T0ZDUlsOhBLl4P
+>>>>> cat /mnt/disk/dir1/u,k20l9HrtrizDjh0zGkw2dTfBkX4T0ZDUlsOhBLl4P
+cat: /mnt/disk/dir1/u,k20l9HrtrizDjh0zGkw2dTfBkX4T0ZDUlsOhBLl4P: Required key not available
+
+# Unlocking the directory makes the contents available
+>>>>> fscrypt unlock /mnt/disk/dir1
+Enter custom passphrase for protector "Super Secret":
+"/mnt/disk/dir1" is now unlocked and ready for use.
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy:   16382f282d7b29ee27e6460151d03382
+Options:  padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR         LINKED  DESCRIPTION
+7626382168311a9d  No      custom protector "Super Secret"
+>>>>> cat /mnt/disk/dir1/secret.txt
+Hello World
+```
+
+#### Quiet version
+```bash
+>>>>> fscrypt lock /mnt/disk/dir1 --quiet
+>>>>> echo "hunter2" | fscrypt unlock /mnt/disk/dir1 --quiet
+```
+
+### Protecting a directory with your login passphrase
+
+First, ensure that you have properly [set up your system for login
+protectors](#setting-up-for-login-protectors).
+
+Then, you can protect directories with your login passphrase as follows:
+
+```bash
+# Select your login passphrase as the desired source.
+>>>>> mkdir /mnt/disk/dir2
+>>>>> fscrypt encrypt /mnt/disk/dir2
+Should we create a new protector? [y/N] y
+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 joerichey:
+"/mnt/disk/dir2" is now encrypted, unlocked, and ready for use.
+
+# Note that the login protector actually sits on the root filesystem
+>>>>> fscrypt status /mnt/disk/dir2
+"/mnt/disk/dir2" is encrypted with fscrypt.
+
+Policy:   fe1c92009abc1cff4f3257c77e8134e3
+Options:  padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR         LINKED   DESCRIPTION
+6891f0a901f0065e  Yes (/)  login protector for joerichey
+>>>>> fscrypt status /mnt/disk
+ext4 filesystem "/mnt/disk" has 2 protectors and 2 policies
+
+PROTECTOR         LINKED   DESCRIPTION
+7626382168311a9d  No       custom protector "Super Secret"
+6891f0a901f0065e  Yes (/)  login protector for joerichey
+
+POLICY                            UNLOCKED  PROTECTORS
+16382f282d7b29ee27e6460151d03382  Yes       7626382168311a9d
+fe1c92009abc1cff4f3257c77e8134e3  Yes       6891f0a901f0065e
+>>>>> fscrypt status /
+ext4 filesystem "/" has 1 protector(s) and 0 policy(ies)
+
+PROTECTOR         LINKED  DESCRIPTION
+6891f0a901f0065e  No      login protector for joerichey
+```
+
+#### Quiet version
+```bash
+>>>>> mkdir /mnt/disk/dir2
+>>>>> echo "password" | fscrypt encrypt /mnt/disk/dir2 --source=pam_passphrase --quiet
+```
+
+### Changing a custom passphrase
+```bash
+# First we have to figure out which protector we wish to change.
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy:   16382f282d7b29ee27e6460151d03382
+Options:  padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR         LINKED  DESCRIPTION
+7626382168311a9d  No      custom protector "Super Secret"
+
+# Now specify the protector directly to the metadata command
+>>>>> fscrypt metadata change-passphrase --protector=/mnt/disk:7626382168311a9d
+Enter old custom passphrase for protector "Super Secret":
+Enter new custom passphrase for protector "Super Secret":
+Confirm passphrase:
+Passphrase for protector 7626382168311a9d successfully changed.
+```
+
+#### Quiet version
+```bash
+>>>>> printf "hunter2\nhunter3" | fscrypt metadata change-passphrase --protector=/mnt/disk:7626382168311a9d --quiet
+```
+
+### Using a raw key protector
+
+`fscrypt` also supports protectors which use raw key files as the user-provided
+secret. These key files must be exactly 32 bytes long and contain the raw binary
+data of the key. Obviously, make sure to store the key file securely (and not in
+the directory you are encrypting with it). If generating the keys on Linux make
+sure you are aware of
+[how randomness works](http://man7.org/linux/man-pages/man7/random.7.html) and
+[some common myths](https://www.2uo.de/myths-about-urandom/).
+
+```bash
+# Generate a 256-bit key file
+>>>>> head --bytes=32 /dev/urandom > secret.key
+
+# Now create a key file protector without using it on a directory. Note that we
+# could also use `fscrypt encrypt --key=secret.key` to achieve the same thing.
+>>>>> fscrypt metadata create protector /mnt/disk
+Create new protector on "/mnt/disk" [Y/n] y
+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]: 3
+Enter a name for the new protector: Skeleton
+Enter key file for protector "Skeleton": secret.key
+Protector 2c75f519b9c9959d created on filesystem "/mnt/disk".
+>>>>> fscrypt status /mnt/disk
+ext4 filesystem "/mnt/disk" has 3 protectors and 2 policies
+
+PROTECTOR         LINKED   DESCRIPTION
+7626382168311a9d  No       custom protector "Super Secret"
+2c75f519b9c9959d  No       raw key protector "Skeleton"
+6891f0a901f0065e  Yes (/)  login protector for joerichey
+
+POLICY                            UNLOCKED  PROTECTORS
+16382f282d7b29ee27e6460151d03382  Yes       7626382168311a9d
+fe1c92009abc1cff4f3257c77e8134e3  Yes       6891f0a901f0065e
+
+# Finally, we could apply this key to a directory
+>>>>> mkdir /mnt/disk/dir3
+>>>>> fscrypt encrypt /mnt/disk/dir3 --protector=/mnt/disk:2c75f519b9c9959d
+Enter key file for protector "Skeleton": secret.key
+"/mnt/disk/dir3" is now encrypted, unlocked, and ready for use.
+```
+
+#### Quiet version
+```bash
+>>>>> head --bytes=32 /dev/urandom > secret.key
+>>>>> fscrypt encrypt /mnt/disk/dir3 --key=secret.key --source=raw_key --name=Skeleton
+```
+
+### Using multiple protectors for a policy
+
+`fscrypt` supports the idea of protecting a single directory with multiple
+protectors. This means having access to any of the protectors is sufficient to
+decrypt the directory. This is useful for sharing data or setting up access
+control systems.
+
+```bash
+# Add an existing protector to the policy for some directory
+>>>>> fscrypt status /mnt/disk
+ext4 filesystem "/mnt/disk" has 3 protectors and 3 policies
+
+PROTECTOR         LINKED   DESCRIPTION
+7626382168311a9d  No       custom protector "Super Secret"
+2c75f519b9c9959d  No       raw key protector "Skeleton"
+6891f0a901f0065e  Yes (/)  login protector for joerichey
+
+POLICY                            UNLOCKED  PROTECTORS
+d03fb894584a4318d1780e9a7b0b47eb  No        2c75f519b9c9959d
+16382f282d7b29ee27e6460151d03382  No        7626382168311a9d
+fe1c92009abc1cff4f3257c77e8134e3  No        6891f0a901f0065e
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy:   16382f282d7b29ee27e6460151d03382
+Options:  padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: No
+
+Protected with 1 protector:
+PROTECTOR         LINKED  DESCRIPTION
+7626382168311a9d  No      custom protector "Super Secret"
+>>>>> fscrypt metadata add-protector-to-policy --protector=/mnt/disk:2c75f519b9c9959d --policy=/mnt/disk:16382f282d7b29ee27e6460151d03382
+WARNING: All files using this policy will be accessible with this protector!!
+Protect policy 16382f282d7b29ee27e6460151d03382 with protector 2c75f519b9c9959d? [Y/n]
+Enter key file for protector "Skeleton": secret.key
+Enter custom passphrase for protector "Super Secret":
+Protector 2c75f519b9c9959d now protecting policy 16382f282d7b29ee27e6460151d03382.
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy:   16382f282d7b29ee27e6460151d03382
+Options:  padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: No
+
+Protected with 2 protectors:
+PROTECTOR         LINKED  DESCRIPTION
+7626382168311a9d  No      custom protector "Super Secret"
+2c75f519b9c9959d  No      raw key protector "Skeleton"
+
+# Now the unlock command will prompt for which protector we want to use
+>>>>> fscrypt unlock /mnt/disk/dir1
+The available protectors are:
+0 - custom protector "Super Secret"
+1 - raw key protector "Skeleton"
+Enter the number of protector to use: 1
+Enter key file for protector "Skeleton": secret.key
+"/mnt/disk/dir1" is now unlocked and ready for use.
+
+# The protector can also be removed from the policy (if it is not the only one)
+>>>>> fscrypt metadata remove-protector-from-policy --protector=/mnt/disk:2c75f519b9c9959d --policy=/mnt/disk:16382f282d7b29ee27e6460151d03382
+WARNING: All files using this policy will NO LONGER be accessible with this protector!!
+Stop protecting policy 16382f282d7b29ee27e6460151d03382 with protector 2c75f519b9c9959d? [y/N] y
+Protector 2c75f519b9c9959d no longer protecting policy 16382f282d7b29ee27e6460151d03382.
+```
+
+#### Quiet version
+```bash
+>>>>> echo "hunter2" | fscrypt metadata add-protector-to-policy --protector=/mnt/disk:2c75f519b9c9959d --policy=/mnt/disk:16382f282d7b29ee27e6460151d03382 --key=secret.key --quiet
+>>>>> fscrypt metadata remove-protector-from-policy --protector=/mnt/disk:2c75f519b9c9959d --policy=/mnt/disk:16382f282d7b29ee27e6460151d03382 --quiet --force
+```
+
+## Contributing
+
+We would love to accept your contributions to `fscrypt`. See the
+`CONTRIBUTING.md` file for more information about signing the CLA and submitting
+a pull request.
+
+## Troubleshooting
+
+In general, if you are encountering issues with `fscrypt`,
+[open an issue](https://github.com/google/fscrypt/issues/new), following the
+guidelines in `CONTRIBUTING.md`. We will try our best to help.
+
+#### I changed my login passphrase, now all my directories are inaccessible
+
+Usually, the PAM module `pam_fscrypt.so` will automatically detect changes to a
+user's login passphrase and update the user's `fscrypt` login protector so that
+they retain access their login-passphrase protected directories.  However,
+sometimes a user's login passphrase can become desynchronized from their
+`fscrypt` login protector.  This can happen if `root` assigns the user a new
+passphrase without providing the old one, if the user's login passphrase is
+managed by an external system such as LDAP, if the PAM module is not installed,
+or if the PAM module is not properly configured.  See [Enabling the PAM
+module](#enabling-the-pam-module) for how to configure the PAM module.
+
+To fix a user's login protector, find the corresponding protector ID by running
+`fscrypt status "/"`.  Then, change this protector's passphrase by running:
+```
+fscrypt metadata change-passphrase --protector=/:ID
+```
+
+#### Directories using my login passphrase are not automatically unlocking
+
+First, directories won't unlock if your session starts without password
+authentication.  The most common case of this is public-key ssh login.  To
+trigger a password authentication event, run `su $USER -c exit`.
+
+If your session did start with password authentication, then the following may
+be causing the issue:
+
+* The PAM module might not be configured correctly.  Ensure you have correctly
+  [configured the PAM module](#enabling-the-pam-module).
+
+* If your login passphrase recently changed, then it might have gotten out of
+  sync with your login protector.  To fix this, [manually change your login
+  protector's
+  passphrase](#i-changed-my-login-passphrase-now-all-my-directories-are-inaccessible)
+  to get it back in sync with your actual login passphrase.
+
+* Due to a [bug in sshd](https://bugzilla.mindrot.org/show_bug.cgi?id=2548),
+  encrypted directories won't auto-unlock when logging in with ssh using the
+  `ChallengeResponseAuthentication` ssh authentication method, which is also
+  called `KbdInteractiveAuthentication`.  This ssh authentication method
+  implements password authentication by default, so it might appear similar to
+  `PasswordAuthentication`.  However, only `PasswordAuthentication` works with
+  `fscrypt`.  To avoid this issue, make sure that your `/etc/ssh/sshd_config`
+  file contains `PasswordAuthentication yes`, `UsePAM yes`, and either
+  `ChallengeResponseAuthentication no` or `KbdInteractiveAuthentication no`.
+
+#### Getting "encryption not enabled" on an ext4 filesystem
+
+This is usually caused by your ext4 filesystem not having the `encrypt` feature
+flag enabled.  The `encrypt` feature flag allows the filesystem to contain
+encrypted files.  (It doesn't actually encrypt anything by itself.)
+
+Before enabling `encrypt` on your ext4 filesystem, first ensure that all of the
+following are true for you:
+
+* You only need to use your filesystem on kernels v4.1 and later.
+
+  (Kernels v4.0 and earlier can't mount ext4 filesystems that have the `encrypt`
+  feature flag.)
+
+* Either you only need to use your filesystem on kernels v5.5 and later, or your
+  kernel page size (run `getconf PAGE_SIZE`) and filesystem block size (run
+  `tune2fs -l /dev/device | grep 'Block size'`) are the same.
+
+  (Both values will almost always be 4096, but they may differ if your
+  filesystem is very small, if your system uses the PowerPC CPU architecture, or
+  if you overrode the default block size when you created the filesystem.  Only
+  kernels v5.5 and later support ext4 encryption in such cases.)
+
+* Either you aren't using GRUB to boot directly off the filesystem in question,
+  or you are using GRUB 2.04 or later.
+
+  (Old versions of GRUB can't boot from ext4 filesystems that have `encrypt`
+  enabled.  If, like most people, you have a separate `/boot` partition, you are
+  fine.  You are also fine if you are using the GRUB Debian package `2.02-2` or
+  later [*not* `2.02_beta*`], including the version in Ubuntu 18.04 and later,
+  since the patch to support `encrypt` was backported.)
+
+After verifying all of the above, enable `encrypt` by running:
+```
+tune2fs -O encrypt /dev/device
+```
+
+If you need to undo this, first delete all encrypted files and directories on
+the filesystem.  Then, run:
+```
+fsck -fn /dev/device
+debugfs -w -R "feature -encrypt" /dev/device
+fsck -fn /dev/device
+``` 
+
+If you've enabled `encrypt` but you still get the "encryption not enabled"
+error, then the problem is that ext4 encryption isn't enabled in your kernel
+config.  See [Runtime dependencies](#runtime-dependencies) for how to enable it.
+
+#### Getting "user keyring not linked into session keyring"
+
+Some older versions of Ubuntu didn't link the user keyring into the session
+keyring, which caused problems with `fscrypt`.
+
+To avoid this issue, upgrade to Ubuntu 20.04 or later.
+
+#### Getting "Operation not permitted" when moving files into an encrypted directory
+
+Originally, filesystems didn't return the correct error code when attempting to
+rename unencrypted files (or files with a different encryption policy) into an
+encrypted directory.  Specifically, they returned `EPERM` instead of `EXDEV`,
+which caused `mv` to fail rather than fall back to a copy as expected.
+
+This bug was fixed in version 5.1 of the mainline Linux kernel, as well as in
+versions 4.4 and later of the LTS (Long Term Support) branches of the Linux
+kernel; specifically v4.19.155, 4.14.204, v4.9.242, and v4.4.242.
+
+If the kernel can't be upgraded, this bug can be worked around by explicitly
+copying the files instead, e.g. with `cp`.
+
+__IMPORTANT:__ Encrypting existing files can be insecure.  Before doing so, read
+[Encrypting existing files](#encrypting-existing-files).
+
+#### Getting "Package not installed" when trying to use an encrypted directory
+
+Trying to create or open an encrypted file will fail with `ENOPKG` ("Package not
+installed") when the kernel doesn't support one or more of the cryptographic
+algorithms used by the file or its directory.  Note that `fscrypt encrypt` and
+`fscrypt unlock` will still succeed; it's only using the directory afterwards
+that will fail.
+
+The kernel will always support the algorithms that `fscrypt` uses by default.
+However, if you changed the contents and/or filenames encryption algorithms in
+[`/etc/fscrypt.conf`](#configuration-file), then you may run into this issue.
+To fix it, enable the needed `CONFIG_CRYPTO_*` options in your Linux kernel
+configuration.  See the [kernel
+documentation](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html#encryption-modes-and-usage)
+for details about which option(s) are required for each encryption mode.
+
+#### Some processes can't access unlocked encrypted files
+
+This issue is caused by a limitation in the original design of Linux native
+filesystem encryption which made it difficult to ensure that all processes can
+access unlocked encrypted files.  This issue can manifest in many ways, such as:
+
+* SSH to a user with an encrypted home directory not working, even when that
+  directory is already unlocked
+
+* Docker containers being unable to access encrypted files that were unlocked
+  from outside the container
+
+* NetworkManager being unable to access certificates stored in the user's
+  already-unlocked encrypted home directory
+
+* Other system services being unable to access already-unlocked encrypted files
+
+* `sudo` sessions being unable to access already-unlocked encrypted files
+
+* A user being unable to access encrypted files that were unlocked by root
+
+If an OS-level error is shown, it is `ENOKEY` ("Required key not available").
+
+To fix this issue, first run `fscrypt status $dir`, where `$dir` is your
+encrypted directory.  If the output contains `policy_version:2`, then your issue
+is something else, so stop reading now.  If the output contains
+`policy_version:1` or doesn't contain any mention of `policy_version`, then
+you'll need to upgrade your directory(s) to policy version 2.  To do this:
+
+1. Upgrade to Linux kernel v5.4 or later.
+
+2. Upgrade to `fscrypt` v0.2.7 or later.
+
+3. Run `sudo fscrypt setup --force`.
+
+4. Re-encrypt your encrypted directory(s).  Since files cannot be (re-)encrypted
+   in-place, this requires replacing them with new directories.  For example:
+   ```
+     fscrypt unlock dir  # if not already unlocked
+     mkdir dir.new
+     fscrypt encrypt dir.new
+     cp -a -T dir dir.new
+     find dir -type f -print0 | xargs -0 shred -n1 --remove=unlink
+     rm -rf dir
+     mv dir.new dir
+   ```
+
+   You don't need to create a new protector.  I.e., when `fscrypt encrypt` asks
+   for a protector, just choose the one you were using before.
+
+5. `fscrypt status` on your directory(s) should now show `policy_version:2`,
+   and the issue should be gone.
+
+Note that once your directories are using policy version 2, they will only be
+usable with Linux kernel v5.4 and later and `fscrypt` v0.2.6 and later.  So be
+careful not to downgrade your software past those versions.
+
+This issue can also be fixed by setting `"use_fs_keyring_for_v1_policies": true`
+in `/etc/fscrypt.conf`, as described in [Configuration
+file](#configuration-file).  This avoids needing to upgrade directories to
+policy version 2.  However, this has some limitations, and the same kernel and
+`fscrypt` prerequisites still apply for this option to take effect.  It is
+recommended to upgrade your directories to policy version 2 instead.
+
+#### Users can access other users' unlocked encrypted files
+
+This is working as intended.  When an encrypted directory is unlocked (or
+locked), it is unlocked (or locked) for all users.  Encryption is not access
+control; the Linux kernel already has many access control mechanisms, such as
+the standard UNIX file permissions, that can be used to control access to files.
+
+Setting the mode of your encrypted directory to `0700` will prevent users other
+than the directory's owner and `root` from accessing it while it is unlocked.
+In `fscrypt` v0.2.5 and later, `fscrypt encrypt` sets this mode automatically.
+
+Having the locked/unlocked status of directories be global instead of per-user
+may seem unintuitive, but it is actually the only logical way.  The encryption
+is done by the filesystem, so in reality the filesystem either has the key or it
+doesn't.  And once it has the key, any additional checks of whether particular
+users "have" the key would be OS-level access control checks (not cryptography)
+that are redundant with existing OS-level access control mechanisms.
+
+Similarly, any attempt of the filesystem encryption feature to prevent `root`
+from accessing unlocked encrypted files would be pointless.  On Linux systems,
+`root` is usually all-powerful and can always get access to files in ways that
+cannot be prevented, e.g. `setuid()` and `ptrace()`.  The only reliable way to
+limit what `root` can do is via a mandatory access control system, e.g. SELinux.
+
+The original design of Linux native filesystem encryption actually did put the
+keys into per-user keyrings.  However, this caused a [massive number of
+problems](#some-processes-cant-access-unlocked-encrypted-files), as it's
+actually very common that encrypted files need to be accessed by processes
+running under different user IDs -- even if it may not be immediately apparent.
+
+#### Getting "Required key not available" when backing up locked encrypted files
+
+Encrypted files can't be backed up while locked; you need to unlock them first.
+For details, see [Backup, restore, and recovery](#backup-restore-and-recovery).
+
+#### The reported size of encrypted symlinks is wrong
+
+Originally, filesystems didn't conform to POSIX when reporting the size of
+encrypted symlinks, as they gave the size of the ciphertext symlink target
+rather than the size of the plaintext target.  This would make the reported size
+of symlinks appear to be slightly too large when queried using ``lstat()`` or
+similar system calls.  Most programs don't care about this, but in rare cases
+programs can depend on the filesystem reporting symlink sizes correctly.
+
+This bug was fixed in version 5.15 of the mainline Linux kernel, as well as in
+versions 4.19 and later of the LTS (Long Term Support) branches of the Linux
+kernel; specifically v5.10.63, v5.4.145, and v4.19.207.
+
+If the kernel can't be upgraded, the only workaround for this bug is to update
+any affected programs to not depend on symlink sizes being reported correctly.
+
+## Legal
+
+Copyright 2017 Google Inc. under the
+[Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0); see the
+`LICENSE` file for more information.
+
+Author: Joe Richey <joerichey@google.com>
+
+This is not an official Google product.
diff --git a/actions/callback.go b/actions/callback.go
new file mode 100644 (file)
index 0000000..f15893d
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * callback.go - defines how the caller of an action function passes along a key
+ * to be used in this package.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package actions
+
+import (
+       "log"
+
+       "github.com/pkg/errors"
+
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/metadata"
+)
+
+// ProtectorInfo is the information a caller will receive about a Protector
+// before they have to return the corresponding key. This is currently a
+// read-only view of metadata.ProtectorData.
+type ProtectorInfo struct {
+       data *metadata.ProtectorData
+}
+
+// Descriptor is the Protector's descriptor used to uniquely identify it.
+func (pi *ProtectorInfo) Descriptor() string { return pi.data.GetProtectorDescriptor() }
+
+// Source indicates the type of the descriptor (how it should be unlocked).
+func (pi *ProtectorInfo) Source() metadata.SourceType { return pi.data.GetSource() }
+
+// Name is used to describe custom passphrase and raw key descriptors.
+func (pi *ProtectorInfo) Name() string { return pi.data.GetName() }
+
+// UID is used to identify the user for login passphrases.
+func (pi *ProtectorInfo) UID() int64 { return pi.data.GetUid() }
+
+// KeyFunc is passed to a function that will require some type of key.
+// The info parameter is provided so the callback knows which key to provide.
+// The retry parameter indicates that a previous key provided by this callback
+// was incorrect (this allows for user feedback like "incorrect passphrase").
+//
+// For passphrase sources, the returned key should be a passphrase. For raw
+// sources, the returned key should be a 256-bit cryptographic key. Consumers
+// of the callback will wipe the returned key. An error returned by the callback
+// will be propagated back to the caller.
+type KeyFunc func(info ProtectorInfo, retry bool) (*crypto.Key, error)
+
+// getWrappingKey uses the provided callback to get the wrapping key
+// corresponding to the ProtectorInfo. This runs the passphrase hash for
+// passphrase sources or just relays the callback for raw sources.
+func getWrappingKey(info ProtectorInfo, keyFn KeyFunc, retry bool) (*crypto.Key, error) {
+       // For raw key sources, we can just use the key directly.
+       if info.Source() == metadata.SourceType_raw_key {
+               return keyFn(info, retry)
+       }
+
+       // Run the passphrase hash for other sources.
+       passphrase, err := keyFn(info, retry)
+       if err != nil {
+               return nil, err
+       }
+       defer passphrase.Wipe()
+
+       log.Printf("running passphrase hash for protector %s", info.Descriptor())
+       return crypto.PassphraseHash(passphrase, info.data.Salt, info.data.Costs)
+}
+
+// unwrapProtectorKey uses the provided callback and ProtectorInfo to return
+// the unwrapped protector key. This will repeatedly call keyFn to get the
+// wrapping key until the correct key is returned by the callback or the
+// callback returns an error.
+func unwrapProtectorKey(info ProtectorInfo, keyFn KeyFunc) (*crypto.Key, error) {
+       retry := false
+       for {
+               wrappingKey, err := getWrappingKey(info, keyFn, retry)
+               if err != nil {
+                       return nil, err
+               }
+
+               protectorKey, err := crypto.Unwrap(wrappingKey, info.data.WrappedKey)
+               wrappingKey.Wipe()
+
+               switch errors.Cause(err) {
+               case nil:
+                       log.Printf("valid wrapping key for protector %s", info.Descriptor())
+                       return protectorKey, nil
+               case crypto.ErrBadAuth:
+                       // After the first failure, we let the callback know we are retrying.
+                       log.Printf("invalid wrapping key for protector %s", info.Descriptor())
+                       retry = true
+                       continue
+               default:
+                       return nil, err
+               }
+       }
+}
+
+// ProtectorOption is information about a protector relative to a Policy.
+type ProtectorOption struct {
+       ProtectorInfo
+       // LinkedMount is the mountpoint for a linked protector. It is nil if
+       // the protector is not a linked protector (or there is a LoadError).
+       LinkedMount *filesystem.Mount
+       // LoadError is non-nil if there was an error in getting the data for
+       // the protector.
+       LoadError error
+}
+
+// OptionFunc is passed to a function that needs to unlock a Policy.
+// The callback is used to specify which protector should be used to unlock a
+// Policy. The descriptor indicates which Policy we are using, while the options
+// correspond to the valid Protectors protecting the Policy.
+//
+// The OptionFunc should either return a valid index into options, which
+// corresponds to the desired protector, or an error (which will be propagated
+// back to the caller).
+type OptionFunc func(policyDescriptor string, options []*ProtectorOption) (int, error)
diff --git a/actions/config.go b/actions/config.go
new file mode 100644 (file)
index 0000000..7c7c0e6
--- /dev/null
@@ -0,0 +1,301 @@
+/*
+ * config.go - Actions for creating a new config file, which includes new
+ * hashing costs and the config file's location.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package actions
+
+import (
+       "bytes"
+       "fmt"
+       "log"
+       "os"
+       "runtime"
+       "time"
+
+       "golang.org/x/sys/unix"
+       "google.golang.org/protobuf/proto"
+
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+// ConfigFileLocation is the location of fscrypt's global settings. This can be
+// overridden by the user of this package.
+var ConfigFileLocation = "/etc/fscrypt.conf"
+
+// ErrBadConfig is an internal error that indicates that the config struct is invalid.
+type ErrBadConfig struct {
+       Config          *metadata.Config
+       UnderlyingError error
+}
+
+func (err *ErrBadConfig) Error() string {
+       return fmt.Sprintf(`internal error: config is invalid: %s
+
+       The invalid config is %s`, err.UnderlyingError, err.Config)
+}
+
+// ErrBadConfigFile indicates that the config file is invalid.
+type ErrBadConfigFile struct {
+       Path            string
+       UnderlyingError error
+}
+
+func (err *ErrBadConfigFile) Error() string {
+       return fmt.Sprintf("%q is invalid: %s", err.Path, err.UnderlyingError)
+}
+
+// ErrConfigFileExists indicates that the config file already exists.
+type ErrConfigFileExists struct {
+       Path string
+}
+
+func (err *ErrConfigFileExists) Error() string {
+       return fmt.Sprintf("%q already exists", err.Path)
+}
+
+// ErrNoConfigFile indicates that the config file doesn't exist.
+type ErrNoConfigFile struct {
+       Path string
+}
+
+func (err *ErrNoConfigFile) Error() string {
+       return fmt.Sprintf("%q doesn't exist", err.Path)
+}
+
+const (
+       // Permissions of the config file (global readable)
+       configPermissions = 0644
+       // Config file should be created for writing and not already exist
+       createFlags = os.O_CREATE | os.O_WRONLY | os.O_EXCL
+       // 128 MiB is a large enough amount of memory to make the password hash
+       // very difficult to brute force on specialized hardware, but small
+       // enough to work on most GNU/Linux systems.
+       maxMemoryBytes = 128 * 1024 * 1024
+)
+
+var (
+       timingPassphrase = []byte("I am a fake passphrase")
+       timingSalt       = bytes.Repeat([]byte{42}, metadata.SaltLen)
+)
+
+// CreateConfigFile creates a new config file at the appropriate location with
+// the appropriate hashing costs and encryption parameters. The hashing will be
+// configured to take as long as the specified time target. In addition, the
+// version of encryption policy to use may be overridden from the default of v1.
+func CreateConfigFile(target time.Duration, policyVersion int64) error {
+       // Create the config file before computing the hashing costs, so we fail
+       // immediately if the program has insufficient permissions.
+       configFile, err := filesystem.OpenFileOverridingUmask(ConfigFileLocation,
+               createFlags, configPermissions)
+       switch {
+       case os.IsExist(err):
+               return &ErrConfigFileExists{ConfigFileLocation}
+       case err != nil:
+               return err
+       }
+       defer configFile.Close()
+
+       config := &metadata.Config{
+               Source:  metadata.DefaultSource,
+               Options: metadata.DefaultOptions,
+       }
+
+       if policyVersion != 0 {
+               config.Options.PolicyVersion = policyVersion
+       }
+
+       if config.HashCosts, err = getHashingCosts(target); err != nil {
+               return err
+       }
+
+       log.Printf("Creating config at %q with %v\n", ConfigFileLocation, config)
+       return metadata.WriteConfig(config, configFile)
+}
+
+// getConfig returns the current configuration struct. Any fields not specified
+// in the config file use the system defaults. An error is returned if the
+// config file hasn't been setup with CreateConfigFile yet or the config
+// contains invalid data.
+func getConfig() (*metadata.Config, error) {
+       configFile, err := os.Open(ConfigFileLocation)
+       switch {
+       case os.IsNotExist(err):
+               return nil, &ErrNoConfigFile{ConfigFileLocation}
+       case err != nil:
+               return nil, err
+       }
+       defer configFile.Close()
+
+       log.Printf("Reading config from %q\n", ConfigFileLocation)
+       config, err := metadata.ReadConfig(configFile)
+       if err != nil {
+               return nil, &ErrBadConfigFile{ConfigFileLocation, err}
+       }
+
+       // Use system defaults if not specified
+       if config.Source == metadata.SourceType_default {
+               config.Source = metadata.DefaultSource
+               log.Printf("Falling back to source of %q", config.Source.String())
+       }
+       if config.Options.Padding == 0 {
+               config.Options.Padding = metadata.DefaultOptions.Padding
+               log.Printf("Falling back to padding of %d", config.Options.Padding)
+       }
+       if config.Options.Contents == metadata.EncryptionOptions_default {
+               config.Options.Contents = metadata.DefaultOptions.Contents
+               log.Printf("Falling back to contents mode of %q", config.Options.Contents)
+       }
+       if config.Options.Filenames == metadata.EncryptionOptions_default {
+               config.Options.Filenames = metadata.DefaultOptions.Filenames
+               log.Printf("Falling back to filenames mode of %q", config.Options.Filenames)
+       }
+       if config.Options.PolicyVersion == 0 {
+               config.Options.PolicyVersion = metadata.DefaultOptions.PolicyVersion
+               log.Printf("Falling back to policy version of %d", config.Options.PolicyVersion)
+       }
+
+       if err := config.CheckValidity(); err != nil {
+               return nil, &ErrBadConfigFile{ConfigFileLocation, err}
+       }
+
+       return config, nil
+}
+
+// getHashingCosts returns hashing costs so that hashing a password will take
+// approximately the target time. This is done using the total amount of RAM,
+// the number of CPUs present, and by running the passphrase hash many times.
+func getHashingCosts(target time.Duration) (*metadata.HashingCosts, error) {
+       log.Printf("Finding hashing costs that take %v\n", target)
+
+       // Start out with the minimal possible costs that use all the CPUs.
+       parallelism := int64(runtime.NumCPU())
+       // golang.org/x/crypto/argon2 only supports parallelism up to 255.
+       // For compatibility, don't use more than that amount.
+       if parallelism > metadata.MaxParallelism {
+               parallelism = metadata.MaxParallelism
+       }
+       costs := &metadata.HashingCosts{
+               Time:            1,
+               Memory:          8 * parallelism,
+               Parallelism:     parallelism,
+               TruncationFixed: true,
+       }
+
+       // If even the minimal costs are not fast enough, just return the
+       // minimal costs and log a warning.
+       t, err := timeHashingCosts(costs)
+       if err != nil {
+               return nil, err
+       }
+       log.Printf("Min Costs={%v}\t-> %v\n", costs, t)
+
+       if t > target {
+               log.Printf("time exceeded the target of %v.\n", target)
+               return costs, nil
+       }
+
+       // Now we start doubling the costs until we reach the target.
+       memoryKiBLimit := memoryBytesLimit() / 1024
+       for {
+               // Store a copy of the previous costs
+               costsPrev := proto.Clone(costs).(*metadata.HashingCosts)
+               tPrev := t
+
+               // Double the memory up to the max, then double the time.
+               if costs.Memory < memoryKiBLimit {
+                       costs.Memory = util.MinInt64(2*costs.Memory, memoryKiBLimit)
+               } else {
+                       costs.Time *= 2
+               }
+
+               // If our hashing failed, return the last good set of costs.
+               if t, err = timeHashingCosts(costs); err != nil {
+                       log.Printf("Hashing with costs={%v} failed: %v\n", costs, err)
+                       return costsPrev, nil
+               }
+               log.Printf("Costs={%v}\t-> %v\n", costs, t)
+
+               // If we have reached the target time, we return a set of costs
+               // based on the linear interpolation between the last two times.
+               if t >= target {
+                       f := float64(target-tPrev) / float64(t-tPrev)
+                       return &metadata.HashingCosts{
+                               Time:            betweenCosts(costsPrev.Time, costs.Time, f),
+                               Memory:          betweenCosts(costsPrev.Memory, costs.Memory, f),
+                               Parallelism:     costs.Parallelism,
+                               TruncationFixed: costs.TruncationFixed,
+                       }, nil
+               }
+       }
+}
+
+// memoryBytesLimit returns the maximum amount of memory we will use for
+// passphrase hashing. This will never be more than a reasonable maximum (for
+// compatibility) or an 8th the available system RAM.
+func memoryBytesLimit() int64 {
+       // The sysinfo syscall only fails if given a bad address
+       var info unix.Sysinfo_t
+       err := unix.Sysinfo(&info)
+       util.NeverError(err)
+
+       totalRAMBytes := int64(info.Totalram)
+       return util.MinInt64(totalRAMBytes/8, maxMemoryBytes)
+}
+
+// betweenCosts returns a cost between a and b. Specifically, it returns the
+// floor of a + f*(b-a). This way, f=0 returns a and f=1 returns b.
+func betweenCosts(a, b int64, f float64) int64 {
+       return a + int64(f*float64(b-a))
+}
+
+// timeHashingCosts runs the passphrase hash with the specified costs and
+// returns the time it takes to hash the passphrase.
+func timeHashingCosts(costs *metadata.HashingCosts) (time.Duration, error) {
+       passphrase, err := crypto.NewKeyFromReader(bytes.NewReader(timingPassphrase))
+       if err != nil {
+               return 0, err
+       }
+       defer passphrase.Wipe()
+
+       // Be sure to measure CPU time, not wall time (time.Now)
+       begin := cpuTimeInNanoseconds()
+       hash, err := crypto.PassphraseHash(passphrase, timingSalt, costs)
+       if err == nil {
+               hash.Wipe()
+       }
+       end := cpuTimeInNanoseconds()
+
+       // This uses a lot of memory, run the garbage collector
+       runtime.GC()
+
+       return time.Duration((end - begin) / costs.Parallelism), nil
+}
+
+// cpuTimeInNanoseconds returns the nanosecond count based on the process's CPU usage.
+// This number has no absolute meaning, only relative meaning to other calls.
+func cpuTimeInNanoseconds() int64 {
+       var ts unix.Timespec
+       err := unix.ClockGettime(unix.CLOCK_PROCESS_CPUTIME_ID, &ts)
+       // ClockGettime fails if given a bad address or on a VERY old system.
+       util.NeverError(err)
+       return unix.TimespecToNsec(ts)
+}
diff --git a/actions/config_test.go b/actions/config_test.go
new file mode 100644 (file)
index 0000000..49838e3
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * config_test.go - tests for creating the config file
+ *
+ * Copyright 2019 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.
+ */
+
+package actions
+
+import (
+       "os"
+       "path/filepath"
+       "testing"
+       "time"
+
+       "golang.org/x/sys/unix"
+
+       "github.com/google/fscrypt/metadata"
+)
+
+// Test that the global config file is created with mode 0644, regardless of the
+// current umask.
+func TestConfigFileIsCreatedWithCorrectMode(t *testing.T) {
+       oldMask := unix.Umask(0)
+       defer unix.Umask(oldMask)
+       unix.Umask(0077)
+
+       tempDir, err := os.MkdirTemp("", "fscrypt")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(tempDir)
+       ConfigFileLocation = filepath.Join(tempDir, "test.conf")
+
+       if err = CreateConfigFile(time.Millisecond, 0); err != nil {
+               t.Fatal(err)
+       }
+       fileInfo, err := os.Stat(ConfigFileLocation)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if fileInfo.Mode().Perm() != 0644 {
+               t.Error("Expected newly created config file to have mode 0644")
+       }
+}
+
+func TestCreateConfigFileV2Policy(t *testing.T) {
+       tempDir, err := os.MkdirTemp("", "fscrypt")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(tempDir)
+       ConfigFileLocation = filepath.Join(tempDir, "test.conf")
+
+       if err = CreateConfigFile(time.Millisecond, 2); err != nil {
+               t.Fatal(err)
+       }
+
+       var config *metadata.Config
+       config, err = getConfig()
+       if err != nil {
+               t.Fatal(err)
+       }
+       if config.Options.PolicyVersion != 2 {
+               t.Error("Expected PolicyVersion 2")
+       }
+}
diff --git a/actions/context.go b/actions/context.go
new file mode 100644 (file)
index 0000000..4253de2
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * context.go - top-level interface to fscrypt packages
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+// Package actions is the high-level interface to the fscrypt packages. The
+// functions here roughly correspond with commands for the tool in cmd/fscrypt.
+// All of the actions include a significant amount of logging, so that good
+// output can be provided for cmd/fscrypt's verbose mode.
+// The top-level actions currently include:
+//   - Creating a new config file
+//   - Creating a context on which to perform actions
+//   - Creating, unlocking, and modifying Protectors
+//   - Creating, unlocking, and modifying Policies
+package actions
+
+import (
+       "log"
+       "os/user"
+
+       "github.com/pkg/errors"
+
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/keyring"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+// ErrLocked indicates that the key hasn't been unwrapped yet.
+var ErrLocked = errors.New("key needs to be unlocked first")
+
+// Context contains the necessary global state to perform most of fscrypt's
+// actions.
+type Context struct {
+       // Config is the struct loaded from the global config file. It can be
+       // modified after being loaded to customise parameters.
+       Config *metadata.Config
+       // Mount is the filesystem relative to which all Protectors and Policies
+       // are added, edited, removed, and applied, and to which policies using
+       // the filesystem keyring are provisioned.
+       Mount *filesystem.Mount
+       // TargetUser is the user for whom protectors are created, and to whose
+       // keyring policies using the user keyring are provisioned.  It's also
+       // the user for whom the keys are claimed in the filesystem keyring when
+       // v2 policies are provisioned.
+       TargetUser *user.User
+       // TrustedUser is the user for whom policies and protectors are allowed
+       // to be read.  Specifically, if TrustedUser is set, then only
+       // policies and protectors owned by TrustedUser or by root will be
+       // allowed to be read.  If it's nil, then all policies and protectors
+       // the process has filesystem-level read access to will be allowed.
+       TrustedUser *user.User
+}
+
+// NewContextFromPath makes a context for the filesystem containing the
+// specified path and whose Config is loaded from the global config file. On
+// success, the Context contains a valid Config and Mount. The target user
+// defaults to the current effective user if none is specified.
+func NewContextFromPath(path string, targetUser *user.User) (*Context, error) {
+       ctx, err := newContextFromUser(targetUser)
+       if err != nil {
+               return nil, err
+       }
+       if ctx.Mount, err = filesystem.FindMount(path); err != nil {
+               return nil, err
+       }
+
+       log.Printf("%s is on %s filesystem %q (%s)", path,
+               ctx.Mount.FilesystemType, ctx.Mount.Path, ctx.Mount.Device)
+       return ctx, nil
+}
+
+// NewContextFromMountpoint makes a context for the filesystem at the specified
+// mountpoint and whose Config is loaded from the global config file. On
+// success, the Context contains a valid Config and Mount. The target user
+// defaults to the current effective user if none is specified.
+func NewContextFromMountpoint(mountpoint string, targetUser *user.User) (*Context, error) {
+       ctx, err := newContextFromUser(targetUser)
+       if err != nil {
+               return nil, err
+       }
+       if ctx.Mount, err = filesystem.GetMount(mountpoint); err != nil {
+               return nil, err
+       }
+
+       log.Printf("found %s filesystem %q (%s)", ctx.Mount.FilesystemType,
+               ctx.Mount.Path, ctx.Mount.Device)
+       return ctx, nil
+}
+
+// newContextFromUser makes a context with the corresponding target user, and
+// whose Config is loaded from the global config file. If the target user is
+// nil, the effective user is used.
+func newContextFromUser(targetUser *user.User) (*Context, error) {
+       var err error
+       if targetUser == nil {
+               if targetUser, err = util.EffectiveUser(); err != nil {
+                       return nil, err
+               }
+       }
+
+       ctx := &Context{TargetUser: targetUser}
+       if ctx.Config, err = getConfig(); err != nil {
+               return nil, err
+       }
+
+       // By default, when running as a non-root user we only read policies and
+       // protectors owned by the user or root.  When running as root, we allow
+       // reading all policies and protectors.
+       if !ctx.Config.GetAllowCrossUserMetadata() && !util.IsUserRoot() {
+               ctx.TrustedUser, err = util.EffectiveUser()
+               if err != nil {
+                       return nil, err
+               }
+       }
+
+       log.Printf("creating context for user %q", targetUser.Username)
+       return ctx, nil
+}
+
+// checkContext verifies that the context contains a valid config and a mount
+// which is being used with fscrypt.
+func (ctx *Context) checkContext() error {
+       if err := ctx.Config.CheckValidity(); err != nil {
+               return &ErrBadConfig{ctx.Config, err}
+       }
+       return ctx.Mount.CheckSetup(ctx.TrustedUser)
+}
+
+func (ctx *Context) getKeyringOptions() *keyring.Options {
+       return &keyring.Options{
+               Mount:                     ctx.Mount,
+               User:                      ctx.TargetUser,
+               UseFsKeyringForV1Policies: ctx.Config.GetUseFsKeyringForV1Policies(),
+       }
+}
+
+// getProtectorOption returns the ProtectorOption for the protector on the
+// context's mountpoint with the specified descriptor.
+func (ctx *Context) getProtectorOption(protectorDescriptor string) *ProtectorOption {
+       mnt, data, err := ctx.Mount.GetProtector(protectorDescriptor, ctx.TrustedUser)
+       if err != nil {
+               return &ProtectorOption{ProtectorInfo{}, nil, err}
+       }
+
+       info := ProtectorInfo{data}
+       // No linked path if on the same mountpoint
+       if mnt == ctx.Mount {
+               return &ProtectorOption{info, nil, nil}
+       }
+       return &ProtectorOption{info, mnt, nil}
+}
+
+// ProtectorOptions creates a slice of all the options for all of the Protectors
+// on the Context's mountpoint.
+func (ctx *Context) ProtectorOptions() ([]*ProtectorOption, error) {
+       if err := ctx.checkContext(); err != nil {
+               return nil, err
+       }
+       descriptors, err := ctx.Mount.ListProtectors(ctx.TrustedUser)
+       if err != nil {
+               return nil, err
+       }
+
+       options := make([]*ProtectorOption, len(descriptors))
+       for i, descriptor := range descriptors {
+               options[i] = ctx.getProtectorOption(descriptor)
+       }
+       return options, nil
+}
diff --git a/actions/context_test.go b/actions/context_test.go
new file mode 100644 (file)
index 0000000..6e28857
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * context_test.go - tests for creating new contexts
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package actions
+
+import (
+       "fmt"
+       "log"
+       "os"
+       "path/filepath"
+       "testing"
+       "time"
+
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/util"
+       "github.com/pkg/errors"
+)
+
+const testTime = 10 * time.Millisecond
+
+// holds the context we will use throughout the actions tests
+var testContext *Context
+
+// Makes a context using the testing locations for the filesystem and
+// configuration file.
+func setupContext() (ctx *Context, err error) {
+       mountpoint, err := util.TestRoot()
+       if err != nil {
+               return nil, err
+       }
+
+       ConfigFileLocation = filepath.Join(mountpoint, "test.conf")
+
+       // Should not be able to setup without a config file
+       os.Remove(ConfigFileLocation)
+       if badCtx, badCtxErr := NewContextFromMountpoint(mountpoint, nil); badCtxErr == nil {
+               badCtx.Mount.RemoveAllMetadata()
+               return nil, fmt.Errorf("created context at %q without config file", badCtx.Mount.Path)
+       }
+
+       if err = CreateConfigFile(testTime, 0); err != nil {
+               return nil, err
+       }
+       defer func() {
+               if err != nil {
+                       os.RemoveAll(ConfigFileLocation)
+               }
+       }()
+
+       ctx, err = NewContextFromMountpoint(mountpoint, nil)
+       if err != nil {
+               return nil, err
+       }
+
+       return ctx, ctx.Mount.Setup(filesystem.WorldWritable)
+}
+
+// Cleans up the testing config file and testing filesystem data.
+func cleaupContext(ctx *Context) error {
+       err1 := os.RemoveAll(ConfigFileLocation)
+       err2 := ctx.Mount.RemoveAllMetadata()
+       if err1 != nil {
+               return err1
+       }
+       return err2
+}
+
+func TestMain(m *testing.M) {
+       log.SetFlags(log.LstdFlags | log.Lmicroseconds)
+       var err error
+       testContext, err = setupContext()
+       if err != nil {
+               fmt.Println(err)
+               if errors.Cause(err) != util.ErrSkipIntegration {
+                       os.Exit(1)
+               }
+               os.Exit(0)
+       }
+
+       returnCode := m.Run()
+       err = cleaupContext(testContext)
+       if err != nil {
+               fmt.Printf("cleanupContext() = %v\n", err)
+               os.Exit(1)
+       }
+       os.Exit(returnCode)
+}
diff --git a/actions/hashing_test.go b/actions/hashing_test.go
new file mode 100644 (file)
index 0000000..26f627b
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * hashing_test.go - tests for computing and benchmarking hashing costs
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package actions
+
+import (
+       "io"
+       "log"
+       "testing"
+       "time"
+)
+
+// Tests that we can find valid hashing costs for various time targets and the
+// estimations are somewhat close to the targets.
+func TestCostsSearch(t *testing.T) {
+       for _, target := range []time.Duration{
+               100 * time.Millisecond,
+               200 * time.Millisecond,
+               500 * time.Millisecond,
+       } {
+               costs, err := getHashingCosts(target)
+               if err != nil {
+                       t.Error(err)
+               }
+               actual, err := timeHashingCosts(costs)
+               if err != nil {
+                       t.Error(err)
+               }
+
+               if actual*3 < target {
+                       t.Errorf("actual=%v is too small (target=%v)", actual, target)
+               }
+               if target*3 < actual {
+                       t.Errorf("actual=%v is too big (target=%v)", actual, target)
+               }
+       }
+}
+
+func benchmarkCostsSearch(b *testing.B, target time.Duration) {
+       // Disable logging for benchmarks
+       log.SetOutput(io.Discard)
+       for i := 0; i < b.N; i++ {
+               _, err := getHashingCosts(target)
+               if err != nil {
+                       b.Fatal(err)
+               }
+       }
+}
+
+func BenchmarkCostsSearch10ms(b *testing.B) {
+       benchmarkCostsSearch(b, 10*time.Millisecond)
+}
+
+func BenchmarkCostsSearch100ms(b *testing.B) {
+       benchmarkCostsSearch(b, 100*time.Millisecond)
+}
+
+func BenchmarkCostsSearch1s(b *testing.B) {
+       benchmarkCostsSearch(b, time.Second)
+}
diff --git a/actions/policy.go b/actions/policy.go
new file mode 100644 (file)
index 0000000..c621725
--- /dev/null
@@ -0,0 +1,622 @@
+/*
+ * policy.go - functions for dealing with policies
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package actions
+
+import (
+       "fmt"
+       "log"
+       "os"
+       "os/user"
+
+       "github.com/pkg/errors"
+       "google.golang.org/protobuf/proto"
+
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/keyring"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+// ErrAccessDeniedPossiblyV2 indicates that a directory's encryption policy
+// couldn't be retrieved due to "permission denied", but it looks like it's due
+// to the directory using a v2 policy but the kernel not supporting it.
+type ErrAccessDeniedPossiblyV2 struct {
+       DirPath string
+}
+
+func (err *ErrAccessDeniedPossiblyV2) Error() string {
+       return fmt.Sprintf(`
+       failed to get encryption policy of %s: permission denied
+
+       This may be caused by the directory using a v2 encryption policy and the
+       current kernel not supporting it. If indeed the case, then this
+       directory can only be used on kernel v5.4 and later. You can create
+       directories accessible on older kernels by changing policy_version to 1
+       in %s.`,
+               err.DirPath, ConfigFileLocation)
+}
+
+// ErrAlreadyProtected indicates that a policy is already protected by the given
+// protector.
+type ErrAlreadyProtected struct {
+       Policy    *Policy
+       Protector *Protector
+}
+
+func (err *ErrAlreadyProtected) Error() string {
+       return fmt.Sprintf("policy %s is already protected by protector %s",
+               err.Policy.Descriptor(), err.Protector.Descriptor())
+}
+
+// ErrDifferentFilesystem indicates that a policy can't be applied to a
+// directory on a different filesystem.
+type ErrDifferentFilesystem struct {
+       PolicyMount *filesystem.Mount
+       PathMount   *filesystem.Mount
+}
+
+func (err *ErrDifferentFilesystem) Error() string {
+       return fmt.Sprintf(`cannot apply policy from filesystem %q to a
+       directory on filesystem %q. Policies may only protect files on the same
+       filesystem.`, err.PolicyMount.Path, err.PathMount.Path)
+}
+
+// ErrMissingPolicyMetadata indicates that a directory is encrypted but its
+// policy metadata cannot be found.
+type ErrMissingPolicyMetadata struct {
+       Mount      *filesystem.Mount
+       DirPath    string
+       Descriptor string
+}
+
+func (err *ErrMissingPolicyMetadata) Error() string {
+       return fmt.Sprintf(`filesystem %q does not contain the policy metadata
+       for %q. This directory has either been encrypted with another tool (such
+       as e4crypt), or the file %q has been deleted.`,
+               err.Mount.Path, err.DirPath,
+               err.Mount.PolicyPath(err.Descriptor))
+}
+
+// ErrNotProtected indicates that the given policy is not protected by the given
+// protector.
+type ErrNotProtected struct {
+       PolicyDescriptor    string
+       ProtectorDescriptor string
+}
+
+func (err *ErrNotProtected) Error() string {
+       return fmt.Sprintf(`policy %s is not protected by protector %s`,
+               err.PolicyDescriptor, err.ProtectorDescriptor)
+}
+
+// ErrOnlyProtector indicates that the last protector can't be removed from a
+// policy.
+type ErrOnlyProtector struct {
+       Policy *Policy
+}
+
+func (err *ErrOnlyProtector) Error() string {
+       return fmt.Sprintf(`cannot remove the only protector from policy %s. A
+       policy must have at least one protector.`, err.Policy.Descriptor())
+}
+
+// ErrPolicyMetadataMismatch indicates that the policy metadata for an encrypted
+// directory is inconsistent with that directory.
+type ErrPolicyMetadataMismatch struct {
+       DirPath   string
+       Mount     *filesystem.Mount
+       PathData  *metadata.PolicyData
+       MountData *metadata.PolicyData
+}
+
+func (err *ErrPolicyMetadataMismatch) Error() string {
+       return fmt.Sprintf(`inconsistent metadata between encrypted directory %q
+       and its corresponding metadata file %q.
+
+       Directory has descriptor:%s %s
+
+       Metadata file has descriptor:%s %s`,
+               err.DirPath, err.Mount.PolicyPath(err.PathData.KeyDescriptor),
+               err.PathData.KeyDescriptor, err.PathData.Options,
+               err.MountData.KeyDescriptor, err.MountData.Options)
+}
+
+// PurgeAllPolicies removes all policy keys on the filesystem from the kernel
+// keyring. In order for this to fully take effect, the filesystem may also need
+// to be unmounted or caches dropped.
+func PurgeAllPolicies(ctx *Context) error {
+       if err := ctx.checkContext(); err != nil {
+               return err
+       }
+       policies, err := ctx.Mount.ListPolicies(nil)
+       if err != nil {
+               return err
+       }
+
+       for _, policyDescriptor := range policies {
+               err = keyring.RemoveEncryptionKey(policyDescriptor, ctx.getKeyringOptions(), false)
+               switch errors.Cause(err) {
+               case nil, keyring.ErrKeyNotPresent:
+                       // We don't care if the key has already been removed
+               case keyring.ErrKeyFilesOpen:
+                       log.Printf("Key for policy %s couldn't be fully removed because some files are still in-use",
+                               policyDescriptor)
+               case keyring.ErrKeyAddedByOtherUsers:
+                       log.Printf("Key for policy %s couldn't be fully removed because other user(s) have added it too",
+                               policyDescriptor)
+               default:
+                       return err
+               }
+       }
+       return nil
+}
+
+// Policy represents an unlocked policy, so it contains the PolicyData as well
+// as the actual protector key. These unlocked Polices can then be applied to a
+// directory, or have their key material inserted into the keyring (which will
+// allow encrypted files to be accessed). As with the key struct, a Policy
+// should be wiped after use.
+type Policy struct {
+       Context             *Context
+       data                *metadata.PolicyData
+       key                 *crypto.Key
+       created             bool
+       ownerIfCreating     *user.User
+       newLinkedProtectors []string
+}
+
+// CreatePolicy creates a Policy protected by given Protector and stores the
+// appropriate data on the filesystem. On error, no data is changed on the
+// filesystem.
+func CreatePolicy(ctx *Context, protector *Protector) (*Policy, error) {
+       if err := ctx.checkContext(); err != nil {
+               return nil, err
+       }
+       // Randomly create the underlying policy key (and wipe if we fail)
+       key, err := crypto.NewRandomKey(metadata.PolicyKeyLen)
+       if err != nil {
+               return nil, err
+       }
+
+       keyDescriptor, err := crypto.ComputeKeyDescriptor(key, ctx.Config.Options.PolicyVersion)
+       if err != nil {
+               key.Wipe()
+               return nil, err
+       }
+
+       policy := &Policy{
+               Context: ctx,
+               data: &metadata.PolicyData{
+                       Options:       ctx.Config.Options,
+                       KeyDescriptor: keyDescriptor,
+               },
+               key:     key,
+               created: true,
+       }
+
+       policy.ownerIfCreating, err = getOwnerOfMetadataForProtector(protector)
+       if err != nil {
+               policy.Lock()
+               return nil, err
+       }
+
+       if err = policy.AddProtector(protector); err != nil {
+               policy.Lock()
+               return nil, err
+       }
+
+       return policy, nil
+}
+
+// GetPolicy retrieves a locked policy with a specific descriptor. The Policy is
+// still locked in this case, so it must be unlocked before using certain
+// methods.
+func GetPolicy(ctx *Context, descriptor string) (*Policy, error) {
+       if err := ctx.checkContext(); err != nil {
+               return nil, err
+       }
+       data, err := ctx.Mount.GetPolicy(descriptor, ctx.TrustedUser)
+       if err != nil {
+               return nil, err
+       }
+       log.Printf("got data for %s from %q", descriptor, ctx.Mount.Path)
+
+       return &Policy{Context: ctx, data: data}, nil
+}
+
+// GetPolicyFromPath returns the locked policy descriptor for a file on the
+// filesystem. The Policy is still locked in this case, so it must be unlocked
+// before using certain methods. An error is returned if the metadata is
+// inconsistent or the path is not encrypted.
+func GetPolicyFromPath(ctx *Context, path string) (*Policy, error) {
+       if err := ctx.checkContext(); err != nil {
+               return nil, err
+       }
+
+       // We double check that the options agree for both the data we get from
+       // the path, and the data we get from the mountpoint.
+       pathData, err := metadata.GetPolicy(path)
+       err = ctx.Mount.EncryptionSupportError(err)
+       if err != nil {
+               // On kernels that don't support v2 encryption policies, trying
+               // to open a directory with a v2 policy simply gave EACCES. This
+               // is ambiguous with other errors, but try to detect this case
+               // and show a better error message.
+               if os.IsPermission(err) &&
+                       filesystem.HaveReadAccessTo(path) &&
+                       !keyring.IsFsKeyringSupported(ctx.Mount) {
+                       return nil, &ErrAccessDeniedPossiblyV2{path}
+               }
+               return nil, err
+       }
+       descriptor := pathData.KeyDescriptor
+       log.Printf("found policy %s for %q", descriptor, path)
+
+       mountData, err := ctx.Mount.GetPolicy(descriptor, ctx.TrustedUser)
+       if err != nil {
+               log.Printf("getting policy metadata: %v", err)
+               if _, ok := err.(*filesystem.ErrPolicyNotFound); ok {
+                       return nil, &ErrMissingPolicyMetadata{ctx.Mount, path, descriptor}
+               }
+               return nil, err
+       }
+       log.Printf("found data for policy %s on %q", descriptor, ctx.Mount.Path)
+
+       if !proto.Equal(pathData.Options, mountData.Options) ||
+               pathData.KeyDescriptor != mountData.KeyDescriptor {
+               return nil, &ErrPolicyMetadataMismatch{path, ctx.Mount, pathData, mountData}
+       }
+       log.Print("data from filesystem and path agree")
+
+       return &Policy{Context: ctx, data: mountData}, nil
+}
+
+// ProtectorOptions creates a slice of ProtectorOptions for the protectors
+// protecting this policy.
+func (policy *Policy) ProtectorOptions() []*ProtectorOption {
+       options := make([]*ProtectorOption, len(policy.data.WrappedPolicyKeys))
+       for i, wrappedPolicyKey := range policy.data.WrappedPolicyKeys {
+               options[i] = policy.Context.getProtectorOption(wrappedPolicyKey.ProtectorDescriptor)
+       }
+       return options
+}
+
+// ProtectorDescriptors creates a slice of the Protector descriptors for the
+// protectors protecting this policy.
+func (policy *Policy) ProtectorDescriptors() []string {
+       descriptors := make([]string, len(policy.data.WrappedPolicyKeys))
+       for i, wrappedPolicyKey := range policy.data.WrappedPolicyKeys {
+               descriptors[i] = wrappedPolicyKey.ProtectorDescriptor
+       }
+       return descriptors
+}
+
+// Descriptor returns the key descriptor for this policy.
+func (policy *Policy) Descriptor() string {
+       return policy.data.KeyDescriptor
+}
+
+// Options returns the encryption options of this policy.
+func (policy *Policy) Options() *metadata.EncryptionOptions {
+       return policy.data.Options
+}
+
+// Version returns the version of this policy.
+func (policy *Policy) Version() int64 {
+       return policy.data.Options.PolicyVersion
+}
+
+// Destroy removes a policy from the filesystem. It also removes any new
+// protector links that were created for the policy. This does *not* wipe the
+// policy's internal key from memory; use Lock() to do that.
+func (policy *Policy) Destroy() error {
+       for _, protectorDescriptor := range policy.newLinkedProtectors {
+               policy.Context.Mount.RemoveProtector(protectorDescriptor)
+       }
+       return policy.Context.Mount.RemovePolicy(policy.Descriptor())
+}
+
+// Revert destroys a policy if it was created, but does nothing if it was just
+// queried from the filesystem.
+func (policy *Policy) Revert() error {
+       if !policy.created {
+               return nil
+       }
+       return policy.Destroy()
+}
+
+func (policy *Policy) String() string {
+       return fmt.Sprintf("Policy: %s\nMountpoint: %s\nOptions: %v\nProtectors:%+v",
+               policy.Descriptor(), policy.Context.Mount, policy.data.Options,
+               policy.ProtectorDescriptors())
+}
+
+// Unlock unwraps the Policy's internal key. As a Protector is needed to unlock
+// the Policy, callbacks to select the Policy and get the key are needed. This
+// method will retry the keyFn as necessary to get the correct key for the
+// selected protector. Does nothing if policy is already unlocked.
+func (policy *Policy) Unlock(optionFn OptionFunc, keyFn KeyFunc) error {
+       if policy.key != nil {
+               return nil
+       }
+       options := policy.ProtectorOptions()
+
+       // The OptionFunc indicates which option and wrapped key we should use.
+       idx, err := optionFn(policy.Descriptor(), options)
+       if err != nil {
+               return err
+       }
+       option := options[idx]
+       if option.LoadError != nil {
+               return option.LoadError
+       }
+
+       log.Printf("protector %s selected in callback", option.Descriptor())
+       protectorKey, err := unwrapProtectorKey(option.ProtectorInfo, keyFn)
+       if err != nil {
+               return err
+       }
+       defer protectorKey.Wipe()
+
+       log.Printf("unwrapping policy %s with protector", policy.Descriptor())
+       wrappedPolicyKey := policy.data.WrappedPolicyKeys[idx].WrappedKey
+       policy.key, err = crypto.Unwrap(protectorKey, wrappedPolicyKey)
+       return err
+}
+
+// UnlockWithProtector uses an unlocked Protector to unlock a policy. An error
+// is returned if the Protector is not yet unlocked or does not protect the
+// policy. Does nothing if policy is already unlocked.
+func (policy *Policy) UnlockWithProtector(protector *Protector) error {
+       if policy.key != nil {
+               return nil
+       }
+       if protector.key == nil {
+               return ErrLocked
+       }
+       idx, ok := policy.findWrappedKeyIndex(protector.Descriptor())
+       if !ok {
+               return &ErrNotProtected{policy.Descriptor(), protector.Descriptor()}
+       }
+
+       var err error
+       wrappedPolicyKey := policy.data.WrappedPolicyKeys[idx].WrappedKey
+       policy.key, err = crypto.Unwrap(protector.key, wrappedPolicyKey)
+       return err
+}
+
+// Lock wipes a Policy's internal Key. It should always be called after using a
+// Policy. This is often done with a defer statement. There is no effect if
+// called multiple times.
+func (policy *Policy) Lock() error {
+       err := policy.key.Wipe()
+       policy.key = nil
+       return err
+}
+
+// UsesProtector returns if the policy is protected with the protector
+func (policy *Policy) UsesProtector(protector *Protector) bool {
+       _, ok := policy.findWrappedKeyIndex(protector.Descriptor())
+       return ok
+}
+
+// getOwnerOfMetadataForProtector returns the User to whom the owner of any new
+// policies or protector links for the given protector should be set.
+//
+// This will return a non-nil value only when the protector is a login protector
+// and the process is running as root.  In this scenario, root is setting up
+// encryption on the user's behalf, so we need to make new policies and
+// protector links owned by the user (rather than root) to allow them to be read
+// by the user, just like the login protector itself which is handled elsewhere.
+func getOwnerOfMetadataForProtector(protector *Protector) (*user.User, error) {
+       if protector.data.Source == metadata.SourceType_pam_passphrase && util.IsUserRoot() {
+               owner, err := util.UserFromUID(protector.data.Uid)
+               if err != nil {
+                       return nil, err
+               }
+               return owner, nil
+       }
+       return nil, nil
+}
+
+// AddProtector updates the data that is wrapping the Policy Key so that the
+// provided Protector is now protecting the specified Policy. If an error is
+// returned, no data has been changed. If the policy and protector are on
+// different filesystems, a link will be created between them. The policy and
+// protector must both be unlocked.
+func (policy *Policy) AddProtector(protector *Protector) error {
+       if policy.UsesProtector(protector) {
+               return &ErrAlreadyProtected{policy, protector}
+       }
+       if policy.key == nil || protector.key == nil {
+               return ErrLocked
+       }
+
+       // If the protector is on a different filesystem, we need to add a link
+       // to it on the policy's filesystem.
+       if policy.Context.Mount != protector.Context.Mount {
+               log.Printf("policy on %s\n protector on %s\n", policy.Context.Mount, protector.Context.Mount)
+               ownerIfCreating, err := getOwnerOfMetadataForProtector(protector)
+               if err != nil {
+                       return err
+               }
+               isNewLink, err := policy.Context.Mount.AddLinkedProtector(
+                       protector.Descriptor(), protector.Context.Mount,
+                       protector.Context.TrustedUser, ownerIfCreating)
+               if err != nil {
+                       return err
+               }
+               if isNewLink {
+                       policy.newLinkedProtectors = append(policy.newLinkedProtectors,
+                               protector.Descriptor())
+               }
+       } else {
+               log.Printf("policy and protector both on %q", policy.Context.Mount)
+       }
+
+       // Create the wrapped policy key
+       wrappedKey, err := crypto.Wrap(protector.key, policy.key)
+       if err != nil {
+               return err
+       }
+
+       // Append the wrapped key to the data
+       policy.addKey(&metadata.WrappedPolicyKey{
+               ProtectorDescriptor: protector.Descriptor(),
+               WrappedKey:          wrappedKey,
+       })
+
+       if err := policy.commitData(); err != nil {
+               // revert the addition on failure
+               policy.removeKey(len(policy.data.WrappedPolicyKeys) - 1)
+               return err
+       }
+       return nil
+}
+
+// RemoveProtector updates the data that is wrapping the Policy Key so that the
+// protector with the given descriptor is no longer protecting the specified
+// Policy.  If an error is returned, no data has been changed.  Note that the
+// protector itself won't be removed, nor will a link to the protector be
+// removed (in the case where the protector and policy are on different
+// filesystems).  The policy can be locked or unlocked.
+func (policy *Policy) RemoveProtector(protectorDescriptor string) error {
+       idx, ok := policy.findWrappedKeyIndex(protectorDescriptor)
+       if !ok {
+               return &ErrNotProtected{policy.Descriptor(), protectorDescriptor}
+       }
+
+       if len(policy.data.WrappedPolicyKeys) == 1 {
+               return &ErrOnlyProtector{policy}
+       }
+
+       // Remove the wrapped key from the data
+       toRemove := policy.removeKey(idx)
+
+       if err := policy.commitData(); err != nil {
+               // revert the removal on failure (order is irrelevant)
+               policy.addKey(toRemove)
+               return err
+       }
+       return nil
+}
+
+// Apply sets the Policy on a specified directory. Currently we impose the
+// additional constraint that policies and the directories they are applied to
+// must reside on the same filesystem.
+func (policy *Policy) Apply(path string) error {
+       if pathMount, err := filesystem.FindMount(path); err != nil {
+               return err
+       } else if pathMount != policy.Context.Mount {
+               return &ErrDifferentFilesystem{policy.Context.Mount, pathMount}
+       }
+
+       err := metadata.SetPolicy(path, policy.data)
+       return policy.Context.Mount.EncryptionSupportError(err)
+}
+
+// GetProvisioningStatus returns the status of this policy's key in the keyring.
+func (policy *Policy) GetProvisioningStatus() keyring.KeyStatus {
+       status, _ := keyring.GetEncryptionKeyStatus(policy.Descriptor(),
+               policy.Context.getKeyringOptions())
+       return status
+}
+
+// IsProvisionedByTargetUser returns true if the policy's key is present in the
+// target kernel keyring, but not if that keyring is a filesystem keyring and
+// the key only been added by users other than Context.TargetUser.
+func (policy *Policy) IsProvisionedByTargetUser() bool {
+       return policy.GetProvisioningStatus() == keyring.KeyPresent
+}
+
+// Provision inserts the Policy key into the kernel keyring. This allows reading
+// and writing of files encrypted with this directory. Requires unlocked Policy.
+func (policy *Policy) Provision() error {
+       if policy.key == nil {
+               return ErrLocked
+       }
+       return keyring.AddEncryptionKey(policy.key, policy.Descriptor(),
+               policy.Context.getKeyringOptions())
+}
+
+// Deprovision removes the Policy key from the kernel keyring. This prevents
+// reading and writing to the directory --- unless the target keyring is a user
+// keyring, in which case caches must be dropped too. If the Policy key was
+// already removed, returns keyring.ErrKeyNotPresent.
+func (policy *Policy) Deprovision(allUsers bool) error {
+       return keyring.RemoveEncryptionKey(policy.Descriptor(),
+               policy.Context.getKeyringOptions(), allUsers)
+}
+
+// NeedsUserKeyring returns true if Provision and Deprovision for this policy
+// will use a user keyring (deprecated), not a filesystem keyring.
+func (policy *Policy) NeedsUserKeyring() bool {
+       return policy.Version() == 1 && !policy.Context.Config.GetUseFsKeyringForV1Policies()
+}
+
+// NeedsRootToProvision returns true if Provision and Deprovision will require
+// root for this policy in the current configuration.
+func (policy *Policy) NeedsRootToProvision() bool {
+       return policy.Version() == 1 && policy.Context.Config.GetUseFsKeyringForV1Policies()
+}
+
+// CanBeAppliedWithoutProvisioning returns true if this process can apply this
+// policy to a directory without first calling Provision.
+func (policy *Policy) CanBeAppliedWithoutProvisioning() bool {
+       return policy.Version() == 1 || util.IsUserRoot()
+}
+
+// commitData writes the Policy's current data to the filesystem.
+func (policy *Policy) commitData() error {
+       return policy.Context.Mount.AddPolicy(policy.data, policy.ownerIfCreating)
+}
+
+// findWrappedPolicyKey returns the index of the wrapped policy key
+// corresponding to this policy and protector. The returned bool is false if no
+// wrapped policy key corresponds to the specified protector, true otherwise.
+func (policy *Policy) findWrappedKeyIndex(protectorDescriptor string) (int, bool) {
+       for idx, wrappedPolicyKey := range policy.data.WrappedPolicyKeys {
+               if wrappedPolicyKey.ProtectorDescriptor == protectorDescriptor {
+                       return idx, true
+               }
+       }
+       return 0, false
+}
+
+// addKey adds the wrapped policy key to end of the wrapped key data.
+func (policy *Policy) addKey(toAdd *metadata.WrappedPolicyKey) {
+       policy.data.WrappedPolicyKeys = append(policy.data.WrappedPolicyKeys, toAdd)
+}
+
+// removeKey removes the wrapped policy key at the specified index. This
+// does not preserve the order of the wrapped policy key array. If no index is
+// specified the last key is removed.
+func (policy *Policy) removeKey(index int) *metadata.WrappedPolicyKey {
+       lastIdx := len(policy.data.WrappedPolicyKeys) - 1
+       toRemove := policy.data.WrappedPolicyKeys[index]
+
+       // See https://github.com/golang/go/wiki/SliceTricks
+       policy.data.WrappedPolicyKeys[index] = policy.data.WrappedPolicyKeys[lastIdx]
+       policy.data.WrappedPolicyKeys[lastIdx] = nil
+       policy.data.WrappedPolicyKeys = policy.data.WrappedPolicyKeys[:lastIdx]
+
+       return toRemove
+}
diff --git a/actions/policy_test.go b/actions/policy_test.go
new file mode 100644 (file)
index 0000000..8248862
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+ * policy_test.go - tests for creating and modifying policies
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package actions
+
+import (
+       "testing"
+
+       "github.com/pkg/errors"
+)
+
+// Makes a protector and policy
+func makeBoth() (*Protector, *Policy, error) {
+       protector, err := CreateProtector(testContext, testProtectorName, goodCallback, nil)
+       if err != nil {
+               return nil, nil, err
+       }
+       policy, err := CreatePolicy(testContext, protector)
+       if err != nil {
+               cleanupProtector(protector)
+               return nil, nil, err
+       }
+       return protector, policy, nil
+}
+
+func cleanupProtector(protector *Protector) {
+       protector.Lock()
+       protector.Destroy()
+}
+
+func cleanupPolicy(policy *Policy) {
+       policy.Lock()
+       policy.Destroy()
+}
+
+// Tests that we can make a policy/protector pair
+func TestCreatePolicy(t *testing.T) {
+       pro, pol, err := makeBoth()
+       if err != nil {
+               t.Error(err)
+       }
+       cleanupPolicy(pol)
+       cleanupProtector(pro)
+}
+
+// Tests that we can add another protector to the policy
+func TestPolicyGoodAddProtector(t *testing.T) {
+       pro1, pol, err := makeBoth()
+       defer cleanupProtector(pro1)
+       defer cleanupPolicy(pol)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback, nil)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer cleanupProtector(pro2)
+
+       err = pol.AddProtector(pro2)
+       if err != nil {
+               t.Error(err)
+       }
+}
+
+// Tests that we cannot add a protector to a policy twice
+func TestPolicyBadAddProtector(t *testing.T) {
+       pro, pol, err := makeBoth()
+       defer cleanupProtector(pro)
+       defer cleanupPolicy(pol)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if pol.AddProtector(pro) == nil {
+               t.Error("we should not be able to add the same protector twice")
+       }
+}
+
+// Tests that we can remove a protector we added
+func TestPolicyGoodRemoveProtector(t *testing.T) {
+       pro1, pol, err := makeBoth()
+       defer cleanupProtector(pro1)
+       defer cleanupPolicy(pol)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback, nil)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer cleanupProtector(pro2)
+
+       err = pol.AddProtector(pro2)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       err = pol.RemoveProtector(pro1.Descriptor())
+       if err != nil {
+               t.Error(err)
+       }
+}
+
+// Tests various bad ways to remove protectors
+func TestPolicyBadRemoveProtector(t *testing.T) {
+       pro1, pol, err := makeBoth()
+       defer cleanupProtector(pro1)
+       defer cleanupPolicy(pol)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       pro2, err := CreateProtector(testContext, testProtectorName2, goodCallback, nil)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer cleanupProtector(pro2)
+
+       if pol.RemoveProtector(pro2.Descriptor()) == nil {
+               t.Error("we should not be able to remove a protector we did not add")
+       }
+
+       if pol.RemoveProtector(pro1.Descriptor()) == nil {
+               t.Error("we should not be able to remove all the protectors from a policy")
+       }
+}
+
+// Tests that policy can be unlocked with a callback.
+func TestPolicyUnlockWithCallback(t *testing.T) {
+       // Our optionFunc just selects the first protector
+       optionFn := func(policyDescriptor string, options []*ProtectorOption) (int, error) {
+               return 0, nil
+       }
+
+       pro1, pol, err := makeBoth()
+       defer cleanupProtector(pro1)
+       defer cleanupPolicy(pol)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err := pol.Lock(); err != nil {
+               t.Fatal(err)
+       }
+       if err := pol.Unlock(optionFn, goodCallback); err != nil {
+               t.Error(err)
+       }
+       if err := pol.Lock(); err != nil {
+               t.Error(err)
+       }
+}
+
+// Tests that policy can be unlock with an unlocked protector.
+func TestPolicyUnlockWithProtector(t *testing.T) {
+       pro1, pol, err := makeBoth()
+       defer cleanupProtector(pro1)
+       defer cleanupPolicy(pol)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err := pol.Lock(); err != nil {
+               t.Fatal(err)
+       }
+       if err := pol.UnlockWithProtector(pro1); err != nil {
+               t.Error(err)
+       }
+       if err := pol.Lock(); err != nil {
+               t.Error(err)
+       }
+}
+
+// Tests that locked protectors cannot unlock a policy.
+func TestPolicyUnlockWithLockedProtector(t *testing.T) {
+       pro1, pol, err := makeBoth()
+       defer cleanupProtector(pro1)
+       defer cleanupPolicy(pol)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err := pol.Lock(); err != nil {
+               t.Fatal(err)
+       }
+       if err := pro1.Lock(); err != nil {
+               t.Fatal(err)
+       }
+
+       if err := pol.UnlockWithProtector(pro1); errors.Cause(err) != ErrLocked {
+               t.Errorf("Expected a cause of %v got %v", ErrLocked, err)
+               if err == nil {
+                       pol.Lock()
+               }
+       }
+}
diff --git a/actions/protector.go b/actions/protector.go
new file mode 100644 (file)
index 0000000..b986eb0
--- /dev/null
@@ -0,0 +1,300 @@
+/*
+ * protector.go - functions for dealing with protectors
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package actions
+
+import (
+       "fmt"
+       "log"
+       "os/user"
+
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+// LoginProtectorMountpoint is the mountpoint where login protectors are stored.
+// This can be overridden by the user of this package.
+var LoginProtectorMountpoint = "/"
+
+// ErrLoginProtectorExists indicates that a user already has a login protector.
+type ErrLoginProtectorExists struct {
+       User *user.User
+}
+
+func (err *ErrLoginProtectorExists) Error() string {
+       return fmt.Sprintf("user %q already has a login protector", err.User.Username)
+}
+
+// ErrLoginProtectorName indicates that a name was given for a login protector.
+type ErrLoginProtectorName struct {
+       Name string
+       User *user.User
+}
+
+func (err *ErrLoginProtectorName) Error() string {
+       return fmt.Sprintf(`cannot assign name %q to new login protector for
+       user %q because login protectors are identified by user, not by name.`,
+               err.Name, err.User.Username)
+}
+
+// ErrMissingProtectorName indicates that a protector name is needed.
+type ErrMissingProtectorName struct {
+       Source metadata.SourceType
+}
+
+func (err *ErrMissingProtectorName) Error() string {
+       return fmt.Sprintf("%s protectors must be named", err.Source)
+}
+
+// ErrProtectorNameExists indicates that a protector name already exists.
+type ErrProtectorNameExists struct {
+       Name string
+}
+
+func (err *ErrProtectorNameExists) Error() string {
+       return fmt.Sprintf("there is already a protector named %q", err.Name)
+}
+
+// checkForProtectorWithName returns an error if there is already a protector
+// on the filesystem with a specific name (or if we cannot read the necessary
+// data).
+func checkForProtectorWithName(ctx *Context, name string) error {
+       options, err := ctx.ProtectorOptions()
+       if err != nil {
+               return err
+       }
+       for _, option := range options {
+               if option.Name() == name {
+                       return &ErrProtectorNameExists{name}
+               }
+       }
+       return nil
+}
+
+// checkIfUserHasLoginProtector returns an error if there is already a login
+// protector on the filesystem for a specific user (or if we cannot read the
+// necessary data).
+func checkIfUserHasLoginProtector(ctx *Context, uid int64) error {
+       options, err := ctx.ProtectorOptions()
+       if err != nil {
+               return err
+       }
+       for _, option := range options {
+               if option.Source() == metadata.SourceType_pam_passphrase && option.UID() == uid {
+                       return &ErrLoginProtectorExists{ctx.TargetUser}
+               }
+       }
+       return nil
+}
+
+// Protector represents an unlocked protector, so it contains the ProtectorData
+// as well as the actual protector key. These unlocked Protectors are necessary
+// to unlock policies and create new polices. As with the key struct, a
+// Protector should be wiped after use.
+type Protector struct {
+       Context         *Context
+       data            *metadata.ProtectorData
+       key             *crypto.Key
+       created         bool
+       ownerIfCreating *user.User
+}
+
+// CreateProtector creates an unlocked protector with a given name (name only
+// needed for custom and raw protector types). The keyFn provided to create the
+// Protector key will only be called once. If an error is returned, no data has
+// been changed on the filesystem.
+func CreateProtector(ctx *Context, name string, keyFn KeyFunc, owner *user.User) (*Protector, error) {
+       if err := ctx.checkContext(); err != nil {
+               return nil, err
+       }
+       // Sanity checks for names
+       if ctx.Config.Source == metadata.SourceType_pam_passphrase {
+               // login protectors don't need a name (we use the username instead)
+               if name != "" {
+                       return nil, &ErrLoginProtectorName{name, ctx.TargetUser}
+               }
+       } else {
+               // non-login protectors need a name (so we can distinguish between them)
+               if name == "" {
+                       return nil, &ErrMissingProtectorName{ctx.Config.Source}
+               }
+               // we don't want to duplicate naming
+               if err := checkForProtectorWithName(ctx, name); err != nil {
+                       return nil, err
+               }
+       }
+
+       var err error
+       protector := &Protector{
+               Context: ctx,
+               data: &metadata.ProtectorData{
+                       Name:   name,
+                       Source: ctx.Config.Source,
+               },
+               created:         true,
+               ownerIfCreating: owner,
+       }
+
+       // Extra data is needed for some SourceTypes
+       switch protector.data.Source {
+       case metadata.SourceType_pam_passphrase:
+               // As the pam passphrases are user specific, we also store the
+               // UID for this kind of source.
+               protector.data.Uid = int64(util.AtoiOrPanic(ctx.TargetUser.Uid))
+               // Make sure we aren't duplicating protectors
+               if err = checkIfUserHasLoginProtector(ctx, protector.data.Uid); err != nil {
+                       return nil, err
+               }
+               fallthrough
+       case metadata.SourceType_custom_passphrase:
+               // Our passphrase sources need costs and a random salt.
+               if protector.data.Salt, err = crypto.NewRandomBuffer(metadata.SaltLen); err != nil {
+                       return nil, err
+               }
+
+               protector.data.Costs = ctx.Config.HashCosts
+       }
+
+       // Randomly create the underlying protector key (and wipe if we fail)
+       if protector.key, err = crypto.NewRandomKey(metadata.InternalKeyLen); err != nil {
+               return nil, err
+       }
+       protector.data.ProtectorDescriptor, err = crypto.ComputeKeyDescriptor(protector.key, 1)
+       if err != nil {
+               protector.Lock()
+               return nil, err
+       }
+
+       if err = protector.Rewrap(keyFn); err != nil {
+               protector.Lock()
+               return nil, err
+       }
+
+       return protector, nil
+}
+
+// GetProtector retrieves a Protector with a specific descriptor. The Protector
+// is still locked in this case, so it must be unlocked before using certain
+// methods.
+func GetProtector(ctx *Context, descriptor string) (*Protector, error) {
+       log.Printf("Getting protector %s", descriptor)
+       err := ctx.checkContext()
+       if err != nil {
+               return nil, err
+       }
+
+       protector := &Protector{Context: ctx}
+       protector.data, err = ctx.Mount.GetRegularProtector(descriptor, ctx.TrustedUser)
+       return protector, err
+}
+
+// GetProtectorFromOption retrieves a protector based on a protector option.
+// If the option had a load error, this function returns that error. The
+// Protector is still locked in this case, so it must be unlocked before using
+// certain methods.
+func GetProtectorFromOption(ctx *Context, option *ProtectorOption) (*Protector, error) {
+       log.Printf("Getting protector %s from option", option.Descriptor())
+       if err := ctx.checkContext(); err != nil {
+               return nil, err
+       }
+       if option.LoadError != nil {
+               return nil, option.LoadError
+       }
+
+       // Replace the context if this is a linked protector
+       if option.LinkedMount != nil {
+               ctx = &Context{ctx.Config, option.LinkedMount, ctx.TargetUser, ctx.TrustedUser}
+       }
+       return &Protector{Context: ctx, data: option.data}, nil
+}
+
+// Descriptor returns the protector descriptor.
+func (protector *Protector) Descriptor() string {
+       return protector.data.ProtectorDescriptor
+}
+
+// Destroy removes a protector from the filesystem. The internal key should
+// still be wiped with Lock().
+func (protector *Protector) Destroy() error {
+       return protector.Context.Mount.RemoveProtector(protector.Descriptor())
+}
+
+// Revert destroys a protector if it was created, but does nothing if it was
+// just queried from the filesystem.
+func (protector *Protector) Revert() error {
+       if !protector.created {
+               return nil
+       }
+       return protector.Destroy()
+}
+
+func (protector *Protector) String() string {
+       return fmt.Sprintf("Protector: %s\nMountpoint: %s\nSource: %s\nName: %s\nCosts: %v\nUID: %d",
+               protector.Descriptor(), protector.Context.Mount, protector.data.Source,
+               protector.data.Name, protector.data.Costs, protector.data.Uid)
+}
+
+// Unlock unwraps the Protector's internal key. The keyFn provided to unwrap the
+// Protector key will be retried as necessary to get the correct key. Lock()
+// should be called after use. Does nothing if protector is already unlocked.
+func (protector *Protector) Unlock(keyFn KeyFunc) (err error) {
+       if protector.key != nil {
+               return
+       }
+       protector.key, err = unwrapProtectorKey(ProtectorInfo{protector.data}, keyFn)
+       return
+}
+
+// Lock wipes a Protector's internal Key. It should always be called after using
+// an unlocked Protector. This is often done with a defer statement. There is
+// no effect if called multiple times.
+func (protector *Protector) Lock() error {
+       err := protector.key.Wipe()
+       protector.key = nil
+       return err
+}
+
+// Rewrap updates the data that is wrapping the Protector Key. This is useful if
+// a user's password has changed, for example. The keyFn provided to rewrap
+// the Protector key will only be called once. Requires unlocked Protector.
+func (protector *Protector) Rewrap(keyFn KeyFunc) error {
+       if protector.key == nil {
+               return ErrLocked
+       }
+       wrappingKey, err := getWrappingKey(ProtectorInfo{protector.data}, keyFn, false)
+       if err != nil {
+               return err
+       }
+
+       // Revert change to wrapped key on failure
+       oldWrappedKey := protector.data.WrappedKey
+       defer func() {
+               wrappingKey.Wipe()
+               if err != nil {
+                       protector.data.WrappedKey = oldWrappedKey
+               }
+       }()
+
+       if protector.data.WrappedKey, err = crypto.Wrap(wrappingKey, protector.key); err != nil {
+               return err
+       }
+
+       return protector.Context.Mount.AddProtector(protector.data, protector.ownerIfCreating)
+}
diff --git a/actions/protector_test.go b/actions/protector_test.go
new file mode 100644 (file)
index 0000000..f20dbcf
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * protector_test.go - tests for creating protectors
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package actions
+
+import (
+       "bytes"
+       "testing"
+
+       "github.com/pkg/errors"
+
+       "github.com/google/fscrypt/crypto"
+)
+
+const testProtectorName = "my favorite protector"
+const testProtectorName2 = testProtectorName + "2"
+
+var errCallback = errors.New("bad callback")
+
+func goodCallback(info ProtectorInfo, retry bool) (*crypto.Key, error) {
+       return crypto.NewFixedLengthKeyFromReader(bytes.NewReader(timingPassphrase), len(timingPassphrase))
+}
+
+func badCallback(info ProtectorInfo, retry bool) (*crypto.Key, error) {
+       return nil, errCallback
+}
+
+// Tests that we can create a valid protector.
+func TestCreateProtector(t *testing.T) {
+       p, err := CreateProtector(testContext, testProtectorName, goodCallback, nil)
+       if err != nil {
+               t.Error(err)
+       } else {
+               p.Lock()
+               p.Destroy()
+       }
+}
+
+// Tests that a failure in the callback is relayed back to the caller.
+func TestBadCallback(t *testing.T) {
+       p, err := CreateProtector(testContext, testProtectorName, badCallback, nil)
+       if err == nil {
+               p.Lock()
+               p.Destroy()
+       }
+       if err != errCallback {
+               t.Error("callback error was not relayed back to caller")
+       }
+}
diff --git a/actions/recovery.go b/actions/recovery.go
new file mode 100644 (file)
index 0000000..2bb8a23
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * recovery.go - support for generating recovery passphrases
+ *
+ * Copyright 2019 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.
+ */
+
+package actions
+
+import (
+       "fmt"
+       "os"
+       "strconv"
+
+       "google.golang.org/protobuf/proto"
+
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+// modifiedContextWithSource returns a copy of ctx with the protector source
+// replaced by source.
+func modifiedContextWithSource(ctx *Context, source metadata.SourceType) *Context {
+       modifiedConfig := proto.Clone(ctx.Config).(*metadata.Config)
+       modifiedConfig.Source = source
+       modifiedCtx := *ctx
+       modifiedCtx.Config = modifiedConfig
+       return &modifiedCtx
+}
+
+// AddRecoveryPassphrase randomly generates a recovery passphrase and adds it as
+// a custom_passphrase protector for the given Policy.
+func AddRecoveryPassphrase(policy *Policy, dirname string) (*crypto.Key, *Protector, error) {
+       // 20 random characters in a-z is 94 bits of entropy, which is way more
+       // than enough for a passphrase which still goes through the usual
+       // passphrase hashing which makes it extremely costly to brute force.
+       passphrase, err := crypto.NewRandomPassphrase(20)
+       if err != nil {
+               return nil, nil, err
+       }
+       defer func() {
+               if err != nil {
+                       passphrase.Wipe()
+               }
+       }()
+       getPassphraseFn := func(info ProtectorInfo, retry bool) (*crypto.Key, error) {
+               // CreateProtector() wipes the passphrase, but in this case we
+               // still need it for later, so make a copy.
+               return passphrase.Clone()
+       }
+       var recoveryProtector *Protector
+       customCtx := modifiedContextWithSource(policy.Context, metadata.SourceType_custom_passphrase)
+       seq := 1
+       for {
+               // Automatically generate a name for the recovery protector.
+               name := "Recovery passphrase for " + dirname
+               if seq != 1 {
+                       name += " (" + strconv.Itoa(seq) + ")"
+               }
+               recoveryProtector, err = CreateProtector(customCtx, name, getPassphraseFn, policy.ownerIfCreating)
+               if err == nil {
+                       break
+               }
+               if _, ok := err.(*ErrProtectorNameExists); !ok {
+                       return nil, nil, err
+               }
+               seq++
+       }
+       if err := policy.AddProtector(recoveryProtector); err != nil {
+               recoveryProtector.Revert()
+               return nil, nil, err
+       }
+       return passphrase, recoveryProtector, nil
+}
+
+// WriteRecoveryInstructions writes a recovery passphrase and instructions to a
+// file.  This file should initially be located in the encrypted directory
+// protected by the passphrase itself.  It's up to the user to store the
+// passphrase in a different location if they actually need it.
+func WriteRecoveryInstructions(recoveryPassphrase *crypto.Key, recoveryProtector *Protector,
+       policy *Policy, path string) error {
+       file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
+       if err != nil {
+               return err
+       }
+       defer file.Close()
+       str := fmt.Sprintf(
+               `fscrypt automatically generated a recovery passphrase for this directory:
+
+    %s
+
+It did this because you chose to protect this directory with your login
+passphrase, but this directory is not on the root filesystem.
+
+Copy this passphrase to a safe place if you want to still be able to unlock this
+directory if you re-install the operating system or connect this storage media
+to a different system (which would result in your login protector being lost).
+
+To unlock this directory using this recovery passphrase, run 'fscrypt unlock'
+and select the protector named %q.
+
+If you want to disable recovery passphrase generation (not recommended),
+re-create this directory and pass the --no-recovery option to 'fscrypt encrypt'.
+Alternatively, you can remove this recovery passphrase protector using:
+
+    fscrypt metadata remove-protector-from-policy --force --protector=%s:%s --policy=%s:%s
+
+It is safe to keep it around though, as the recovery passphrase is high-entropy.
+`, recoveryPassphrase.Data(), recoveryProtector.data.Name,
+               recoveryProtector.Context.Mount.Path, recoveryProtector.data.ProtectorDescriptor,
+               policy.Context.Mount.Path, policy.data.KeyDescriptor)
+       if _, err = file.WriteString(str); err != nil {
+               return err
+       }
+       if recoveryProtector.ownerIfCreating != nil {
+               if err = util.Chown(file, recoveryProtector.ownerIfCreating); err != nil {
+                       return err
+               }
+       }
+       return file.Sync()
+}
diff --git a/actions/recovery_test.go b/actions/recovery_test.go
new file mode 100644 (file)
index 0000000..35ade0e
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * recovery_test.go - tests for recovery passphrases
+ *
+ * Copyright 2019 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.
+ */
+
+package actions
+
+import (
+       "os"
+       "path/filepath"
+       "strings"
+       "testing"
+
+       "github.com/google/fscrypt/crypto"
+)
+
+func TestRecoveryPassphrase(t *testing.T) {
+       tempDir, err := os.MkdirTemp("", "fscrypt")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(tempDir)
+       recoveryFile := filepath.Join(tempDir, "recovery.txt")
+
+       firstProtector, policy, err := makeBoth()
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer cleanupPolicy(policy)
+       defer cleanupProtector(firstProtector)
+
+       // Add a recovery passphrase and verify that it worked correctly.
+       passphrase, recoveryProtector, err := AddRecoveryPassphrase(policy, "foo")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer cleanupProtector(recoveryProtector)
+       if passphrase.Len() != 20 {
+               t.Error("Recovery passphrase has wrong length")
+       }
+       if recoveryProtector.data.Name != "Recovery passphrase for foo" {
+               t.Error("Recovery passphrase protector has wrong name")
+       }
+       if len(policy.ProtectorDescriptors()) != 2 {
+               t.Error("There should be 2 protectors now")
+       }
+       getPassphraseFn := func(info ProtectorInfo, retry bool) (*crypto.Key, error) {
+               return passphrase.Clone()
+       }
+       recoveryProtector.Lock()
+       if err = recoveryProtector.Unlock(getPassphraseFn); err != nil {
+               t.Fatal(err)
+       }
+
+       // Test writing the recovery instructions.
+       if err = WriteRecoveryInstructions(passphrase, recoveryProtector, policy,
+               recoveryFile); err != nil {
+               t.Fatal(err)
+       }
+       contentsBytes, err := os.ReadFile(recoveryFile)
+       if err != nil {
+               t.Fatal(err)
+       }
+       contents := string(contentsBytes)
+       if !strings.Contains(contents, string(passphrase.Data())) {
+               t.Error("Recovery instructions don't actually contain the passphrase!")
+       }
+
+       // Test for protector naming collision.
+       if passphrase, recoveryProtector, err = AddRecoveryPassphrase(policy, "foo"); err != nil {
+               t.Fatal(err)
+       }
+       defer cleanupProtector(recoveryProtector)
+       if recoveryProtector.data.Name != "Recovery passphrase for foo (2)" {
+               t.Error("Recovery passphrase protector has wrong name (after naming collision)")
+       }
+}
diff --git a/bin/files-changed b/bin/files-changed
new file mode 100755 (executable)
index 0000000..3ffbad6
--- /dev/null
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+# Detect if any files have changed in the git repository. Output an appropriate
+# error message if they have changed.
+
+if [[ -n $(git status -s) ]]; then
+  git diff --minimal HEAD
+  echo
+  echo "**************************************************"
+  case "$1" in
+  "proto")
+    echo "* .pb.go files and .proto files are out of sync. *"
+    echo "*        Run \"make gen\" to generate them.        *"
+    ;;
+  "format")
+    echo "*    C or Go files have incorrect formatting.    *"
+    echo "*         Run \"make format\" to fix them.         *"
+    ;;
+  *)
+    echo "*     Files have changed in this repository.     *"
+    ;;
+  esac
+  echo "**************************************************"
+  git reset HEAD --hard
+  exit 1
+fi
\ No newline at end of file
diff --git a/cli-tests/README.md b/cli-tests/README.md
new file mode 100644 (file)
index 0000000..dfcc1d0
--- /dev/null
@@ -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 (file)
index 0000000..1d7b17b
--- /dev/null
@@ -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 (executable)
index 0000000..9ab5b78
--- /dev/null
@@ -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 (file)
index 0000000..747ed89
--- /dev/null
@@ -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\r
+Enter old custom passphrase for protector "prot": \r
+Enter new custom passphrase for protector "prot": \r
+Confirm passphrase: \r
+[ERROR] fscrypt metadata change-passphrase: entered passphrases do not match\r
+
+# Change passphrase (interactively)
+spawn fscrypt metadata change-passphrase --protector=MNT:desc1\r
+Enter old custom passphrase for protector "prot": \r
+Enter new custom passphrase for protector "prot": \r
+Confirm passphrase: \r
+Passphrase for protector desc1 successfully changed.\r
+
+# 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 (executable)
index 0000000..1360bc2
--- /dev/null
@@ -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 (file)
index 0000000..4de05e4
--- /dev/null
@@ -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 (executable)
index 0000000..ffd6165
--- /dev/null
@@ -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 (file)
index 0000000..2f1c03c
--- /dev/null
@@ -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\r
+The following protector sources are available:\r
+1 - Your login passphrase (pam_passphrase)\r
+2 - A custom passphrase (custom_passphrase)\r
+3 - A raw 256-bit key (raw_key)\r
+Enter the source number for the new protector [2 - custom_passphrase]: 2\r
+Enter a name for the new protector: prot\r
+Enter custom passphrase for protector "prot": \r
+Confirm passphrase: \r
+"MNT/dir" is now encrypted, unlocked, and ready for use.\r
+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 (executable)
index 0000000..48cbe25
--- /dev/null
@@ -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 (file)
index 0000000..b1f6c82
--- /dev/null
@@ -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\r
+The following protector sources are available:\r
+1 - Your login passphrase (pam_passphrase)\r
+2 - A custom passphrase (custom_passphrase)\r
+3 - A raw 256-bit key (raw_key)\r
+Enter the source number for the new protector [2 - custom_passphrase]: 1\r
+\r
+IMPORTANT: Before continuing, ensure you have properly set up your system for\r
+           login protectors.  See\r
+           https://github.com/google/fscrypt#setting-up-for-login-protectors\r
+\r
+Enter login passphrase for fscrypt-test-user: \r
+\r
+IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for\r
+           important recovery instructions. It is *strongly recommended* to\r
+           record the recovery passphrase in a secure location; otherwise you\r
+           will lose access to this directory if you reinstall the operating\r
+           system or move this filesystem to another system.\r
+\r
+"MNT/dir" is now encrypted, unlocked, and ready for use.\r
+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 (executable)
index 0000000..b6ae2d8
--- /dev/null
@@ -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 (file)
index 0000000..78aa0b7
--- /dev/null
@@ -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 (executable)
index 0000000..e5c6d20
--- /dev/null
@@ -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 (file)
index 0000000..ce27713
--- /dev/null
@@ -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 (executable)
index 0000000..e5df4df
--- /dev/null
@@ -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 (file)
index 0000000..bbcc0f2
--- /dev/null
@@ -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 (executable)
index 0000000..e688eda
--- /dev/null
@@ -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 (file)
index 0000000..07c9aa3
--- /dev/null
@@ -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 (executable)
index 0000000..fae1094
--- /dev/null
@@ -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 (file)
index 0000000..68e0897
--- /dev/null
@@ -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 (executable)
index 0000000..8b52392
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/cli-tests/t_passphrase_hashing.sh b/cli-tests/t_passphrase_hashing.sh
new file mode 100755 (executable)
index 0000000..a67dd7c
--- /dev/null
@@ -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 (file)
index 0000000..ab0052c
--- /dev/null
@@ -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 (executable)
index 0000000..f7e302d
--- /dev/null
@@ -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 (file)
index 0000000..d038d52
--- /dev/null
@@ -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 (executable)
index 0000000..c569f20
--- /dev/null
@@ -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 (file)
index 0000000..058c62c
--- /dev/null
@@ -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 (executable)
index 0000000..cfc3616
--- /dev/null
@@ -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 (file)
index 0000000..b3c9b2a
--- /dev/null
@@ -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 (executable)
index 0000000..e32b0f7
--- /dev/null
@@ -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 (file)
index 0000000..f14f357
--- /dev/null
@@ -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 (executable)
index 0000000..46ccdaf
--- /dev/null
@@ -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 (file)
index 0000000..9f0f0ab
--- /dev/null
@@ -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 (executable)
index 0000000..a8fd333
--- /dev/null
@@ -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'"
diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go
new file mode 100644 (file)
index 0000000..30aa3a7
--- /dev/null
@@ -0,0 +1,1151 @@
+/*
+ * commands.go - Implementations of all of the fscrypt commands and subcommands.
+ * This mostly just calls into the fscrypt/actions package.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import (
+       "fmt"
+       "log"
+       "os"
+       "path/filepath"
+       "strings"
+
+       "github.com/pkg/errors"
+       "github.com/urfave/cli"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/keyring"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/security"
+       "github.com/google/fscrypt/util"
+)
+
+// Setup is a command which can do global or per-filesystem initialization.
+var Setup = cli.Command{
+       Name:      "setup",
+       ArgsUsage: fmt.Sprintf("[%s]", mountpointArg),
+       Usage:     "perform global setup or filesystem setup",
+       Description: fmt.Sprintf(`This command creates fscrypt's global config
+               file and/or prepares a filesystem for use with fscrypt.
+
+               (1) When used without %[1]s, this command creates the global
+               config file %[2]s and the fscrypt metadata directory for the
+               root filesystem (i.e. /.fscrypt). This requires root privileges.
+               The passphrase hashing parameters in %[2]s are automatically set
+               to an appropriate hardness, as determined by %[3]s. The root
+               filesystem's metadata directory is created even if the root
+               filesystem doesn't support encryption itself, since it's where
+               login passphrase protectors are stored.
+
+               (2) When used with %[1]s, this command creates the fscrypt
+               metadata directory for the filesystem mounted at %[1]s. This
+               allows fscrypt to be used on that filesystem, provided that any
+               kernel and filesystem-specific prerequisites are also met (see
+               the README). This may require root privileges.`,
+               mountpointArg, actions.ConfigFileLocation,
+               shortDisplay(timeTargetFlag)),
+       Flags:  []cli.Flag{timeTargetFlag, forceFlag, allUsersSetupFlag},
+       Action: setupAction,
+}
+
+func setupAction(c *cli.Context) error {
+       switch c.NArg() {
+       case 0:
+               // Case (1) - global setup
+               if err := createGlobalConfig(c.App.Writer, actions.ConfigFileLocation); err != nil {
+                       return newExitError(c, err)
+               }
+               if err := setupFilesystem(c.App.Writer, actions.LoginProtectorMountpoint); err != nil {
+                       if _, ok := err.(*filesystem.ErrAlreadySetup); !ok {
+                               return newExitError(c, err)
+                       }
+                       fmt.Fprintf(c.App.Writer,
+                               "Skipping creating %s because it already exists.\n",
+                               filepath.Join(actions.LoginProtectorMountpoint, ".fscrypt"))
+               }
+       case 1:
+               // Case (2) - filesystem setup
+               if err := setupFilesystem(c.App.Writer, c.Args().Get(0)); err != nil {
+                       return newExitError(c, err)
+               }
+       default:
+               return expectedArgsErr(c, 1, true)
+       }
+       return nil
+}
+
+// Encrypt performs the functions of setupDirectory and Unlock in one command.
+var Encrypt = cli.Command{
+       Name:      "encrypt",
+       ArgsUsage: directoryArg,
+       Usage:     "enable filesystem encryption for a directory",
+       Description: fmt.Sprintf(`This command enables filesystem encryption on
+               %[1]s. This may involve creating a new policy (if one is not
+               specified with %[2]s) or a new protector (if one is not
+               specified with %[3]s). This command requires that the
+               corresponding filesystem has been setup with "fscrypt setup
+               %[4]s". By default, after %[1]s is setup, it is unlocked and can
+               immediately be used.`, directoryArg, shortDisplay(policyFlag),
+               shortDisplay(protectorFlag), mountpointArg),
+       Flags: []cli.Flag{policyFlag, unlockWithFlag, protectorFlag, sourceFlag,
+               userFlag, nameFlag, keyFileFlag, skipUnlockFlag, noRecoveryFlag},
+       Action: encryptAction,
+}
+
+func encryptAction(c *cli.Context) error {
+       if c.NArg() != 1 {
+               return expectedArgsErr(c, 1, false)
+       }
+
+       path := c.Args().Get(0)
+       if err := encryptPath(path); err != nil {
+               return newExitError(c, err)
+       }
+
+       // Most people expect that other users can't see their encrypted files
+       // while they're unlocked, so change the directory's mode to 0700.
+       if err := os.Chmod(path, 0700); err != nil {
+               fmt.Fprintf(c.App.Writer, "Warning: unable to chmod %q to 0700 [%v]\n", path, err)
+               // Continue on; don't consider this a fatal error.
+       }
+
+       if !skipUnlockFlag.Value {
+               fmt.Fprintf(c.App.Writer,
+                       "%q is now encrypted, unlocked, and ready for use.\n", path)
+       } else {
+               fmt.Fprintf(c.App.Writer,
+                       "%q is now encrypted, but it is still locked.\n", path)
+               fmt.Fprintln(c.App.Writer, `It can be unlocked with "fscrypt unlock".`)
+       }
+       return nil
+}
+
+// validateKeyringPrereqs ensures we're ready to add, remove, or get the status
+// of the key for the given encryption policy (if policy != nil) or for the
+// current default encryption policy (if policy == nil).
+func validateKeyringPrereqs(ctx *actions.Context, policy *actions.Policy) error {
+       var policyVersion int64
+       if policy == nil {
+               policyVersion = ctx.Config.Options.PolicyVersion
+       } else {
+               policyVersion = policy.Version()
+       }
+       // If it's a v2 policy, we're good to go, since non-root users can
+       // add/remove v2 policy keys directly to/from the filesystem, where they
+       // are usable by the filesystem on behalf of any process.
+       if policyVersion != 1 {
+               return nil
+       }
+       if ctx.Config.GetUseFsKeyringForV1Policies() {
+               // We'll be using the filesystem keyring, but it's a v1
+               // encryption policy so root is required.
+               if !util.IsUserRoot() {
+                       return ErrFsKeyringPerm
+               }
+               return nil
+       }
+       // We'll be using the target user's user keyring, so make sure a user
+       // was explicitly specified if the command is being run as root, and
+       // make sure that user's keyring is accessible.
+       if userFlag.Value == "" && util.IsUserRoot() {
+               return ErrSpecifyUser
+       }
+       if _, err := keyring.UserKeyringID(ctx.TargetUser, true); err != nil {
+               return err
+       }
+       return nil
+}
+
+func writeRecoveryInstructions(recoveryPassphrase *crypto.Key, recoveryProtector *actions.Protector,
+       policy *actions.Policy, dirPath string) error {
+       if recoveryPassphrase == nil {
+               return nil
+       }
+       recoveryFile := filepath.Join(dirPath, "fscrypt_recovery_readme.txt")
+       if err := actions.WriteRecoveryInstructions(recoveryPassphrase, recoveryProtector,
+               policy, recoveryFile); err != nil {
+               return err
+       }
+       msg := fmt.Sprintf(`See %q 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.`, recoveryFile)
+       hdr := "IMPORTANT: "
+       fmt.Print("\n" + hdr + wrapText(msg, len(hdr)) + "\n\n")
+       return nil
+}
+
+// encryptPath sets up encryption on path and provisions the policy to the
+// keyring unless --skip-unlock is used. On failure, an error is returned, any
+// metadata creation is reverted, and the directory is unmodified.
+func encryptPath(path string) (err error) {
+       targetUser, err := parseUserFlag()
+       if err != nil {
+               return
+       }
+       ctx, err := actions.NewContextFromPath(path, targetUser)
+       if err != nil {
+               return
+       }
+       if err = checkEncryptable(ctx, path); err != nil {
+               return
+       }
+
+       var policy *actions.Policy
+       var recoveryPassphrase *crypto.Key
+       var recoveryProtector *actions.Protector
+       if policyFlag.Value != "" {
+               log.Printf("getting policy for %q", path)
+
+               if policy, err = getPolicyFromFlag(policyFlag.Value, ctx.TargetUser); err != nil {
+                       return
+               }
+               defer policy.Lock()
+
+               if !skipUnlockFlag.Value {
+                       if err = validateKeyringPrereqs(ctx, policy); err != nil {
+                               return
+                       }
+               }
+       } else {
+               log.Printf("creating policy for %q", path)
+
+               if !skipUnlockFlag.Value {
+                       if err = validateKeyringPrereqs(ctx, nil); err != nil {
+                               return
+                       }
+               }
+
+               protector, created, protErr := selectOrCreateProtector(ctx)
+               if protErr != nil {
+                       return protErr
+               }
+               defer func() {
+                       protector.Lock()
+                       // Successfully created protector should be reverted on failure.
+                       if err != nil && created {
+                               protector.Revert()
+                       }
+               }()
+
+               if err = protector.Unlock(existingKeyFn); err != nil {
+                       return
+               }
+               if policy, err = actions.CreatePolicy(ctx, protector); err != nil {
+                       return
+               }
+               defer func() {
+                       policy.Lock()
+                       // Successfully created policy should be reverted on failure.
+                       if err != nil {
+                               policy.Revert()
+                       }
+               }()
+
+               // Generate a recovery passphrase if needed.
+               if ctx.Mount != protector.Context.Mount && !noRecoveryFlag.Value {
+                       if recoveryPassphrase, recoveryProtector, err = actions.AddRecoveryPassphrase(
+                               policy, filepath.Base(path)); err != nil {
+                               return
+                       }
+                       defer func() {
+                               recoveryPassphrase.Wipe()
+                               recoveryProtector.Lock()
+                               // Successfully created protector should be reverted on failure.
+                               if err != nil {
+                                       recoveryProtector.Revert()
+                               }
+                       }()
+               }
+       }
+
+       // Unlock() and Provision() first, so if that if these fail the
+       // directory isn't changed, and also because v2 policies can't be
+       // applied while deprovisioned unless the process is running as root.
+       if !skipUnlockFlag.Value || !policy.CanBeAppliedWithoutProvisioning() {
+               if err = policy.Unlock(optionFn, existingKeyFn); err != nil {
+                       return
+               }
+               if err = policy.Provision(); err != nil {
+                       return
+               }
+               defer func() {
+                       if err != nil || skipUnlockFlag.Value {
+                               policy.Deprovision(false)
+                       }
+               }()
+       }
+       if err = policy.Apply(path); err != nil {
+               return
+       }
+       return writeRecoveryInstructions(recoveryPassphrase, recoveryProtector, policy, path)
+}
+
+// checkEncryptable returns an error if the path cannot be encrypted.
+func checkEncryptable(ctx *actions.Context, path string) error {
+
+       log.Printf("checking whether %q is already encrypted", path)
+       if _, err := metadata.GetPolicy(path); err == nil {
+               return &metadata.ErrAlreadyEncrypted{Path: path}
+       }
+
+       log.Printf("checking whether filesystem %s supports encryption", ctx.Mount.Path)
+       if err := ctx.Mount.CheckSupport(); err != nil {
+               return err
+       }
+
+       log.Printf("checking whether %q is an empty and readable directory", path)
+       f, err := os.Open(path)
+       if err != nil {
+               return err
+       }
+       defer f.Close()
+
+       switch names, err := f.Readdirnames(-1); {
+       case err != nil:
+               // Could not read directory (might not be a directory)
+               err = errors.Wrap(err, path)
+               log.Print(err)
+               return err
+       case len(names) > 0:
+               return &ErrDirNotEmpty{path}
+       }
+       return err
+}
+
+// selectOrCreateProtector uses user input (or flags) to either create a new
+// protector or select an existing one. The boolean return value is true if we
+// created a new protector.
+func selectOrCreateProtector(ctx *actions.Context) (*actions.Protector, bool, error) {
+       if protectorFlag.Value != "" {
+               protector, err := getProtectorFromFlag(protectorFlag.Value, ctx.TargetUser)
+               return protector, false, err
+       }
+
+       options, err := expandedProtectorOptions(ctx)
+       if err != nil {
+               return nil, false, err
+       }
+
+       // Having no existing options to choose from or using creation-only
+       // flags indicates we should make a new protector.
+       if len(options) == 0 || nameFlag.Value != "" || sourceFlag.Value != "" {
+               protector, err := createProtectorFromContext(ctx)
+               return protector, true, err
+       }
+
+       shouldCreate, err := askQuestion("Should we create a new protector?", false)
+       if err != nil {
+               return nil, false, err
+       }
+       if shouldCreate {
+               protector, err := createProtectorFromContext(ctx)
+               return protector, true, err
+       }
+
+       log.Print("finding an existing protector to use")
+       protector, err := selectExistingProtector(ctx, options)
+       return protector, false, err
+}
+
+// Unlock takes an encrypted directory and unlocks it for reading and writing.
+var Unlock = cli.Command{
+       Name:      "unlock",
+       ArgsUsage: directoryArg,
+       Usage:     "unlock an encrypted directory",
+       Description: fmt.Sprintf(`This command takes %s, a directory setup for
+               use with fscrypt, and unlocks the directory by passing the
+               appropriate key into the keyring. This requires unlocking one of
+               the protectors protecting this directory (either by selecting a
+               protector or specifying one with %s). This directory will be
+               locked again upon reboot, or after running "fscrypt lock" or
+               "fscrypt purge".`, directoryArg,
+               shortDisplay(unlockWithFlag)),
+       Flags:  []cli.Flag{unlockWithFlag, keyFileFlag, userFlag},
+       Action: unlockAction,
+}
+
+func unlockAction(c *cli.Context) error {
+       if c.NArg() != 1 {
+               return expectedArgsErr(c, 1, false)
+       }
+
+       targetUser, err := parseUserFlag()
+       if err != nil {
+               return newExitError(c, err)
+       }
+       path := c.Args().Get(0)
+       ctx, err := actions.NewContextFromPath(path, targetUser)
+       if err != nil {
+               return newExitError(c, err)
+       }
+
+       log.Printf("performing sanity checks")
+       // Ensure path is encrypted and filesystem is using fscrypt.
+       policy, err := actions.GetPolicyFromPath(ctx, path)
+       if err != nil {
+               return newExitError(c, err)
+       }
+       // Ensure the keyring is ready.
+       if err = validateKeyringPrereqs(ctx, policy); err != nil {
+               return newExitError(c, err)
+       }
+       // Check if directory is already unlocked
+       if policy.IsProvisionedByTargetUser() {
+               log.Printf("policy %s is already provisioned by %v",
+                       policy.Descriptor(), ctx.TargetUser.Username)
+               return newExitError(c, errors.Wrapf(ErrDirAlreadyUnlocked, path))
+       }
+
+       if err := policy.Unlock(optionFn, existingKeyFn); err != nil {
+               return newExitError(c, err)
+       }
+       defer policy.Lock()
+
+       if err := policy.Provision(); err != nil {
+               return newExitError(c, err)
+       }
+
+       fmt.Fprintf(c.App.Writer, "%q is now unlocked and ready for use.\n", path)
+       return nil
+}
+
+func dropCachesIfRequested(c *cli.Context, ctx *actions.Context) error {
+       if dropCachesFlag.Value {
+               if err := security.DropFilesystemCache(); err != nil {
+                       return err
+               }
+               fmt.Fprintf(c.App.Writer, "Encrypted data removed from filesystem cache.\n")
+       } else {
+               fmt.Fprintf(c.App.Writer, "Filesystem %q should now be unmounted.\n", ctx.Mount.Path)
+       }
+       return nil
+}
+
+// Lock takes an encrypted directory and locks it, undoing Unlock.
+var Lock = cli.Command{
+       Name:      "lock",
+       ArgsUsage: directoryArg,
+       Usage:     "lock an encrypted directory",
+       Description: fmt.Sprintf(`This command takes %s, an encrypted directory
+               which has been unlocked by fscrypt, and locks the directory by
+               removing the encryption key from the kernel. I.e., it undoes the
+               effect of 'fscrypt unlock'.
+
+               For this to be effective, all files in the directory must first
+               be closed.
+
+               If the directory uses a v1 encryption policy, then the %s=true
+               option may be needed to properly lock it. Root is required for
+               this.
+
+               If the directory uses a v2 encryption policy, then a non-root
+               user can lock it, but only if it's the same user who unlocked it
+               originally and if no other users have unlocked it too.
+
+               WARNING: even after the key has been removed, decrypted data may
+               still be present in freed memory, where it may still be
+               recoverable by an attacker who compromises system memory. To be
+               fully safe, you must reboot with a power cycle.`,
+               directoryArg, shortDisplay(dropCachesFlag)),
+       Flags:  []cli.Flag{dropCachesFlag, userFlag, allUsersLockFlag},
+       Action: lockAction,
+}
+
+func lockAction(c *cli.Context) error {
+       if c.NArg() != 1 {
+               return expectedArgsErr(c, 1, false)
+       }
+
+       targetUser, err := parseUserFlag()
+       if err != nil {
+               return newExitError(c, err)
+       }
+       path := c.Args().Get(0)
+       ctx, err := actions.NewContextFromPath(path, targetUser)
+       if err != nil {
+               return newExitError(c, err)
+       }
+
+       log.Printf("performing sanity checks")
+       // Ensure path is encrypted and filesystem is using fscrypt.
+       policy, err := actions.GetPolicyFromPath(ctx, path)
+       if err != nil {
+               return newExitError(c, err)
+       }
+       // Ensure the keyring is ready.
+       if err = validateKeyringPrereqs(ctx, policy); err != nil {
+               return newExitError(c, err)
+       }
+       // Check for permission to drop caches, if it may be needed.
+       if policy.NeedsUserKeyring() && dropCachesFlag.Value && !util.IsUserRoot() {
+               return newExitError(c, ErrDropCachesPerm)
+       }
+
+       if err = policy.Deprovision(allUsersLockFlag.Value); err != nil {
+               switch err {
+               case keyring.ErrKeyNotPresent:
+                       break
+               case keyring.ErrKeyAddedByOtherUsers:
+                       return newExitError(c, &ErrDirUnlockedByOtherUsers{path})
+               case keyring.ErrKeyFilesOpen:
+                       return newExitError(c, &ErrDirFilesOpen{path})
+               default:
+                       return newExitError(c, err)
+               }
+               // Key is no longer present.  Normally that means the directory
+               // is already locked; in that case we exit with an error.  But
+               // if the policy uses the user keyring (v1 policies only), then
+               // the directory might have been incompletely locked earlier,
+               // due to open files.  Try to detect that case and finish
+               // locking the directory by dropping caches again.
+               if !policy.NeedsUserKeyring() || !isDirUnlockedHeuristic(path) {
+                       log.Printf("policy %s is already fully deprovisioned", policy.Descriptor())
+                       return newExitError(c, errors.Wrapf(ErrDirAlreadyLocked, path))
+               }
+       }
+
+       if policy.NeedsUserKeyring() {
+               if err = dropCachesIfRequested(c, ctx); err != nil {
+                       return newExitError(c, err)
+               }
+               if isDirUnlockedHeuristic(path) {
+                       return newExitError(c, &ErrDirFilesOpen{path})
+               }
+       }
+
+       fmt.Fprintf(c.App.Writer, "%q is now locked.\n", path)
+       return nil
+}
+
+func isPossibleNoKeyName(filename string) bool {
+       // No-key names are at least 22 bytes long, since they are
+       // base64-encoded and ciphertext filenames are at least 16 bytes.
+       if len(filename) < 22 {
+               return false
+       }
+       // On the latest kernels, no-key names contain only base64url characters
+       // (A-Z, a-z, 0-9, -, and _).  On older kernels, the + and , characters
+       // were used too.  Allow all of these characters.
+       validChars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_+,"
+       for _, char := range filename {
+               if !strings.ContainsRune(validChars, char) {
+                       return false
+               }
+       }
+       return true
+}
+
+// isDirUnlockedHeuristic returns true if the directory is definitely still
+// unlocked.  This is the case if we can create a subdirectory or if the
+// directory contains filenames that aren't valid no-key names.  It returns
+// false if the directory is probably locked (though it could also be unlocked).
+//
+// This is only useful if the directory's policy uses the user keyring, since
+// otherwise the status can be easily found via the filesystem keyring.
+func isDirUnlockedHeuristic(dirPath string) bool {
+       subdirPath := filepath.Join(dirPath, "fscrypt-is-dir-unlocked")
+       if err := os.Mkdir(subdirPath, 0700); err == nil {
+               os.Remove(subdirPath)
+               return true
+       }
+       dir, err := os.Open(dirPath)
+       if err != nil {
+               return false
+       }
+       defer dir.Close()
+
+       names, err := dir.Readdirnames(-1)
+       if err != nil {
+               return false
+       }
+       for _, name := range names {
+               if !isPossibleNoKeyName(name) {
+                       return true
+               }
+       }
+       return false
+}
+
+// Purge removes all the policy keys from the keyring (also need unmount).
+var Purge = cli.Command{
+       Name:      "purge",
+       ArgsUsage: mountpointArg,
+       Usage:     "Remove a filesystem's keys",
+       Description: fmt.Sprintf(`This command removes a user's policy keys for
+               directories on %[1]s. This is intended to lock all files and
+               directories encrypted by the user on %[1]s, in that unlocking
+               them for reading will require providing a key again. However,
+               there are four important things to note about this command:
+
+               (1) When run with the default options, this command also clears
+               the reclaimable dentries and inodes, so that the encrypted files
+               and directories will no longer be visible. However, this
+               requires root privileges. Note that any open file descriptors to
+               plaintext data will not be affected by this command.
+
+               (2) When run with %[2]s=false, the keyring is cleared and root
+               permissions are not required, but recently accessed encrypted
+               directories and files will remain cached for some time. Because
+               of this, after purging a filesystem's keys in this manner, it
+               is recommended to unmount the filesystem.
+
+               (3) When run as root, this command removes the policy keys for
+               all users. However, this will only work if the PAM module has
+               been enabled. Otherwise, only root's keys may be removed.
+
+               (4) Even after unmounting the filesystem or clearing the
+               caches, the kernel may keep contents of files in memory. This
+               means direct memory access (either though physical compromise or
+               a kernel exploit) could compromise encrypted data. This weakness
+               can be eliminated by cycling the power or mitigated by using
+               page cache and slab cache poisoning.`, mountpointArg,
+               shortDisplay(dropCachesFlag)),
+       Flags:  []cli.Flag{forceFlag, dropCachesFlag, userFlag},
+       Action: purgeAction,
+}
+
+func purgeAction(c *cli.Context) error {
+       if c.NArg() != 1 {
+               return expectedArgsErr(c, 1, false)
+       }
+
+       if dropCachesFlag.Value {
+               if !util.IsUserRoot() {
+                       return newExitError(c, ErrDropCachesPerm)
+               }
+       }
+
+       targetUser, err := parseUserFlag()
+       if err != nil {
+               return newExitError(c, err)
+       }
+       mountpoint := c.Args().Get(0)
+       ctx, err := actions.NewContextFromMountpoint(mountpoint, targetUser)
+       if err != nil {
+               return newExitError(c, err)
+       }
+       if err = validateKeyringPrereqs(ctx, nil); err != nil {
+               return newExitError(c, err)
+       }
+
+       question := fmt.Sprintf("Purge all policy keys from %q", ctx.Mount.Path)
+       if dropCachesFlag.Value {
+               question += " and drop global inode cache"
+       }
+       warning := "Encrypted data on this filesystem will be inaccessible until unlocked again!!"
+       if err = askConfirmation(question+"?", false, warning); err != nil {
+               return newExitError(c, err)
+       }
+
+       if err = actions.PurgeAllPolicies(ctx); err != nil {
+               return newExitError(c, err)
+       }
+       fmt.Fprintf(c.App.Writer, "Policies purged for %q.\n", ctx.Mount.Path)
+
+       if err = dropCachesIfRequested(c, ctx); err != nil {
+               return newExitError(c, err)
+       }
+       return nil
+}
+
+// Status is a command with three subcommands relating to printing out status.
+var Status = cli.Command{
+       Name:      "status",
+       ArgsUsage: fmt.Sprintf("[%s]", pathArg),
+       Usage:     "print the global, filesystem, or file status",
+       Description: fmt.Sprintf(`This command prints out the global,
+               per-filesystem, or per-file status.
+
+               (1) When used without %[1]s, print all of the currently visible
+               filesystems which support use with fscrypt. For each of
+               the filesystems, this command also notes if they are actually
+               being used by fscrypt. This command will fail if no there is no
+               support for fscrypt anywhere on the system.
+
+               (2) When %[1]s is a filesystem mountpoint, list information
+               about all the policies and protectors which exist on %[1]s. This
+               command will fail if %[1]s is not being used with fscrypt. For
+               each policy, this command also notes if the policy is currently
+               unlocked.
+
+               (3) When %[1]s is just a normal path, print information about
+               the policy being used on %[1]s and the protectors protecting
+               this file or directory. This command will fail if %[1]s is not
+               setup for encryption with fscrypt.`, pathArg),
+       Action: statusAction,
+}
+
+func statusAction(c *cli.Context) error {
+       var err error
+
+       switch c.NArg() {
+       case 0:
+               // Case (1) - global status
+               err = writeGlobalStatus(c.App.Writer)
+       case 1:
+               path := c.Args().Get(0)
+
+               var ctx *actions.Context
+               ctx, err = actions.NewContextFromMountpoint(path, nil)
+               if err == nil {
+                       // Case (2) - mountpoint status
+                       err = writeFilesystemStatus(c.App.Writer, ctx)
+               } else if _, ok := err.(*filesystem.ErrNotAMountpoint); ok {
+                       // Case (3) - file or directory status
+                       err = writePathStatus(c.App.Writer, path)
+               }
+       default:
+               return expectedArgsErr(c, 1, true)
+       }
+
+       if err != nil {
+               return newExitError(c, err)
+       }
+       return nil
+}
+
+// Metadata is a collection of commands for manipulating the metadata files.
+var Metadata = cli.Command{
+       Name:  "metadata",
+       Usage: "[ADVANCED] manipulate the policy or protector metadata",
+       Description: `These commands allow a user to directly create, delete, or
+               change the metadata files. It is important to note that using
+               these commands, especially the destructive ones, can make files
+               encrypted with fscrypt unavailable. For instance, deleting a
+               policy effectively deletes all the contents of the corresponding
+               directory. Some example use cases include:
+
+               (1) Directly creating protectors and policies using the "create"
+               subcommand. These can then be applied with "fscrypt encrypt".
+
+               (2) Changing the passphrase for a passphrase protector using the
+               "change-passphrase" subcommand.
+
+               (3) Creating a policy protected with multiple protectors using
+               the "create policy" and "add-protector-to-policy" subcommands.
+
+               (4) Changing the protector protecting a policy using the
+               "add-protector-to-policy" and "remove-protector-from-policy"
+               subcommands.`,
+       Subcommands: []cli.Command{createMetadata, destroyMetadata, changePassphrase,
+               addProtectorToPolicy, removeProtectorFromPolicy, dumpMetadata},
+}
+
+var createMetadata = cli.Command{
+       Name:        "create",
+       ArgsUsage:   fmt.Sprintf("[protector | policy] %s", mountpointArg),
+       Usage:       "manually create new metadata on a filesystem",
+       Subcommands: []cli.Command{createProtector, createPolicy},
+}
+
+var createProtector = cli.Command{
+       Name:      "protector",
+       ArgsUsage: mountpointArg,
+       Usage:     "create a new protector on a filesystem",
+       Description: fmt.Sprintf(`This command creates a new protector on %s
+               that does not (yet) protect any policy. After creation, the user
+               can use %s with "fscrypt encrypt" to protect a directory with
+               this new protector. The creation process is identical to the
+               first step of "fscrypt encrypt" when the user has requested to
+               create a new passphrase. The user will be prompted for the
+               source, name, and secret data for the new protector (when
+               applicable). As with "fscrypt encrypt", these prompts can be
+               disabled with the appropriate flags.`, mountpointArg,
+               shortDisplay(protectorFlag)),
+       Flags:  []cli.Flag{sourceFlag, nameFlag, keyFileFlag, userFlag},
+       Action: createProtectorAction,
+}
+
+func createProtectorAction(c *cli.Context) error {
+       if c.NArg() != 1 {
+               return expectedArgsErr(c, 1, false)
+       }
+
+       targetUser, err := parseUserFlag()
+       if err != nil {
+               return newExitError(c, err)
+       }
+       mountpoint := c.Args().Get(0)
+       ctx, err := actions.NewContextFromMountpoint(mountpoint, targetUser)
+       if err != nil {
+               return newExitError(c, err)
+       }
+
+       prompt := fmt.Sprintf("Create new protector on %q", ctx.Mount.Path)
+       if err = askConfirmation(prompt, true, ""); err != nil {
+               return newExitError(c, err)
+       }
+
+       protector, err := createProtectorFromContext(ctx)
+       if err != nil {
+               return newExitError(c, err)
+       }
+       protector.Lock()
+
+       fmt.Fprintf(c.App.Writer, "Protector %s created on filesystem %q.\n",
+               protector.Descriptor(), ctx.Mount.Path)
+       return nil
+}
+
+var createPolicy = cli.Command{
+       Name:      "policy",
+       ArgsUsage: fmt.Sprintf("%s %s", mountpointArg, shortDisplay(protectorFlag)),
+       Usage:     "create a new policy on a filesystem",
+       Description: fmt.Sprintf(`This command creates a new policy on %s
+               that has not (yet) been applied to any directory. After
+               creation, the user can use %s with "fscrypt encrypt" to encrypt
+               a directory with this new policy. As all policies must be
+               protected with at least one protector, this command requires
+               specifying one with %s. To create a policy protected by many
+               protectors, use this command and "fscrypt metadata
+               add-protector-to-policy".`, mountpointArg,
+               shortDisplay(policyFlag), shortDisplay(protectorFlag)),
+       Flags:  []cli.Flag{protectorFlag, keyFileFlag},
+       Action: createPolicyAction,
+}
+
+func createPolicyAction(c *cli.Context) error {
+       if c.NArg() != 1 {
+               return expectedArgsErr(c, 1, false)
+       }
+
+       ctx, err := actions.NewContextFromMountpoint(c.Args().Get(0), nil)
+       if err != nil {
+               return newExitError(c, err)
+       }
+
+       if err = checkRequiredFlags(c, []*stringFlag{protectorFlag}); err != nil {
+               return err
+       }
+       protector, err := getProtectorFromFlag(protectorFlag.Value, ctx.TargetUser)
+       if err != nil {
+               return newExitError(c, err)
+       }
+       if err = protector.Unlock(existingKeyFn); err != nil {
+               return newExitError(c, err)
+       }
+       defer protector.Lock()
+
+       prompt := fmt.Sprintf("Create new policy on %q", ctx.Mount.Path)
+       if err = askConfirmation(prompt, true, ""); err != nil {
+               return newExitError(c, err)
+       }
+
+       policy, err := actions.CreatePolicy(ctx, protector)
+       if err != nil {
+               return newExitError(c, err)
+       }
+       policy.Lock()
+
+       fmt.Fprintf(c.App.Writer, "Policy %s created on filesystem %q.\n",
+               policy.Descriptor(), ctx.Mount.Path)
+       return nil
+}
+
+var destroyMetadata = cli.Command{
+       Name: "destroy",
+       ArgsUsage: fmt.Sprintf("[%s | %s | %s]", shortDisplay(protectorFlag),
+               shortDisplay(policyFlag), mountpointArg),
+       Usage: "delete a filesystem's, protector's, or policy's metadata",
+       Description: fmt.Sprintf(`This command can be used to perform three
+               different destructive operations. Note that in all of these
+               cases, data will usually be lost, so use with care.
+
+               (1) If used with %[1]s, this command deletes all the data
+               associated with that protector. This means all directories
+               protected with that protector will become PERMANENTLY
+               inaccessible (unless the policies were protected by multiple
+               protectors).
+
+               (2) If used with %[2]s, this command deletes all the data
+               associated with that policy. This means all directories (usually
+               just one) using this policy will become PERMANENTLY
+               inaccessible.
+
+               (3) If used with %[3]s, all the metadata on that filesystem will
+               be deleted, causing all directories on that filesystem using
+               fscrypt to become PERMANENTLY inaccessible. To start using this
+               directory again, "fscrypt setup %[3]s" will need to be rerun.`,
+               shortDisplay(protectorFlag), shortDisplay(policyFlag),
+               mountpointArg),
+       Flags:  []cli.Flag{protectorFlag, policyFlag, forceFlag},
+       Action: destroyMetadataAction,
+}
+
+func destroyMetadataAction(c *cli.Context) error {
+       switch c.NArg() {
+       case 0:
+               switch {
+               case protectorFlag.Value != "":
+                       // Case (1) - protector destroy
+                       protector, err := getProtectorFromFlag(protectorFlag.Value, nil)
+                       if err != nil {
+                               return newExitError(c, err)
+                       }
+
+                       prompt := fmt.Sprintf("Destroy protector %s on %q?",
+                               protector.Descriptor(), protector.Context.Mount.Path)
+                       warning := "All files protected only with this protector will be lost!!"
+                       if err := askConfirmation(prompt, false, warning); err != nil {
+                               return newExitError(c, err)
+                       }
+                       if err := protector.Destroy(); err != nil {
+                               return newExitError(c, err)
+                       }
+
+                       fmt.Fprintf(c.App.Writer, "Protector %s deleted from filesystem %q.\n",
+                               protector.Descriptor(), protector.Context.Mount.Path)
+               case policyFlag.Value != "":
+                       // Case (2) - policy destroy
+                       policy, err := getPolicyFromFlag(policyFlag.Value, nil)
+                       if err != nil {
+                               return newExitError(c, err)
+                       }
+
+                       prompt := fmt.Sprintf("Destroy policy %s on %q?",
+                               policy.Descriptor(), policy.Context.Mount.Path)
+                       warning := "All files using this policy will be lost!!"
+                       if err := askConfirmation(prompt, false, warning); err != nil {
+                               return newExitError(c, err)
+                       }
+                       if err := policy.Destroy(); err != nil {
+                               return newExitError(c, err)
+                       }
+
+                       fmt.Fprintf(c.App.Writer, "Policy %s deleted from filesystem %q.\n",
+                               policy.Descriptor(), policy.Context.Mount.Path)
+               default:
+                       message := fmt.Sprintf("Must specify one of: %s, %s, or %s",
+                               mountpointArg,
+                               shortDisplay(protectorFlag),
+                               shortDisplay(policyFlag))
+                       return &usageError{c, message}
+               }
+       case 1:
+               // Case (3) - mountpoint destroy
+               path := c.Args().Get(0)
+               ctx, err := actions.NewContextFromMountpoint(path, nil)
+               if err != nil {
+                       return newExitError(c, err)
+               }
+
+               prompt := fmt.Sprintf("Destroy all the metadata on %q?", ctx.Mount.Path)
+               warning := "All the encrypted files on this filesystem will be lost!!"
+               if err := askConfirmation(prompt, false, warning); err != nil {
+                       return newExitError(c, err)
+               }
+               if err := ctx.Mount.RemoveAllMetadata(); err != nil {
+                       return newExitError(c, err)
+               }
+
+               fmt.Fprintf(c.App.Writer, "All metadata on %q deleted.\n", ctx.Mount.Path)
+       default:
+               return expectedArgsErr(c, 1, true)
+       }
+       return nil
+}
+
+var changePassphrase = cli.Command{
+       Name:      "change-passphrase",
+       ArgsUsage: shortDisplay(protectorFlag),
+       Usage:     "change the passphrase used for a protector",
+       Description: `This command takes a specified passphrase protector and
+               changes the corresponding passphrase. Note that this does not
+               create or destroy any protectors.`,
+       Flags:  []cli.Flag{protectorFlag},
+       Action: changePassphraseAction,
+}
+
+func changePassphraseAction(c *cli.Context) error {
+       if c.NArg() != 0 {
+               return expectedArgsErr(c, 0, false)
+       }
+       if err := checkRequiredFlags(c, []*stringFlag{protectorFlag}); err != nil {
+               return err
+       }
+
+       protector, err := getProtectorFromFlag(protectorFlag.Value, nil)
+       if err != nil {
+               return newExitError(c, err)
+       }
+       if err := protector.Unlock(oldExistingKeyFn); err != nil {
+               return newExitError(c, err)
+       }
+       defer protector.Lock()
+       if err := protector.Rewrap(newCreateKeyFn); err != nil {
+               return newExitError(c, err)
+       }
+
+       fmt.Fprintf(c.App.Writer, "Passphrase for protector %s successfully changed.\n",
+               protector.Descriptor())
+       return nil
+}
+
+var addProtectorToPolicy = cli.Command{
+       Name:      "add-protector-to-policy",
+       ArgsUsage: fmt.Sprintf("%s %s", shortDisplay(protectorFlag), shortDisplay(policyFlag)),
+       Usage:     "start protecting a policy with some protector",
+       Description: `This command changes the specified policy to be
+               protected with the specified protector. This means that any
+               directories using this policy will now be accessible with this
+               protector. This command will fail if the policy is already
+               protected with this protector.`,
+       Flags:  []cli.Flag{protectorFlag, policyFlag, unlockWithFlag, keyFileFlag},
+       Action: addProtectorAction,
+}
+
+func addProtectorAction(c *cli.Context) error {
+       if c.NArg() != 0 {
+               return expectedArgsErr(c, 0, false)
+       }
+       if err := checkRequiredFlags(c, []*stringFlag{protectorFlag, policyFlag}); err != nil {
+               return err
+       }
+
+       protector, err := getProtectorFromFlag(protectorFlag.Value, nil)
+       if err != nil {
+               return newExitError(c, err)
+       }
+       policy, err := getPolicyFromFlag(policyFlag.Value, protector.Context.TargetUser)
+       if err != nil {
+               return newExitError(c, err)
+       }
+       // Sanity check before unlocking everything
+       if err := policy.AddProtector(protector); errors.Cause(err) != actions.ErrLocked {
+               if err == nil {
+                       err = errors.New("policy and protector are not locked")
+               }
+               return newExitError(c, err)
+       }
+
+       prompt := fmt.Sprintf("Protect policy %s with protector %s?",
+               policy.Descriptor(), protector.Descriptor())
+       warning := "All files using this policy will be accessible with this protector!!"
+       if err := askConfirmation(prompt, true, warning); err != nil {
+               return newExitError(c, err)
+       }
+
+       if err := protector.Unlock(existingKeyFn); err != nil {
+               return newExitError(c, err)
+       }
+       if err := policy.Unlock(optionFn, existingKeyFn); err != nil {
+               return newExitError(c, err)
+       }
+       if err := policy.AddProtector(protector); err != nil {
+               return newExitError(c, err)
+       }
+
+       fmt.Fprintf(c.App.Writer, "Protector %s now protecting policy %s.\n",
+               protector.Descriptor(), policy.Descriptor())
+       return nil
+}
+
+var removeProtectorFromPolicy = cli.Command{
+       Name:      "remove-protector-from-policy",
+       ArgsUsage: fmt.Sprintf("%s %s", shortDisplay(protectorFlag), shortDisplay(policyFlag)),
+       Usage:     "stop protecting a policy with some protector",
+       Description: `This command changes the specified policy to no longer be
+               protected with the specified protector. This means that any
+               directories using this policy will cannot be accessed with this
+               protector. This command will fail if the policy not already
+               protected with this protector or if it is the policy's only
+               protector.`,
+       Flags:  []cli.Flag{protectorFlag, policyFlag, forceFlag},
+       Action: removeProtectorAction,
+}
+
+func removeProtectorAction(c *cli.Context) error {
+       if c.NArg() != 0 {
+               return expectedArgsErr(c, 0, false)
+       }
+       if err := checkRequiredFlags(c, []*stringFlag{protectorFlag, policyFlag}); err != nil {
+               return err
+       }
+
+       // We only need the protector descriptor, not the protector itself.
+       ctx, protectorDescriptor, err := parseMetadataFlag(protectorFlag.Value, nil)
+       if err != nil {
+               return newExitError(c, err)
+       }
+       // We don't need to unlock the policy for this operation.
+       policy, err := getPolicyFromFlag(policyFlag.Value, ctx.TargetUser)
+       if err != nil {
+               return newExitError(c, err)
+       }
+
+       prompt := fmt.Sprintf("Stop protecting policy %s with protector %s?",
+               policy.Descriptor(), protectorDescriptor)
+       warning := "All files using this policy will NO LONGER be accessible with this protector!!"
+       if err := askConfirmation(prompt, false, warning); err != nil {
+               return newExitError(c, err)
+       }
+
+       if err := policy.RemoveProtector(protectorDescriptor); err != nil {
+               return newExitError(c, err)
+       }
+
+       fmt.Fprintf(c.App.Writer, "Protector %s no longer protecting policy %s.\n",
+               protectorDescriptor, policy.Descriptor())
+       return nil
+}
+
+var dumpMetadata = cli.Command{
+       Name:      "dump",
+       ArgsUsage: fmt.Sprintf("[%s | %s]", shortDisplay(protectorFlag), shortDisplay(policyFlag)),
+       Usage:     "print debug data for a policy or protector",
+       Description: fmt.Sprintf(`This commands dumps all of the debug data for
+               a protector (if %s is used) or policy (if %s is used). This data
+               includes the data pulled from the %q config file, the
+               appropriate mountpoint data, and any options for the policy or
+               hashing costs for the protector. Any cryptographic keys are
+               wiped and are not printed out.`, shortDisplay(protectorFlag),
+               shortDisplay(policyFlag), actions.ConfigFileLocation),
+       Flags:  []cli.Flag{protectorFlag, policyFlag},
+       Action: dumpMetadataAction,
+}
+
+func dumpMetadataAction(c *cli.Context) error {
+       switch {
+       case protectorFlag.Value != "":
+               // Case (1) - protector print
+               protector, err := getProtectorFromFlag(protectorFlag.Value, nil)
+               if err != nil {
+                       return newExitError(c, err)
+               }
+               fmt.Fprintln(c.App.Writer, protector)
+       case policyFlag.Value != "":
+               // Case (2) - policy print
+               policy, err := getPolicyFromFlag(policyFlag.Value, nil)
+               if err != nil {
+                       return newExitError(c, err)
+               }
+               fmt.Fprintln(c.App.Writer, policy)
+       default:
+               message := fmt.Sprintf("Must specify one of: %s or %s",
+                       shortDisplay(protectorFlag),
+                       shortDisplay(policyFlag))
+               return &usageError{c, message}
+       }
+       return nil
+}
diff --git a/cmd/fscrypt/errors.go b/cmd/fscrypt/errors.go
new file mode 100644 (file)
index 0000000..c4814f4
--- /dev/null
@@ -0,0 +1,382 @@
+/*
+ * errors.go - File which contains common error handling code for fscrypt
+ * commands. This includes handling for bad usage, invalid commands, and errors
+ * from the other packages
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import (
+       "bytes"
+       "fmt"
+       "os"
+       "path/filepath"
+       "unicode/utf8"
+
+       "github.com/pkg/errors"
+       "github.com/urfave/cli"
+       "golang.org/x/sys/unix"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/keyring"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+// failureExitCode is the value fscrypt will return on failure.
+const failureExitCode = 1
+
+// Various errors used for the top level user interface
+var (
+       ErrCanceled           = errors.New("operation canceled")
+       ErrNoDestructiveOps   = errors.New("operation would be destructive")
+       ErrInvalidSource      = errors.New("invalid source type")
+       ErrPassphraseMismatch = errors.New("entered passphrases do not match")
+       ErrSpecifyProtector   = errors.New("multiple protectors available")
+       ErrWrongKey           = errors.New("incorrect key provided")
+       ErrSpecifyKeyFile     = errors.New("no key file specified")
+       ErrKeyFileLength      = errors.Errorf("key file must be %d bytes", metadata.InternalKeyLen)
+       ErrAllLoadsFailed     = errors.New("could not load any protectors")
+       ErrMustBeRoot         = errors.New("this command must be run as root")
+       ErrDirAlreadyUnlocked = errors.New("this file or directory is already unlocked")
+       ErrDirAlreadyLocked   = errors.New("this file or directory is already locked")
+       ErrNotPassphrase      = errors.New("protector does not use a passphrase")
+       ErrUnknownUser        = errors.New("unknown user")
+       ErrDropCachesPerm     = errors.New("inode cache can only be dropped as root")
+       ErrSpecifyUser        = errors.New("user must be specified when run as root")
+       ErrFsKeyringPerm      = errors.New("root is required to add/remove v1 encryption policy keys to/from filesystem")
+)
+
+// ErrDirFilesOpen indicates that a directory can't be fully locked because
+// files protected by the directory's policy are still open.
+type ErrDirFilesOpen struct {
+       DirPath string
+}
+
+func (err *ErrDirFilesOpen) Error() string {
+       return `Directory was incompletely locked because some files are still
+       open. These files remain accessible.`
+}
+
+// ErrDirUnlockedByOtherUsers indicates that a directory can't be locked because
+// the directory's policy is still provisioned by other users.
+type ErrDirUnlockedByOtherUsers struct {
+       DirPath string
+}
+
+func (err *ErrDirUnlockedByOtherUsers) Error() string {
+       return fmt.Sprintf(`Directory %q couldn't be fully locked because other
+       user(s) have unlocked it.`, err.DirPath)
+}
+
+// ErrDirNotEmpty indicates that a directory can't be encrypted because it's not
+// empty.
+type ErrDirNotEmpty struct {
+       DirPath string
+}
+
+func (err *ErrDirNotEmpty) Error() string {
+       return fmt.Sprintf("Directory %q cannot be encrypted because it is non-empty.", err.DirPath)
+}
+
+var loadHelpText = fmt.Sprintf("You may need to mount a linked filesystem. Run with %s for more information.", shortDisplay(verboseFlag))
+
+// getFullName returns the full name of the application or command being used.
+func getFullName(c *cli.Context) string {
+       if c.Command.HelpName != "" {
+               return c.Command.HelpName
+       }
+       return c.App.HelpName
+}
+
+func isGrubInstalledOnFilesystem(mnt *filesystem.Mount) bool {
+       dir := filepath.Join(mnt.Path, "boot/grub")
+       grubDirMount, _ := filesystem.FindMount(dir)
+       return grubDirMount == mnt
+}
+
+func suggestEnablingEncryption(mnt *filesystem.Mount) string {
+       kconfig := "CONFIG_FS_ENCRYPTION=y"
+       switch mnt.FilesystemType {
+       case "ext4":
+               // Recommend running tune2fs -O encrypt.  But be really careful;
+               // old kernels didn't support block_size != PAGE_SIZE, and old
+               // GRUB didn't support encryption.
+               var statfs unix.Statfs_t
+               if err := unix.Statfs(mnt.Path, &statfs); err != nil {
+                       return ""
+               }
+               pagesize := os.Getpagesize()
+               if int64(statfs.Bsize) != int64(pagesize) && !util.IsKernelVersionAtLeast(5, 5) {
+                       return fmt.Sprintf(`This filesystem uses a block size
+                       (%d) other than the system page size (%d). Ext4
+                       encryption didn't support this case until kernel v5.5.
+                       Do *not* enable encryption on this filesystem. Either
+                       upgrade your kernel to v5.5 or later, or re-create this
+                       filesystem using 'mkfs.ext4 -b %d -O encrypt %s'
+                       (WARNING: that will erase all data on it).`,
+                               statfs.Bsize, pagesize, pagesize, mnt.Device)
+               }
+               if !util.IsKernelVersionAtLeast(5, 1) {
+                       kconfig = "CONFIG_EXT4_ENCRYPTION=y"
+               }
+               s := fmt.Sprintf(`To enable encryption support on this
+               filesystem, run:
+
+               > sudo tune2fs -O encrypt %q
+               `, mnt.Device)
+               if isGrubInstalledOnFilesystem(mnt) {
+                       s += `
+                       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.
+                       `
+               }
+               s += fmt.Sprintf(`
+               Also ensure that your kernel has %s. See the documentation for
+               more details.`, kconfig)
+               return s
+       case "f2fs":
+               if !util.IsKernelVersionAtLeast(5, 1) {
+                       kconfig = "CONFIG_F2FS_FS_ENCRYPTION=y"
+               }
+               return fmt.Sprintf(`To enable encryption support on this
+               filesystem, you'll need to run:
+
+               > sudo fsck.f2fs -O encrypt %q
+
+               Also ensure that your kernel has %s. See the documentation for
+               more details.`, mnt.Device, kconfig)
+       default:
+               return `See the documentation for how to enable encryption
+               support on this filesystem.`
+       }
+}
+
+// getErrorSuggestions returns a string containing suggestions about how to fix
+// an error. If no suggestion is necessary or available, return empty string.
+func getErrorSuggestions(err error) string {
+       switch e := err.(type) {
+       case *ErrDirFilesOpen:
+               return fmt.Sprintf(`Try killing any processes using files in the
+               directory, for example using:
+
+               > find %q -print0 | xargs -0 fuser -k
+
+               Then re-run:
+
+               > fscrypt lock %q`, e.DirPath, e.DirPath)
+       case *ErrDirNotEmpty:
+               dir := filepath.Clean(e.DirPath)
+               newDir := dir + ".new"
+               return fmt.Sprintf(`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 %q
+               > fscrypt encrypt %q
+               > cp -a -T %q %q
+               > find %q -type f -print0 | xargs -0 shred -n1 --remove=unlink
+               > rm -rf %q
+               > mv %q %q
+
+               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.`, newDir, newDir, dir, newDir, dir, dir, newDir, dir)
+       case *ErrDirUnlockedByOtherUsers:
+               return fmt.Sprintf(`If you want to force the directory to be
+               locked, use:
+
+               > sudo fscrypt lock --all-users %q`, e.DirPath)
+       case *actions.ErrBadConfigFile:
+               return `Either fix this file manually, or run "sudo fscrypt setup" to recreate it.`
+       case *actions.ErrLoginProtectorName:
+               return fmt.Sprintf("To fix this, don't specify the %s option.", shortDisplay(nameFlag))
+       case *actions.ErrMissingProtectorName:
+               return fmt.Sprintf("Use %s to specify a protector name.", shortDisplay(nameFlag))
+       case *actions.ErrNoConfigFile:
+               return `Run "sudo fscrypt setup" to create this file.`
+       case *filesystem.ErrEncryptionNotEnabled:
+               return suggestEnablingEncryption(e.Mount)
+       case *filesystem.ErrEncryptionNotSupported:
+               switch e.Mount.FilesystemType {
+               case "ext4":
+                       if !util.IsKernelVersionAtLeast(4, 1) {
+                               return "ext4 encryption requires kernel v4.1 or later."
+                       }
+               case "f2fs":
+                       if !util.IsKernelVersionAtLeast(4, 2) {
+                               return "f2fs encryption requires kernel v4.2 or later."
+                       }
+               case "ubifs":
+                       if !util.IsKernelVersionAtLeast(4, 10) {
+                               return "ubifs encryption requires kernel v4.10 or later."
+                       }
+               case "ceph":
+                       if !util.IsKernelVersionAtLeast(6, 6) {
+                               return "CephFS encryption requires kernel v6.6 or later."
+                       }
+               }
+               return ""
+       case *filesystem.ErrNoCreatePermission:
+               return `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`
+       case *filesystem.ErrNotSetup:
+               return fmt.Sprintf(`Run "sudo fscrypt setup %s" to use fscrypt
+                       on this filesystem.`, e.Mount.Path)
+       case *keyring.ErrAccessUserKeyring:
+               return fmt.Sprintf(`You can only use %s to access the user
+                       keyring of another user if you are running as root.`,
+                       shortDisplay(userFlag))
+       case *keyring.ErrSessionUserKeyring:
+               return `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".`
+       case *metadata.ErrLockedRegularFile:
+               return `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.)`
+       }
+       switch errors.Cause(err) {
+       case crypto.ErrMlockUlimit:
+               return `Too much memory was requested to be locked in RAM. The
+                       current limit for this user can be checked with "ulimit
+                       -l". The limit can be modified by either changing the
+                       "memlock" item in /etc/security/limits.conf or by
+                       changing the "LimitMEMLOCK" value in systemd.`
+       case keyring.ErrV2PoliciesUnsupported:
+               return fmt.Sprintf(`v2 encryption policies are only supported by kernel
+               version 5.4 and later. Either use a newer kernel, or change
+               policy_version to 1 in %s.`, actions.ConfigFileLocation)
+       case ErrNoDestructiveOps:
+               return fmt.Sprintf("If desired, use %s to automatically run destructive operations.",
+                       shortDisplay(forceFlag))
+       case ErrSpecifyProtector:
+               return fmt.Sprintf("Use %s to specify a protector.", shortDisplay(protectorFlag))
+       case ErrSpecifyKeyFile:
+               return fmt.Sprintf("Use %s to specify a key file.", shortDisplay(keyFileFlag))
+       case ErrDropCachesPerm:
+               return fmt.Sprintf(`Either this command should be run as root to
+                       properly clear the inode cache, or it should be run with
+                       %s=false (this may leave encrypted files and directories
+                       in an accessible state).`, shortDisplay(dropCachesFlag))
+       case ErrFsKeyringPerm:
+               return `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).`
+       case ErrSpecifyUser:
+               return fmt.Sprintf(`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 %s. To
+                       use the root user's keyring or passphrase, use
+                       --%s=root.`, shortDisplay(userFlag), userFlag.GetName())
+       case ErrAllLoadsFailed:
+               return loadHelpText
+       default:
+               return ""
+       }
+}
+
+// newExitError creates a new error for a given context and normal error. The
+// returned error prepends an error tag and the name of the relevant command,
+// and it will make fscrypt return a non-zero exit value.
+func newExitError(c *cli.Context, err error) error {
+       // Prepend the error tag and full name, and append suggestions (if any)
+       prefix := "[ERROR] " + getFullName(c) + ": "
+       message := prefix + wrapText(err.Error(), utf8.RuneCountInString(prefix))
+
+       if suggestion := getErrorSuggestions(err); suggestion != "" {
+               message += "\n\n" + wrapText(suggestion, 0)
+       }
+
+       return cli.NewExitError(message, failureExitCode)
+}
+
+// usageError implements cli.ExitCoder to print the usage and return a non-zero
+// value. This error should be used when a command is used incorrectly.
+type usageError struct {
+       c       *cli.Context
+       message string
+}
+
+func (u *usageError) Error() string {
+       return fmt.Sprintf("%s: %s", getFullName(u.c), u.message)
+}
+
+// We get the help to print after the error by having it run right before the
+// application exits. This is very nasty, but there isn't a better way to do it
+// with the constraints of urfave/cli.
+func (u *usageError) ExitCode() int {
+       // Redirect help output to a buffer, so we can customize it.
+       buf := new(bytes.Buffer)
+       oldWriter := u.c.App.Writer
+       u.c.App.Writer = buf
+
+       // Get the appropriate help
+       if getFullName(u.c) == filepath.Base(os.Args[0]) {
+               cli.ShowAppHelp(u.c)
+       } else {
+               cli.ShowCommandHelp(u.c, u.c.Command.Name)
+       }
+
+       // Remove first line from help and print it out
+       buf.ReadBytes('\n')
+       buf.WriteTo(oldWriter)
+       u.c.App.Writer = oldWriter
+       return failureExitCode
+}
+
+// expectedArgsErr creates a usage error for the incorrect number of arguments
+// being specified. atMost should be true only if any number of arguments from 0
+// to expectedArgs would be acceptable.
+func expectedArgsErr(c *cli.Context, expectedArgs int, atMost bool) error {
+       message := "expected "
+       if atMost {
+               message += "at most "
+       }
+       message += fmt.Sprintf("%s, got %s",
+               pluralize(expectedArgs, "argument"), pluralize(c.NArg(), "argument"))
+       return &usageError{c, message}
+}
+
+// onUsageError is a function handler for the application and each command.
+func onUsageError(c *cli.Context, err error, _ bool) error {
+       return &usageError{c, err.Error()}
+}
+
+// checkRequiredFlags makes sure that all of the specified string flags have
+// been given nonempty values. Returns a usage error on failure.
+func checkRequiredFlags(c *cli.Context, flags []*stringFlag) error {
+       for _, flag := range flags {
+               if flag.Value == "" {
+                       message := fmt.Sprintf("required flag %s not provided", shortDisplay(flag))
+                       return &usageError{c, message}
+               }
+       }
+       return nil
+}
diff --git a/cmd/fscrypt/flags.go b/cmd/fscrypt/flags.go
new file mode 100644 (file)
index 0000000..7285133
--- /dev/null
@@ -0,0 +1,309 @@
+/*
+ * flags.go - File which contains all the flags used by the application. This
+ * includes both global flags and command specific flags. When applicable, it
+ * also includes the default values.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import (
+       "flag"
+       "fmt"
+       "log"
+       "os/user"
+       "regexp"
+       "strconv"
+       "time"
+
+       "github.com/urfave/cli"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/util"
+)
+
+// We define the types boolFlag, durationFlag, and stringFlag here instead of
+// using those present in urfave/cli because we need them to conform to the
+// prettyFlag interface (in format.go). The Getters just get the corresponding
+// variables, String() just uses longDisplay, and Apply just sets the
+// corresponding type of flag.
+type boolFlag struct {
+       Name    string
+       Usage   string
+       Default bool
+       Value   bool
+}
+
+func (b *boolFlag) GetName() string    { return b.Name }
+func (b *boolFlag) GetArgName() string { return "" }
+func (b *boolFlag) GetUsage() string   { return b.Usage }
+
+func (b *boolFlag) String() string {
+       if !b.Default {
+               return longDisplay(b)
+       }
+       return longDisplay(b, strconv.FormatBool(b.Default))
+}
+
+func (b *boolFlag) Apply(set *flag.FlagSet) {
+       set.BoolVar(&b.Value, b.Name, b.Default, b.Usage)
+}
+
+type durationFlag struct {
+       Name    string
+       ArgName string
+       Usage   string
+       Default time.Duration
+       Value   time.Duration
+}
+
+func (d *durationFlag) GetName() string    { return d.Name }
+func (d *durationFlag) GetArgName() string { return d.ArgName }
+func (d *durationFlag) GetUsage() string   { return d.Usage }
+
+func (d *durationFlag) String() string {
+       if d.Default == 0 {
+               return longDisplay(d)
+       }
+       return longDisplay(d, d.Value.String())
+}
+
+func (d *durationFlag) Apply(set *flag.FlagSet) {
+       set.DurationVar(&d.Value, d.Name, d.Default, d.Usage)
+}
+
+type stringFlag struct {
+       Name    string
+       ArgName string
+       Usage   string
+       Default string
+       Value   string
+}
+
+func (s *stringFlag) GetName() string    { return s.Name }
+func (s *stringFlag) GetArgName() string { return s.ArgName }
+func (s *stringFlag) GetUsage() string   { return s.Usage }
+
+func (s *stringFlag) String() string {
+       if s.Default == "" {
+               return longDisplay(s)
+       }
+       return longDisplay(s, strconv.Quote(s.Default))
+}
+
+func (s *stringFlag) Apply(set *flag.FlagSet) {
+       set.StringVar(&s.Value, s.Name, s.Default, s.Usage)
+}
+
+var (
+       // allFlags contains every defined flag (used for formatting).
+       // UPDATE THIS ARRAY WHEN ADDING NEW FLAGS!!!
+       // TODO(joerichey) add presubmit rule to enforce this
+       allFlags = []prettyFlag{helpFlag, versionFlag, verboseFlag, quietFlag,
+               forceFlag, skipUnlockFlag, timeTargetFlag,
+               sourceFlag, nameFlag, keyFileFlag, protectorFlag,
+               unlockWithFlag, policyFlag, allUsersLockFlag, allUsersSetupFlag,
+               noRecoveryFlag}
+       // universalFlags contains flags that should be on every command
+       universalFlags = []cli.Flag{verboseFlag, quietFlag, helpFlag}
+)
+
+// Bool flags: used to switch some behavior on or off
+var (
+       helpFlag = &boolFlag{
+               Name:  "help",
+               Usage: `Prints help screen for commands and subcommands.`,
+       }
+       versionFlag = &boolFlag{
+               Name:  "version",
+               Usage: `Prints version information.`,
+       }
+       verboseFlag = &boolFlag{
+               Name:  "verbose",
+               Usage: `Prints additional debug messages to standard output.`,
+       }
+       quietFlag = &boolFlag{
+               Name: "quiet",
+               Usage: `Prints nothing to standard output except for errors.
+                       Selects the default for any options that would normally
+                       show a prompt.`,
+       }
+       forceFlag = &boolFlag{
+               Name: "force",
+               Usage: `Suppresses all confirmation prompts and warnings,
+                       causing any action to automatically proceed.  WARNING:
+                       This bypasses confirmations for protective operations,
+                       use with care.`,
+       }
+       skipUnlockFlag = &boolFlag{
+               Name: "skip-unlock",
+               Usage: `Leave the directory in a locked state after setup.
+                       "fscrypt unlock" will need to be run in order to use the
+                       directory.`,
+       }
+       dropCachesFlag = &boolFlag{
+               Name: "drop-caches",
+               Usage: `After removing the key(s) from the keyring, drop the
+                       kernel's filesystem caches if needed. Without this flag,
+                       files encrypted with v1 encryption policies may still be
+                       accessible. This flag is not needed for v2 encryption
+                       policies. This flag, if actually needed, requires root
+                       privileges.`,
+               Default: true,
+       }
+       allUsersLockFlag = &boolFlag{
+               Name: "all-users",
+               Usage: `Lock the directory no matter which user(s) have unlocked
+                       it. Requires root privileges. This flag is only
+                       necessary if the directory was unlocked by a user
+                       different from the one you're locking it as. This flag
+                       is only implemented for v2 encryption policies.`,
+       }
+       allUsersSetupFlag = &boolFlag{
+               Name: "all-users",
+               Usage: `When setting up a filesystem for fscrypt, allow users
+                       other than the calling user (typically root) to create
+                       fscrypt policies and protectors on the filesystem. Note
+                       that this will create a world-writable directory, which
+                       users could use to fill up the entire filesystem. Hence,
+                       this option may not be appropriate for some systems.`,
+       }
+       noRecoveryFlag = &boolFlag{
+               Name:  "no-recovery",
+               Usage: `Don't generate a recovery passphrase.`,
+       }
+)
+
+// Option flags: used to specify options instead of being prompted for them
+var (
+       timeTargetFlag = &durationFlag{
+               Name:    "time",
+               ArgName: "TIME",
+               Usage: `Set the global options so that passphrase hashing takes
+                       TIME long. TIME should be formatted as a sequence of
+                       decimal numbers, each with optional fraction and a unit
+                       suffix, such as "300ms", "1.5s" or "2h45m". Valid time
+                       units are "ms", "s", "m", and "h".`,
+               Default: 1 * time.Second,
+       }
+       sourceFlag = &stringFlag{
+               Name:    "source",
+               ArgName: "SOURCE",
+               Usage: fmt.Sprintf(`New protectors will have type SOURCE. SOURCE
+                       can be one of pam_passphrase, custom_passphrase, or
+                       raw_key. If not specified, the user will be prompted for
+                       the source, with a default pulled from %s.`,
+                       actions.ConfigFileLocation),
+       }
+       nameFlag = &stringFlag{
+               Name:    "name",
+               ArgName: "PROTECTOR_NAME",
+               Usage: `New custom_passphrase and raw_key protectors will be
+                       named PROTECTOR_NAME. If not specified, the user will be
+                       prompted for a name.`,
+       }
+       keyFileFlag = &stringFlag{
+               Name:    "key",
+               ArgName: "FILE",
+               Usage: `Use the contents of FILE as the wrapping key when
+                       creating or unlocking raw_key protectors. FILE should be
+                       formatted as raw binary and should be exactly 32 bytes
+                       long.`,
+       }
+       userFlag = &stringFlag{
+               Name:    "user",
+               ArgName: "USERNAME",
+               Usage: `Specify which user should be used for login passphrases
+                       or to which user's keyring keys should be provisioned.`,
+       }
+       protectorFlag = &stringFlag{
+               Name:    "protector",
+               ArgName: "MOUNTPOINT:ID",
+               Usage: `Specify an existing protector on filesystem MOUNTPOINT
+                       with protector descriptor ID which should be used in the
+                       command.`,
+       }
+       unlockWithFlag = &stringFlag{
+               Name:    "unlock-with",
+               ArgName: "MOUNTPOINT:ID",
+               Usage: `Specify an existing protector on filesystem MOUNTPOINT
+                       with protector descriptor ID which should be used to
+                       unlock a policy (usually specified with --policy). This
+                       flag is only useful if a policy is protected with
+                       multiple protectors. If not specified, the user will be
+                       prompted for a protector.`,
+       }
+       policyFlag = &stringFlag{
+               Name:    "policy",
+               ArgName: "MOUNTPOINT:ID",
+               Usage: `Specify an existing policy on filesystem MOUNTPOINT with
+                       key descriptor ID which should be used in the command.`,
+       }
+)
+
+// The first group is optional and corresponds to the mountpoint. The second
+// group is required and corresponds to the descriptor.
+var idFlagRegex = regexp.MustCompile("^([[:print:]]+):([[:alnum:]]+)$")
+
+func matchMetadataFlag(flagValue string) (mountpoint, descriptor string, err error) {
+       matches := idFlagRegex.FindStringSubmatch(flagValue)
+       if matches == nil {
+               return "", "", fmt.Errorf("flag value %q does not have format %s",
+                       flagValue, mountpointIDArg)
+       }
+       log.Printf("parsed flag: mountpoint=%q descriptor=%s", matches[1], matches[2])
+       return matches[1], matches[2], nil
+}
+
+// parseMetadataFlag takes the value of either protectorFlag or policyFlag
+// formatted as MOUNTPOINT:DESCRIPTOR, and returns a context for the mountpoint
+// and a string for the descriptor.
+func parseMetadataFlag(flagValue string, targetUser *user.User) (*actions.Context, string, error) {
+       mountpoint, descriptor, err := matchMetadataFlag(flagValue)
+       if err != nil {
+               return nil, "", err
+       }
+       ctx, err := actions.NewContextFromMountpoint(mountpoint, targetUser)
+       return ctx, descriptor, err
+}
+
+// getProtectorFromFlag gets an existing locked protector from protectorFlag.
+func getProtectorFromFlag(flagValue string, targetUser *user.User) (*actions.Protector, error) {
+       ctx, descriptor, err := parseMetadataFlag(flagValue, targetUser)
+       if err != nil {
+               return nil, err
+       }
+       return actions.GetProtector(ctx, descriptor)
+}
+
+// getPolicyFromFlag gets an existing locked policy from policyFlag.
+func getPolicyFromFlag(flagValue string, targetUser *user.User) (*actions.Policy, error) {
+       ctx, descriptor, err := parseMetadataFlag(flagValue, targetUser)
+       if err != nil {
+               return nil, err
+       }
+       return actions.GetPolicy(ctx, descriptor)
+}
+
+// parseUserFlag returns the user specified by userFlag or the current effective
+// user if the flag value is missing.
+func parseUserFlag() (targetUser *user.User, err error) {
+       if userFlag.Value != "" {
+               return user.Lookup(userFlag.Value)
+       }
+       return util.EffectiveUser()
+}
diff --git a/cmd/fscrypt/format.go b/cmd/fscrypt/format.go
new file mode 100644 (file)
index 0000000..21253ad
--- /dev/null
@@ -0,0 +1,178 @@
+/*
+ * format.go - Contains all the functionality for formatting the command line
+ * output. This includes formatting the description and flags so that the whole
+ * text is <= LineLength characters.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import (
+       "bytes"
+       "fmt"
+       "os"
+       "strings"
+       "unicode/utf8"
+
+       "github.com/urfave/cli"
+       "golang.org/x/term"
+
+       "github.com/google/fscrypt/util"
+)
+
+var (
+       // lineLength is the maximum width of fscrypt's formatted output. It is
+       // usually the width of the terminal.
+       lineLength         int
+       fallbackLineLength = 80 // fallback is punch cards
+       maxLineLength      = 120
+       // IndentLength is the number spaces to indent by.
+       indentLength = 2
+       // length of the longest shortDisplay for a flag
+       maxShortDisplay int
+       // how much the a flag's usage text needs to be moved over
+       flagPaddingLength int
+)
+
+// We use the init() function to compute our longest short display length. This
+// is then used to compute the formatting and padding strings. This ensures we
+// will always have room to display our flags, and the flag descriptions always
+// appear in the same place.
+func init() {
+       for _, flag := range allFlags {
+               displayLength := utf8.RuneCountInString(shortDisplay(flag))
+               if displayLength > maxShortDisplay {
+                       maxShortDisplay = displayLength
+               }
+       }
+
+       // Pad usage enough so the flags have room.
+       flagPaddingLength = maxShortDisplay + 2*indentLength
+
+       // We use the width of the terminal unless we cannot get the width.
+       width, _, err := term.GetSize(int(os.Stdout.Fd()))
+       if err != nil || width <= 0 {
+               lineLength = fallbackLineLength
+       } else {
+               lineLength = util.MinInt(width, maxLineLength)
+       }
+
+}
+
+// Flags that conform to this interface can be used with a urfave/cli
+// application and can be printed in the correct format.
+type prettyFlag interface {
+       cli.Flag
+       GetArgName() string
+       GetUsage() string
+}
+
+// How a flag should appear on the command line. We have two formats:
+//
+//     --name
+//     --name=ARG_NAME
+//
+// The ARG_NAME appears if the prettyFlag's GetArgName() method returns a
+// non-empty string. The returned string from shortDisplay() does not include
+// any leading or trailing whitespace.
+func shortDisplay(f prettyFlag) string {
+       if argName := f.GetArgName(); argName != "" {
+               return fmt.Sprintf("--%s=%s", f.GetName(), argName)
+       }
+       return fmt.Sprintf("--%s", f.GetName())
+}
+
+// How our flags should appear when displaying their usage. An example would be:
+//
+//     --help                     Prints help screen for commands and subcommands.
+//
+// If a default is specified, then it is appended to the usage. Example:
+//
+//     --time=TIME                Calibrate passphrase hashing to take the
+//                                specified amount of TIME (default: 1s)
+func longDisplay(f prettyFlag, defaultString ...string) string {
+       usage := f.GetUsage()
+       if len(defaultString) > 0 {
+               usage += fmt.Sprintf(" (default: %v)", defaultString[0])
+       }
+
+       // We pad the shortDisplay on the right with enough spaces to equal the
+       // longest flag's display
+       shortDisp := shortDisplay(f)
+       length := utf8.RuneCountInString(shortDisp)
+       shortDisp += strings.Repeat(" ", maxShortDisplay-length)
+
+       return indent + shortDisp + indent + wrapText(usage, flagPaddingLength)
+}
+
+// Takes an input string text, and wraps the text so that each line begins with
+// padding spaces (except for the first line), ends with a newline (except the
+// last line), and each line has length less than lineLength. If the text
+// contains a word which is too long, that word gets its own line. Paragraphs
+// and "code blocks" are preserved.
+func wrapText(text string, padding int) string {
+       // We use a buffer to format the wrapped text so we get O(n) runtime
+       var buffer bytes.Buffer
+       filled := 0
+       delimiter := strings.Repeat(" ", padding)
+
+       for _, line := range strings.Split(text, "\n") {
+               words := strings.Fields(line)
+
+               // Preserve empty lines (paragraph separators).
+               if len(words) == 0 {
+                       if filled != 0 {
+                               buffer.WriteString("\n")
+                       }
+                       buffer.WriteString("\n")
+                       filled = 0
+                       continue
+               }
+
+               codeBlock := (words[0] == ">")
+               if codeBlock {
+                       words[0] = "    "
+                       if filled != 0 {
+                               buffer.WriteString("\n")
+                               filled = 0
+                       }
+               }
+               for _, word := range words {
+                       wordLen := utf8.RuneCountInString(word)
+                       // Write a newline if needed.
+                       if filled != 0 && filled+1+wordLen > lineLength && !codeBlock {
+                               buffer.WriteString("\n")
+                               filled = 0
+                       }
+                       // Write a delimiter or space if needed.
+                       if filled == 0 {
+                               if buffer.Len() != 0 {
+                                       buffer.WriteString(delimiter)
+                               }
+                               filled += padding
+                       } else {
+                               buffer.WriteByte(' ')
+                               filled++
+                       }
+                       // Write the word.
+                       buffer.WriteString(word)
+                       filled += wordLen
+               }
+       }
+
+       return buffer.String()
+}
diff --git a/cmd/fscrypt/fscrypt.go b/cmd/fscrypt/fscrypt.go
new file mode 100644 (file)
index 0000000..93f97de
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * fscrypt.go - File which starts up and runs the application. Initializes
+ * information about the application like the name, version, author, etc...
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+/*
+fscrypt is a command line tool for managing linux filesystem encryption.
+*/
+
+package main
+
+import (
+       "fmt"
+       "io"
+       "log"
+       "os"
+
+       "github.com/urfave/cli"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/filesystem"
+)
+
+// Current version of the program (set by Makefile)
+var version string
+
+func main() {
+       cli.AppHelpTemplate = appHelpTemplate
+       cli.CommandHelpTemplate = commandHelpTemplate
+       cli.SubcommandHelpTemplate = subcommandHelpTemplate
+
+       if conffile := os.Getenv("FSCRYPT_CONF"); conffile != "" {
+               actions.ConfigFileLocation = conffile
+       }
+       if rootmnt := os.Getenv("FSCRYPT_ROOT_MNT"); rootmnt != "" {
+               actions.LoginProtectorMountpoint = rootmnt
+       }
+       if consistent := os.Getenv("FSCRYPT_CONSISTENT_OUTPUT"); consistent == "1" {
+               filesystem.SortDescriptorsByLastMtime = true
+       }
+
+       // Create our command line application
+       app := cli.NewApp()
+       app.Usage = shortUsage
+
+       // Grab the version passed in from the Makefile.
+       app.Version = version
+       app.OnUsageError = onUsageError
+
+       // Setup global flags
+       cli.HelpFlag = helpFlag
+       cli.VersionFlag = versionFlag
+       app.Flags = universalFlags
+
+       // We hide the help subcommand so that "fscrypt <command> --help" works
+       // and "fscrypt <command> help" does not.
+       app.HideHelp = true
+
+       // Initialize command list and setup all of the commands.
+       app.Action = defaultAction
+       app.Commands = []cli.Command{Setup, Encrypt, Unlock, Lock, Purge, Status, Metadata}
+       for i := range app.Commands {
+               setupCommand(&app.Commands[i])
+       }
+
+       app.Run(os.Args)
+}
+
+// setupCommand performs some common setup for each command. This includes
+// hiding the help, formatting the description, adding in the necessary
+// flags, setting up error handlers, etc... Note that the command is modified
+// in place and its subcommands are also setup.
+func setupCommand(command *cli.Command) {
+       command.Description = wrapText(command.Description, indentLength)
+       command.HideHelp = true
+       command.Flags = append(command.Flags, universalFlags...)
+
+       if command.Action == nil {
+               command.Action = defaultAction
+       }
+
+       // Setup function handlers
+       command.OnUsageError = onUsageError
+       if len(command.Subcommands) == 0 {
+               command.Before = setupBefore
+       } else {
+               // Setup subcommands (if applicable)
+               for i := range command.Subcommands {
+                       setupCommand(&command.Subcommands[i])
+               }
+       }
+}
+
+// setupBefore makes sure our logs, errors, and output are going to the correct
+// io.Writers and that we haven't over-specified our flags. We only print the
+// logs when using verbose, and only print normal stuff when not using quiet.
+func setupBefore(c *cli.Context) error {
+       log.SetOutput(io.Discard)
+       c.App.Writer = io.Discard
+
+       if verboseFlag.Value {
+               log.SetOutput(os.Stdout)
+       }
+       if !quietFlag.Value {
+               c.App.Writer = os.Stdout
+       }
+       return nil
+}
+
+// defaultAction will be run when no command is specified.
+func defaultAction(c *cli.Context) error {
+       // Always default to showing the help
+       if helpFlag.Value {
+               cli.ShowAppHelp(c)
+               return nil
+       }
+
+       // Only exit when not calling with the help command
+       var message string
+       if args := c.Args(); args.Present() {
+               message = fmt.Sprintf("command \"%s\" not found", args.First())
+       } else {
+               message = "no command was specified"
+       }
+       return &usageError{c, message}
+}
diff --git a/cmd/fscrypt/fscrypt_bash_completion b/cmd/fscrypt/fscrypt_bash_completion
new file mode 100644 (file)
index 0000000..110d2d4
--- /dev/null
@@ -0,0 +1,332 @@
+# fscrypt_bash_completion
+#
+# Copyright 2017 Google Inc.
+# Author: Henry-Joseph Audéoud (h.audeoud@gmail.com)
+#
+# 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.
+
+#
+# bash completion scripts require exercising some unusual shell script
+# features/quirks, so we have to disable some shellcheck warnings:
+#
+# Disable SC2016 ("Expressions don't expand in single quotes, use double quotes
+# for that") because the 'compgen' built-in expands the argument passed to -W,
+# so that argument *must* be single-quoted to avoid command injection.
+# shellcheck disable=SC2016
+#
+# Disable SC2034 ("{Variable} appears unused. Verify use (or export if used
+# externally)") because of the single quoting mentioned above as well as the
+# fact that we have to declare "local" variables used only by a called function
+# (_init_completion()) and not by the function itself.
+# shellcheck disable=SC2034
+#
+# Disable SC2207 ("Prefer mapfile or read -a to split command output (or quote
+# to avoid splitting)") because bash completion scripts conventionally use
+# COMPREPLY=($(...)) assignments.
+# shellcheck disable=SC2207
+#
+true  # To apply the above shellcheck directives to the entire file
+
+
+# Generate the completion list for possible mountpoints.
+#
+# We need to be super careful here because mountpoints can contain whitespace
+# and shell meta-characters.  To avoid most problems, we do the following:
+#
+#   1.) To avoid parsing ambiguities, 'fscrypt status' replaces the space, tab,
+#       newline, and backslash characters with octal escape sequences -- like
+#       what /proc/self/mountinfo does.  To properly process its output, we need
+#       to split lines on space only (and not on other whitespace which might
+#       not be escaped), and unescape these characters.  Exception: we don't
+#       unescape newlines, as we need to reserve newline as the separator for
+#       the words passed to compgen.  (This causes mountpoints containing
+#       newlines to not be completed correctly, which we have to tolerate.)
+#
+#   2.) We backslash-escape all shell meta-characters, and single-quote the
+#       argument passed to compgen -W.  Without either step, command injection
+#       would be possible.  Without both steps, completions would be incorrect.
+#       The list of shell meta-characters used comes from that used by the
+#       completion script for umount, which has to solve this same problem.
+#
+_fscrypt_compgen_mountpoints()
+{
+    local IFS=$'\n'
+    compgen -W '$(_fscrypt_mountpoints_internal)' -- "${cur}"
+}
+
+_fscrypt_mountpoints_internal()
+{
+    fscrypt status 2>/dev/null | command awk -F " " \
+    'substr($0, 1, 1) == "/" && $5 == "Yes" {
+        gsub(/\\040/, " ", $1)
+        gsub(/\\011/, "\t", $1)
+        gsub(/\\134/, "\\", $1)
+        gsub(/[\]\[(){}<>",:;^&!$=?`|'\''\\ \t\f\n\r\v]/, "\\\\&", $1)
+        print $1
+    }'
+}
+
+# Complete with all possible mountpoints
+_fscrypt_complete_mountpoint()
+{
+    COMPREPLY=($(_fscrypt_compgen_mountpoints))
+}
+
+
+# Output list of possible policy or protector IDs
+# $1: the mount point on which policies are looked for.
+# $2: the section (policy or protector) to retrieve
+_fscrypt_status_section()
+{
+    local section=${2^^}
+    fscrypt status "$1" 2>/dev/null | \
+        command awk '/^[[:xdigit:]]{16}/ && section == "'"$section"'" { print $1; next; }
+                     { section = $1 }'
+}
+
+
+# Complete with policies or protectors
+_fscrypt_complete_policy_or_protector()
+{
+    local status_section="$1"
+    if [[ $cur = *:* ]]; then
+        # Complete with IDs of the given mountpoint
+        local mountpoint="${cur%:*}" id="${cur#*:}"
+        # Note: compgen expands the argument to -W, so it *must* be single-quoted.
+        COMPREPLY=($(compgen \
+            -W '$(_fscrypt_status_section "${mountpoint}" "${status_section}")' \
+            -- "${id}"))
+    else
+        # Complete with mountpoints, with colon and without ending space
+        COMPREPLY=($(_fscrypt_compgen_mountpoints | sed s/\$/:/))
+        compopt -o nospace
+    fi
+}
+
+
+# Complete with all arguments of that function
+_fscrypt_complete_word()
+{
+    # Note: compgen expands the argument to -W, so it *must* be single-quoted.
+    COMPREPLY=($(compgen -W '$*' -- "${cur}"))
+}
+
+
+# Complete with all arguments of that function, plus global options
+_fscrypt_complete_option()
+{
+    local additional_opts=( "$@" )
+    # Add global options, always correct
+    additional_opts+=( --verbose --quiet --help )
+    # Note: compgen expands the argument to -W, so it *must* be single-quoted.
+    COMPREPLY=($(compgen -W '${additional_opts[*]}' -- "${cur}"))
+}
+
+
+_fscrypt()
+{
+    # Initialize completion: compute some local variables to easily
+    # detect what is written on the command line.  -s is for splitting
+    # long options on `=`, and -n is for splitting them also on `:`
+    # (used in the protectors/policies `MOUNTPOINT:ID` forms).
+    #
+    # `split` is set by `_init_completion -s`, we must declare it local
+    # even if we don't use it, not to modify the environment.
+    local cur prev words cword split
+    _init_completion -s -n : || return
+
+    # Complete the options with argument here, if previous word were such
+    # an option.  It would be too difficult to check if they take place in
+    # the correct command (such as `fscrypt status # --key ...`)—and that
+    # is the command's job—so just complete them first.
+    case $prev in
+        --key)
+            # Any file is accepted
+            _filedir
+            return ;;
+        --name)
+            # New value, nothing to complete
+            return ;;
+        --policy|--protector|--unlock-with)
+            local p_or_p="${prev#--}"
+            [[ $p_or_p = unlock-with ]] && p_or_p=protector
+            _fscrypt_complete_policy_or_protector "${p_or_p}"
+            return ;;
+        --source)
+            # Complete with keywords
+            _fscrypt_complete_word \
+                pam_passphrase custom_passphrase raw_key
+            return ;;
+        --time)
+            # It's a time, hard to complete a number…
+            return ;;
+        --user)
+            # Complete with a user
+            COMPREPLY=($(compgen -u -- "${cur}"))
+            return ;;
+    esac
+
+    # Fetch positional arguments (i.e. subcommands)
+    local positional
+    positional=()
+    local iword
+    for ((iword = 1; iword < ${#words[@]} - 1; iword++)); do
+        [[ ${words[iword - 1]} == --@(key|name|policy|protector|unlock-with|source|time|user) ]] \
+            && continue  # Argument of previous option, skip
+        [[ ${words[iword]} == -* ]] && continue  # Option, skip
+        positional+=("${words[iword]}")
+    done
+
+    # If completing the first positional, complete with all possible commands
+    if [[ ${#positional[@]} == 0 ]]; then
+        if [[ $cur == -* ]]; then
+            _fscrypt_complete_option
+        else
+            _fscrypt_complete_word \
+                encrypt lock metadata purge setup status unlock
+        fi
+        return
+    fi
+
+    # Complete according to that provided
+    case ${positional[0]-} in
+        encrypt)  # Directory or option
+            if [[ $cur == -* ]]; then
+                _fscrypt_complete_option \
+                    --policy= --unlock-with= --protector= --source= \
+                    --user= --name= --key= --skip-unlock --no-recovery
+            else
+                _filedir -d
+            fi ;;
+        lock)  # Directory or option
+            if [[ $cur == -* ]]; then
+                _fscrypt_complete_option --user= --all-users
+            else
+                _filedir -d
+            fi ;;
+        purge)  # Mountpoint or options
+            if [[ $cur == -* ]]; then
+                _fscrypt_complete_option --user= --force
+            else
+                _fscrypt_complete_mountpoint
+            fi ;;
+        setup)  # Mountpoint or options
+            if [[ $cur == -* ]]; then
+                _fscrypt_complete_option --time= --force
+            else
+                _fscrypt_complete_mountpoint
+            fi ;;
+        status)  # Directory (only global options for this command)
+            if [[ $cur == -* ]]; then
+                _fscrypt_complete_option
+            else
+                _filedir -d
+            fi ;;
+        unlock)  # Directory or option
+            if [[ $cur == -* ]]; then
+                _fscrypt_complete_option --unlock-with= --user= --key=
+            else
+                _filedir -d
+            fi ;;
+        metadata)
+            # This command has subcommands
+            if [[ ${#positional[@]} = 1 ]]; then
+                if [[ $cur = -* ]]; then
+                    _fscrypt_complete_option
+                else
+                    # Still no subcommand, complete with them
+                    _fscrypt_complete_word \
+                        add-protector-to-policy create change-passphrase \
+                        destroy dump remove-protector-from-policy
+                fi
+                return
+            fi
+            # We have a subcommand, complete according to it
+            case ${positional[1]-} in
+                add-protector-to-policy)  # Options only
+                    _fscrypt_complete_option \
+                        --protector= --policy= --unlock-with= --key=
+                    ;;
+                change-passphrase)  # Options only
+                    _fscrypt_complete_option --protector=
+                    ;;
+                destroy)  # Mountpoint or option
+                    if [[ $cur == -* ]]; then
+                        _fscrypt_complete_option \
+                            --protector= --policy= --force
+                    else
+                        _fscrypt_complete_mountpoint
+                    fi ;;
+                dump)  # Options only
+                    _fscrypt_complete_option --protector= --policy=
+                    ;;
+                remove-protector-from-policy)  # Options only
+                    _fscrypt_complete_option \
+                        --protector= --policy= --force
+                    ;;
+                create)
+                    # This subcommand has subsubcommands
+                    if [[ ${#positional[@]} = 2 ]]; then
+                        if [[ $cur = -* ]]; then
+                            _fscrypt_complete_option
+                        else
+                            # Still no subcommand, complete with them
+                            _fscrypt_complete_word protector policy
+                        fi
+                        return
+                    fi
+                    # We have a subsubcommand, complete according to it
+                    case ${positional[2]-} in
+                        policy)  # Mountpoint or option
+                            if [[ $cur = -* ]]; then
+                                _fscrypt_complete_option --protector= --key=
+                            else
+                                _fscrypt_complete_mountpoint
+                            fi ;;
+                        protector)  # Mountpoint or option
+                            if [[ $cur = -* ]]; then
+                                _fscrypt_complete_option \
+                                    --source= --name= --key= --user=
+                            else
+                                _fscrypt_complete_mountpoint
+                            fi ;;
+                        *)
+                            # Unrecognized subsubcommand…  Suppose a new
+                            # unknown subsubcommand and complete with
+                            # global options only
+                            _fscrypt_complete_option
+                            ;;
+                    esac
+                    ;;
+                *)
+                    # Unrecognized subcommand…  Suppose a new unknown
+                    # subcommand and complete with global options only
+                    _fscrypt_complete_option
+                    ;;
+             esac
+            ;;
+        *)
+            # Unrecognized command…  Suppose a new unknown command and
+            # complete with global options only
+            _fscrypt_complete_option
+            ;;
+    esac
+
+    # When the sole offered completion is --*=, do not put a space after
+    # the equal sign as we wait for the argument value.
+    [[ ${#COMPREPLY[@]} == 1 ]] && [[ ${COMPREPLY[0]} == "--"*"=" ]] \
+        && compopt -o nospace
+} &&
+    complete -F _fscrypt fscrypt
+
+# ex: filetype=bash
diff --git a/cmd/fscrypt/fscrypt_test.go b/cmd/fscrypt/fscrypt_test.go
new file mode 100644 (file)
index 0000000..1d09bf8
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * fscrypt_test.go - Stub test file that has one test that always passes.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import "testing"
+
+func TestTrivial(t *testing.T) {}
diff --git a/cmd/fscrypt/keys.go b/cmd/fscrypt/keys.go
new file mode 100644 (file)
index 0000000..b57c01d
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * keys.go - Functions and readers for getting passphrases and raw keys via
+ * the command line. Includes ability to hide the entered passphrase, or use a
+ * raw key as input.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import (
+       "bufio"
+       "fmt"
+       "io"
+       "log"
+       "os"
+
+       "github.com/pkg/errors"
+       "golang.org/x/term"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/pam"
+)
+
+// The file descriptor for standard input
+const stdinFd = 0
+
+// actions.KeyFuncs for getting or creating cryptographic keys
+var (
+       // getting an existing key
+       existingKeyFn = makeKeyFunc(true, false, "")
+       // getting an existing key when changing passphrases
+       oldExistingKeyFn = makeKeyFunc(true, false, "old ")
+       // creating a new key
+       createKeyFn = makeKeyFunc(false, true, "")
+       // creating a new key when changing passphrases
+       newCreateKeyFn = makeKeyFunc(false, true, "new ")
+)
+
+// passphraseReader is an io.Reader intended for terminal passphrase input. The
+// struct is empty as the reader needs to maintain no internal state.
+type passphraseReader struct{}
+
+// Read gets input from the terminal until a newline is encountered or the given
+// buffer is full.
+func (p passphraseReader) Read(buf []byte) (int, error) {
+       // We read one byte at a time to handle backspaces
+       position := 0
+       for {
+               if position == len(buf) {
+                       return position, nil
+               }
+               if _, err := io.ReadFull(os.Stdin, buf[position:position+1]); err != nil {
+                       return position, err
+               }
+               switch buf[position] {
+               case '\r', '\n':
+                       return position, io.EOF
+               case 3, 4:
+                       return position, ErrCanceled
+               case 8, 127:
+                       if position > 0 {
+                               position--
+                       }
+               default:
+                       position++
+               }
+       }
+}
+
+// getPassphraseKey puts the terminal into raw mode for the entry of the user's
+// passphrase into a key. If we are not reading from a terminal, just read into
+// the passphrase into the key normally.
+func getPassphraseKey(prompt string) (*crypto.Key, error) {
+
+       // Only disable echo if stdin is actually a terminal.
+       if term.IsTerminal(stdinFd) {
+               state, err := term.MakeRaw(stdinFd)
+               if err != nil {
+                       return nil, err
+               }
+               defer func() {
+                       term.Restore(stdinFd, state)
+                       fmt.Println() // To align input
+               }()
+       }
+
+       if !quietFlag.Value {
+               fmt.Print(prompt)
+       }
+
+       return crypto.NewKeyFromReader(passphraseReader{})
+}
+
+func makeRawKey(info actions.ProtectorInfo) (*crypto.Key, error) {
+       // When running non-interactively and no key was provided,
+       // try to read it from stdin
+       if keyFileFlag.Value == "" && !term.IsTerminal(stdinFd) {
+               return crypto.NewFixedLengthKeyFromReader(bufio.NewReader(os.Stdin),
+                       metadata.InternalKeyLen)
+       }
+
+       prompt := fmt.Sprintf("Enter key file for protector %q: ", info.Name())
+       // Raw keys use a file containing the key data.
+       file, err := promptForKeyFile(prompt)
+       if err != nil {
+               return nil, err
+       }
+       defer file.Close()
+
+       fileInfo, err := file.Stat()
+       if err != nil {
+               return nil, err
+       }
+
+       if fileInfo.Size() != metadata.InternalKeyLen {
+               return nil, errors.Wrap(ErrKeyFileLength, file.Name())
+       }
+       return crypto.NewFixedLengthKeyFromReader(file, metadata.InternalKeyLen)
+}
+
+// makeKeyFunc creates an actions.KeyFunc. This function customizes the KeyFunc
+// to whether or not it supports retrying, whether it confirms the passphrase,
+// and custom prefix for printing (if any).
+func makeKeyFunc(supportRetry, shouldConfirm bool, prefix string) actions.KeyFunc {
+       return func(info actions.ProtectorInfo, retry bool) (*crypto.Key, error) {
+               log.Printf("KeyFunc(%s, %v)", formatInfo(info), retry)
+               if retry {
+                       if !supportRetry {
+                               panic("this KeyFunc does not support retrying")
+                       }
+                       // Don't retry for non-interactive sessions
+                       if quietFlag.Value {
+                               return nil, ErrWrongKey
+                       }
+                       fmt.Println("Incorrect Passphrase")
+               }
+
+               switch info.Source() {
+               case metadata.SourceType_pam_passphrase:
+                       prompt := fmt.Sprintf("Enter %slogin passphrase for %s: ",
+                               prefix, formatUsername(info.UID()))
+                       key, err := getPassphraseKey(prompt)
+                       if err != nil {
+                               return nil, err
+                       }
+
+                       // To confirm, check that the passphrase is the user's
+                       // login passphrase.
+                       if shouldConfirm {
+                               username, err := usernameFromID(info.UID())
+                               if err != nil {
+                                       key.Wipe()
+                                       return nil, err
+                               }
+
+                               err = pam.IsUserLoginToken(username, key, quietFlag.Value)
+                               if err != nil {
+                                       key.Wipe()
+                                       return nil, err
+                               }
+                       }
+                       return key, nil
+
+               case metadata.SourceType_custom_passphrase:
+                       prompt := fmt.Sprintf("Enter %scustom passphrase for protector %q: ",
+                               prefix, info.Name())
+                       key, err := getPassphraseKey(prompt)
+                       if err != nil {
+                               return nil, err
+                       }
+
+                       // To confirm, make sure the user types the same
+                       // passphrase in again.
+                       if shouldConfirm && !quietFlag.Value {
+                               key2, err := getPassphraseKey("Confirm passphrase: ")
+                               if err != nil {
+                                       key.Wipe()
+                                       return nil, err
+                               }
+                               defer key2.Wipe()
+
+                               if !key.Equals(key2) {
+                                       key.Wipe()
+                                       return nil, ErrPassphraseMismatch
+                               }
+                       }
+                       return key, nil
+
+               case metadata.SourceType_raw_key:
+                       // Only use prefixes with passphrase protectors.
+                       if prefix != "" {
+                               return nil, ErrNotPassphrase
+                       }
+                       return makeRawKey(info)
+
+               default:
+                       return nil, ErrInvalidSource
+               }
+       }
+}
diff --git a/cmd/fscrypt/prompt.go b/cmd/fscrypt/prompt.go
new file mode 100644 (file)
index 0000000..d34a18a
--- /dev/null
@@ -0,0 +1,328 @@
+/*
+ * prompt.go - Functions for handling user input and options
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import (
+       "fmt"
+       "log"
+       "os"
+       "os/user"
+       "strconv"
+       "strings"
+
+       "github.com/pkg/errors"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+const (
+       // Suffixes for questions with a yes or no default
+       defaultYesSuffix = " [Y/n] "
+       defaultNoSuffix  = " [y/N] "
+)
+
+// Descriptions for each of the protector sources
+var sourceDescriptions = map[metadata.SourceType]string{
+       metadata.SourceType_pam_passphrase:    "Your login passphrase",
+       metadata.SourceType_custom_passphrase: "A custom passphrase",
+       metadata.SourceType_raw_key:           "A raw 256-bit key",
+}
+
+// askQuestion asks the user a yes or no question. Returning a boolean on a
+// successful answer and an error if there was not a response from the user.
+// Returns the defaultChoice on empty input (or in quiet mode).
+func askQuestion(question string, defaultChoice bool) (bool, error) {
+       // If in quiet mode, we just use the default
+       if quietFlag.Value {
+               return defaultChoice, nil
+       }
+       // Loop until failure or valid input
+       for {
+               if defaultChoice {
+                       fmt.Print(question + defaultYesSuffix)
+               } else {
+                       fmt.Print(question + defaultNoSuffix)
+               }
+
+               input, err := util.ReadLine()
+               if err != nil {
+                       return false, err
+               }
+
+               switch strings.ToLower(input) {
+               case "y", "yes":
+                       return true, nil
+               case "n", "no":
+                       return false, nil
+               case "":
+                       return defaultChoice, nil
+               }
+       }
+}
+
+// askConfirmation asks the user for confirmation of a specific action. An error
+// is returned if the user declines or IO fails.
+func askConfirmation(question string, defaultChoice bool, warning string) error {
+       // All confirmations are "yes" if we are forcing.
+       if forceFlag.Value {
+               return nil
+       }
+
+       // Defaults of "no" require forcing.
+       if !defaultChoice {
+               if quietFlag.Value {
+                       return ErrNoDestructiveOps
+               }
+       }
+
+       if warning != "" && !quietFlag.Value {
+               fmt.Println(wrapText("WARNING: "+warning, 0))
+       }
+
+       confirmed, err := askQuestion(question, defaultChoice)
+       if err != nil {
+               return err
+       }
+       if !confirmed {
+               return ErrCanceled
+       }
+       return nil
+}
+
+// usernameFromID returns the username for the provided UID. If the UID does not
+// correspond to a user or the username is blank, an error is returned.
+func usernameFromID(uid int64) (string, error) {
+       u, err := user.LookupId(strconv.Itoa(int(uid)))
+       if err != nil || u.Username == "" {
+               return "", errors.Wrapf(ErrUnknownUser, "uid %d", uid)
+       }
+       return u.Username, nil
+}
+
+// formatUsername either returns the username for the provided UID, or a string
+// containing the error for unknown UIDs.
+func formatUsername(uid int64) string {
+       username, err := usernameFromID(uid)
+       if err != nil {
+               return fmt.Sprintf("[%v]", err)
+       }
+       return username
+}
+
+// formatInfo gives a string description of metadata.ProtectorData.
+func formatInfo(data actions.ProtectorInfo) string {
+       switch data.Source() {
+       case metadata.SourceType_pam_passphrase:
+               return "login protector for " + formatUsername(data.UID())
+       case metadata.SourceType_custom_passphrase:
+               return fmt.Sprintf("custom protector %q", data.Name())
+       case metadata.SourceType_raw_key:
+               return fmt.Sprintf("raw key protector %q", data.Name())
+       default:
+               panic(ErrInvalidSource)
+       }
+}
+
+// promptForName gets a name from user input (or flags) and returns it.
+func promptForName(ctx *actions.Context) (string, error) {
+       // A name flag means we do not need to prompt
+       if nameFlag.Value != "" {
+               return nameFlag.Value, nil
+       }
+
+       // Don't ask for a name if we do not need it
+       if quietFlag.Value || ctx.Config.Source == metadata.SourceType_pam_passphrase {
+               return "", nil
+       }
+
+       for {
+               fmt.Print("Enter a name for the new protector: ")
+               name, err := util.ReadLine()
+               if err != nil {
+                       return "", err
+               }
+               if name != "" {
+                       return name, nil
+               }
+       }
+}
+
+// promptForSource gets a source type from user input (or flags) and modifies
+// the context to use that source.
+func promptForSource(ctx *actions.Context) error {
+       // A source flag overrides everything else.
+       if sourceFlag.Value != "" {
+               val, ok := metadata.SourceType_value[sourceFlag.Value]
+               if !ok || val == 0 {
+                       return ErrInvalidSource
+               }
+               ctx.Config.Source = metadata.SourceType(val)
+               return nil
+       }
+
+       // Just use the default in quiet mode
+       if quietFlag.Value {
+               return nil
+       }
+
+       // We print all the sources with their number, description, and name.
+       fmt.Println("The following protector sources are available:")
+       for idx := 1; idx < len(metadata.SourceType_value); idx++ {
+               source := metadata.SourceType(idx)
+               description := sourceDescriptions[source]
+               fmt.Printf("%d - %s (%s)\n", idx, description, source)
+       }
+
+       for {
+               fmt.Printf("Enter the source number for the new protector [%d - %s]: ",
+                       ctx.Config.Source, ctx.Config.Source)
+               input, err := util.ReadLine()
+               if err != nil {
+                       return err
+               }
+
+               // Use the default if the user just hits enter
+               if input == "" {
+                       return nil
+               }
+
+               // Check for a valid index, reprompt if invalid.
+               index, err := strconv.Atoi(input)
+               if err == nil && index >= 1 && index < len(metadata.SourceType_value) {
+                       ctx.Config.Source = metadata.SourceType(index)
+                       return nil
+               }
+       }
+}
+
+// promptForKeyFile returns an open file that should be used to create or unlock
+// a raw_key protector. Be sure to close the file when done.
+func promptForKeyFile(prompt string) (*os.File, error) {
+       // If specified on the command line, we only try no open it once.
+       if keyFileFlag.Value != "" {
+               return os.Open(keyFileFlag.Value)
+       }
+       if quietFlag.Value {
+               return nil, ErrSpecifyKeyFile
+       }
+
+       // Prompt for a valid path until we get a file we can open.
+       for {
+               fmt.Print(prompt)
+               filename, err := util.ReadLine()
+               if err != nil {
+                       return nil, err
+               }
+               file, err := os.Open(filename)
+               if err == nil {
+                       return file, nil
+               }
+               fmt.Println(err)
+       }
+
+}
+
+// promptForProtector, given a non-empty list of protector options, uses user
+// input to select the desired protector. If there is only one option to choose
+// from, that protector is automatically selected.
+func promptForProtector(options []*actions.ProtectorOption) (int, error) {
+       numOptions := len(options)
+       log.Printf("selecting from %s", pluralize(numOptions, "protector"))
+
+       // Get the number of load errors.
+       numLoadErrors := 0
+       for _, option := range options {
+               if option.LoadError != nil {
+                       log.Printf("when loading option: %v", option.LoadError)
+                       numLoadErrors++
+               }
+       }
+
+       if numLoadErrors == numOptions {
+               return 0, ErrAllLoadsFailed
+       }
+       if numOptions == 1 {
+               return 0, nil
+       }
+       if quietFlag.Value {
+               return 0, ErrSpecifyProtector
+       }
+
+       // List all of the protector options which did not have a load error.
+       fmt.Println("The available protectors are: ")
+       for idx, option := range options {
+               if option.LoadError != nil {
+                       continue
+               }
+
+               description := fmt.Sprintf("%d - %s", idx, formatInfo(option.ProtectorInfo))
+               if option.LinkedMount != nil {
+                       description += fmt.Sprintf(" (linked protector on %q)", option.LinkedMount.Path)
+               }
+               fmt.Println(description)
+       }
+
+       if numLoadErrors > 0 {
+               loadWarning := fmt.Sprintf("NOTE: %d of the %d protectors failed to load. ", numLoadErrors, numOptions)
+               fmt.Print(wrapText(loadWarning+loadHelpText, 0) + "\n")
+       }
+
+       for {
+               fmt.Print("Enter the number of protector to use: ")
+               input, err := util.ReadLine()
+               if err != nil {
+                       return 0, err
+               }
+
+               // Check for a valid index, reprompt if invalid.
+               index, err := strconv.Atoi(input)
+               if err == nil && index >= 0 && index < len(options) {
+                       return index, nil
+               }
+       }
+}
+
+// optionFn is an actions.OptionFunc which handles selecting an option for a
+// specific policy. This is either done interactively, or by deferring to the
+// protectorFlag.
+func optionFn(policyDescriptor string, options []*actions.ProtectorOption) (int, error) {
+       // If we have an unlock-with flag, we directly select the specified
+       // protector to unlock the policy.
+       if unlockWithFlag.Value != "" {
+               log.Printf("optionFn(%s) w/ unlock flag", policyDescriptor)
+               protector, err := getProtectorFromFlag(unlockWithFlag.Value, nil)
+               if err != nil {
+                       return 0, err
+               }
+
+               for idx, option := range options {
+                       if option.Descriptor() == protector.Descriptor() {
+                               return idx, nil
+                       }
+               }
+               return 0, &actions.ErrNotProtected{PolicyDescriptor: policyDescriptor,
+                       ProtectorDescriptor: protector.Descriptor()}
+       }
+
+       log.Printf("optionFn(%s)", policyDescriptor)
+       return promptForProtector(options)
+}
diff --git a/cmd/fscrypt/protector.go b/cmd/fscrypt/protector.go
new file mode 100644 (file)
index 0000000..186ca7a
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * protector.go - Functions for creating and getting action.Protectors which
+ * ensure that login passphrases are on the correct filesystem.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import (
+       "fmt"
+       "log"
+       "os/user"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+// createProtector makes a new protector on either ctx.Mount or if the requested
+// source is a pam_passphrase, creates it on the root filesystem. Prompts for
+// user input are used to get the source, name and keys.
+func createProtectorFromContext(ctx *actions.Context) (*actions.Protector, error) {
+       if err := promptForSource(ctx); err != nil {
+               return nil, err
+       }
+       log.Printf("using source: %s", ctx.Config.Source.String())
+       if ctx.Config.Source == metadata.SourceType_pam_passphrase {
+               if userFlag.Value == "" && util.IsUserRoot() {
+                       return nil, ErrSpecifyUser
+               }
+               if !quietFlag.Value {
+                       fmt.Print(`
+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
+
+`)
+               }
+       }
+
+       name, err := promptForName(ctx)
+       if err != nil {
+               return nil, err
+       }
+       log.Printf("using name: %s", name)
+
+       // We only want to create new login protectors on the root filesystem.
+       // So we make a new context if necessary.
+       if ctx.Config.Source == metadata.SourceType_pam_passphrase &&
+               ctx.Mount.Path != actions.LoginProtectorMountpoint {
+               log.Printf("creating login protector on %q instead of %q",
+                       actions.LoginProtectorMountpoint, ctx.Mount.Path)
+               if ctx, err = modifiedContext(ctx); err != nil {
+                       return nil, err
+               }
+       }
+
+       var owner *user.User
+       if ctx.Config.Source == metadata.SourceType_pam_passphrase && util.IsUserRoot() {
+               owner = ctx.TargetUser
+       }
+       return actions.CreateProtector(ctx, name, createKeyFn, owner)
+}
+
+// selectExistingProtector returns a locked Protector which corresponds to an
+// option in the non-empty slice of options. Prompts for user input are used to
+// get the keys and select the option.
+func selectExistingProtector(ctx *actions.Context, options []*actions.ProtectorOption) (*actions.Protector, error) {
+       idx, err := promptForProtector(options)
+       if err != nil {
+               return nil, err
+       }
+       option := options[idx]
+
+       log.Printf("using %s", formatInfo(option.ProtectorInfo))
+       return actions.GetProtectorFromOption(ctx, option)
+}
+
+// expandedProtectorOptions gets all the actions.ProtectorOptions for ctx.Mount
+// as well as any pam_passphrase protectors for the root filesystem.
+func expandedProtectorOptions(ctx *actions.Context) ([]*actions.ProtectorOption, error) {
+       options, err := ctx.ProtectorOptions()
+       if err != nil {
+               return nil, err
+       }
+
+       // Do nothing different if we are at the root, or cannot load the root.
+       if ctx.Mount.Path == actions.LoginProtectorMountpoint {
+               return options, nil
+       }
+       if ctx, err = modifiedContext(ctx); err != nil {
+               log.Print(err)
+               return options, nil
+       }
+       rootOptions, err := ctx.ProtectorOptions()
+       if err != nil {
+               log.Print(err)
+               return options, nil
+       }
+       log.Print("adding additional ProtectorOptions")
+
+       // Keep track of what we have seen, so we don't have duplicates
+       seenOptions := make(map[string]bool)
+       for _, option := range options {
+               seenOptions[option.Descriptor()] = true
+       }
+
+       for _, option := range rootOptions {
+               // Add in unseen passphrase protectors on the root filesystem
+               // to the options list as potential linked protectors.
+               if option.Source() == metadata.SourceType_pam_passphrase &&
+                       !seenOptions[option.Descriptor()] {
+                       option.LinkedMount = ctx.Mount
+                       options = append(options, option)
+               }
+       }
+
+       return options, nil
+}
+
+// modifiedContext returns a copy of ctx with the mountpoint replaced by
+// LoginProtectorMountpoint.
+func modifiedContext(ctx *actions.Context) (*actions.Context, error) {
+       mnt, err := filesystem.GetMount(actions.LoginProtectorMountpoint)
+       if err != nil {
+               return nil, err
+       }
+
+       modifiedCtx := *ctx
+       modifiedCtx.Mount = mnt
+       return &modifiedCtx, nil
+}
diff --git a/cmd/fscrypt/setup.go b/cmd/fscrypt/setup.go
new file mode 100644 (file)
index 0000000..b9a16e8
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * setup.go - File containing the functionality for initializing directories and
+ * the global config file.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import (
+       "fmt"
+       "io"
+       "os"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/util"
+)
+
+// createGlobalConfig creates (or overwrites) the global config file
+func createGlobalConfig(w io.Writer, path string) error {
+       if !util.IsUserRoot() {
+               return ErrMustBeRoot
+       }
+
+       // If the config file already exists, ask to replace it
+       _, err := os.Stat(path)
+       switch {
+       case err == nil:
+               err = askConfirmation(fmt.Sprintf("Replace %q?", path), false, "")
+               if err == nil {
+                       err = os.Remove(path)
+               }
+       case os.IsNotExist(err):
+               err = nil
+       }
+       if err != nil {
+               return err
+       }
+
+       // v2 encryption policies are recommended, so set policy_version 2 when
+       // the kernel supports it. v2 policies are supported by upstream Linux
+       // v5.4 and later. For now we simply check the kernel version. Ideally
+       // we'd instead check whether setting a v2 policy actually works, in
+       // order to also detect backports of the kernel patches. However, that's
+       // hard because from this context (creating /etc/fscrypt.conf) we may
+       // not yet have access to a filesystem that supports encryption.
+       var policyVersion int64
+       if util.IsKernelVersionAtLeast(5, 4) {
+               fmt.Fprintln(w, "Defaulting to policy_version 2 because kernel supports it.")
+               policyVersion = 2
+       } else {
+               fmt.Fprintln(w, "Defaulting to policy_version 1 because kernel doesn't support v2.")
+       }
+       fmt.Fprintln(w, "Customizing passphrase hashing difficulty for this system...")
+       err = actions.CreateConfigFile(timeTargetFlag.Value, policyVersion)
+       if err != nil {
+               return err
+       }
+
+       fmt.Fprintf(w, "Created global config file at %q.\n", path)
+       return nil
+}
+
+// setupFilesystem creates the directories for a filesystem to use fscrypt.
+func setupFilesystem(w io.Writer, path string) error {
+       ctx, err := actions.NewContextFromMountpoint(path, nil)
+       if err != nil {
+               return err
+       }
+       username := ctx.TargetUser.Username
+
+       err = ctx.Mount.CheckSetup(ctx.TrustedUser)
+       if err == nil {
+               return &filesystem.ErrAlreadySetup{Mount: ctx.Mount}
+       }
+       if _, ok := err.(*filesystem.ErrNotSetup); !ok {
+               return err
+       }
+
+       allUsers := allUsersSetupFlag.Value
+       if !allUsers {
+               thisFilesystem := "this filesystem"
+               if ctx.Mount.Path == "/" {
+                       thisFilesystem = "the root filesystem"
+               }
+               prompt := fmt.Sprintf(`Allow users other than %s to create
+fscrypt metadata on %s? (See
+https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem)`,
+                       username, thisFilesystem)
+               allUsers, err = askQuestion(wrapText(prompt, 0), false)
+               if err != nil {
+                       return err
+               }
+       }
+       var setupMode filesystem.SetupMode
+       if allUsers {
+               setupMode = filesystem.WorldWritable
+       } else {
+               setupMode = filesystem.SingleUserWritable
+       }
+       if err = ctx.Mount.Setup(setupMode); err != nil {
+               return err
+       }
+
+       if allUsers {
+               fmt.Fprintf(w, "Metadata directories created at %q, writable by everyone.\n",
+                       ctx.Mount.BaseDir())
+       } else {
+               fmt.Fprintf(w, "Metadata directories created at %q, writable by %s only.\n",
+                       ctx.Mount.BaseDir(), username)
+       }
+       return nil
+}
diff --git a/cmd/fscrypt/status.go b/cmd/fscrypt/status.go
new file mode 100644 (file)
index 0000000..bc8f1ee
--- /dev/null
@@ -0,0 +1,230 @@
+/*
+ * status.go - File which contains the functions for outputting the status of
+ * fscrypt, a filesystem, or a directory.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import (
+       "fmt"
+       "io"
+       "log"
+       "strings"
+       "text/tabwriter"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/keyring"
+)
+
+// Creates a writer which correctly aligns tabs with the specified header.
+// Must call Flush() when done.
+func makeTableWriter(w io.Writer, header string) *tabwriter.Writer {
+       tableWriter := tabwriter.NewWriter(w, 0, indentLength, indentLength, ' ', 0)
+       fmt.Fprintln(tableWriter, header)
+       return tableWriter
+}
+
+// encryptionStatus will be printed in the ENCRYPTION column. An empty string
+// indicates the filesystem should not be printed.
+func encryptionStatus(err error) string {
+       if err == nil {
+               return "supported"
+       }
+       switch err.(type) {
+       case *filesystem.ErrEncryptionNotEnabled:
+               return "not enabled"
+       case *filesystem.ErrEncryptionNotSupported:
+               return "not supported"
+       default:
+               // Unknown error regarding support
+               return ""
+       }
+}
+
+func yesNoString(b bool) string {
+       if b {
+               return "Yes"
+       }
+       return "No"
+}
+
+func policyUnlockedStatus(policy *actions.Policy, path string) string {
+       status := policy.GetProvisioningStatus()
+
+       // Due to a limitation in the old kernel API for fscrypt, for v1
+       // policies using the user keyring that are incompletely locked or are
+       // unlocked by another user, we'll get KeyAbsent.  If we have a
+       // directory path, use a heuristic to try to detect these cases.
+       if status == keyring.KeyAbsent && policy.NeedsUserKeyring() &&
+               path != "" && isDirUnlockedHeuristic(path) {
+               return "Partially (incompletely locked, or unlocked by another user)"
+       }
+
+       switch status {
+       case keyring.KeyPresent, keyring.KeyPresentButOnlyOtherUsers:
+               return "Yes"
+       case keyring.KeyAbsent:
+               return "No"
+       case keyring.KeyAbsentButFilesBusy:
+               return "Partially (incompletely locked)"
+       default:
+               return "Unknown"
+       }
+}
+
+// writeGlobalStatus prints all the filesystems that use (or could use) fscrypt.
+func writeGlobalStatus(w io.Writer) error {
+       mounts, err := filesystem.AllFilesystems()
+       if err != nil {
+               return err
+       }
+
+       supportCount := 0
+       useCount := 0
+
+       t := makeTableWriter(w, "MOUNTPOINT\tDEVICE\tFILESYSTEM\tENCRYPTION\tFSCRYPT")
+       for _, mount := range mounts {
+               // Only print mountpoints backed by devices or using fscrypt.
+               usingFscrypt := mount.CheckSetup(nil) == nil
+               if !usingFscrypt && mount.Device == "" {
+                       continue
+               }
+
+               // Only print a mountpoint if we can determine its support.
+               supportErr := mount.CheckSupport()
+               supportString := encryptionStatus(supportErr)
+               if supportString == "" {
+                       log.Print(supportErr)
+                       continue
+               }
+
+               fmt.Fprintf(t, "%s\t%s\t%s\t%s\t%s\n",
+                       filesystem.EscapeString(mount.Path),
+                       filesystem.EscapeString(mount.Device),
+                       filesystem.EscapeString(mount.FilesystemType),
+                       supportString, yesNoString(usingFscrypt))
+
+               if supportErr == nil {
+                       supportCount++
+               }
+               if usingFscrypt {
+                       useCount++
+               }
+       }
+
+       fmt.Fprintf(w, "filesystems supporting encryption: %d\n", supportCount)
+       fmt.Fprintf(w, "filesystems with fscrypt metadata: %d\n\n", useCount)
+       return t.Flush()
+}
+
+// writeOptions writes a table of the status for a slice of protector options.
+func writeOptions(w io.Writer, options []*actions.ProtectorOption) {
+       t := makeTableWriter(w, "PROTECTOR\tLINKED\tDESCRIPTION")
+       for _, option := range options {
+               if option.LoadError != nil {
+                       fmt.Fprintf(t, "%s\t\t[%s]\n", option.Descriptor(), option.LoadError)
+                       continue
+               }
+
+               // For linked protectors, indicate which filesystem.
+               isLinked := option.LinkedMount != nil
+               linkedText := yesNoString(isLinked)
+               if isLinked {
+                       linkedText += fmt.Sprintf(" (%s)", option.LinkedMount.Path)
+               }
+               fmt.Fprintf(t, "%s\t%s\t%s\n", option.Descriptor(), linkedText,
+                       formatInfo(option.ProtectorInfo))
+       }
+       t.Flush()
+}
+
+func writeFilesystemStatus(w io.Writer, ctx *actions.Context) error {
+       options, err := ctx.ProtectorOptions()
+       if err != nil {
+               return err
+       }
+
+       policyDescriptors, err := ctx.Mount.ListPolicies(ctx.TrustedUser)
+       if err != nil {
+               return err
+       }
+
+       filterDescription := ""
+       if ctx.TrustedUser != nil {
+               filterDescription = fmt.Sprintf(" (only including ones owned by %s or root)", ctx.TrustedUser.Username)
+       }
+       fmt.Fprintf(w, "%s filesystem %q has %s and %s%s.\n", ctx.Mount.FilesystemType,
+               ctx.Mount.Path, pluralize(len(options), "protector"),
+               pluralize(len(policyDescriptors), "policy"), filterDescription)
+       if setupMode, user, err := ctx.Mount.GetSetupMode(); err == nil {
+               switch setupMode {
+               case filesystem.WorldWritable:
+                       fmt.Fprintf(w, "All users can create fscrypt metadata on this filesystem.\n")
+               case filesystem.SingleUserWritable:
+                       fmt.Fprintf(w, "Only %s can create fscrypt metadata on this filesystem.\n", user.Username)
+               }
+       }
+       fmt.Fprintf(w, "\n")
+
+       if len(options) > 0 {
+               writeOptions(w, options)
+       }
+
+       if len(policyDescriptors) == 0 {
+               return nil
+       }
+
+       fmt.Fprintln(w)
+       t := makeTableWriter(w, "POLICY\tUNLOCKED\tPROTECTORS")
+       for _, descriptor := range policyDescriptors {
+               policy, err := actions.GetPolicy(ctx, descriptor)
+               if err != nil {
+                       fmt.Fprintf(t, "%s\t\t[%s]\n", descriptor, err)
+                       continue
+               }
+
+               fmt.Fprintf(t, "%s\t%s\t%s\n", descriptor,
+                       policyUnlockedStatus(policy, ""),
+                       strings.Join(policy.ProtectorDescriptors(), ", "))
+       }
+       return t.Flush()
+}
+
+func writePathStatus(w io.Writer, path string) error {
+       ctx, err := actions.NewContextFromPath(path, nil)
+       if err != nil {
+               return err
+       }
+       policy, err := actions.GetPolicyFromPath(ctx, path)
+       if err != nil {
+               return err
+       }
+
+       fmt.Fprintf(w, "%q is encrypted with fscrypt.\n", path)
+       fmt.Fprintln(w)
+       fmt.Fprintf(w, "Policy:   %s\n", policy.Descriptor())
+       fmt.Fprintf(w, "Options:  %s\n", policy.Options())
+       fmt.Fprintf(w, "Unlocked: %s\n", policyUnlockedStatus(policy, path))
+       fmt.Fprintln(w)
+
+       options := policy.ProtectorOptions()
+       fmt.Fprintf(w, "Protected with %s:\n", pluralize(len(options), "protector"))
+       writeOptions(w, options)
+       return nil
+}
diff --git a/cmd/fscrypt/strings.go b/cmd/fscrypt/strings.go
new file mode 100644 (file)
index 0000000..cd51968
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * strings.go - File which contains the specific strings used for output and
+ * formatting in fscrypt.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import (
+       "fmt"
+       "strings"
+)
+
+const shortUsage = "A tool for managing Linux native filesystem encryption"
+
+// Argument usage strings
+const (
+       directoryArg    = "DIRECTORY"
+       mountpointArg   = "MOUNTPOINT"
+       pathArg         = "PATH"
+       mountpointIDArg = mountpointArg + ":ID"
+)
+
+// Text Templates which format our command line output (using text/template)
+var (
+       // indent is the prefix for the output lines in each section
+       indent = strings.Repeat(" ", indentLength)
+       // Top level help output: what is printed for "fscrypt" or "fscrypt --help"
+       appHelpTemplate = `{{.HelpName}} - {{.Usage}}
+
+Usage:
+` + indent + `{{.HelpName}} COMMAND [arguments] [options]
+
+Commands:{{range .VisibleCommands}}
+` + indent + `{{join .Names ", "}}{{"\t- "}}{{.Usage}}{{end}}
+{{if .Description}}
+Description:
+` + indent + `{{.Description}}
+{{end}}
+Options:
+{{range .VisibleFlags}}{{.}}
+
+{{end}}`
+
+       // Command help output, used when a command has no subcommands
+       commandHelpTemplate = `{{.HelpName}} - {{.Usage}}
+
+Usage:
+` + indent + `{{.HelpName}}{{if .ArgsUsage}} {{.ArgsUsage}}{{end}}{{if .VisibleFlags}} [options]{{end}}
+{{if .Description}}
+Description:
+` + indent + `{{.Description}}
+{{end}}{{if .VisibleFlags}}
+Options:
+{{range .VisibleFlags}}{{.}}
+
+{{end}}{{end}}`
+
+       // Subcommand help output, used when a command has subcommands
+       subcommandHelpTemplate = `{{.HelpName}} - {{.Usage}}
+
+Usage:
+` + indent + `{{.HelpName}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}SUBCOMMAND [arguments]{{end}}{{if .VisibleFlags}} [options]{{end}}
+
+Subcommands:{{range .VisibleCommands}}
+` + indent + `{{join .Names ", "}}{{"\t- "}}{{.Usage}}{{end}}
+{{if .Description}}
+Description:
+` + indent + `{{.Description}}
+{{end}}{{if .VisibleFlags}}
+Options:
+{{range .VisibleFlags}}{{.}}
+
+{{end}}{{end}}`
+)
+
+// Add words to this map to have pluralize support them.
+var plurals = map[string]string{
+       "argument":   "arguments",
+       "filesystem": "filesystems",
+       "protector":  "protectors",
+       "policy":     "policies",
+}
+
+// pluralize prints out the correct pluralization of a word along with the
+// specified count. This means pluralize(1, "policy") = "1 policy" but
+// pluralize(2, "policy") = "2 policies"
+func pluralize(count int, word string) string {
+       if count != 1 {
+               word = plurals[word]
+       }
+       return fmt.Sprintf("%d %s", count, word)
+}
diff --git a/crypto/crypto.go b/crypto/crypto.go
new file mode 100644 (file)
index 0000000..6a719dd
--- /dev/null
@@ -0,0 +1,228 @@
+/*
+ * crypto.go - Cryptographic algorithms used by the rest of fscrypt.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+// Package crypto manages all the cryptography for fscrypt. This includes:
+//  1. Key management (key.go)
+//     - Securely holding keys in memory
+//     - Making recovery keys
+//  2. Randomness (rand.go)
+//  3. Cryptographic algorithms (crypto.go)
+//     - encryption (AES256-CTR)
+//     - authentication (SHA256-based HMAC)
+//     - key stretching (SHA256-based HKDF)
+//     - key wrapping/unwrapping (Encrypt then MAC)
+//     - passphrase-based key derivation (Argon2id)
+//     - key descriptor computation (double SHA512, or HKDF-SHA512)
+package crypto
+
+import (
+       "crypto/aes"
+       "crypto/cipher"
+       "crypto/hmac"
+       "crypto/sha256"
+       "crypto/sha512"
+       "encoding/hex"
+       "io"
+
+       "github.com/pkg/errors"
+       "golang.org/x/crypto/argon2"
+       "golang.org/x/crypto/hkdf"
+
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+// Crypto error values
+var (
+       ErrBadAuth      = errors.New("key authentication check failed")
+       ErrRecoveryCode = errors.New("invalid recovery code")
+       ErrMlockUlimit  = errors.New("could not lock key in memory")
+)
+
+// panicInputLength panics if "name" has invalid length (expected != actual)
+func panicInputLength(name string, expected, actual int) {
+       if err := util.CheckValidLength(expected, actual); err != nil {
+               panic(errors.Wrap(err, name))
+       }
+}
+
+// checkWrappingKey returns an error if the wrapping key has the wrong length
+func checkWrappingKey(wrappingKey *Key) error {
+       err := util.CheckValidLength(metadata.InternalKeyLen, wrappingKey.Len())
+       return errors.Wrap(err, "wrapping key")
+}
+
+// stretchKey stretches a key of length InternalKeyLen using unsalted HKDF to
+// make two keys of length InternalKeyLen.
+func stretchKey(key *Key) (encKey, authKey *Key) {
+       panicInputLength("hkdf key", metadata.InternalKeyLen, key.Len())
+
+       // The new hkdf function uses the hash and key to create a reader that
+       // can be used to securely initialize multiple keys. This means that
+       // reads on the hkdf give independent cryptographic keys. The hkdf will
+       // also always have enough entropy to read two keys.
+       hkdf := hkdf.New(sha256.New, key.data, nil, nil)
+
+       encKey, err := NewFixedLengthKeyFromReader(hkdf, metadata.InternalKeyLen)
+       util.NeverError(err)
+       authKey, err = NewFixedLengthKeyFromReader(hkdf, metadata.InternalKeyLen)
+       util.NeverError(err)
+
+       return
+}
+
+// aesCTR runs AES256-CTR on the input using the provided key and iv. This
+// function can be used to either encrypt or decrypt input of any size. Note
+// that input and output must be the same size.
+func aesCTR(key *Key, iv, input, output []byte) {
+       panicInputLength("aesCTR key", metadata.InternalKeyLen, key.Len())
+       panicInputLength("aesCTR iv", metadata.IVLen, len(iv))
+       panicInputLength("aesCTR output", len(input), len(output))
+
+       blockCipher, err := aes.NewCipher(key.data)
+       util.NeverError(err) // Key is checked to have correct length
+
+       stream := cipher.NewCTR(blockCipher, iv)
+       stream.XORKeyStream(output, input)
+}
+
+// getHMAC returns the SHA256-based HMAC of some data using the provided key.
+func getHMAC(key *Key, data ...[]byte) []byte {
+       panicInputLength("hmac key", metadata.InternalKeyLen, key.Len())
+
+       mac := hmac.New(sha256.New, key.data)
+       for _, buffer := range data {
+               // SHA256 HMAC should never be unable to write the data
+               _, err := mac.Write(buffer)
+               util.NeverError(err)
+       }
+
+       return mac.Sum(nil)
+}
+
+// Wrap takes a wrapping Key of length InternalKeyLen, and uses it to wrap a
+// secret Key of any length. This wrapping uses a random IV, the encrypted data,
+// and an HMAC to verify the wrapping key was correct. All of this is included
+// in the returned WrappedKeyData structure.
+func Wrap(wrappingKey, secretKey *Key) (*metadata.WrappedKeyData, error) {
+       if err := checkWrappingKey(wrappingKey); err != nil {
+               return nil, err
+       }
+
+       data := &metadata.WrappedKeyData{EncryptedKey: make([]byte, secretKey.Len())}
+
+       // Get random IV
+       var err error
+       if data.IV, err = NewRandomBuffer(metadata.IVLen); err != nil {
+               return nil, err
+       }
+
+       // Stretch key for encryption and authentication (unsalted).
+       encKey, authKey := stretchKey(wrappingKey)
+       defer encKey.Wipe()
+       defer authKey.Wipe()
+
+       // Encrypt the secret and include the HMAC of the output ("Encrypt-then-MAC").
+       aesCTR(encKey, data.IV, secretKey.data, data.EncryptedKey)
+
+       data.Hmac = getHMAC(authKey, data.IV, data.EncryptedKey)
+       return data, nil
+}
+
+// Unwrap takes a wrapping Key of length InternalKeyLen, and uses it to unwrap
+// the WrappedKeyData to get the unwrapped secret Key. The Wrapped Key data
+// includes an authentication check, so an error will be returned if that check
+// fails.
+func Unwrap(wrappingKey *Key, data *metadata.WrappedKeyData) (*Key, error) {
+       if err := checkWrappingKey(wrappingKey); err != nil {
+               return nil, err
+       }
+
+       // Stretch key for encryption and authentication (unsalted).
+       encKey, authKey := stretchKey(wrappingKey)
+       defer encKey.Wipe()
+       defer authKey.Wipe()
+
+       // Check validity of the HMAC
+       if !hmac.Equal(getHMAC(authKey, data.IV, data.EncryptedKey), data.Hmac) {
+               return nil, ErrBadAuth
+       }
+
+       secretKey, err := NewBlankKey(len(data.EncryptedKey))
+       if err != nil {
+               return nil, err
+       }
+       aesCTR(encKey, data.IV, data.EncryptedKey, secretKey.data)
+
+       return secretKey, nil
+}
+
+func computeKeyDescriptorV1(key *Key) string {
+       h1 := sha512.Sum512(key.data)
+       h2 := sha512.Sum512(h1[:])
+       length := hex.DecodedLen(metadata.PolicyDescriptorLenV1)
+       return hex.EncodeToString(h2[:length])
+}
+
+func computeKeyDescriptorV2(key *Key) (string, error) {
+       // This algorithm is specified by the kernel.  It uses unsalted
+       // HKDF-SHA512, where the application-information string is the prefix
+       // "fscrypt\0" followed by the HKDF_CONTEXT_KEY_IDENTIFIER byte.
+       hkdf := hkdf.New(sha512.New, key.data, nil, []byte("fscrypt\x00\x01"))
+       h := make([]byte, hex.DecodedLen(metadata.PolicyDescriptorLenV2))
+       if _, err := io.ReadFull(hkdf, h); err != nil {
+               return "", err
+       }
+       return hex.EncodeToString(h), nil
+}
+
+// ComputeKeyDescriptor computes the descriptor for a given cryptographic key.
+// If policyVersion=1, it uses the first 8 bytes of the double application of
+// SHA512 on the key. Use this for protectors and v1 policy keys.
+// If policyVersion=2, it uses HKDF-SHA512 to compute a key identifier that's
+// compatible with the kernel's key identifiers for v2 policy keys.
+// In both cases, the resulting bytes are formatted as hex.
+func ComputeKeyDescriptor(key *Key, policyVersion int64) (string, error) {
+       switch policyVersion {
+       case 1:
+               return computeKeyDescriptorV1(key), nil
+       case 2:
+               return computeKeyDescriptorV2(key)
+       default:
+               return "", errors.Errorf("policy version of %d is invalid", policyVersion)
+       }
+}
+
+// PassphraseHash uses Argon2id to produce a Key given the passphrase, salt, and
+// hashing costs. This method is designed to take a long time and consume
+// considerable memory. For more information, see the documentation at
+// https://godoc.org/golang.org/x/crypto/argon2.
+func PassphraseHash(passphrase *Key, salt []byte, costs *metadata.HashingCosts) (*Key, error) {
+       t := uint32(costs.Time)
+       m := uint32(costs.Memory)
+       p := uint8(costs.Parallelism)
+       key := argon2.IDKey(passphrase.data, salt, t, m, p, metadata.InternalKeyLen)
+
+       hash, err := NewBlankKey(metadata.InternalKeyLen)
+       if err != nil {
+               return nil, err
+       }
+       copy(hash.data, key)
+       return hash, nil
+}
diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go
new file mode 100644 (file)
index 0000000..1fa5a0c
--- /dev/null
@@ -0,0 +1,656 @@
+/*
+ * crypto_test.go - tests for the crypto package
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package crypto
+
+import (
+       "bytes"
+       "compress/zlib"
+       "crypto/aes"
+       "crypto/sha256"
+       "encoding/hex"
+       "fmt"
+       "io"
+       "os"
+       "testing"
+
+       "github.com/google/fscrypt/metadata"
+)
+
+// Reader that always returns the same byte
+type ConstReader byte
+
+func (r ConstReader) Read(b []byte) (n int, err error) {
+       for i := range b {
+               b[i] = byte(r)
+       }
+       return len(b), nil
+}
+
+// Makes a key of the same repeating byte
+func makeKey(b byte, n int) (*Key, error) {
+       return NewFixedLengthKeyFromReader(ConstReader(b), n)
+}
+
+var (
+       fakeSalt     = bytes.Repeat([]byte{'a'}, metadata.SaltLen)
+       fakePassword = []byte("password")
+
+       fakeValidPolicyKey, _ = makeKey(42, metadata.PolicyKeyLen)
+       fakeWrappingKey, _    = makeKey(17, metadata.InternalKeyLen)
+)
+
+// As the passphrase hashing function clears the passphrase, we need to make
+// a new passphrase key for each test
+func fakePassphraseKey() (*Key, error) {
+       return NewFixedLengthKeyFromReader(bytes.NewReader(fakePassword), len(fakePassword))
+}
+
+// Values for test cases pulled from argon2 command line tool.
+// To generate run:
+//
+//     echo "password" | argon2 "aaaaaaaaaaaaaaaa" -id -t <t> -m <m> -p <p> -l 32
+//
+// where costs.Time = <t>, costs.Memory = 2^<m>, and costs.Parallelism = <p>.
+type hashTestCase struct {
+       costs   *metadata.HashingCosts
+       hexHash string
+}
+
+var hashTestCases = []hashTestCase{
+       {
+               costs:   &metadata.HashingCosts{Time: 1, Memory: 1 << 10, Parallelism: 1},
+               hexHash: "a66f5398e33761bf161fdf1273e99b148f07d88d12d85b7673fddd723f95ec34",
+       },
+       // Make sure we maintain our backwards compatible behavior, where
+       // Parallelism is truncated to 8-bits unless TruncationFixed is true.
+       {
+               costs:   &metadata.HashingCosts{Time: 1, Memory: 1 << 10, Parallelism: 257},
+               hexHash: "a66f5398e33761bf161fdf1273e99b148f07d88d12d85b7673fddd723f95ec34",
+       },
+       {
+               costs:   &metadata.HashingCosts{Time: 10, Memory: 1 << 10, Parallelism: 1},
+               hexHash: "5fa2cb89db1f7413ba1776258b7c8ee8c377d122078d28fe1fd645c353787f50",
+       },
+       {
+               costs:   &metadata.HashingCosts{Time: 1, Memory: 1 << 15, Parallelism: 1},
+               hexHash: "f474a213ed14d16ead619568000939b938ddfbd2ac4a82d253afa81b5ebaef84",
+       },
+       {
+               costs:   &metadata.HashingCosts{Time: 1, Memory: 1 << 10, Parallelism: 10},
+               hexHash: "b7c3d7a0be222680b5ea3af3fb1a0b7b02b92cbd7007821dc8b84800c86c7783",
+       },
+       {
+               costs:   &metadata.HashingCosts{Time: 1, Memory: 1 << 11, Parallelism: 255},
+               hexHash: "d51af3775bbdd0cba31d96fd6d921d9de27f521ceffe667618cd7624f6643071",
+       },
+       // Adding TruncationFixed shouldn't matter if Parallelism < 256.
+       {
+               costs:   &metadata.HashingCosts{Time: 1, Memory: 1 << 11, Parallelism: 255, TruncationFixed: true},
+               hexHash: "d51af3775bbdd0cba31d96fd6d921d9de27f521ceffe667618cd7624f6643071",
+       },
+}
+
+// Checks that len(array) == expected
+func lengthCheck(name string, array []byte, expected int) error {
+       if len(array) != expected {
+               return fmt.Errorf("length of %s should be %d", name, expected)
+       }
+       return nil
+}
+
+// Tests the two ways of making keys
+func TestMakeKeys(t *testing.T) {
+       data := []byte("1234\n6789")
+
+       key1, err := NewKeyFromReader(bytes.NewReader(data))
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer key1.Wipe()
+       if !bytes.Equal(data, key1.data) {
+               t.Error("Key from reader contained incorrect data")
+       }
+
+       key2, err := NewFixedLengthKeyFromReader(bytes.NewReader(data), 6)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer key2.Wipe()
+       if !bytes.Equal([]byte("1234\n6"), key2.data) {
+               t.Error("Fixed length key from reader contained incorrect data")
+       }
+}
+
+// Tests that wipe succeeds
+func TestWipe(t *testing.T) {
+       key, err := makeKey(1, 1000)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if err := key.Wipe(); err != nil {
+               t.Error(err)
+       }
+}
+
+// Making keys with negative length should fail
+func TestInvalidLength(t *testing.T) {
+       key, err := NewFixedLengthKeyFromReader(ConstReader(1), -1)
+       if err == nil {
+               key.Wipe()
+               t.Error("Negative lengths should cause failure")
+       }
+}
+
+// Test making keys of zero length
+func TestZeroLength(t *testing.T) {
+       key1, err := NewFixedLengthKeyFromReader(os.Stdin, 0)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer key1.Wipe()
+       if key1.data != nil {
+               t.Error("Fixed length key from reader contained data")
+       }
+
+       key2, err := NewKeyFromReader(bytes.NewReader(nil))
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer key2.Wipe()
+       if key2.data != nil {
+               t.Error("Key from empty reader contained data")
+       }
+}
+
+// Test that enabling then disabling memory locking succeeds even if a key is
+// active when the variable changes.
+func TestEnableDisableMemoryLocking(t *testing.T) {
+       // Mlock on for creation, off for wiping
+       key, err := NewRandomKey(metadata.InternalKeyLen)
+       UseMlock = false
+       defer func() {
+               UseMlock = true
+       }()
+
+       if err != nil {
+               t.Fatal(err)
+       }
+       if err := key.Wipe(); err != nil {
+               t.Error(err)
+       }
+}
+
+// Test that disabling then enabling memory locking succeeds even if a key is
+// active when the variable changes.
+func TestDisableEnableMemoryLocking(t *testing.T) {
+       // Mlock off for creation, on for wiping
+       UseMlock = false
+       key2, err := NewRandomKey(metadata.InternalKeyLen)
+       UseMlock = true
+
+       if err != nil {
+               t.Fatal(err)
+       }
+       if err := key2.Wipe(); err != nil {
+               t.Error(err)
+       }
+}
+
+// Test making keys long enough that the keys will have to resize
+func TestKeyResize(t *testing.T) {
+       // Key will have to resize once
+       r := io.LimitReader(ConstReader(1), int64(os.Getpagesize())+1)
+       key, err := NewKeyFromReader(r)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer key.Wipe()
+       for i, b := range key.data {
+               if b != 1 {
+                       t.Fatalf("Byte %d contained invalid data %q", i, b)
+               }
+       }
+}
+
+// Test making keys so long that many resizes are necessary
+func TestKeyLargeResize(t *testing.T) {
+       // Key will have to resize 7 times
+       r := io.LimitReader(ConstReader(1), int64(os.Getpagesize())*65)
+
+       // Turn off Mlocking as the key will exceed the limit on some systems.
+       UseMlock = false
+       key, err := NewKeyFromReader(r)
+       UseMlock = true
+
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer key.Wipe()
+       for i, b := range key.data {
+               if b != 1 {
+                       t.Fatalf("Byte %d contained invalid data %q", i, b)
+               }
+       }
+}
+
+// Check that we can create random keys. All this test does to test the
+// "randomness" is generate a page of random bytes and attempts compression.
+// If the data can be compressed it is probably not very random. This isn't
+// intended to be a sufficient test for randomness (which is impossible), but a
+// way to catch simple regressions (key is all zeros or contains a repeating
+// pattern).
+func TestRandomKeyGen(t *testing.T) {
+       key, err := NewRandomKey(os.Getpagesize())
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer key.Wipe()
+
+       if didCompress(key.data) {
+               t.Errorf("Random key (%d bytes) should not be compressible", key.Len())
+       }
+}
+
+func TestBigKeyGen(t *testing.T) {
+       key, err := NewRandomKey(4096 * 4096)
+       switch err {
+       case nil:
+               key.Wipe()
+               return
+       case ErrMlockUlimit:
+               // Don't fail just because "ulimit -l" is too low.
+               return
+       default:
+               t.Fatal(err)
+       }
+}
+
+// didCompress checks if the given data can be compressed. Specifically, it
+// returns true if running zlib on the provided input produces a shorter output.
+func didCompress(input []byte) bool {
+       var output bytes.Buffer
+
+       w := zlib.NewWriter(&output)
+       _, err := w.Write(input)
+       w.Close()
+
+       return err == nil && len(input) > output.Len()
+}
+
+// Checks that the input arrays are all distinct
+func buffersDistinct(buffers ...[]byte) bool {
+       for i := 0; i < len(buffers); i++ {
+               for j := i + 1; j < len(buffers); j++ {
+                       if bytes.Equal(buffers[i], buffers[j]) {
+                               // Different entry, but equal arrays
+                               return false
+                       }
+               }
+       }
+       return true
+}
+
+// Checks that our cryptographic operations all produce distinct data
+func TestKeysAndOutputsDistinct(t *testing.T) {
+       data, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       encKey, authKey := stretchKey(fakeWrappingKey)
+       defer encKey.Wipe()
+       defer authKey.Wipe()
+
+       if !buffersDistinct(fakeWrappingKey.data, fakeValidPolicyKey.data,
+               encKey.data, authKey.data, data.IV, data.EncryptedKey, data.Hmac) {
+               t.Error("Key wrapping produced duplicate data")
+       }
+}
+
+// Check that Wrap() works with fixed keys
+func TestWrapSucceeds(t *testing.T) {
+       data, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err = lengthCheck("IV", data.IV, aes.BlockSize); err != nil {
+               t.Error(err)
+       }
+       if err = lengthCheck("Encrypted Key", data.EncryptedKey, metadata.PolicyKeyLen); err != nil {
+               t.Error(err)
+       }
+       if err = lengthCheck("HMAC", data.Hmac, sha256.Size); err != nil {
+               t.Error(err)
+       }
+}
+
+// Checks that applying Wrap then Unwrap gives the original data
+func testWrapUnwrapEqual(wrappingKey *Key, secretKey *Key) error {
+       data, err := Wrap(wrappingKey, secretKey)
+       if err != nil {
+               return err
+       }
+
+       secret, err := Unwrap(wrappingKey, data)
+       if err != nil {
+               return err
+       }
+       defer secret.Wipe()
+
+       if !bytes.Equal(secretKey.data, secret.data) {
+               return fmt.Errorf("Got %x after wrap/unwrap with w=%x and s=%x",
+                       secret.data, wrappingKey.data, secretKey.data)
+       }
+       return nil
+}
+
+// Check that Unwrap(Wrap(x)) == x with fixed keys
+func TestWrapUnwrapEqual(t *testing.T) {
+       if err := testWrapUnwrapEqual(fakeWrappingKey, fakeValidPolicyKey); err != nil {
+               t.Error(err)
+       }
+}
+
+// Check that Unwrap(Wrap(x)) == x with random keys
+func TestRandomWrapUnwrapEqual(t *testing.T) {
+       for i := 0; i < 10; i++ {
+               wk, err := NewRandomKey(metadata.InternalKeyLen)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               sk, err := NewRandomKey(metadata.InternalKeyLen)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if err = testWrapUnwrapEqual(wk, sk); err != nil {
+                       t.Error(err)
+               }
+               wk.Wipe()
+               sk.Wipe()
+       }
+}
+
+// Check that Unwrap(Wrap(x)) == x with differing lengths of secret key
+func TestDifferentLengthSecretKey(t *testing.T) {
+       wk, err := makeKey(1, metadata.InternalKeyLen)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer wk.Wipe()
+       for i := 0; i < 100; i++ {
+               sk, err := makeKey(2, i)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if err = testWrapUnwrapEqual(wk, sk); err != nil {
+                       t.Error(err)
+               }
+               sk.Wipe()
+       }
+}
+
+// Wrong length of wrapping key should fail
+func TestWrongWrappingKeyLength(t *testing.T) {
+       _, err := Wrap(fakeValidPolicyKey, fakeWrappingKey)
+       if err == nil {
+               t.Fatal("using a policy key for wrapping should fail")
+       }
+}
+
+// Wrong length of unwrapping key should fail
+func TestWrongUnwrappingKeyLength(t *testing.T) {
+       data, err := Wrap(fakeWrappingKey, fakeWrappingKey)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if k, err := Unwrap(fakeValidPolicyKey, data); err == nil {
+               k.Wipe()
+               t.Fatal("using a policy key for unwrapping should fail")
+       }
+}
+
+// Wrapping twice with the same keys should give different components
+func TestWrapTwiceDistinct(t *testing.T) {
+       data1, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+       if err != nil {
+               t.Fatal(err)
+       }
+       data2, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if !buffersDistinct(data1.IV, data1.EncryptedKey, data1.Hmac,
+               data2.IV, data2.EncryptedKey, data2.Hmac) {
+               t.Error("Wrapping same keys twice should give distinct results")
+       }
+}
+
+// Attempts to Unwrap data with key after altering tweak, should fail
+func testFailWithTweak(key *Key, data *metadata.WrappedKeyData, tweak []byte) error {
+       tweak[0]++
+       key, err := Unwrap(key, data)
+       if err == nil {
+               key.Wipe()
+       }
+       tweak[0]--
+       return err
+}
+
+// Wrapping then unwrapping with different components altered
+func TestUnwrapWrongKey(t *testing.T) {
+       data, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if testFailWithTweak(fakeWrappingKey, data, fakeWrappingKey.data) == nil {
+               t.Error("using a different wrapping key should make unwrap fail")
+       }
+}
+
+func TestUnwrapWrongData(t *testing.T) {
+       data, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if testFailWithTweak(fakeWrappingKey, data, data.EncryptedKey) == nil {
+               t.Error("changing encryption key should make unwrap fail")
+       }
+       if testFailWithTweak(fakeWrappingKey, data, data.IV) == nil {
+               t.Error("changing IV should make unwrap fail")
+       }
+       if testFailWithTweak(fakeWrappingKey, data, data.Hmac) == nil {
+               t.Error("changing HMAC should make unwrap fail")
+       }
+}
+
+func TestComputeKeyDescriptorV1(t *testing.T) {
+       descriptor, err := ComputeKeyDescriptor(fakeValidPolicyKey, 1)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if descriptor != "8290608a029c5aae" {
+               t.Errorf("wrong v1 descriptor: %s", descriptor)
+       }
+}
+
+func TestComputeKeyDescriptorV2(t *testing.T) {
+       descriptor, err := ComputeKeyDescriptor(fakeValidPolicyKey, 2)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if descriptor != "2139f52bf8386ee99845818ac7e91c4a" {
+               t.Errorf("wrong v2 descriptor: %s", descriptor)
+       }
+}
+
+func TestComputeKeyDescriptorBadVersion(t *testing.T) {
+       _, err := ComputeKeyDescriptor(fakeValidPolicyKey, 0)
+       if err == nil {
+               t.Error("computing key descriptor with bad version should fail")
+       }
+}
+
+// Run our test cases for passphrase hashing
+func TestPassphraseHashing(t *testing.T) {
+       pk, err := fakePassphraseKey()
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer pk.Wipe()
+
+       for i, testCase := range hashTestCases {
+               if err := testCase.costs.CheckValidity(); err != nil {
+                       t.Errorf("Hash test %d: for costs=%+v hashing failed: %v", i, testCase.costs, err)
+                       continue
+               }
+               hash, err := PassphraseHash(pk, fakeSalt, testCase.costs)
+               if err != nil {
+                       t.Errorf("Hash test %d: for costs=%+v hashing failed: %v", i, testCase.costs, err)
+                       continue
+               }
+               defer hash.Wipe()
+
+               actual := hex.EncodeToString(hash.data)
+               if actual != testCase.hexHash {
+                       t.Errorf("Hash test %d: for costs=%+v expected hash of %q got %q",
+                               i, testCase.costs, testCase.hexHash, actual)
+               }
+       }
+}
+
+var badCosts = []*metadata.HashingCosts{
+       // Bad Time costs
+       {Time: 0, Memory: 1 << 11, Parallelism: 1},
+       {Time: 1 << 33, Memory: 1 << 11, Parallelism: 1},
+       // Bad Memory costs
+       {Time: 1, Memory: 5, Parallelism: 1},
+       {Time: 1, Memory: 1 << 33, Parallelism: 1},
+       // Bad Parallelism costs
+       {Time: 1, Memory: 1 << 11, Parallelism: 0, TruncationFixed: false},
+       {Time: 1, Memory: 1 << 11, Parallelism: 0, TruncationFixed: true},
+       {Time: 1, Memory: 1 << 11, Parallelism: 256, TruncationFixed: false},
+       {Time: 1, Memory: 1 << 11, Parallelism: 256, TruncationFixed: true},
+       {Time: 1, Memory: 1 << 11, Parallelism: 257, TruncationFixed: true},
+}
+
+func TestBadParameters(t *testing.T) {
+       for i, costs := range badCosts {
+               if costs.CheckValidity() == nil {
+                       t.Errorf("Hash test %d: expected error for costs=%+v", i, costs)
+               }
+       }
+}
+
+func BenchmarkWrap(b *testing.B) {
+       for n := 0; n < b.N; n++ {
+               Wrap(fakeWrappingKey, fakeValidPolicyKey)
+       }
+}
+
+func BenchmarkUnwrap(b *testing.B) {
+       b.StopTimer()
+
+       data, _ := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+
+       b.StartTimer()
+       for n := 0; n < b.N; n++ {
+               key, err := Unwrap(fakeWrappingKey, data)
+               if err != nil {
+                       b.Fatal(err)
+               }
+               key.Wipe()
+       }
+}
+
+func BenchmarkUnwrapNolock(b *testing.B) {
+       b.StopTimer()
+
+       UseMlock = false
+       defer func() {
+               UseMlock = true
+       }()
+       data, _ := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+
+       b.StartTimer()
+       for n := 0; n < b.N; n++ {
+               key, err := Unwrap(fakeWrappingKey, data)
+               if err != nil {
+                       b.Fatal(err)
+               }
+               key.Wipe()
+       }
+}
+
+func BenchmarkRandomWrapUnwrap(b *testing.B) {
+       for n := 0; n < b.N; n++ {
+               wk, _ := NewRandomKey(metadata.InternalKeyLen)
+               sk, _ := NewRandomKey(metadata.InternalKeyLen)
+
+               testWrapUnwrapEqual(wk, sk)
+               // Must manually call wipe here, or test will use too much memory.
+               wk.Wipe()
+               sk.Wipe()
+       }
+}
+
+func benchmarkPassphraseHashing(b *testing.B, costs *metadata.HashingCosts) {
+       b.StopTimer()
+
+       pk, err := fakePassphraseKey()
+       if err != nil {
+               b.Fatal(err)
+       }
+       defer pk.Wipe()
+
+       b.StartTimer()
+       for n := 0; n < b.N; n++ {
+               hash, err := PassphraseHash(pk, fakeSalt, costs)
+               hash.Wipe()
+               if err != nil {
+                       b.Fatal(err)
+               }
+       }
+}
+
+func BenchmarkPassphraseHashing_1MB_1Thread(b *testing.B) {
+       benchmarkPassphraseHashing(b,
+               &metadata.HashingCosts{Time: 1, Memory: 1 << 10, Parallelism: 1})
+}
+
+func BenchmarkPassphraseHashing_1GB_1Thread(b *testing.B) {
+       benchmarkPassphraseHashing(b,
+               &metadata.HashingCosts{Time: 1, Memory: 1 << 20, Parallelism: 1})
+}
+
+func BenchmarkPassphraseHashing_128MB_1Thread(b *testing.B) {
+       benchmarkPassphraseHashing(b,
+               &metadata.HashingCosts{Time: 1, Memory: 1 << 17, Parallelism: 1})
+}
+
+func BenchmarkPassphraseHashing_128MB_8Thread(b *testing.B) {
+       benchmarkPassphraseHashing(b,
+               &metadata.HashingCosts{Time: 1, Memory: 1 << 17, Parallelism: 8})
+}
+
+func BenchmarkPassphraseHashing_128MB_8Pass(b *testing.B) {
+       benchmarkPassphraseHashing(b,
+               &metadata.HashingCosts{Time: 8, Memory: 1 << 17, Parallelism: 1})
+}
diff --git a/crypto/key.go b/crypto/key.go
new file mode 100644 (file)
index 0000000..2e57443
--- /dev/null
@@ -0,0 +1,354 @@
+/*
+ * key.go - Cryptographic key management for fscrypt. Ensures that sensitive
+ * material is properly handled throughout the program.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package crypto
+
+/*
+#include <stdlib.h>
+#include <string.h>
+*/
+import "C"
+
+import (
+       "bytes"
+       "crypto/subtle"
+       "encoding/base32"
+       "io"
+       "log"
+       "os"
+       "runtime"
+       "unsafe"
+
+       "github.com/pkg/errors"
+       "golang.org/x/sys/unix"
+
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+const (
+       // Keys need to readable and writable, but hidden from other processes.
+       keyProtection = unix.PROT_READ | unix.PROT_WRITE
+       keyMmapFlags  = unix.MAP_PRIVATE | unix.MAP_ANONYMOUS
+)
+
+/*
+UseMlock determines whether we should use the mlock/munlock syscalls to
+prevent sensitive data like keys and passphrases from being paged to disk.
+UseMlock defaults to true, but can be set to false if the application calling
+into this library has insufficient privileges to lock memory. Code using this
+package could also bind this setting to a flag by using:
+
+       flag.BoolVar(&crypto.UseMlock, "lock-memory", true, "lock keys in memory")
+*/
+var UseMlock = true
+
+/*
+Key protects some arbitrary buffer of cryptographic material. Its methods
+ensure that the Key's data is locked in memory before being used (if
+UseMlock is set to true), and is wiped and unlocked after use (via the Wipe()
+method). This data is never accessed outside of the fscrypt/crypto package
+(except for the UnsafeData method). If a key is successfully created, the
+Wipe() method should be called after it's use. For example:
+
+       func UseKeyFromStdin() error {
+               key, err := NewKeyFromReader(os.Stdin)
+               if err != nil {
+                       return err
+               }
+               defer key.Wipe()
+
+               // Do stuff with key
+
+               return nil
+       }
+
+The Wipe() method will also be called when a key is garbage collected; however,
+it is best practice to clear the key as soon as possible, so it spends a minimal
+amount of time in memory.
+
+Note that Key is not thread safe, as a key could be wiped while another thread
+is using it. Also, calling Wipe() from two threads could cause an error as
+memory could be freed twice.
+*/
+type Key struct {
+       data []byte
+}
+
+// NewBlankKey constructs a blank key of a specified length and returns an error
+// if we are unable to allocate or lock the necessary memory.
+func NewBlankKey(length int) (*Key, error) {
+       if length == 0 {
+               return &Key{data: nil}, nil
+       } else if length < 0 {
+               return nil, errors.Errorf("requested key length %d is negative", length)
+       }
+
+       flags := keyMmapFlags
+       if UseMlock {
+               flags |= unix.MAP_LOCKED
+       }
+
+       // See MAP_ANONYMOUS in http://man7.org/linux/man-pages/man2/mmap.2.html
+       data, err := unix.Mmap(-1, 0, length, keyProtection, flags)
+       if err == unix.EAGAIN {
+               return nil, ErrMlockUlimit
+       }
+       if err != nil {
+               return nil, errors.Wrapf(err,
+                       "failed to allocate (mmap) key buffer of length %d", length)
+       }
+
+       key := &Key{data: data}
+
+       // Backup finalizer in case user forgets to "defer key.Wipe()"
+       runtime.SetFinalizer(key, (*Key).Wipe)
+       return key, nil
+}
+
+// Wipe destroys a Key by zeroing and freeing the memory. The data is zeroed
+// even if Wipe returns an error, which occurs if we are unable to unlock or
+// free the key memory. Wipe does nothing if the key is already wiped or is nil.
+func (key *Key) Wipe() error {
+       // We do nothing if key or key.data is nil so that Wipe() is idempotent
+       // and so Wipe() can be called on keys which have already been cleared.
+       if key != nil && key.data != nil {
+               data := key.data
+               key.data = nil
+
+               for i := range data {
+                       data[i] = 0
+               }
+
+               if err := unix.Munmap(data); err != nil {
+                       log.Printf("unix.Munmap() failed: %v", err)
+                       return errors.Wrapf(err, "failed to free (munmap) key buffer")
+               }
+       }
+       return nil
+}
+
+// Len is the underlying data buffer's length.
+func (key *Key) Len() int {
+       return len(key.data)
+}
+
+// Equals compares the contents of two keys, returning true if they have the same
+// key data. This function runs in constant time.
+func (key *Key) Equals(key2 *Key) bool {
+       return subtle.ConstantTimeCompare(key.data, key2.data) == 1
+}
+
+// resize returns a new key with size requestedSize and the appropriate data
+// copied over. The original data is wiped. This method does nothing and returns
+// itself if the key's length equals requestedSize.
+func (key *Key) resize(requestedSize int) (*Key, error) {
+       if key.Len() == requestedSize {
+               return key, nil
+       }
+       defer key.Wipe()
+
+       resizedKey, err := NewBlankKey(requestedSize)
+       if err != nil {
+               return nil, err
+       }
+       copy(resizedKey.data, key.data)
+       return resizedKey, nil
+}
+
+// Data returns a slice of the key's underlying data. Note that this may become
+// outdated if the key is resized.
+func (key *Key) Data() []byte {
+       return key.data
+}
+
+// UnsafePtr returns an unsafe pointer to the key's underlying data. Note that
+// this will only be valid as long as the key is not resized.
+func (key *Key) UnsafePtr() unsafe.Pointer {
+       return util.Ptr(key.data)
+}
+
+// UnsafeToCString makes a copy of the string's data into a null-terminated C
+// string allocated by C. Note that this method is unsafe as this C copy has no
+// locking or wiping functionality. The key shouldn't contain any `\0` bytes.
+func (key *Key) UnsafeToCString() unsafe.Pointer {
+       size := C.size_t(key.Len())
+       data := C.calloc(size+1, 1)
+       C.memcpy(data, util.Ptr(key.data), size)
+       return data
+}
+
+// Clone creates a key as a copy of another one.
+func (key *Key) Clone() (*Key, error) {
+       newKey, err := NewBlankKey(key.Len())
+       if err != nil {
+               return nil, err
+       }
+       copy(newKey.data, key.data)
+       return newKey, nil
+}
+
+// NewKeyFromCString creates of a copy of some C string's data in a key. Note
+// that the original C string is not modified at all, so steps must be taken to
+// ensure that this original copy is secured.
+func NewKeyFromCString(str unsafe.Pointer) (*Key, error) {
+       size := C.strlen((*C.char)(str))
+       key, err := NewBlankKey(int(size))
+       if err != nil {
+               return nil, err
+       }
+       C.memcpy(util.Ptr(key.data), str, size)
+       return key, nil
+}
+
+// NewKeyFromReader constructs a key of arbitrary length by reading from reader
+// until hitting EOF.
+func NewKeyFromReader(reader io.Reader) (*Key, error) {
+       // Use an initial key size of a page. As Mmap allocates a page anyway,
+       // there isn't much additional overhead from starting with a whole page.
+       key, err := NewBlankKey(os.Getpagesize())
+       if err != nil {
+               return nil, err
+       }
+
+       totalBytesRead := 0
+       for {
+               bytesRead, err := reader.Read(key.data[totalBytesRead:])
+               totalBytesRead += bytesRead
+
+               switch err {
+               case nil:
+                       // Need to continue reading. Grow key if necessary
+                       if key.Len() == totalBytesRead {
+                               if key, err = key.resize(2 * key.Len()); err != nil {
+                                       return nil, err
+                               }
+                       }
+               case io.EOF:
+                       // Getting the EOF error means we are done
+                       return key.resize(totalBytesRead)
+               default:
+                       // Fail if Read() has a failure
+                       key.Wipe()
+                       return nil, err
+               }
+       }
+}
+
+// NewFixedLengthKeyFromReader constructs a key with a specified length by
+// reading exactly length bytes from reader.
+func NewFixedLengthKeyFromReader(reader io.Reader, length int) (*Key, error) {
+       key, err := NewBlankKey(length)
+       if err != nil {
+               return nil, err
+       }
+       if _, err := io.ReadFull(reader, key.data); err != nil {
+               key.Wipe()
+               return nil, err
+       }
+       return key, nil
+}
+
+var (
+       // The recovery code is base32 with a dash between each block of 8 characters.
+       encoding      = base32.StdEncoding
+       blockSize     = 8
+       separator     = []byte("-")
+       encodedLength = encoding.EncodedLen(metadata.PolicyKeyLen)
+       decodedLength = encoding.DecodedLen(encodedLength)
+       // RecoveryCodeLength is the number of bytes in every recovery code
+       RecoveryCodeLength = (encodedLength/blockSize)*(blockSize+len(separator)) - len(separator)
+)
+
+// WriteRecoveryCode outputs key's recovery code to the provided writer.
+// WARNING: This recovery key is enough to derive the original key, so it must
+// be given the same level of protection as a raw cryptographic key.
+func WriteRecoveryCode(key *Key, writer io.Writer) error {
+       if err := util.CheckValidLength(metadata.PolicyKeyLen, key.Len()); err != nil {
+               return errors.Wrap(err, "recovery key")
+       }
+
+       // We store the base32 encoded data (without separators) in a temp key
+       encodedKey, err := NewBlankKey(encodedLength)
+       if err != nil {
+               return err
+       }
+       defer encodedKey.Wipe()
+       encoding.Encode(encodedKey.data, key.data)
+
+       w := util.NewErrWriter(writer)
+
+       // Write the blocks with separators between them
+       w.Write(encodedKey.data[:blockSize])
+       for blockStart := blockSize; blockStart < encodedLength; blockStart += blockSize {
+               w.Write(separator)
+
+               blockEnd := util.MinInt(blockStart+blockSize, encodedLength)
+               w.Write(encodedKey.data[blockStart:blockEnd])
+       }
+
+       // If any writes have failed, return the error
+       return w.Err()
+}
+
+// ReadRecoveryCode gets the recovery code from the provided reader and returns
+// the corresponding cryptographic key.
+// WARNING: This recovery key is enough to derive the original key, so it must
+// be given the same level of protection as a raw cryptographic key.
+func ReadRecoveryCode(reader io.Reader) (*Key, error) {
+       // We store the base32 encoded data (without separators) in a temp key
+       encodedKey, err := NewBlankKey(encodedLength)
+       if err != nil {
+               return nil, err
+       }
+       defer encodedKey.Wipe()
+
+       r := util.NewErrReader(reader)
+
+       // Read the other blocks, checking the separators between them
+       r.Read(encodedKey.data[:blockSize])
+       inputSeparator := make([]byte, len(separator))
+
+       for blockStart := blockSize; blockStart < encodedLength; blockStart += blockSize {
+               r.Read(inputSeparator)
+               if r.Err() == nil && !bytes.Equal(separator, inputSeparator) {
+                       err = errors.Wrapf(ErrRecoveryCode, "invalid separator %q", inputSeparator)
+                       return nil, err
+               }
+
+               blockEnd := util.MinInt(blockStart+blockSize, encodedLength)
+               r.Read(encodedKey.data[blockStart:blockEnd])
+       }
+
+       // If any reads have failed, return the error
+       if r.Err() != nil {
+               return nil, errors.Wrapf(ErrRecoveryCode, "read error %v", r.Err())
+       }
+
+       // Now we decode the key, resizing if necessary
+       decodedKey, err := NewBlankKey(decodedLength)
+       if err != nil {
+               return nil, err
+       }
+       if _, err = encoding.Decode(decodedKey.data, encodedKey.data); err != nil {
+               return nil, errors.Wrap(ErrRecoveryCode, err.Error())
+       }
+       return decodedKey.resize(metadata.PolicyKeyLen)
+}
diff --git a/crypto/rand.go b/crypto/rand.go
new file mode 100644 (file)
index 0000000..527f841
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * rand.go - Reader used to generate secure random data for fscrypt.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package crypto
+
+import (
+       "io"
+
+       "github.com/pkg/errors"
+       "golang.org/x/sys/unix"
+)
+
+// NewRandomBuffer uses the Linux Getrandom() syscall to create random bytes. If
+// the operating system has insufficient randomness, the buffer creation will
+// fail. This is an improvement over Go's built-in crypto/rand which will still
+// return bytes if the system has insufficiency entropy.
+//
+//     See: https://github.com/golang/go/issues/19274
+//
+// While this syscall was only introduced in Kernel v3.17, it predates the
+// introduction of filesystem encryption, so it introduces no additional
+// compatibility issues.
+func NewRandomBuffer(length int) ([]byte, error) {
+       buffer := make([]byte, length)
+       if _, err := io.ReadFull(randReader{}, buffer); err != nil {
+               return nil, err
+       }
+       return buffer, nil
+}
+
+// NewRandomKey creates a random key of the specified length. This function uses
+// the same random number generation process as NewRandomBuffer.
+func NewRandomKey(length int) (*Key, error) {
+       return NewFixedLengthKeyFromReader(randReader{}, length)
+}
+
+// NewRandomPassphrase creates a random passphrase of the specified length
+// containing random alphabetic characters.
+func NewRandomPassphrase(length int) (*Key, error) {
+       chars := []byte("abcdefghijklmnopqrstuvwxyz")
+       passphrase, err := NewBlankKey(length)
+       if err != nil {
+               return nil, err
+       }
+       for i := 0; i < length; {
+               // Get some random bytes.
+               raw, err := NewRandomKey((length - i) * 2)
+               if err != nil {
+                       return nil, err
+               }
+               // Translate the random bytes into random characters.
+               for _, b := range raw.data {
+                       if int(b) >= 256-(256%len(chars)) {
+                               // Avoid bias towards the first characters in the list.
+                               continue
+                       }
+                       c := chars[int(b)%len(chars)]
+                       passphrase.data[i] = c
+                       i++
+                       if i == length {
+                               break
+                       }
+               }
+               raw.Wipe()
+       }
+       return passphrase, nil
+}
+
+// randReader just calls into Getrandom, so no internal data is needed.
+type randReader struct{}
+
+func (r randReader) Read(buffer []byte) (int, error) {
+       n, err := unix.Getrandom(buffer, unix.GRND_NONBLOCK)
+       switch err {
+       case nil:
+               return n, nil
+       case unix.EAGAIN:
+               err = errors.New("insufficient entropy in pool")
+       case unix.ENOSYS:
+               err = errors.New("kernel must be v3.17 or later")
+       }
+       return 0, errors.Wrap(err, "getrandom() failed")
+}
diff --git a/crypto/recovery_test.go b/crypto/recovery_test.go
new file mode 100644 (file)
index 0000000..4a89e6d
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+ * recovery_test.go - tests for recovery codes in the crypto package
+ * tests key wrapping/unwrapping and key generation
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package crypto
+
+import (
+       "bytes"
+       "fmt"
+       "testing"
+
+       "github.com/google/fscrypt/metadata"
+)
+
+const fakeSecretRecoveryCode = "EYTCMJRG-EYTCMJRG-EYTCMJRG-EYTCMJRG-EYTCMJRG-EYTCMJRG-EYTCMJRG-EYTCMJRG-EYTCMJRG-EYTCMJRG-EYTCMJRG-EYTCMJRG-EYTCMJQ="
+
+var fakeSecretKey, _ = makeKey(38, metadata.PolicyKeyLen)
+
+// Note that this function is INSECURE. FOR TESTING ONLY
+func getRecoveryCodeFromKey(key *Key) ([]byte, error) {
+       var buf bytes.Buffer
+       if err := WriteRecoveryCode(key, &buf); err != nil {
+               return nil, err
+       }
+       return buf.Bytes(), nil
+}
+
+func getRandomRecoveryCodeBuffer() ([]byte, error) {
+       key, err := NewRandomKey(metadata.PolicyKeyLen)
+       if err != nil {
+               return nil, err
+       }
+       defer key.Wipe()
+       return getRecoveryCodeFromKey(key)
+}
+
+func getKeyFromRecoveryCode(buf []byte) (*Key, error) {
+       return ReadRecoveryCode(bytes.NewReader(buf))
+}
+
+// Given a key, make a recovery code from that key, use that code to rederive
+// another key and check if they are the same.
+func testKeyEncodeDecode(key *Key) error {
+       buf, err := getRecoveryCodeFromKey(key)
+       if err != nil {
+               return err
+       }
+
+       key2, err := getKeyFromRecoveryCode(buf)
+       if err != nil {
+               return err
+       }
+       defer key2.Wipe()
+
+       if !bytes.Equal(key.data, key2.data) {
+               return fmt.Errorf("encoding then decoding %x didn't yield the same key", key.data)
+       }
+       return nil
+}
+
+// Given a recovery code, make a key from that recovery code, use that key to
+// rederive another recovery code and check if they are the same.
+func testRecoveryDecodeEncode(buf []byte) error {
+       key, err := getKeyFromRecoveryCode(buf)
+       if err != nil {
+               return err
+       }
+       defer key.Wipe()
+
+       buf2, err := getRecoveryCodeFromKey(key)
+       if err != nil {
+               return err
+       }
+
+       if !bytes.Equal(buf, buf2) {
+               return fmt.Errorf("decoding then encoding %x didn't yield the same key", buf)
+       }
+       return nil
+}
+
+func TestGetRandomRecoveryString(t *testing.T) {
+       b, err := getRandomRecoveryCodeBuffer()
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       t.Log(string(b))
+       // t.Fail() // Uncomment to see an example random recovery code
+}
+
+func TestFakeSecretKey(t *testing.T) {
+       buf, err := getRecoveryCodeFromKey(fakeSecretKey)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       recoveryCode := string(buf)
+       if recoveryCode != fakeSecretRecoveryCode {
+               t.Errorf("got '%s' instead of '%s'", recoveryCode, fakeSecretRecoveryCode)
+       }
+}
+
+func TestEncodeDecode(t *testing.T) {
+       key, err := NewRandomKey(metadata.PolicyKeyLen)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer key.Wipe()
+
+       if err = testKeyEncodeDecode(key); err != nil {
+               t.Error(err)
+       }
+}
+
+func TestDecodeEncode(t *testing.T) {
+       buf, err := getRandomRecoveryCodeBuffer()
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err = testRecoveryDecodeEncode(buf); err != nil {
+               t.Error(err)
+       }
+}
+
+func TestWrongLengthError(t *testing.T) {
+       key, err := NewRandomKey(metadata.PolicyKeyLen - 1)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer key.Wipe()
+
+       if _, err = getRecoveryCodeFromKey(key); err == nil {
+               t.Error("key with wrong length should have failed to encode")
+       }
+}
+
+func TestBadCharacterError(t *testing.T) {
+       buf, err := getRandomRecoveryCodeBuffer()
+       if err != nil {
+               t.Fatal(err)
+       }
+       // Lowercase letters not allowed
+       buf[3] = 'k'
+       if key, err := getKeyFromRecoveryCode(buf); err == nil {
+               key.Wipe()
+               t.Error("lowercase letters should make decoding fail")
+       }
+}
+
+func TestBadEndCharacterError(t *testing.T) {
+       buf, err := getRandomRecoveryCodeBuffer()
+       if err != nil {
+               t.Fatal(err)
+       }
+       // Separator must be '-'
+       buf[blockSize] = '_'
+       if key, err := getKeyFromRecoveryCode(buf); err == nil {
+               key.Wipe()
+               t.Error("any separator that isn't '-' should make decoding fail")
+       }
+}
+
+func BenchmarkEncode(b *testing.B) {
+       b.StopTimer()
+
+       key, err := NewRandomKey(metadata.PolicyKeyLen)
+       if err != nil {
+               b.Fatal(err)
+       }
+       defer key.Wipe()
+
+       b.StartTimer()
+       for n := 0; n < b.N; n++ {
+               if _, err = getRecoveryCodeFromKey(key); err != nil {
+                       b.Fatal(err)
+               }
+       }
+}
+
+func BenchmarkDecode(b *testing.B) {
+       b.StopTimer()
+
+       buf, err := getRandomRecoveryCodeBuffer()
+       if err != nil {
+               b.Fatal(err)
+       }
+
+       b.StartTimer()
+       for n := 0; n < b.N; n++ {
+               key, err := getKeyFromRecoveryCode(buf)
+               if err != nil {
+                       b.Fatal(err)
+               }
+               key.Wipe()
+       }
+}
+
+func BenchmarkEncodeDecode(b *testing.B) {
+       b.StopTimer()
+
+       key, err := NewRandomKey(metadata.PolicyKeyLen)
+       if err != nil {
+               b.Fatal(err)
+       }
+       defer key.Wipe()
+
+       b.StartTimer()
+       for n := 0; n < b.N; n++ {
+               if err = testKeyEncodeDecode(key); err != nil {
+                       b.Fatal(err)
+               }
+       }
+}
+
+func BenchmarkDecodeEncode(b *testing.B) {
+       b.StopTimer()
+
+       buf, err := getRandomRecoveryCodeBuffer()
+       if err != nil {
+               b.Fatal(err)
+       }
+
+       b.StartTimer()
+       for n := 0; n < b.N; n++ {
+               if err = testRecoveryDecodeEncode(buf); err != nil {
+                       b.Fatal(err)
+               }
+       }
+}
diff --git a/filesystem/filesystem.go b/filesystem/filesystem.go
new file mode 100644 (file)
index 0000000..9829435
--- /dev/null
@@ -0,0 +1,1089 @@
+/*
+ * filesystem.go - Contains the functionality for a specific filesystem. This
+ * includes the commands to setup the filesystem, apply policies, and locate
+ * metadata.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+// Package filesystem deals with the structure of the files on disk used to
+// store the metadata for fscrypt. Specifically, this package includes:
+//  1. mountpoint management (mountpoint.go)
+//     - querying existing mounted filesystems
+//     - getting filesystems from a UUID
+//     - finding the filesystem for a specific path
+//  2. metadata organization (filesystem.go)
+//     - setting up a mounted filesystem for use with fscrypt
+//     - adding/querying/deleting metadata
+//     - making links to other filesystems' metadata
+//     - following links to get data from other filesystems
+package filesystem
+
+import (
+       "fmt"
+       "io"
+       "log"
+       "os"
+       "os/user"
+       "path/filepath"
+       "sort"
+       "strings"
+       "syscall"
+       "time"
+
+       "github.com/pkg/errors"
+       "golang.org/x/sys/unix"
+       "google.golang.org/protobuf/proto"
+
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+// ErrAlreadySetup indicates that a filesystem is already setup for fscrypt.
+type ErrAlreadySetup struct {
+       Mount *Mount
+}
+
+func (err *ErrAlreadySetup) Error() string {
+       return fmt.Sprintf("filesystem %s is already setup for use with fscrypt",
+               err.Mount.Path)
+}
+
+// ErrCorruptMetadata indicates that an fscrypt metadata file is corrupt.
+type ErrCorruptMetadata struct {
+       Path            string
+       UnderlyingError error
+}
+
+func (err *ErrCorruptMetadata) Error() string {
+       return fmt.Sprintf("fscrypt metadata file at %q is corrupt: %s",
+               err.Path, err.UnderlyingError)
+}
+
+// ErrFollowLink indicates that a protector link can't be followed.
+type ErrFollowLink struct {
+       Link            string
+       UnderlyingError error
+}
+
+func (err *ErrFollowLink) Error() string {
+       return fmt.Sprintf("cannot follow filesystem link %q: %s",
+               err.Link, err.UnderlyingError)
+}
+
+// ErrInsecurePermissions indicates that a filesystem is not considered to be
+// setup for fscrypt because a metadata directory has insecure permissions.
+type ErrInsecurePermissions struct {
+       Path string
+}
+
+func (err *ErrInsecurePermissions) Error() string {
+       return fmt.Sprintf("%q has insecure permissions (world-writable without sticky bit)",
+               err.Path)
+}
+
+// ErrMakeLink indicates that a protector link can't be created.
+type ErrMakeLink struct {
+       Target          *Mount
+       UnderlyingError error
+}
+
+func (err *ErrMakeLink) Error() string {
+       return fmt.Sprintf("cannot create filesystem link to %q: %s",
+               err.Target.Path, err.UnderlyingError)
+}
+
+// ErrMountOwnedByAnotherUser indicates that the mountpoint root directory is
+// owned by a user that isn't trusted in the current context, so we don't
+// consider fscrypt to be properly setup on the filesystem.
+type ErrMountOwnedByAnotherUser struct {
+       Mount *Mount
+}
+
+func (err *ErrMountOwnedByAnotherUser) Error() string {
+       return fmt.Sprintf("another non-root user owns the root directory of %s", err.Mount.Path)
+}
+
+// ErrNoCreatePermission indicates that the current user lacks permission to
+// create fscrypt metadata on the given filesystem.
+type ErrNoCreatePermission struct {
+       Mount *Mount
+}
+
+func (err *ErrNoCreatePermission) Error() string {
+       return fmt.Sprintf("user lacks permission to create fscrypt metadata on %s", err.Mount.Path)
+}
+
+// ErrNotAMountpoint indicates that a path is not a mountpoint.
+type ErrNotAMountpoint struct {
+       Path string
+}
+
+func (err *ErrNotAMountpoint) Error() string {
+       return fmt.Sprintf("%q is not a mountpoint", err.Path)
+}
+
+// ErrNotSetup indicates that a filesystem is not setup for fscrypt.
+type ErrNotSetup struct {
+       Mount *Mount
+}
+
+func (err *ErrNotSetup) Error() string {
+       return fmt.Sprintf("filesystem %s is not setup for use with fscrypt", err.Mount.Path)
+}
+
+// ErrSetupByAnotherUser indicates that one or more of the fscrypt metadata
+// directories is owned by a user that isn't trusted in the current context, so
+// we don't consider fscrypt to be properly setup on the filesystem.
+type ErrSetupByAnotherUser struct {
+       Mount *Mount
+}
+
+func (err *ErrSetupByAnotherUser) Error() string {
+       return fmt.Sprintf("another non-root user owns fscrypt metadata directories on %s", err.Mount.Path)
+}
+
+// ErrSetupNotSupported indicates that the given filesystem type is not
+// supported for fscrypt setup.
+type ErrSetupNotSupported struct {
+       Mount *Mount
+}
+
+func (err *ErrSetupNotSupported) Error() string {
+       return fmt.Sprintf("filesystem type %s is not supported for fscrypt setup",
+               err.Mount.FilesystemType)
+}
+
+// ErrPolicyNotFound indicates that the policy metadata was not found.
+type ErrPolicyNotFound struct {
+       Descriptor string
+       Mount      *Mount
+}
+
+func (err *ErrPolicyNotFound) Error() string {
+       return fmt.Sprintf("policy metadata for %s not found on filesystem %s",
+               err.Descriptor, err.Mount.Path)
+}
+
+// ErrProtectorNotFound indicates that the protector metadata was not found.
+type ErrProtectorNotFound struct {
+       Descriptor string
+       Mount      *Mount
+}
+
+func (err *ErrProtectorNotFound) Error() string {
+       return fmt.Sprintf("protector metadata for %s not found on filesystem %s",
+               err.Descriptor, err.Mount.Path)
+}
+
+// SortDescriptorsByLastMtime indicates whether descriptors are sorted by last
+// modification time when being listed.  This can be set to true to get
+// consistent output for testing.
+var SortDescriptorsByLastMtime = false
+
+// Mount contains information for a specific mounted filesystem.
+//
+//     Path           - Absolute path where the directory is mounted
+//     FilesystemType - Type of the mounted filesystem, e.g. "ext4"
+//     Device         - Device for filesystem (empty string if we cannot find one)
+//     DeviceNumber   - Device number of the filesystem.  This is set even if
+//                      Device isn't, since all filesystems have a device
+//                      number assigned by the kernel, even pseudo-filesystems.
+//     Subtree        - The mounted subtree of the filesystem.  This is usually
+//                      "/", meaning that the entire filesystem is mounted, but
+//                      it can differ for bind mounts.
+//     ReadOnly       - True if this is a read-only mount
+//
+// In order to use a Mount to store fscrypt metadata, some directories must be
+// setup first. Specifically, the directories created look like:
+// <mountpoint>
+// â””── .fscrypt
+//
+//     â”œâ”€â”€ policies
+//     â””── protectors
+//
+// These "policies" and "protectors" directories will contain files that are
+// the corresponding metadata structures for policies and protectors. The public
+// interface includes functions for setting up these directories and Adding,
+// Getting, and Removing these files.
+//
+// There is also the ability to reference another filesystem's metadata. This is
+// used when a Policy on filesystem A is protected with Protector on filesystem
+// B. In this scenario, we store a "link file" in the protectors directory.
+//
+// We also allow ".fscrypt" to be a symlink which was previously created. This
+// allows login protectors to be created when the root filesystem is read-only,
+// provided that "/.fscrypt" is a symlink pointing to a writable location.
+type Mount struct {
+       Path           string
+       FilesystemType string
+       Device         string
+       DeviceNumber   DeviceNumber
+       Subtree        string
+       ReadOnly       bool
+}
+
+// PathSorter allows mounts to be sorted by Path.
+type PathSorter []*Mount
+
+func (p PathSorter) Len() int           { return len(p) }
+func (p PathSorter) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
+func (p PathSorter) Less(i, j int) bool { return p[i].Path < p[j].Path }
+
+const (
+       // Names of the various directories used in fscrypt
+       baseDirName       = ".fscrypt"
+       policyDirName     = "policies"
+       protectorDirName  = "protectors"
+       tempPrefix        = ".tmp"
+       linkFileExtension = ".link"
+
+       // The base directory should be read-only (except for the creator)
+       basePermissions = 0755
+
+       // The metadata files shouldn't be readable or writable by other users.
+       // Having them be world-readable wouldn't necessarily be a huge issue,
+       // but given that some of these files contain (strong) password hashes,
+       // we error on the side of caution -- similar to /etc/shadow.
+       // Note: existing files on-disk might have mode 0644, as that was the
+       // mode used by fscrypt v0.3.2 and earlier.
+       filePermissions = os.FileMode(0600)
+
+       // Maximum size of a metadata file.  This value is arbitrary, and it can
+       // be changed.  We just set a reasonable limit that shouldn't be reached
+       // in practice, except by users trying to cause havoc by creating
+       // extremely large files in the metadata directories.
+       maxMetadataFileSize = 16384
+)
+
+// SetupMode is a mode for creating the fscrypt metadata directories.
+type SetupMode int
+
+const (
+       // SingleUserWritable specifies to make the fscrypt metadata directories
+       // writable by a single user (usually root) only.
+       SingleUserWritable SetupMode = iota
+       // WorldWritable specifies to make the fscrypt metadata directories
+       // world-writable (with the sticky bit set).
+       WorldWritable
+)
+
+func (m *Mount) String() string {
+       return fmt.Sprintf(`%s
+       FilesystemType: %s
+       Device:         %s`, m.Path, m.FilesystemType, m.Device)
+}
+
+// BaseDir returns the path to the base fscrypt directory for this filesystem.
+func (m *Mount) BaseDir() string {
+       rawBaseDir := filepath.Join(m.Path, baseDirName)
+       // We allow the base directory to be a symlink, but some callers need
+       // the real path, so dereference the symlink here if needed. Since the
+       // directory the symlink points to may not exist yet, we have to read
+       // the symlink manually rather than use filepath.EvalSymlinks.
+       target, err := os.Readlink(rawBaseDir)
+       if err != nil {
+               return rawBaseDir // not a symlink
+       }
+       if filepath.IsAbs(target) {
+               return target
+       }
+       return filepath.Join(m.Path, target)
+}
+
+// ProtectorDir returns the directory containing the protector metadata.
+func (m *Mount) ProtectorDir() string {
+       return filepath.Join(m.BaseDir(), protectorDirName)
+}
+
+// protectorPath returns the full path to a regular protector file with the
+// specified descriptor.
+func (m *Mount) protectorPath(descriptor string) string {
+       return filepath.Join(m.ProtectorDir(), descriptor)
+}
+
+// linkedProtectorPath returns the full path to a linked protector file with the
+// specified descriptor.
+func (m *Mount) linkedProtectorPath(descriptor string) string {
+       return m.protectorPath(descriptor) + linkFileExtension
+}
+
+// PolicyDir returns the directory containing the policy metadata.
+func (m *Mount) PolicyDir() string {
+       return filepath.Join(m.BaseDir(), policyDirName)
+}
+
+// PolicyPath returns the full path to a regular policy file with the
+// specified descriptor.
+func (m *Mount) PolicyPath(descriptor string) string {
+       return filepath.Join(m.PolicyDir(), descriptor)
+}
+
+// tempMount creates a temporary directory alongside this Mount's base fscrypt
+// directory and returns a temporary Mount which represents this temporary
+// directory. The caller is responsible for removing this temporary directory.
+func (m *Mount) tempMount() (*Mount, error) {
+       tempDir, err := os.MkdirTemp(filepath.Dir(m.BaseDir()), tempPrefix)
+       return &Mount{Path: tempDir}, err
+}
+
+// ErrEncryptionNotEnabled indicates that encryption is not enabled on the given
+// filesystem.
+type ErrEncryptionNotEnabled struct {
+       Mount *Mount
+}
+
+func (err *ErrEncryptionNotEnabled) Error() string {
+       return fmt.Sprintf("encryption not enabled on filesystem %s (%s).",
+               err.Mount.Path, err.Mount.Device)
+}
+
+// ErrEncryptionNotSupported indicates that encryption is not supported on the
+// given filesystem.
+type ErrEncryptionNotSupported struct {
+       Mount *Mount
+}
+
+func (err *ErrEncryptionNotSupported) Error() string {
+       return fmt.Sprintf("This kernel doesn't support encryption on %s filesystems.",
+               err.Mount.FilesystemType)
+}
+
+// EncryptionSupportError adds filesystem-specific context to the
+// ErrEncryptionNotEnabled and ErrEncryptionNotSupported errors from the
+// metadata package.
+func (m *Mount) EncryptionSupportError(err error) error {
+       switch err {
+       case metadata.ErrEncryptionNotEnabled:
+               return &ErrEncryptionNotEnabled{m}
+       case metadata.ErrEncryptionNotSupported:
+               return &ErrEncryptionNotSupported{m}
+       }
+       return err
+}
+
+// isFscryptSetupAllowed decides whether the given filesystem is allowed to be
+// set up for fscrypt, without actually accessing it.  This basically checks
+// whether the filesystem type is one of the types that supports encryption, or
+// at least is in some stage of planning for encrption support in the future.
+//
+// We need this list so that we can skip filesystems that are irrelevant for
+// fscrypt without having to look for the fscrypt metadata directories on them,
+// which can trigger errors, long delays, or side effects on some filesystems.
+//
+// Unfortunately, this means that if a completely new filesystem adds encryption
+// support, then it will need to be manually added to this list.  But it seems
+// to be a worthwhile tradeoff to avoid the above issues.
+func (m *Mount) isFscryptSetupAllowed() bool {
+       if m.Path == "/" {
+               // The root filesystem is always allowed, since it's where login
+               // protectors are stored.
+               return true
+       }
+       switch m.FilesystemType {
+       case "ext4", "f2fs", "ubifs", "btrfs", "ceph", "xfs", "lustre":
+               return true
+       default:
+               return false
+       }
+}
+
+// CheckSupport returns an error if this filesystem does not support encryption.
+func (m *Mount) CheckSupport() error {
+       if !m.isFscryptSetupAllowed() {
+               return &ErrEncryptionNotSupported{m}
+       }
+       return m.EncryptionSupportError(metadata.CheckSupport(m.Path))
+}
+
+func checkOwnership(path string, info os.FileInfo, trustedUser *user.User) bool {
+       if trustedUser == nil {
+               return true
+       }
+       trustedUID := uint32(util.AtoiOrPanic(trustedUser.Uid))
+       actualUID := info.Sys().(*syscall.Stat_t).Uid
+       if actualUID != 0 && actualUID != trustedUID {
+               log.Printf("WARNING: %q is owned by uid %d, but expected %d or 0",
+                       path, actualUID, trustedUID)
+               return false
+       }
+       return true
+}
+
+// CheckSetup returns an error if any of the fscrypt metadata directories do not
+// exist. Will log any unexpected errors or incorrect permissions.
+func (m *Mount) CheckSetup(trustedUser *user.User) error {
+       if !m.isFscryptSetupAllowed() {
+               return &ErrNotSetup{m}
+       }
+       // Check that the mountpoint directory itself is not a symlink and has
+       // proper ownership, as otherwise we can't trust anything beneath it.
+       info, err := loggedLstat(m.Path)
+       if err != nil {
+               return &ErrNotSetup{m}
+       }
+       if (info.Mode() & os.ModeSymlink) != 0 {
+               log.Printf("mountpoint directory %q cannot be a symlink", m.Path)
+               return &ErrNotSetup{m}
+       }
+       if !info.IsDir() {
+               log.Printf("mountpoint %q is not a directory", m.Path)
+               return &ErrNotSetup{m}
+       }
+       if !checkOwnership(m.Path, info, trustedUser) {
+               return &ErrMountOwnedByAnotherUser{m}
+       }
+
+       // Check BaseDir similarly.  However, unlike the other directories, we
+       // allow BaseDir to be a symlink, to support the use case of metadata
+       // for a read-only filesystem being redirected to a writable location.
+       info, err = loggedStat(m.BaseDir())
+       if err != nil {
+               return &ErrNotSetup{m}
+       }
+       if !info.IsDir() {
+               log.Printf("%q is not a directory", m.BaseDir())
+               return &ErrNotSetup{m}
+       }
+       if !checkOwnership(m.Path, info, trustedUser) {
+               return &ErrMountOwnedByAnotherUser{m}
+       }
+
+       // Check that the policies and protectors directories aren't symlinks and
+       // have proper ownership.
+       subdirs := []string{m.PolicyDir(), m.ProtectorDir()}
+       for _, path := range subdirs {
+               info, err := loggedLstat(path)
+               if err != nil {
+                       return &ErrNotSetup{m}
+               }
+               if (info.Mode() & os.ModeSymlink) != 0 {
+                       log.Printf("directory %q cannot be a symlink", path)
+                       return &ErrNotSetup{m}
+               }
+               if !info.IsDir() {
+                       log.Printf("%q is not a directory", path)
+                       return &ErrNotSetup{m}
+               }
+               // We are no longer too picky about the mode, given that
+               // 'fscrypt setup' now offers a choice of two different modes,
+               // and system administrators could customize it further.
+               // However, we can at least verify that if the directory is
+               // world-writable, then the sticky bit is also set.
+               if info.Mode()&(os.ModeSticky|0002) == 0002 {
+                       log.Printf("%q is world-writable but doesn't have sticky bit set", path)
+                       return &ErrInsecurePermissions{path}
+               }
+               if !checkOwnership(path, info, trustedUser) {
+                       return &ErrSetupByAnotherUser{m}
+               }
+       }
+       return nil
+}
+
+// makeDirectories creates the three metadata directories with the correct
+// permissions. Note that this function overrides the umask.
+func (m *Mount) makeDirectories(setupMode SetupMode) error {
+       // Zero the umask so we get the permissions we want
+       oldMask := unix.Umask(0)
+       defer func() {
+               unix.Umask(oldMask)
+       }()
+
+       if err := os.Mkdir(m.BaseDir(), basePermissions); err != nil {
+               return err
+       }
+
+       var dirMode os.FileMode
+       switch setupMode {
+       case SingleUserWritable:
+               dirMode = 0755
+       case WorldWritable:
+               dirMode = os.ModeSticky | 0777
+       }
+       if err := os.Mkdir(m.PolicyDir(), dirMode); err != nil {
+               return err
+       }
+       return os.Mkdir(m.ProtectorDir(), dirMode)
+}
+
+// GetSetupMode returns the current mode for fscrypt metadata creation on this
+// filesystem.
+func (m *Mount) GetSetupMode() (SetupMode, *user.User, error) {
+       info1, err1 := os.Stat(m.PolicyDir())
+       info2, err2 := os.Stat(m.ProtectorDir())
+
+       if err1 == nil && err2 == nil {
+               mask := os.ModeSticky | 0777
+               mode1 := info1.Mode() & mask
+               mode2 := info2.Mode() & mask
+               uid1 := info1.Sys().(*syscall.Stat_t).Uid
+               uid2 := info2.Sys().(*syscall.Stat_t).Uid
+               user, err := util.UserFromUID(int64(uid1))
+               if err == nil && mode1 == mode2 && uid1 == uid2 {
+                       switch mode1 {
+                       case mask:
+                               return WorldWritable, nil, nil
+                       case 0755:
+                               return SingleUserWritable, user, nil
+                       }
+               }
+               log.Printf("filesystem %s uses custom permissions on metadata directories", m.Path)
+       }
+       return -1, nil, errors.New("unable to determine setup mode")
+}
+
+// Setup sets up the filesystem for use with fscrypt. Note that this merely
+// creates the appropriate files on the filesystem. It does not actually modify
+// the filesystem's feature flags. This operation is atomic; it either succeeds
+// or no files in the baseDir are created.
+func (m *Mount) Setup(mode SetupMode) error {
+       if m.CheckSetup(nil) == nil {
+               return &ErrAlreadySetup{m}
+       }
+       if !m.isFscryptSetupAllowed() {
+               return &ErrSetupNotSupported{m}
+       }
+       // We build the directories under a temp Mount and then move into place.
+       temp, err := m.tempMount()
+       if err != nil {
+               return err
+       }
+       defer os.RemoveAll(temp.Path)
+
+       if err = temp.makeDirectories(mode); err != nil {
+               return err
+       }
+
+       // Atomically move directory into place.
+       return os.Rename(temp.BaseDir(), m.BaseDir())
+}
+
+// RemoveAllMetadata removes all the policy and protector metadata from the
+// filesystem. This operation is atomic; it either succeeds or no files in the
+// baseDir are removed.
+// WARNING: Will cause data loss if the metadata is used to encrypt
+// directories (this could include directories on other filesystems).
+func (m *Mount) RemoveAllMetadata() error {
+       if err := m.CheckSetup(nil); err != nil {
+               return err
+       }
+       // temp will hold the old metadata temporarily
+       temp, err := m.tempMount()
+       if err != nil {
+               return err
+       }
+       defer os.RemoveAll(temp.Path)
+
+       // Move directory into temp (to be destroyed on defer)
+       return os.Rename(m.BaseDir(), temp.BaseDir())
+}
+
+func syncDirectory(dirPath string) error {
+       dirFile, err := os.Open(dirPath)
+       if err != nil {
+               return err
+       }
+       if err = dirFile.Sync(); err != nil {
+               dirFile.Close()
+               return err
+       }
+       return dirFile.Close()
+}
+
+func (m *Mount) overwriteDataNonAtomic(path string, data []byte) error {
+       file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|unix.O_NOFOLLOW, 0)
+       if err != nil {
+               return err
+       }
+       if _, err = file.Write(data); err != nil {
+               log.Printf("WARNING: overwrite of %q failed; file will be corrupted!", path)
+               file.Close()
+               return err
+       }
+       if err = file.Sync(); err != nil {
+               file.Close()
+               return err
+       }
+       if err = file.Close(); err != nil {
+               return err
+       }
+       log.Printf("successfully overwrote %q non-atomically", path)
+       return nil
+}
+
+// writeData writes the given data to the given path such that, if possible, the
+// data is either written to stable storage or an error is returned.  If a file
+// already exists at the path, it will be replaced.
+//
+// However, if the process doesn't have write permission to the directory but
+// does have write permission to the file itself, then as a fallback the file is
+// overwritten in-place rather than replaced.  Note that this may be non-atomic.
+func (m *Mount) writeData(path string, data []byte, owner *user.User, mode os.FileMode) error {
+       // Write the data to a temporary file, sync it, then rename into place
+       // so that the operation will be atomic.
+       dirPath := filepath.Dir(path)
+       tempFile, err := os.CreateTemp(dirPath, tempPrefix)
+       if err != nil {
+               log.Print(err)
+               if os.IsPermission(err) {
+                       if _, err = os.Lstat(path); err == nil {
+                               log.Printf("trying non-atomic overwrite of %q", path)
+                               return m.overwriteDataNonAtomic(path, data)
+                       }
+                       return &ErrNoCreatePermission{m}
+               }
+               return err
+       }
+       defer os.Remove(tempFile.Name())
+
+       // Ensure the new file has the right permissions mask.
+       if err = tempFile.Chmod(mode); err != nil {
+               tempFile.Close()
+               return err
+       }
+       // Override the file owner if one was specified.  This happens when root
+       // needs to create files owned by a particular user.
+       if owner != nil {
+               if err = util.Chown(tempFile, owner); err != nil {
+                       log.Printf("could not set owner of %q to %v: %v",
+                               path, owner.Username, err)
+                       tempFile.Close()
+                       return err
+               }
+       }
+       if _, err = tempFile.Write(data); err != nil {
+               tempFile.Close()
+               return err
+       }
+       if err = tempFile.Sync(); err != nil {
+               tempFile.Close()
+               return err
+       }
+       if err = tempFile.Close(); err != nil {
+               return err
+       }
+
+       if err = os.Rename(tempFile.Name(), path); err != nil {
+               return err
+       }
+       // Ensure the rename has been persisted before returning success.
+       return syncDirectory(dirPath)
+}
+
+// addMetadata writes the metadata structure to the file with the specified
+// path. This will overwrite any existing data. The operation is atomic.
+func (m *Mount) addMetadata(path string, md metadata.Metadata, owner *user.User) error {
+       if err := md.CheckValidity(); err != nil {
+               return errors.Wrap(err, "provided metadata is invalid")
+       }
+
+       data, err := proto.Marshal(md)
+       if err != nil {
+               return err
+       }
+
+       mode := filePermissions
+       // If the file already exists, then preserve its owner and mode if
+       // possible.  This is necessary because by default, for atomicity
+       // reasons we'll replace the file rather than overwrite it.
+       info, err := os.Lstat(path)
+       if err == nil {
+               if owner == nil && util.IsUserRoot() {
+                       uid := info.Sys().(*syscall.Stat_t).Uid
+                       if owner, err = util.UserFromUID(int64(uid)); err != nil {
+                               log.Print(err)
+                       }
+               }
+               mode = info.Mode() & 0777
+       } else if !os.IsNotExist(err) {
+               log.Print(err)
+       }
+
+       if owner != nil {
+               log.Printf("writing metadata to %q and setting owner to %s", path, owner.Username)
+       } else {
+               log.Printf("writing metadata to %q", path)
+       }
+       return m.writeData(path, data, owner, mode)
+}
+
+// readMetadataFileSafe gets the contents of a metadata file extra-carefully,
+// considering that it could be a malicious file created to cause a
+// denial-of-service.  Specifically, the following checks are done:
+//
+//   - It must be a regular file, not another type of file like a symlink or FIFO.
+//     (Symlinks aren't bad by themselves, but given that a malicious user could
+//     point one to absolutely anywhere, and there is no known use case for the
+//     metadata files themselves being symlinks, it seems best to disallow them.)
+//   - It must have a reasonable size (<= maxMetadataFileSize).
+//   - If trustedUser is non-nil, then the file must be owned by the given user
+//     or by root.
+//
+// Take care to avoid TOCTOU (time-of-check-time-of-use) bugs when doing these
+// tests.  Notably, we must open the file before checking the file type, as the
+// file type could change between any previous checks and the open.  When doing
+// this, O_NOFOLLOW is needed to avoid following a symlink (this applies to the
+// last path component only), and O_NONBLOCK is needed to avoid blocking if the
+// file is a FIFO.
+//
+// This function returns the data read as well as the UID of the user who owns
+// the file.  The returned UID is needed for login protectors, where the UID
+// needs to be cross-checked with the UID stored in the file itself.
+func readMetadataFileSafe(path string, trustedUser *user.User) ([]byte, int64, error) {
+       file, err := os.OpenFile(path, os.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
+       if err != nil {
+               return nil, -1, err
+       }
+       defer file.Close()
+
+       info, err := file.Stat()
+       if err != nil {
+               return nil, -1, err
+       }
+       if !info.Mode().IsRegular() {
+               return nil, -1, &ErrCorruptMetadata{path, errors.New("not a regular file")}
+       }
+       if !checkOwnership(path, info, trustedUser) {
+               return nil, -1, &ErrCorruptMetadata{path, errors.New("metadata file belongs to another user")}
+       }
+       // Clear O_NONBLOCK, since it has served its purpose when opening the
+       // file, and the behavior of reading from a regular file with O_NONBLOCK
+       // is technically unspecified.
+       if _, err = unix.FcntlInt(file.Fd(), unix.F_SETFL, 0); err != nil {
+               return nil, -1, &os.PathError{Op: "clearing O_NONBLOCK", Path: path, Err: err}
+       }
+       // Read the file contents, allowing at most maxMetadataFileSize bytes.
+       reader := &io.LimitedReader{R: file, N: maxMetadataFileSize + 1}
+       data, err := io.ReadAll(reader)
+       if err != nil {
+               return nil, -1, err
+       }
+       if reader.N == 0 {
+               return nil, -1, &ErrCorruptMetadata{path, errors.New("metadata file size limit exceeded")}
+       }
+       return data, int64(info.Sys().(*syscall.Stat_t).Uid), nil
+}
+
+// getMetadata reads the metadata structure from the file with the specified
+// path. Only reads normal metadata files, not linked metadata.
+func (m *Mount) getMetadata(path string, trustedUser *user.User, md metadata.Metadata) (int64, error) {
+       data, owner, err := readMetadataFileSafe(path, trustedUser)
+       if err != nil {
+               log.Printf("could not read metadata from %q: %v", path, err)
+               return -1, err
+       }
+
+       if err := proto.Unmarshal(data, md); err != nil {
+               return -1, &ErrCorruptMetadata{path, err}
+       }
+
+       if err := md.CheckValidity(); err != nil {
+               return -1, &ErrCorruptMetadata{path, err}
+       }
+
+       log.Printf("successfully read metadata from %q", path)
+       return owner, nil
+}
+
+// removeMetadata deletes the metadata struct from the file with the specified
+// path. Works with regular or linked metadata.
+func (m *Mount) removeMetadata(path string) error {
+       if err := os.Remove(path); err != nil {
+               log.Printf("could not remove metadata file at %q: %v", path, err)
+               return err
+       }
+
+       log.Printf("successfully removed metadata file at %q", path)
+       return nil
+}
+
+// AddProtector adds the protector metadata to this filesystem's storage. This
+// will overwrite the value of an existing protector with this descriptor. This
+// will fail with ErrLinkedProtector if a linked protector with this descriptor
+// already exists on the filesystem.
+func (m *Mount) AddProtector(data *metadata.ProtectorData, owner *user.User) error {
+       var err error
+       if err = m.CheckSetup(nil); err != nil {
+               return err
+       }
+       if isRegularFile(m.linkedProtectorPath(data.ProtectorDescriptor)) {
+               return errors.Errorf("cannot modify linked protector %s on filesystem %s",
+                       data.ProtectorDescriptor, m.Path)
+       }
+       path := m.protectorPath(data.ProtectorDescriptor)
+       return m.addMetadata(path, data, owner)
+}
+
+// AddLinkedProtector adds a link in this filesystem to the protector metadata
+// in the dest filesystem, if one doesn't already exist.  On success, the return
+// value is a nil error and a bool that is true iff the link is newly created.
+func (m *Mount) AddLinkedProtector(descriptor string, dest *Mount, trustedUser *user.User,
+       ownerIfCreating *user.User) (bool, error) {
+       if err := m.CheckSetup(trustedUser); err != nil {
+               return false, err
+       }
+       // Check that the link is good (descriptor exists, filesystem has UUID).
+       if _, err := dest.GetRegularProtector(descriptor, trustedUser); err != nil {
+               return false, err
+       }
+
+       linkPath := m.linkedProtectorPath(descriptor)
+
+       // Check whether the link already exists.
+       existingLink, _, err := readMetadataFileSafe(linkPath, trustedUser)
+       if err == nil {
+               existingLinkedMnt, err := getMountFromLink(string(existingLink))
+               if err != nil {
+                       return false, errors.Wrap(err, linkPath)
+               }
+               if existingLinkedMnt != dest {
+                       return false, errors.Errorf("link %q points to %q, but expected %q",
+                               linkPath, existingLinkedMnt.Path, dest.Path)
+               }
+               return false, nil
+       }
+       if !os.IsNotExist(err) {
+               return false, err
+       }
+
+       var newLink string
+       newLink, err = makeLink(dest)
+       if err != nil {
+               return false, err
+       }
+       return true, m.writeData(linkPath, []byte(newLink), ownerIfCreating, filePermissions)
+}
+
+// GetRegularProtector looks up the protector metadata by descriptor. This will
+// fail with ErrProtectorNotFound if the descriptor is a linked protector.
+func (m *Mount) GetRegularProtector(descriptor string, trustedUser *user.User) (*metadata.ProtectorData, error) {
+       if err := m.CheckSetup(trustedUser); err != nil {
+               return nil, err
+       }
+       data := new(metadata.ProtectorData)
+       path := m.protectorPath(descriptor)
+       owner, err := m.getMetadata(path, trustedUser, data)
+       if os.IsNotExist(err) {
+               err = &ErrProtectorNotFound{descriptor, m}
+       }
+       if err != nil {
+               return nil, err
+       }
+       // Login protectors have their UID stored in the file.  Since normally
+       // any user can create files in the fscrypt metadata directories, for a
+       // login protector to be considered valid it *must* be owned by the
+       // claimed user or by root.  Note: fscrypt v0.3.2 and later always makes
+       // login protectors owned by the user, but previous versions could
+       // create them owned by root -- that is the main reason we allow root.
+       if data.Source == metadata.SourceType_pam_passphrase && owner != 0 && owner != data.Uid {
+               log.Printf("WARNING: %q claims to be the login protector for uid %d, but it is owned by uid %d.  Needs to be %d or 0.",
+                       path, data.Uid, owner, data.Uid)
+               return nil, &ErrCorruptMetadata{path, errors.New("login protector belongs to wrong user")}
+       }
+       return data, nil
+}
+
+// GetProtector returns the Mount of the filesystem containing the information
+// and that protector's data. If the descriptor is a regular (not linked)
+// protector, the mount will return itself.
+func (m *Mount) GetProtector(descriptor string, trustedUser *user.User) (*Mount, *metadata.ProtectorData, error) {
+       if err := m.CheckSetup(trustedUser); err != nil {
+               return nil, nil, err
+       }
+       // Get the link data from the link file
+       path := m.linkedProtectorPath(descriptor)
+       link, _, err := readMetadataFileSafe(path, trustedUser)
+       if err != nil {
+               // If the link doesn't exist, try for a regular protector.
+               if os.IsNotExist(err) {
+                       data, err := m.GetRegularProtector(descriptor, trustedUser)
+                       return m, data, err
+               }
+               return nil, nil, err
+       }
+       log.Printf("following protector link %s", path)
+       linkedMnt, err := getMountFromLink(string(link))
+       if err != nil {
+               return nil, nil, errors.Wrap(err, path)
+       }
+       data, err := linkedMnt.GetRegularProtector(descriptor, trustedUser)
+       if err != nil {
+               return nil, nil, &ErrFollowLink{string(link), err}
+       }
+       return linkedMnt, data, nil
+}
+
+// RemoveProtector deletes the protector metadata (or a link to another
+// filesystem's metadata) from the filesystem storage.
+func (m *Mount) RemoveProtector(descriptor string) error {
+       if err := m.CheckSetup(nil); err != nil {
+               return err
+       }
+       // We first try to remove the linkedProtector. If that metadata does not
+       // exist, we try to remove the normal protector.
+       err := m.removeMetadata(m.linkedProtectorPath(descriptor))
+       if os.IsNotExist(err) {
+               err = m.removeMetadata(m.protectorPath(descriptor))
+               if os.IsNotExist(err) {
+                       err = &ErrProtectorNotFound{descriptor, m}
+               }
+       }
+       return err
+}
+
+// ListProtectors lists the descriptors of all protectors on this filesystem.
+// This does not include linked protectors.  If trustedUser is non-nil, then
+// the protectors are restricted to those owned by the given user or by root.
+func (m *Mount) ListProtectors(trustedUser *user.User) ([]string, error) {
+       return m.listMetadata(m.ProtectorDir(), "protectors", trustedUser)
+}
+
+// AddPolicy adds the policy metadata to the filesystem storage.
+func (m *Mount) AddPolicy(data *metadata.PolicyData, owner *user.User) error {
+       if err := m.CheckSetup(nil); err != nil {
+               return err
+       }
+
+       return m.addMetadata(m.PolicyPath(data.KeyDescriptor), data, owner)
+}
+
+// GetPolicy looks up the policy metadata by descriptor.
+func (m *Mount) GetPolicy(descriptor string, trustedUser *user.User) (*metadata.PolicyData, error) {
+       if err := m.CheckSetup(trustedUser); err != nil {
+               return nil, err
+       }
+       data := new(metadata.PolicyData)
+       _, err := m.getMetadata(m.PolicyPath(descriptor), trustedUser, data)
+       if os.IsNotExist(err) {
+               err = &ErrPolicyNotFound{descriptor, m}
+       }
+       return data, err
+}
+
+// RemovePolicy deletes the policy metadata from the filesystem storage.
+func (m *Mount) RemovePolicy(descriptor string) error {
+       if err := m.CheckSetup(nil); err != nil {
+               return err
+       }
+       err := m.removeMetadata(m.PolicyPath(descriptor))
+       if os.IsNotExist(err) {
+               err = &ErrPolicyNotFound{descriptor, m}
+       }
+       return err
+}
+
+// ListPolicies lists the descriptors of all policies on this filesystem.  If
+// trustedUser is non-nil, then the policies are restricted to those owned by
+// the given user or by root.
+func (m *Mount) ListPolicies(trustedUser *user.User) ([]string, error) {
+       return m.listMetadata(m.PolicyDir(), "policies", trustedUser)
+}
+
+type namesAndTimes struct {
+       names []string
+       times []time.Time
+}
+
+func (c namesAndTimes) Len() int {
+       return len(c.names)
+}
+
+func (c namesAndTimes) Less(i, j int) bool {
+       return c.times[i].Before(c.times[j])
+}
+
+func (c namesAndTimes) Swap(i, j int) {
+       c.names[i], c.names[j] = c.names[j], c.names[i]
+       c.times[i], c.times[j] = c.times[j], c.times[i]
+}
+
+func sortFileListByLastMtime(directoryPath string, names []string) error {
+       c := namesAndTimes{names: names, times: make([]time.Time, len(names))}
+       for i, name := range names {
+               fi, err := os.Lstat(filepath.Join(directoryPath, name))
+               if err != nil {
+                       return err
+               }
+               c.times[i] = fi.ModTime()
+       }
+       sort.Sort(c)
+       return nil
+}
+
+// listDirectory returns a list of descriptors for a metadata directory,
+// including files which are links to other filesystem's metadata.
+func (m *Mount) listDirectory(directoryPath string) ([]string, error) {
+       dir, err := os.Open(directoryPath)
+       if err != nil {
+               return nil, err
+       }
+       defer dir.Close()
+
+       names, err := dir.Readdirnames(-1)
+       if err != nil {
+               return nil, err
+       }
+
+       if SortDescriptorsByLastMtime {
+               if err := sortFileListByLastMtime(directoryPath, names); err != nil {
+                       return nil, err
+               }
+       }
+
+       descriptors := make([]string, 0, len(names))
+       for _, name := range names {
+               // Be sure to include links as well
+               descriptors = append(descriptors, strings.TrimSuffix(name, linkFileExtension))
+       }
+       return descriptors, nil
+}
+
+func (m *Mount) listMetadata(dirPath string, metadataType string, owner *user.User) ([]string, error) {
+       log.Printf("listing %s in %q", metadataType, dirPath)
+       if err := m.CheckSetup(owner); err != nil {
+               return nil, err
+       }
+       names, err := m.listDirectory(dirPath)
+       if err != nil {
+               return nil, err
+       }
+       filesIgnoredDescription := ""
+       if owner != nil {
+               filteredNames := make([]string, 0, len(names))
+               uid := uint32(util.AtoiOrPanic(owner.Uid))
+               for _, name := range names {
+                       info, err := os.Lstat(filepath.Join(dirPath, name))
+                       if err != nil {
+                               continue
+                       }
+                       fileUID := info.Sys().(*syscall.Stat_t).Uid
+                       if fileUID != uid && fileUID != 0 {
+                               continue
+                       }
+                       filteredNames = append(filteredNames, name)
+               }
+               numIgnored := len(names) - len(filteredNames)
+               if numIgnored != 0 {
+                       filesIgnoredDescription =
+                               fmt.Sprintf(" (ignored %d %s not owned by %s or root)",
+                                       numIgnored, metadataType, owner.Username)
+               }
+               names = filteredNames
+       }
+       log.Printf("found %d %s%s", len(names), metadataType, filesIgnoredDescription)
+       return names, nil
+}
diff --git a/filesystem/filesystem_test.go b/filesystem/filesystem_test.go
new file mode 100644 (file)
index 0000000..f9c34ae
--- /dev/null
@@ -0,0 +1,610 @@
+/*
+ * filesystem_test.go - Tests for reading/writing metadata to disk.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package filesystem
+
+import (
+       "os"
+       "os/user"
+       "path/filepath"
+       "syscall"
+       "testing"
+
+       "golang.org/x/sys/unix"
+       "google.golang.org/protobuf/proto"
+
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+var (
+       fakeProtectorKey, _    = crypto.NewRandomKey(metadata.InternalKeyLen)
+       fakePolicyKey, _       = crypto.NewRandomKey(metadata.PolicyKeyLen)
+       wrappedProtectorKey, _ = crypto.Wrap(fakeProtectorKey, fakeProtectorKey)
+       wrappedPolicyKey, _    = crypto.Wrap(fakeProtectorKey, fakePolicyKey)
+)
+
+// Gets the mount corresponding to the integration test path.
+func getTestMount(t *testing.T) (*Mount, error) {
+       mountpoint, err := util.TestRoot()
+       if err != nil {
+               t.Skip(err)
+       }
+       return GetMount(mountpoint)
+}
+
+func getFakeProtector() *metadata.ProtectorData {
+       return &metadata.ProtectorData{
+               ProtectorDescriptor: "fedcba9876543210",
+               Name:                "goodProtector",
+               Source:              metadata.SourceType_raw_key,
+               WrappedKey:          wrappedProtectorKey,
+       }
+}
+
+func getFakeLoginProtector(uid int64) *metadata.ProtectorData {
+       protector := getFakeProtector()
+       protector.Source = metadata.SourceType_pam_passphrase
+       protector.Uid = uid
+       protector.Costs = &metadata.HashingCosts{
+               Time:        1,
+               Memory:      1 << 8,
+               Parallelism: 1,
+       }
+       protector.Salt = make([]byte, 16)
+       return protector
+}
+
+func getFakePolicy() *metadata.PolicyData {
+       return &metadata.PolicyData{
+               KeyDescriptor: "0123456789abcdef",
+               Options:       metadata.DefaultOptions,
+               WrappedPolicyKeys: []*metadata.WrappedPolicyKey{
+                       {
+                               ProtectorDescriptor: "fedcba9876543210",
+                               WrappedKey:          wrappedPolicyKey,
+                       },
+               },
+       }
+}
+
+// Gets the mount and sets it up
+func getSetupMount(t *testing.T) (*Mount, error) {
+       mnt, err := getTestMount(t)
+       if err != nil {
+               return nil, err
+       }
+       return mnt, mnt.Setup(WorldWritable)
+}
+
+// Tests that the setup works and creates the correct files
+func TestSetup(t *testing.T) {
+       mnt, err := getSetupMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err := mnt.CheckSetup(nil); err != nil {
+               t.Error(err)
+       }
+
+       os.RemoveAll(mnt.BaseDir())
+}
+
+// Tests that we can remove all of the metadata
+func TestRemoveAllMetadata(t *testing.T) {
+       mnt, err := getSetupMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err = mnt.RemoveAllMetadata(); err != nil {
+               t.Fatal(err)
+       }
+
+       if isDir(mnt.BaseDir()) {
+               t.Error("metadata was not removed")
+       }
+}
+
+// isSymlink returns true if the path exists and is that of a symlink.
+func isSymlink(path string) bool {
+       info, err := loggedLstat(path)
+       return err == nil && info.Mode()&os.ModeSymlink != 0
+}
+
+// Test that when MOUNTPOINT/.fscrypt is a pre-created symlink, fscrypt will
+// create/delete the metadata at the location pointed to by the symlink.
+//
+// This is a helper function that is called twice: once to test an absolute
+// symlink and once to test a relative symlink.
+func testSetupWithSymlink(t *testing.T, mnt *Mount, symlinkTarget string, realDir string) {
+       rawBaseDir := filepath.Join(mnt.Path, baseDirName)
+       if err := os.Symlink(symlinkTarget, rawBaseDir); err != nil {
+               t.Fatal(err)
+       }
+       defer os.Remove(rawBaseDir)
+
+       if err := mnt.Setup(WorldWritable); err != nil {
+               t.Fatal(err)
+       }
+       defer mnt.RemoveAllMetadata()
+       if err := mnt.CheckSetup(nil); err != nil {
+               t.Fatal(err)
+       }
+       if !isSymlink(rawBaseDir) {
+               t.Fatal("base dir should still be a symlink")
+       }
+       if !isDir(realDir) {
+               t.Fatal("real base dir should exist")
+       }
+       if err := mnt.RemoveAllMetadata(); err != nil {
+               t.Fatal(err)
+       }
+       if !isSymlink(rawBaseDir) {
+               t.Fatal("base dir should still be a symlink")
+       }
+       if isDir(realDir) {
+               t.Fatal("real base dir should no longer exist")
+       }
+}
+
+func TestSetupWithAbsoluteSymlink(t *testing.T) {
+       mnt, err := getTestMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       tempDir, err := os.MkdirTemp("", "fscrypt")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(tempDir)
+       realDir := filepath.Join(tempDir, "realDir")
+       if realDir, err = filepath.Abs(realDir); err != nil {
+               t.Fatal(err)
+       }
+       testSetupWithSymlink(t, mnt, realDir, realDir)
+}
+
+func TestSetupWithRelativeSymlink(t *testing.T) {
+       mnt, err := getTestMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       realDir := filepath.Join(mnt.Path, ".fscrypt-real")
+       testSetupWithSymlink(t, mnt, ".fscrypt-real", realDir)
+}
+
+func testSetupMode(t *testing.T, mnt *Mount, setupMode SetupMode, expectedPerms os.FileMode) {
+       mnt.RemoveAllMetadata()
+       if err := mnt.Setup(setupMode); err != nil {
+               t.Fatal(err)
+       }
+       dirNames := []string{"policies", "protectors"}
+       for _, dirName := range dirNames {
+               fi, err := os.Stat(filepath.Join(mnt.Path, ".fscrypt", dirName))
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if fi.Mode()&(os.ModeSticky|0777) != expectedPerms {
+                       t.Errorf("directory %s doesn't have permissions %o", dirName, expectedPerms)
+               }
+       }
+}
+
+// Tests that the supported setup modes (WorldWritable and SingleUserWritable)
+// work as intended.
+func TestSetupModes(t *testing.T) {
+       mnt, err := getTestMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer mnt.RemoveAllMetadata()
+       testSetupMode(t, mnt, WorldWritable, os.ModeSticky|0777)
+       testSetupMode(t, mnt, SingleUserWritable, 0755)
+}
+
+// Tests that fscrypt refuses to use metadata directories that are
+// world-writable but don't have the sticky bit set.
+func TestInsecurePermissions(t *testing.T) {
+       mnt, err := getTestMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer mnt.RemoveAllMetadata()
+
+       if err = mnt.Setup(WorldWritable); err != nil {
+               t.Fatal(err)
+       }
+       if err = os.Chmod(mnt.PolicyDir(), 0777); err != nil {
+               t.Fatal(err)
+       }
+       defer os.Chmod(mnt.PolicyDir(), os.ModeSticky|0777)
+       err = mnt.CheckSetup(nil)
+       if _, ok := err.(*ErrInsecurePermissions); !ok {
+               t.Fatal("expected ErrInsecurePermissions")
+       }
+}
+
+// Adding a good Protector should succeed, adding a bad one should fail
+func TestAddProtector(t *testing.T) {
+       mnt, err := getSetupMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer mnt.RemoveAllMetadata()
+
+       protector := getFakeProtector()
+       if err = mnt.AddProtector(protector, nil); err != nil {
+               t.Error(err)
+       }
+
+       // Change the source to bad one, or one that requires hashing costs
+       protector.Source = metadata.SourceType_default
+       if mnt.AddProtector(protector, nil) == nil {
+               t.Error("bad source for a descriptor should make metadata invalid")
+       }
+       protector.Source = metadata.SourceType_custom_passphrase
+       if mnt.AddProtector(protector, nil) == nil {
+               t.Error("protectors using passphrases should require hashing costs")
+       }
+       protector.Source = metadata.SourceType_raw_key
+
+       // Use a bad wrapped key
+       protector.WrappedKey = wrappedPolicyKey
+       if mnt.AddProtector(protector, nil) == nil {
+               t.Error("bad length for protector keys should make metadata invalid")
+       }
+       protector.WrappedKey = wrappedProtectorKey
+
+       // Change the descriptor (to a bad length)
+       protector.ProtectorDescriptor = "abcde"
+       if mnt.AddProtector(protector, nil) == nil {
+               t.Error("bad descriptor length should make metadata invalid")
+       }
+
+}
+
+// Adding a good Policy should succeed, adding a bad one should fail
+func TestAddPolicy(t *testing.T) {
+       mnt, err := getSetupMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer mnt.RemoveAllMetadata()
+
+       policy := getFakePolicy()
+       if err = mnt.AddPolicy(policy, nil); err != nil {
+               t.Error(err)
+       }
+
+       // Bad encryption options should make policy invalid
+       policy.Options.Padding = 7
+       if mnt.AddPolicy(policy, nil) == nil {
+               t.Error("padding not a power of 2 should make metadata invalid")
+       }
+       policy.Options.Padding = 16
+       policy.Options.Filenames = metadata.EncryptionOptions_default
+       if mnt.AddPolicy(policy, nil) == nil {
+               t.Error("encryption mode not set should make metadata invalid")
+       }
+       policy.Options.Filenames = metadata.EncryptionOptions_AES_256_CTS
+
+       // Use a bad wrapped key
+       policy.WrappedPolicyKeys[0].WrappedKey = wrappedProtectorKey
+       if mnt.AddPolicy(policy, nil) == nil {
+               t.Error("bad length for policy keys should make metadata invalid")
+       }
+       policy.WrappedPolicyKeys[0].WrappedKey = wrappedPolicyKey
+
+       // Change the descriptor (to a bad length)
+       policy.KeyDescriptor = "abcde"
+       if mnt.AddPolicy(policy, nil) == nil {
+               t.Error("bad descriptor length should make metadata invalid")
+       }
+}
+
+// Tests that we can set a policy and get it back
+func TestSetPolicy(t *testing.T) {
+       mnt, err := getSetupMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer mnt.RemoveAllMetadata()
+
+       policy := getFakePolicy()
+       if err = mnt.AddPolicy(policy, nil); err != nil {
+               t.Fatal(err)
+       }
+
+       realPolicy, err := mnt.GetPolicy(policy.KeyDescriptor, nil)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if !proto.Equal(realPolicy, policy) {
+               t.Errorf("policy %+v does not equal expected policy %+v", realPolicy, policy)
+       }
+
+}
+
+// Tests that we can set a normal protector and get it back
+func TestSetProtector(t *testing.T) {
+       mnt, err := getSetupMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer mnt.RemoveAllMetadata()
+
+       protector := getFakeProtector()
+       if err = mnt.AddProtector(protector, nil); err != nil {
+               t.Fatal(err)
+       }
+
+       realProtector, err := mnt.GetRegularProtector(protector.ProtectorDescriptor, nil)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if !proto.Equal(realProtector, protector) {
+               t.Errorf("protector %+v does not equal expected protector %+v", realProtector, protector)
+       }
+}
+
+// Tests that a login protector whose embedded UID doesn't match the file owner
+// is considered invalid.  (Such a file could be created by a malicious user to
+// try to confuse fscrypt into processing the wrong file.)
+func TestSpoofedLoginProtector(t *testing.T) {
+       myUID := int64(os.Geteuid())
+       badUID := myUID + 1 // anything different from myUID
+       mnt, err := getSetupMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer mnt.RemoveAllMetadata()
+
+       // Control case: protector with matching UID should be accepted.
+       protector := getFakeLoginProtector(myUID)
+       if err = mnt.AddProtector(protector, nil); err != nil {
+               t.Fatal(err)
+       }
+       _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor, nil)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if err = mnt.RemoveProtector(protector.ProtectorDescriptor); err != nil {
+               t.Fatal(err)
+       }
+
+       // The real test: protector with mismatching UID should rejected,
+       // *unless* the process running the tests (and hence the file owner) is
+       // root in which case it should be accepted.
+       protector = getFakeLoginProtector(badUID)
+       if err = mnt.AddProtector(protector, nil); err != nil {
+               t.Fatal(err)
+       }
+       _, err = mnt.GetRegularProtector(protector.ProtectorDescriptor, nil)
+       if myUID == 0 {
+               if err != nil {
+                       t.Fatal(err)
+               }
+       } else {
+               if err == nil {
+                       t.Fatal("reading protector with bad UID unexpectedly succeeded")
+               }
+       }
+}
+
+// Tests that the fscrypt metadata files are given mode 0600.
+func TestMetadataFileMode(t *testing.T) {
+       mnt, err := getSetupMount(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer mnt.RemoveAllMetadata()
+
+       // Policy
+       policy := getFakePolicy()
+       if err = mnt.AddPolicy(policy, nil); err != nil {
+               t.Fatal(err)
+       }
+       fi, err := os.Stat(filepath.Join(mnt.Path, ".fscrypt/policies/", policy.KeyDescriptor))
+       if err != nil {
+               t.Fatal(err)
+       }
+       if fi.Mode()&0777 != 0600 {
+               t.Error("Policy file has wrong mode")
+       }
+
+       // Protector
+       protector := getFakeProtector()
+       if err = mnt.AddProtector(protector, nil); err != nil {
+               t.Fatal(err)
+       }
+       fi, err = os.Stat(filepath.Join(mnt.Path, ".fscrypt/protectors", protector.ProtectorDescriptor))
+       if err != nil {
+               t.Fatal(err)
+       }
+       if fi.Mode()&0777 != 0600 {
+               t.Error("Protector file has wrong mode")
+       }
+}
+
+// Gets a setup mount and a fake second mount
+func getTwoSetupMounts(t *testing.T) (realMnt, fakeMnt *Mount, err error) {
+       if realMnt, err = getSetupMount(t); err != nil {
+               return
+       }
+
+       // Create and setup a fake filesystem
+       fakeMountpoint := filepath.Join(realMnt.Path, "fake")
+       if err = os.MkdirAll(fakeMountpoint, basePermissions); err != nil {
+               return
+       }
+       fakeMnt = &Mount{Path: fakeMountpoint, FilesystemType: realMnt.FilesystemType}
+       err = fakeMnt.Setup(WorldWritable)
+       return
+}
+
+// Removes all the data from the fake and real filesystems
+func cleanupTwoMounts(realMnt, fakeMnt *Mount) {
+       realMnt.RemoveAllMetadata()
+       os.RemoveAll(fakeMnt.Path)
+}
+
+// Tests that we can set a linked protector and get it back
+func TestLinkedProtector(t *testing.T) {
+       realMnt, fakeMnt, err := getTwoSetupMounts(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer cleanupTwoMounts(realMnt, fakeMnt)
+
+       // Add the protector to the first filesystem
+       protector := getFakeProtector()
+       if err = realMnt.AddProtector(protector, nil); err != nil {
+               t.Fatal(err)
+       }
+
+       // Add the link to the second filesystem
+       var isNewLink bool
+       if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt, nil, nil); err != nil {
+               t.Fatal(err)
+       }
+       if !isNewLink {
+               t.Fatal("Link was not new")
+       }
+       if isNewLink, err = fakeMnt.AddLinkedProtector(protector.ProtectorDescriptor, realMnt, nil, nil); err != nil {
+               t.Fatal(err)
+       }
+       if isNewLink {
+               t.Fatal("Link was new")
+       }
+
+       // Get the protector though the second system
+       _, err = fakeMnt.GetRegularProtector(protector.ProtectorDescriptor, nil)
+       if _, ok := err.(*ErrProtectorNotFound); !ok {
+               t.Fatal(err)
+       }
+
+       retMnt, retProtector, err := fakeMnt.GetProtector(protector.ProtectorDescriptor, nil)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if retMnt != realMnt {
+               t.Error("mount returned was incorrect")
+       }
+
+       if !proto.Equal(retProtector, protector) {
+               t.Errorf("protector %+v does not equal expected protector %+v", retProtector, protector)
+       }
+}
+
+func createFile(path string, size int64) error {
+       if err := os.WriteFile(path, []byte{}, 0600); err != nil {
+               return err
+       }
+       return os.Truncate(path, size)
+}
+
+// Tests the readMetadataFileSafe() function.
+func TestReadMetadataFileSafe(t *testing.T) {
+       currentUser, err := util.EffectiveUser()
+       otherUser := &user.User{Uid: "-1"}
+       if err != nil {
+               t.Fatal(err)
+       }
+       tempDir, err := os.MkdirTemp("", "fscrypt")
+       if err != nil {
+               t.Fatal(err)
+       }
+       filePath := filepath.Join(tempDir, "file")
+       defer os.RemoveAll(tempDir)
+
+       // Good file (control case)
+       if err = createFile(filePath, 1000); err != nil {
+               t.Fatal(err)
+       }
+       _, owner, err := readMetadataFileSafe(filePath, nil)
+       if err != nil {
+               t.Fatal("failed to read file")
+       }
+       if owner != int64(os.Geteuid()) {
+               t.Fatal("got wrong owner")
+       }
+       // Also try it with the trustedUser argument set to the current user.
+       if _, _, err = readMetadataFileSafe(filePath, currentUser); err != nil {
+               t.Fatal("failed to read file")
+       }
+       os.Remove(filePath)
+
+       // File owned by another user.  We might not have permission to actually
+       // change the file's ownership, so we simulate this by passing in a bad
+       // value for the trustedUser argument.
+       if err = createFile(filePath, 1000); err != nil {
+               t.Fatal(err)
+       }
+       _, _, err = readMetadataFileSafe(filePath, otherUser)
+       if util.IsUserRoot() {
+               if err != nil {
+                       t.Fatal("root-owned file didn't pass owner validation")
+               }
+       } else {
+               if err == nil {
+                       t.Fatal("unexpectedly could read file owned by another user")
+               }
+       }
+       os.Remove(filePath)
+
+       // Nonexistent file
+       _, _, err = readMetadataFileSafe(filePath, nil)
+       if !os.IsNotExist(err) {
+               t.Fatal("trying to read nonexistent file didn't fail with expected error")
+       }
+
+       // Symlink
+       if err = os.Symlink("target", filePath); err != nil {
+               t.Fatal(err)
+       }
+       _, _, err = readMetadataFileSafe(filePath, nil)
+       if err.(*os.PathError).Err != syscall.ELOOP {
+               t.Fatal("trying to read symlink didn't fail with ELOOP")
+       }
+       os.Remove(filePath)
+
+       // FIFO
+       if err = unix.Mkfifo(filePath, 0600); err != nil {
+               t.Fatal(err)
+       }
+       _, _, err = readMetadataFileSafe(filePath, nil)
+       if _, ok := err.(*ErrCorruptMetadata); !ok {
+               t.Fatal("trying to read FIFO didn't fail with expected error")
+       }
+       os.Remove(filePath)
+
+       // Very large file
+       if err = createFile(filePath, 1000000); err != nil {
+               t.Fatal(err)
+       }
+       _, _, err = readMetadataFileSafe(filePath, nil)
+       if _, ok := err.(*ErrCorruptMetadata); !ok {
+               t.Fatal("trying to read very large file didn't fail with expected error")
+       }
+}
diff --git a/filesystem/mountpoint.go b/filesystem/mountpoint.go
new file mode 100644 (file)
index 0000000..ae432bf
--- /dev/null
@@ -0,0 +1,582 @@
+/*
+ * mountpoint.go - Contains all the functionality for finding mountpoints and
+ * using UUIDs to refer to them. Specifically, we can find the mountpoint of a
+ * path, get info about a mountpoint, and find mountpoints with a specific UUID.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package filesystem
+
+import (
+       "bufio"
+       "fmt"
+       "io"
+       "log"
+       "os"
+       "path/filepath"
+       "sort"
+       "strconv"
+       "strings"
+       "sync"
+
+       "github.com/pkg/errors"
+)
+
+var (
+       // These maps hold data about the state of the system's filesystems.
+       //
+       // They only contain one Mount per filesystem, even if there are
+       // additional bind mounts, since we want to store fscrypt metadata in
+       // only one place per filesystem.  When it is ambiguous which Mount
+       // should be used for a filesystem, mountsByDevice will contain an
+       // explicit nil entry, and mountsByPath won't contain an entry.
+       mountsByDevice map[DeviceNumber]*Mount
+       mountsByPath   map[string]*Mount
+       // Used to make the mount functions thread safe
+       mountMutex sync.Mutex
+       // True if the maps have been successfully initialized.
+       mountsInitialized bool
+       // Supported tokens for filesystem links
+       uuidToken = "UUID"
+       pathToken = "PATH"
+       // Location to perform UUID lookup
+       uuidDirectory = "/dev/disk/by-uuid"
+)
+
+// Unescape octal-encoded escape sequences in a string from the mountinfo file.
+// The kernel encodes the ' ', '\t', '\n', and '\\' bytes this way.  This
+// function exactly inverts what the kernel does, including by preserving
+// invalid UTF-8.
+func unescapeString(str string) string {
+       var sb strings.Builder
+       for i := 0; i < len(str); i++ {
+               b := str[i]
+               if b == '\\' && i+3 < len(str) {
+                       if parsed, err := strconv.ParseInt(str[i+1:i+4], 8, 8); err == nil {
+                               b = uint8(parsed)
+                               i += 3
+                       }
+               }
+               sb.WriteByte(b)
+       }
+       return sb.String()
+}
+
+// EscapeString is the reverse of unescapeString.  Use this to avoid injecting
+// spaces or newlines into output that uses these characters as separators.
+func EscapeString(str string) string {
+       var sb strings.Builder
+       for _, b := range []byte(str) {
+               switch b {
+               case ' ', '\t', '\n', '\\':
+                       sb.WriteString(fmt.Sprintf("\\%03o", b))
+               default:
+                       sb.WriteByte(b)
+               }
+       }
+       return sb.String()
+}
+
+// We get the device name via the device number rather than use the mount source
+// field directly.  This is necessary to handle a rootfs that was mounted via
+// the kernel command line, since mountinfo always shows /dev/root for that.
+// This assumes that the device nodes are in the standard location.
+func getDeviceName(num DeviceNumber) string {
+       linkPath := fmt.Sprintf("/sys/dev/block/%v", num)
+       if target, err := os.Readlink(linkPath); err == nil {
+               return fmt.Sprintf("/dev/%s", filepath.Base(target))
+       }
+       return ""
+}
+
+// Parse one line of /proc/self/mountinfo.
+//
+// The line contains the following space-separated fields:
+//
+//     [0] mount ID
+//     [1] parent ID
+//     [2] major:minor
+//     [3] root
+//     [4] mount point
+//     [5] mount options
+//     [6...n-1] optional field(s)
+//     [n] separator
+//     [n+1] filesystem type
+//     [n+2] mount source
+//     [n+3] super options
+//
+// For more details, see https://www.kernel.org/doc/Documentation/filesystems/proc.txt
+func parseMountInfoLine(line string) *Mount {
+       fields := strings.Split(line, " ")
+       if len(fields) < 10 {
+               return nil
+       }
+
+       // Count the optional fields.  In case new fields are appended later,
+       // don't simply assume that n == len(fields) - 4.
+       n := 6
+       for fields[n] != "-" {
+               n++
+               if n >= len(fields) {
+                       return nil
+               }
+       }
+       if n+3 >= len(fields) {
+               return nil
+       }
+
+       var mnt *Mount = &Mount{}
+       var err error
+       mnt.DeviceNumber, err = newDeviceNumberFromString(fields[2])
+       if err != nil {
+               return nil
+       }
+       mnt.Subtree = unescapeString(fields[3])
+       mnt.Path = unescapeString(fields[4])
+       for _, opt := range strings.Split(fields[5], ",") {
+               if opt == "ro" {
+                       mnt.ReadOnly = true
+               }
+       }
+       mnt.FilesystemType = unescapeString(fields[n+1])
+       mnt.Device = getDeviceName(mnt.DeviceNumber)
+       return mnt
+}
+
+type mountpointTreeNode struct {
+       mount    *Mount
+       parent   *mountpointTreeNode
+       children []*mountpointTreeNode
+}
+
+func addUncontainedSubtreesRecursive(dst map[string]bool,
+       node *mountpointTreeNode, allUncontainedSubtrees map[string]bool) {
+       if allUncontainedSubtrees[node.mount.Subtree] {
+               dst[node.mount.Subtree] = true
+       }
+       for _, child := range node.children {
+               addUncontainedSubtreesRecursive(dst, child, allUncontainedSubtrees)
+       }
+}
+
+// findMainMount finds the "main" Mount of a filesystem.  The "main" Mount is
+// where the filesystem's fscrypt metadata is stored.
+//
+// Normally, there is just one Mount and it's of the entire filesystem
+// (mnt.Subtree == "/").  But in general, the filesystem might be mounted in
+// multiple places, including "bind mounts" where mnt.Subtree != "/".  Also, the
+// filesystem might have a combination of read-write and read-only mounts.
+//
+// To handle most cases, we could just choose a mount with mnt.Subtree == "/",
+// preferably a read-write mount.  However, that doesn't work in containers
+// where the "/" subtree might not be mounted.  Here's a real-world example:
+//
+//     mnt.Subtree               mnt.Path
+//     -----------               --------
+//     /var/lib/lxc/base/rootfs  /
+//     /var/cache/pacman/pkg     /var/cache/pacman/pkg
+//     /srv/repo/x86_64          /srv/http/x86_64
+//
+// In this case, all mnt.Subtree are independent.  To handle this case, we must
+// choose the Mount whose mnt.Path contains the others, i.e. the first one.
+// Note: the fscrypt metadata won't be usable from outside the container since
+// it won't be at the real root of the filesystem, but that may be acceptable.
+//
+// However, we can't look *only* at mnt.Path, since in some cases mnt.Subtree is
+// needed to correctly handle bind mounts.  For example, in the following case,
+// the first Mount should be chosen:
+//
+//     mnt.Subtree               mnt.Path
+//     -----------               --------
+//     /foo                      /foo
+//     /foo/dir                  /dir
+//
+// To solve this, we divide the mounts into non-overlapping trees of mnt.Path.
+// Then, we choose one of these trees which contains (exactly or via path
+// prefix) *all* mnt.Subtree.  We then return the root of this tree.  In both
+// the above examples, this algorithm returns the first Mount.
+func findMainMount(filesystemMounts []*Mount) *Mount {
+       // Index this filesystem's mounts by path.  Note: paths are unique here,
+       // since non-last mounts were already excluded earlier.
+       //
+       // Also build the set of all mounted subtrees.
+       filesystemMountsByPath := make(map[string]*mountpointTreeNode)
+       allSubtrees := make(map[string]bool)
+       for _, mnt := range filesystemMounts {
+               filesystemMountsByPath[mnt.Path] = &mountpointTreeNode{mount: mnt}
+               allSubtrees[mnt.Subtree] = true
+       }
+
+       // Divide the mounts into non-overlapping trees of mountpoints.
+       for path, mntNode := range filesystemMountsByPath {
+               for path != "/" && mntNode.parent == nil {
+                       path = filepath.Dir(path)
+                       if parent := filesystemMountsByPath[path]; parent != nil {
+                               mntNode.parent = parent
+                               parent.children = append(parent.children, mntNode)
+                       }
+               }
+       }
+
+       // Build the set of mounted subtrees that aren't contained in any other
+       // mounted subtree.
+       allUncontainedSubtrees := make(map[string]bool)
+       for subtree := range allSubtrees {
+               contained := false
+               for t := subtree; t != "/" && !contained; {
+                       t = filepath.Dir(t)
+                       contained = allSubtrees[t]
+               }
+               if !contained {
+                       allUncontainedSubtrees[subtree] = true
+               }
+       }
+
+       // Select the root of a mountpoint tree whose mounted subtrees contain
+       // *all* mounted subtrees.  Equivalently, select a mountpoint tree in
+       // which every uncontained subtree is mounted.
+       var mainMount *Mount
+       for _, mntNode := range filesystemMountsByPath {
+               mnt := mntNode.mount
+               if mntNode.parent != nil {
+                       continue
+               }
+               uncontainedSubtrees := make(map[string]bool)
+               addUncontainedSubtreesRecursive(uncontainedSubtrees, mntNode, allUncontainedSubtrees)
+               if len(uncontainedSubtrees) != len(allUncontainedSubtrees) {
+                       continue
+               }
+               // If there's more than one eligible mount, they should have the
+               // same Subtree.  Otherwise it's ambiguous which one to use.
+               if mainMount != nil && mainMount.Subtree != mnt.Subtree {
+                       log.Printf("Unsupported case: %q (%v) has multiple non-overlapping mounts. This filesystem will be ignored!",
+                               mnt.Device, mnt.DeviceNumber)
+                       return nil
+               }
+               // Prefer a read-write mount to a read-only one.
+               if mainMount == nil || mainMount.ReadOnly {
+                       mainMount = mnt
+               }
+       }
+       return mainMount
+}
+
+// This is separate from loadMountInfo() only for unit testing.
+func readMountInfo(r io.Reader) error {
+       mountsByDevice = make(map[DeviceNumber]*Mount)
+       mountsByPath = make(map[string]*Mount)
+       allMountsByDevice := make(map[DeviceNumber][]*Mount)
+       allMountsByPath := make(map[string]*Mount)
+
+       scanner := bufio.NewScanner(r)
+       for scanner.Scan() {
+               line := scanner.Text()
+               mnt := parseMountInfoLine(line)
+               if mnt == nil {
+                       log.Printf("ignoring invalid mountinfo line %q", line)
+                       continue
+               }
+
+               // We can only use mountpoints that are directories for fscrypt.
+               if !isDir(mnt.Path) {
+                       log.Printf("ignoring mountpoint %q because it is not a directory", mnt.Path)
+                       continue
+               }
+
+               // Note this overrides the info if we have seen the mountpoint
+               // earlier in the file. This is correct behavior because the
+               // mountpoints are listed in mount order.
+               allMountsByPath[mnt.Path] = mnt
+       }
+       // For each filesystem, choose a "main" Mount and discard any additional
+       // bind mounts.  fscrypt only cares about the main Mount, since it's
+       // where the fscrypt metadata is stored.  Store all the main Mounts in
+       // mountsByDevice and mountsByPath so that they can be found later.
+       for _, mnt := range allMountsByPath {
+               allMountsByDevice[mnt.DeviceNumber] =
+                       append(allMountsByDevice[mnt.DeviceNumber], mnt)
+       }
+       for deviceNumber, filesystemMounts := range allMountsByDevice {
+               mnt := findMainMount(filesystemMounts)
+               mountsByDevice[deviceNumber] = mnt // may store an explicit nil entry
+               if mnt != nil {
+                       mountsByPath[mnt.Path] = mnt
+               }
+       }
+       return nil
+}
+
+// loadMountInfo populates the Mount mappings by parsing /proc/self/mountinfo.
+// It returns an error if the Mount mappings cannot be populated.
+func loadMountInfo() error {
+       if !mountsInitialized {
+               file, err := os.Open("/proc/self/mountinfo")
+               if err != nil {
+                       return err
+               }
+               defer file.Close()
+               if err := readMountInfo(file); err != nil {
+                       return err
+               }
+               mountsInitialized = true
+       }
+       return nil
+}
+
+func filesystemLacksMainMountError(deviceNumber DeviceNumber) error {
+       return errors.Errorf("Device %q (%v) lacks a \"main\" mountpoint in the current mount namespace, so it's ambiguous where to store the fscrypt metadata.",
+               getDeviceName(deviceNumber), deviceNumber)
+}
+
+// AllFilesystems lists all mounted filesystems ordered by path to their "main"
+// Mount.  Use CheckSetup() to see if they are set up for use with fscrypt.
+func AllFilesystems() ([]*Mount, error) {
+       mountMutex.Lock()
+       defer mountMutex.Unlock()
+       if err := loadMountInfo(); err != nil {
+               return nil, err
+       }
+
+       mounts := make([]*Mount, 0, len(mountsByPath))
+       for _, mount := range mountsByPath {
+               mounts = append(mounts, mount)
+       }
+
+       sort.Sort(PathSorter(mounts))
+       return mounts, nil
+}
+
+// UpdateMountInfo updates the filesystem mountpoint maps with the current state
+// of the filesystem mountpoints. Returns error if the initialization fails.
+func UpdateMountInfo() error {
+       mountMutex.Lock()
+       defer mountMutex.Unlock()
+       mountsInitialized = false
+       return loadMountInfo()
+}
+
+// FindMount returns the main Mount object for the filesystem which contains the
+// file at the specified path. An error is returned if the path is invalid or if
+// we cannot load the required mount data. If a mount has been updated since the
+// last call to one of the mount functions, run UpdateMountInfo to see changes.
+func FindMount(path string) (*Mount, error) {
+       mountMutex.Lock()
+       defer mountMutex.Unlock()
+       if err := loadMountInfo(); err != nil {
+               return nil, err
+       }
+       // First try to find the mount by the number of the containing device.
+       deviceNumber, err := getNumberOfContainingDevice(path)
+       if err != nil {
+               return nil, err
+       }
+       mnt, ok := mountsByDevice[deviceNumber]
+       if ok {
+               if mnt == nil {
+                       return nil, filesystemLacksMainMountError(deviceNumber)
+               }
+               return mnt, nil
+       }
+       // The mount couldn't be found by the number of the containing device.
+       // Fall back to walking up the directory hierarchy and checking for a
+       // mount at each directory path.  This is necessary for btrfs, where
+       // files report a different st_dev from the /proc/self/mountinfo entry.
+       curPath, err := canonicalizePath(path)
+       if err != nil {
+               return nil, err
+       }
+       for {
+               mnt := mountsByPath[curPath]
+               if mnt != nil {
+                       return mnt, nil
+               }
+               // Move to the parent directory unless we have reached the root.
+               parent := filepath.Dir(curPath)
+               if parent == curPath {
+                       return nil, errors.Errorf("couldn't find mountpoint containing %q", path)
+               }
+               curPath = parent
+       }
+}
+
+// GetMount is like FindMount, except GetMount also returns an error if the path
+// doesn't name the same file as the filesystem's "main" Mount.  For example, if
+// a filesystem is fully mounted at "/mnt" and if "/mnt/a" exists, then
+// FindMount("/mnt/a") will succeed whereas GetMount("/mnt/a") will fail.  This
+// is true even if "/mnt/a" is a bind mount of part of the same filesystem.
+func GetMount(mountpoint string) (*Mount, error) {
+       mnt, err := FindMount(mountpoint)
+       if err != nil {
+               return nil, &ErrNotAMountpoint{mountpoint}
+       }
+       // Check whether 'mountpoint' names the same directory as 'mnt.Path'.
+       // Use os.SameFile() (i.e., compare inode numbers) rather than compare
+       // canonical paths, since filesystems may be mounted in multiple places.
+       fi1, err := os.Stat(mountpoint)
+       if err != nil {
+               return nil, err
+       }
+       fi2, err := os.Stat(mnt.Path)
+       if err != nil {
+               return nil, err
+       }
+       if !os.SameFile(fi1, fi2) {
+               return nil, &ErrNotAMountpoint{mountpoint}
+       }
+       return mnt, nil
+}
+
+func uuidToDeviceNumber(uuid string) (DeviceNumber, error) {
+       uuidSymlinkPath := filepath.Join(uuidDirectory, uuid)
+       return getDeviceNumber(uuidSymlinkPath)
+}
+
+func deviceNumberToMount(deviceNumber DeviceNumber) (*Mount, bool) {
+       mountMutex.Lock()
+       defer mountMutex.Unlock()
+       if err := loadMountInfo(); err != nil {
+               log.Print(err)
+               return nil, false
+       }
+       mnt, ok := mountsByDevice[deviceNumber]
+       return mnt, ok
+}
+
+// getMountFromLink returns the main Mount, if any, for the filesystem which the
+// given link points to.  The link should contain a series of token-value pairs
+// (<token>=<value>), one per line.  The supported tokens are "UUID" and "PATH".
+// If the UUID is present and it works, then it is used; otherwise, PATH is used
+// if it is present.  (The fallback from UUID to PATH will keep the link working
+// if the UUID of the target filesystem changes but its mountpoint doesn't.)
+//
+// If a mount has been updated since the last call to one of the mount
+// functions, make sure to run UpdateMountInfo first.
+func getMountFromLink(link string) (*Mount, error) {
+       // Parse the link.
+       uuid := ""
+       path := ""
+       lines := strings.Split(link, "\n")
+       for _, line := range lines {
+               line := strings.TrimSpace(line)
+               if line == "" {
+                       continue
+               }
+               pair := strings.Split(line, "=")
+               if len(pair) != 2 {
+                       log.Printf("ignoring invalid line in filesystem link file: %q", line)
+                       continue
+               }
+               token := pair[0]
+               value := pair[1]
+               switch token {
+               case uuidToken:
+                       uuid = value
+               case pathToken:
+                       path = value
+               default:
+                       log.Printf("ignoring unknown link token %q", token)
+               }
+       }
+       // At least one of UUID and PATH must be present.
+       if uuid == "" && path == "" {
+               return nil, &ErrFollowLink{link, errors.Errorf("invalid filesystem link file")}
+       }
+
+       // Try following the UUID.
+       errMsg := ""
+       if uuid != "" {
+               deviceNumber, err := uuidToDeviceNumber(uuid)
+               if err == nil {
+                       mnt, ok := deviceNumberToMount(deviceNumber)
+                       if mnt != nil {
+                               log.Printf("resolved filesystem link using UUID %q", uuid)
+                               return mnt, nil
+                       }
+                       if ok {
+                               return nil, &ErrFollowLink{link, filesystemLacksMainMountError(deviceNumber)}
+                       }
+                       log.Printf("cannot find filesystem with UUID %q", uuid)
+               } else {
+                       log.Printf("cannot find filesystem with UUID %q: %v", uuid, err)
+               }
+               errMsg += fmt.Sprintf("cannot find filesystem with UUID %q", uuid)
+               if path != "" {
+                       log.Printf("falling back to using mountpoint path instead of UUID")
+               }
+       }
+       // UUID didn't work.  As a fallback, try the mountpoint path.
+       if path != "" {
+               mnt, err := GetMount(path)
+               if mnt != nil {
+                       log.Printf("resolved filesystem link using mountpoint path %q", path)
+                       return mnt, nil
+               }
+               log.Print(err)
+               if errMsg == "" {
+                       errMsg = fmt.Sprintf("cannot find filesystem with main mountpoint %q", path)
+               } else {
+                       errMsg += fmt.Sprintf(" or main mountpoint %q", path)
+               }
+       }
+       // No method worked; return an error.
+       return nil, &ErrFollowLink{link, errors.New(errMsg)}
+}
+
+func (mnt *Mount) getFilesystemUUID() (string, error) {
+       dirEntries, err := os.ReadDir(uuidDirectory)
+       if err != nil {
+               return "", err
+       }
+       for _, dirEntry := range dirEntries {
+               fileInfo, err := dirEntry.Info()
+               if err != nil {
+                       continue
+               }
+               if fileInfo.Mode()&os.ModeSymlink == 0 {
+                       continue // Only interested in UUID symlinks
+               }
+               uuid := fileInfo.Name()
+               deviceNumber, err := uuidToDeviceNumber(uuid)
+               if err != nil {
+                       log.Print(err)
+                       continue
+               }
+               if mnt.DeviceNumber == deviceNumber {
+                       return uuid, nil
+               }
+       }
+       return "", errors.Errorf("cannot determine UUID of device %q (%v)",
+               mnt.Device, mnt.DeviceNumber)
+}
+
+// makeLink creates the contents of a link file which will point to the given
+// filesystem.  This will normally be a string of the form
+// "UUID=<uuid>\nPATH=<path>\n".  If the UUID cannot be determined, the UUID
+// portion will be omitted.
+func makeLink(mnt *Mount) (string, error) {
+       uuid, err := mnt.getFilesystemUUID()
+       if err != nil {
+               // The UUID could not be determined.  This happens for btrfs
+               // filesystems, as the device number found via
+               // /dev/disk/by-uuid/* for btrfs filesystems differs from the
+               // actual device number of the mounted filesystem.  Just rely
+               // entirely on the fallback to mountpoint path.
+               log.Print(err)
+               return fmt.Sprintf("%s=%s\n", pathToken, mnt.Path), nil
+       }
+       return fmt.Sprintf("%s=%s\n%s=%s\n", uuidToken, uuid, pathToken, mnt.Path), nil
+}
diff --git a/filesystem/mountpoint_test.go b/filesystem/mountpoint_test.go
new file mode 100644 (file)
index 0000000..f06219c
--- /dev/null
@@ -0,0 +1,546 @@
+/*
+ * mountpoint_test.go - Tests for reading information about all mountpoints.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+// Note: these tests assume the existence of some well-known directories: /mnt,
+// /home, and /tmp.  This is because the mountpoint loading code only retains
+// mountpoints on valid directories.
+
+package filesystem
+
+import (
+       "fmt"
+       "os"
+       "path/filepath"
+       "strings"
+       "testing"
+)
+
+func TestLoadMountInfo(t *testing.T) {
+       if err := UpdateMountInfo(); err != nil {
+               t.Error(err)
+       }
+}
+
+// Lock the mount maps so that concurrent tests don't interfere with each other.
+func beginLoadMountInfoTest() {
+       mountMutex.Lock()
+}
+
+func endLoadMountInfoTest() {
+       // Invalidate the fake mount information in case a test runs later which
+       // needs the real mount information.
+       mountsInitialized = false
+       mountMutex.Unlock()
+}
+
+func loadMountInfoFromString(str string) {
+       readMountInfo(strings.NewReader(str))
+}
+
+func mountForDevice(deviceNumberStr string) *Mount {
+       deviceNumber, _ := newDeviceNumberFromString(deviceNumberStr)
+       return mountsByDevice[deviceNumber]
+}
+
+// Test basic loading of a single mountpoint.
+func TestLoadMountInfoBasic(t *testing.T) {
+       var mountinfo = `
+15 0 259:3 / / rw,relatime shared:1 - ext4 /dev/root rw,data=ordered
+`
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       if len(mountsByDevice) != 1 {
+               t.Error("Loaded wrong number of mounts")
+       }
+       mnt := mountForDevice("259:3")
+       if mnt == nil {
+               t.Fatal("Failed to load mount")
+       }
+       if mnt.Path != "/" {
+               t.Error("Wrong path")
+       }
+       if mnt.FilesystemType != "ext4" {
+               t.Error("Wrong filesystem type")
+       }
+       if mnt.DeviceNumber.String() != "259:3" {
+               t.Error("Wrong device number")
+       }
+       if mnt.Subtree != "/" {
+               t.Error("Wrong subtree")
+       }
+       if mnt.ReadOnly {
+               t.Error("Wrong readonly flag")
+       }
+       if len(mountsByPath) != 1 {
+               t.Error("mountsByPath doesn't contain exactly one entry")
+       }
+       if mountsByPath[mnt.Path] != mnt {
+               t.Error("mountsByPath doesn't contain the correct entry")
+       }
+}
+
+// Test that Mount.Device is set to the mountpoint's source device if
+// applicable, otherwise it is set to the empty string.
+func TestLoadSourceDevice(t *testing.T) {
+       // The mountinfo parser ignores devices that don't exist.  For the valid
+       // device, try /dev/loop0.  If it doesn't exist, skip the test.
+       if _, err := os.Stat("/dev/loop0"); err != nil {
+               t.Skip("/dev/loop0 does not exist, skipping test")
+       }
+       var mountinfo = `
+15 0 7:0 / / rw shared:1 - foo /dev/loop0 rw,data=ordered
+31 15 0:27 / /tmp rw,nosuid,nodev shared:17 - tmpfs tmpfs rw
+`
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       mnt := mountForDevice("7:0")
+       if mnt.Device != "/dev/loop0" {
+               t.Error("mnt.Device wasn't set to source device")
+       }
+       mnt = mountForDevice("0:27")
+       if mnt.Device != "" {
+               t.Error("mnt.Device wasn't set to empty string for an invalid device")
+       }
+}
+
+// Test that non-directory mounts are ignored.
+func TestNondirectoryMountsIgnored(t *testing.T) {
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       file, err := os.CreateTemp("", "fscrypt_regfile")
+       if err != nil {
+               t.Fatal(err)
+       }
+       file.Close()
+       defer os.Remove(file.Name())
+
+       mountinfo := fmt.Sprintf("15 0 259:3 /foo %s rw,relatime shared:1 - ext4 /dev/root rw", file.Name())
+       loadMountInfoFromString(mountinfo)
+       if len(mountsByDevice) != 0 {
+               t.Error("Non-directory mount wasn't ignored")
+       }
+}
+
+// Test that when multiple mounts are on one directory, the last is the one
+// which is kept.
+func TestNonLatestMountsIgnored(t *testing.T) {
+       mountinfo := `
+15 0 259:3 / / rw shared:1 - ext4 /dev/root rw
+15 0 259:3 / / rw shared:1 - f2fs /dev/root rw
+15 0 259:3 / / rw shared:1 - ubifs /dev/root rw
+`
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       mnt := mountForDevice("259:3")
+       if mnt.FilesystemType != "ubifs" {
+               t.Error("Last mount didn't supersede previous ones")
+       }
+}
+
+// Test that escape sequences in the mountinfo file are unescaped correctly.
+func TestLoadMountWithSpecialCharacters(t *testing.T) {
+       tempDir, err := os.MkdirTemp("", "fscrypt")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(tempDir)
+       tempDir, err = filepath.Abs(tempDir)
+       if err != nil {
+               t.Fatal(err)
+       }
+       mountpoint := filepath.Join(tempDir, "/My Directory\t\n\\")
+       if err := os.Mkdir(mountpoint, 0700); err != nil {
+               t.Fatal(err)
+       }
+       mountinfo := fmt.Sprintf("15 0 259:3 / %s/My\\040Directory\\011\\012\\134 rw shared:1 - ext4 /dev/root rw", tempDir)
+
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       mnt := mountForDevice("259:3")
+       if mnt.Path != mountpoint {
+               t.Fatal("Wrong mountpoint")
+       }
+}
+
+// Tests the EscapeString() and unescapeString() functions.
+func TestStringEscaping(t *testing.T) {
+       charsNeedEscaping := " \t\n\\"
+       charsDontNeedEscaping := "ABCDEF\u2603\xff\xff\v"
+
+       orig := charsNeedEscaping + charsDontNeedEscaping
+       escaped := `\040\011\012\134` + charsDontNeedEscaping
+       if EscapeString(orig) != escaped {
+               t.Fatal("EscapeString gave wrong result")
+       }
+       if unescapeString(escaped) != orig {
+               t.Fatal("unescapeString gave wrong result")
+       }
+}
+
+// Test parsing some invalid mountinfo lines.
+func TestLoadBadMountInfo(t *testing.T) {
+       mountinfos := []string{"a",
+               "a a a a a a a a a a a a a a a",
+               "a a a a a a a a a a a a - a a",
+               "15 0 BAD:3 / / rw,relatime shared:1 - ext4 /dev/root rw,data=ordered"}
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       for _, mountinfo := range mountinfos {
+               loadMountInfoFromString(mountinfo)
+               if len(mountsByDevice) != 0 {
+                       t.Error("Loaded mount from invalid mountinfo line")
+               }
+       }
+}
+
+// Test that the ReadOnly flag is set if the mount is readonly, even if the
+// filesystem is read-write.
+func TestLoadReadOnlyMount(t *testing.T) {
+       mountinfo := `
+222 15 259:3 / /mnt ro,relatime shared:1 - ext4 /dev/root rw,data=ordered
+`
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       mnt := mountForDevice("259:3")
+       if !mnt.ReadOnly {
+               t.Error("Wrong readonly flag")
+       }
+}
+
+// Test that a read-write mount is preferred over a read-only mount.
+func TestReadWriteMountIsPreferredOverReadOnlyMount(t *testing.T) {
+       mountinfo := `
+222 15 259:3 / /home ro shared:1 - ext4 /dev/root rw
+222 15 259:3 / /mnt rw shared:1 - ext4 /dev/root rw
+222 15 259:3 / /tmp ro shared:1 - ext4 /dev/root rw
+`
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       mnt := mountForDevice("259:3")
+       if mnt.Path != "/mnt" {
+               t.Error("Wrong mount was chosen")
+       }
+}
+
+// Test that a mount of the full filesystem is preferred over mounts of non-root
+// subtrees, given independent mountpoints.
+func TestRootSubtreeIsPreferred(t *testing.T) {
+       mountinfo := `
+222 15 259:3 /subtree1 /home rw shared:1 - ext4 /dev/root rw
+222 15 259:3 / /mnt rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /subtree2 /tmp rw shared:1 - ext4 /dev/root rw
+`
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       mnt := mountForDevice("259:3")
+       if mnt.Subtree != "/" {
+               t.Error("Wrong mount was chosen")
+       }
+}
+
+// Test that a mount that is not of the full filesystem but still contains all
+// other mounted subtrees is preferred, given independent mountpoints.
+func TestHighestSubtreeIsPreferred(t *testing.T) {
+       mountinfo := `
+222 15 259:3 /foo/bar /mnt rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /foo /tmp rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /foo/baz /home rw shared:1 - ext4 /dev/root rw
+`
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       deviceNumber, _ := newDeviceNumberFromString("259:3")
+       mnt := mountsByDevice[deviceNumber]
+       if mnt.Subtree != "/foo" {
+               t.Error("Wrong mount was chosen")
+       }
+}
+
+// Test that mountpoint "/" is preferred, given independent subtrees.
+func TestRootMountpointIsPreferred(t *testing.T) {
+       mountinfo := `
+222 15 259:3 /var/cache/pacman/pkg /mnt rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /var/lib/lxc/base/rootfs / rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /srv/repo/x86_64 /home rw shared:1 - ext4 /dev/root rw
+`
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       deviceNumber, _ := newDeviceNumberFromString("259:3")
+       mnt := mountsByDevice[deviceNumber]
+       if mnt.Subtree != "/var/lib/lxc/base/rootfs" {
+               t.Error("Wrong mount was chosen")
+       }
+}
+
+// Test that a mountpoint that is not "/" but still contains all other
+// mountpoints is preferred, given independent subtrees.
+func TestHighestMountpointIsPreferred(t *testing.T) {
+       tempDir, err := os.MkdirTemp("", "fscrypt")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(tempDir)
+       tempDir, err = filepath.Abs(tempDir)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if err := os.MkdirAll(tempDir+"/a/b", 0700); err != nil {
+               t.Fatal(err)
+       }
+       if err := os.Mkdir(tempDir+"/a/c", 0700); err != nil {
+               t.Fatal(err)
+       }
+       mountinfo := fmt.Sprintf(`
+222 15 259:3 /0 %s rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /1 %s rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /2 %s rw shared:1 - ext4 /dev/root rw
+`, tempDir+"/a/b", tempDir+"/a", tempDir+"/a/c")
+
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       deviceNumber, _ := newDeviceNumberFromString("259:3")
+       mnt := mountsByDevice[deviceNumber]
+       if mnt.Subtree != "/1" {
+               t.Error("Wrong mount was chosen")
+       }
+}
+
+// Test that if some subtrees are contained in other subtrees, *and* some
+// mountpoints are contained in other mountpoints, the chosen Mount is the root
+// of a tree of mountpoints whose mounted subtrees contain all mounted subtrees.
+func TestLoadContainedSubtreesAndMountpoints(t *testing.T) {
+       tempDir, err := os.MkdirTemp("", "fscrypt")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(tempDir)
+       tempDir, err = filepath.Abs(tempDir)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if err := os.MkdirAll(tempDir+"/a/b", 0700); err != nil {
+               t.Fatal(err)
+       }
+       if err := os.Mkdir(tempDir+"/a/c", 0700); err != nil {
+               t.Fatal(err)
+       }
+       if err := os.Mkdir(tempDir+"/d", 0700); err != nil {
+               t.Fatal(err)
+       }
+       if err := os.Mkdir(tempDir+"/e", 0700); err != nil {
+               t.Fatal(err)
+       }
+       // The first three mounts form a tree of mountpoints.  The rest have
+       // independent mountpoints but have mounted subtrees contained in the
+       // mounted subtrees of the first mountpoint tree.
+       mountinfo := fmt.Sprintf(`
+222 15 259:3 /0 %s rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /1 %s rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /2 %s rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /1/3 %s rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /2/4 %s rw shared:1 - ext4 /dev/root rw
+`, tempDir+"/a/b", tempDir+"/a", tempDir+"/a/c",
+               tempDir+"/d", tempDir+"/e")
+
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       deviceNumber, _ := newDeviceNumberFromString("259:3")
+       mnt := mountsByDevice[deviceNumber]
+       if mnt.Subtree != "/1" {
+               t.Error("Wrong mount was chosen")
+       }
+}
+
+// Test loading mounts with independent subtrees *and* independent mountpoints.
+// This case is ambiguous, so an explicit nil entry should be stored.
+func TestLoadAmbiguousMounts(t *testing.T) {
+       mountinfo := `
+222 15 259:3 /foo /mnt rw shared:1 - ext4 /dev/root rw
+222 15 259:3 /bar /tmp rw shared:1 - ext4 /dev/root rw
+`
+       beginLoadMountInfoTest()
+       defer endLoadMountInfoTest()
+       loadMountInfoFromString(mountinfo)
+       deviceNumber, _ := newDeviceNumberFromString("259:3")
+       mnt, ok := mountsByDevice[deviceNumber]
+       if !ok {
+               t.Error("Entry should exist")
+       }
+       if mnt != nil {
+               t.Error("Entry should be nil")
+       }
+}
+
+// Test making a filesystem link and following it, and test that leading and
+// trailing whitespace in the link is ignored.
+func TestGetMountFromLink(t *testing.T) {
+       mnt, err := getTestMount(t)
+       if err != nil {
+               t.Skip(err)
+       }
+       link, err := makeLink(mnt)
+       if err != nil {
+               t.Fatal(err)
+       }
+       linkedMnt, err := getMountFromLink(link)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if linkedMnt != mnt {
+               t.Fatal("Link doesn't point to the same Mount")
+       }
+       if linkedMnt, err = getMountFromLink(link + "\n"); err != nil {
+               t.Fatal(err)
+       }
+       if linkedMnt != mnt {
+               t.Fatal("Link doesn't point to the same Mount")
+       }
+       if linkedMnt, err = getMountFromLink("  " + link + "  \r\n"); err != nil {
+               t.Fatal(err)
+       }
+       if linkedMnt != mnt {
+               t.Fatal("Link doesn't point to the same Mount")
+       }
+}
+
+// Test that makeLink() is including the expected information in links.
+func TestMakeLink(t *testing.T) {
+       mnt, err := getTestMount(t)
+       if err != nil {
+               t.Skip(err)
+       }
+       link, err := makeLink(mnt)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       // Normally, both UUID and PATH should be included.
+       if !strings.Contains(link, "UUID=") {
+               t.Fatal("Link doesn't contain UUID")
+       }
+       if !strings.Contains(link, "PATH=") {
+               t.Fatal("Link doesn't contain PATH")
+       }
+
+       // Without a valid device number, only PATH should be included.
+       mntCopy := *mnt
+       mntCopy.DeviceNumber = 0
+       link, err = makeLink(&mntCopy)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if strings.Contains(link, "UUID=") {
+               t.Fatal("Link shouldn't contain UUID")
+       }
+       if !strings.Contains(link, "PATH=") {
+               t.Fatal("Link doesn't contain PATH")
+       }
+}
+
+// Test that old filesystem links that contain a UUID only still work.
+func TestGetMountFromLegacyLink(t *testing.T) {
+       mnt, err := getTestMount(t)
+       if err != nil {
+               t.Skip(err)
+       }
+       uuid, err := mnt.getFilesystemUUID()
+       if uuid == "" || err != nil {
+               t.Fatal("Can't get UUID of test filesystem")
+       }
+
+       link := fmt.Sprintf("UUID=%s", uuid)
+       linkedMnt, err := getMountFromLink(link)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if linkedMnt != mnt {
+               t.Fatal("Link doesn't point to the same Mount")
+       }
+}
+
+// Test that if the UUID in a filesystem link doesn't work, then the PATH is
+// used instead, and vice versa.
+func TestGetMountFromLinkFallback(t *testing.T) {
+       mnt, err := getTestMount(t)
+       if err != nil {
+               t.Skip(err)
+       }
+       badUUID := "00000000-0000-0000-0000-000000000000"
+       badPath := "/NONEXISTENT_MOUNT"
+       goodUUID, err := mnt.getFilesystemUUID()
+       if goodUUID == "" || err != nil {
+               t.Fatal("Can't get UUID of test filesystem")
+       }
+
+       // only PATH valid (should succeed)
+       link := fmt.Sprintf("UUID=%s\nPATH=%s\n", badUUID, mnt.Path)
+       linkedMnt, err := getMountFromLink(link)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if linkedMnt != mnt {
+               t.Fatal("Link doesn't point to the same Mount")
+       }
+
+       // only PATH given at all (should succeed)
+       link = fmt.Sprintf("PATH=%s\n", mnt.Path)
+       linkedMnt, err = getMountFromLink(link)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if linkedMnt != mnt {
+               t.Fatal("Link doesn't point to the same Mount")
+       }
+
+       // only UUID valid (should succeed)
+       link = fmt.Sprintf("UUID=%s\nPATH=%s\n", goodUUID, badPath)
+       if linkedMnt, err = getMountFromLink(link); err != nil {
+               t.Fatal(err)
+       }
+       if linkedMnt != mnt {
+               t.Fatal("Link doesn't point to the same Mount")
+       }
+
+       // neither valid (should fail)
+       link = fmt.Sprintf("UUID=%s\nPATH=%s\n", badUUID, badPath)
+       linkedMnt, err = getMountFromLink(link)
+       if linkedMnt != nil || err == nil {
+               t.Fatal("Following a bad link succeeded")
+       }
+}
+
+// Benchmarks how long it takes to update the mountpoint data
+func BenchmarkLoadFirst(b *testing.B) {
+       for n := 0; n < b.N; n++ {
+               err := UpdateMountInfo()
+               if err != nil {
+                       b.Fatal(err)
+               }
+       }
+}
diff --git a/filesystem/path.go b/filesystem/path.go
new file mode 100644 (file)
index 0000000..8cfb235
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * path.go - Utility functions for dealing with filesystem paths
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package filesystem
+
+import (
+       "fmt"
+       "log"
+       "os"
+       "path/filepath"
+
+       "golang.org/x/sys/unix"
+
+       "github.com/pkg/errors"
+)
+
+// OpenFileOverridingUmask calls os.OpenFile but with the umask overridden so
+// that no permission bits are masked out if the file is created.
+func OpenFileOverridingUmask(name string, flag int, perm os.FileMode) (*os.File, error) {
+       oldMask := unix.Umask(0)
+       defer unix.Umask(oldMask)
+       return os.OpenFile(name, flag, perm)
+}
+
+// canonicalizePath turns path into an absolute path without symlinks.
+func canonicalizePath(path string) (string, error) {
+       path, err := filepath.Abs(path)
+       if err != nil {
+               return "", err
+       }
+       path, err = filepath.EvalSymlinks(path)
+
+       // Get a better error if we have an invalid path
+       if pathErr, ok := err.(*os.PathError); ok {
+               err = errors.Wrap(pathErr.Err, pathErr.Path)
+       }
+
+       return path, err
+}
+
+// loggedStat runs os.Stat, but it logs the error if stat returns any error
+// other than nil or IsNotExist.
+func loggedStat(name string) (os.FileInfo, error) {
+       info, err := os.Stat(name)
+       if err != nil && !os.IsNotExist(err) {
+               log.Print(err)
+       }
+       return info, err
+}
+
+// loggedLstat runs os.Lstat (doesn't dereference trailing symlink), but it logs
+// the error if lstat returns any error other than nil or IsNotExist.
+func loggedLstat(name string) (os.FileInfo, error) {
+       info, err := os.Lstat(name)
+       if err != nil && !os.IsNotExist(err) {
+               log.Print(err)
+       }
+       return info, err
+}
+
+// isDir returns true if the path exists and is that of a directory.
+func isDir(path string) bool {
+       info, err := loggedStat(path)
+       return err == nil && info.IsDir()
+}
+
+// isRegularFile returns true if the path exists and is that of a regular file.
+func isRegularFile(path string) bool {
+       info, err := loggedStat(path)
+       return err == nil && info.Mode().IsRegular()
+}
+
+// HaveReadAccessTo returns true if the process has read access to a file or
+// directory, without actually opening it.
+func HaveReadAccessTo(path string) bool {
+       return unix.Access(path, unix.R_OK) == nil
+}
+
+// DeviceNumber represents a combined major:minor device number.
+type DeviceNumber uint64
+
+func (num DeviceNumber) String() string {
+       return fmt.Sprintf("%d:%d", unix.Major(uint64(num)), unix.Minor(uint64(num)))
+}
+
+func newDeviceNumberFromString(str string) (DeviceNumber, error) {
+       var major, minor uint32
+       if count, _ := fmt.Sscanf(str, "%d:%d", &major, &minor); count != 2 {
+               return 0, errors.Errorf("invalid device number string %q", str)
+       }
+       return DeviceNumber(unix.Mkdev(major, minor)), nil
+}
+
+// getDeviceNumber returns the device number of the device node at the given
+// path.  If there is a symlink at the path, it is dereferenced.
+func getDeviceNumber(path string) (DeviceNumber, error) {
+       var stat unix.Stat_t
+       if err := unix.Stat(path, &stat); err != nil {
+               return 0, err
+       }
+       return DeviceNumber(stat.Rdev), nil
+}
+
+// getNumberOfContainingDevice returns the device number of the filesystem which
+// contains the given file.  If the file is a symlink, it is not dereferenced.
+func getNumberOfContainingDevice(path string) (DeviceNumber, error) {
+       var stat unix.Stat_t
+       if err := unix.Lstat(path, &stat); err != nil {
+               return 0, err
+       }
+       return DeviceNumber(stat.Dev), nil
+}
diff --git a/filesystem/path_test.go b/filesystem/path_test.go
new file mode 100644 (file)
index 0000000..d325054
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * path_test.go - Tests for path utilities.
+ *
+ * Copyright 2019 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.
+ */
+
+package filesystem
+
+import (
+       "fmt"
+       "os"
+       "testing"
+
+       "github.com/google/fscrypt/util"
+)
+
+func TestDeviceNumber(t *testing.T) {
+       num, err := getDeviceNumber("/NONEXISTENT")
+       if num != 0 || err == nil {
+               t.Error("Should have failed to get device number of nonexistent file")
+       }
+       // /dev/null is always device 1:3 on Linux.
+       num, err = getDeviceNumber("/dev/null")
+       if err != nil {
+               t.Fatal(err)
+       }
+       if str := num.String(); str != "1:3" {
+               t.Errorf("Wrong device number string: %q", str)
+       }
+       if str := fmt.Sprintf("%v", num); str != "1:3" {
+               t.Errorf("Wrong device number string: %q", str)
+       }
+       var num2 DeviceNumber
+       num2, err = newDeviceNumberFromString("1:3")
+       if err != nil {
+               t.Error("Failed to parse device number")
+       }
+       if num2 != num {
+               t.Errorf("Wrong device number: %d", num2)
+       }
+       num2, err = newDeviceNumberFromString("foo")
+       if num2 != 0 || err == nil {
+               t.Error("Should have failed to parse invalid device number")
+       }
+}
+
+func TestHaveReadAccessTo(t *testing.T) {
+       if util.IsUserRoot() {
+               t.Skip("This test cannot be run as root")
+       }
+       file, err := os.CreateTemp("", "fscrypt_test")
+       if err != nil {
+               t.Fatal(err)
+       }
+       file.Close()
+       defer os.Remove(file.Name())
+
+       testCases := map[os.FileMode]bool{
+               0444: true,
+               0400: true,
+               0000: false,
+               0040: false, // user bits take priority in Linux
+               0004: false, // user bits take priority in Linux
+       }
+       for mode, readable := range testCases {
+               if err := os.Chmod(file.Name(), mode); err != nil {
+                       t.Error(err)
+               }
+               if HaveReadAccessTo(file.Name()) != readable {
+                       t.Errorf("Expected readable=%v on mode=0%03o", readable, mode)
+               }
+       }
+}
diff --git a/go.mod b/go.mod
new file mode 100644 (file)
index 0000000..fe717a4
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,24 @@
+module github.com/google/fscrypt
+
+go 1.18
+
+require (
+       github.com/client9/misspell v0.3.4
+       github.com/pkg/errors v0.9.1
+       github.com/urfave/cli v1.22.14
+       github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
+       golang.org/x/crypto v0.17.0
+       golang.org/x/sys v0.15.0
+       golang.org/x/term v0.15.0
+       golang.org/x/tools v0.13.0
+       google.golang.org/protobuf v1.31.0
+       honnef.co/go/tools v0.4.5
+)
+
+require (
+       github.com/BurntSushi/toml v1.3.2 // indirect
+       github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+       github.com/russross/blackfriday/v2 v2.1.0 // indirect
+       golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect
+       golang.org/x/mod v0.12.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644 (file)
index 0000000..1aaec45
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,54 @@
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
+github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
+github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad h1:W0LEBv82YCGEtcmPA3uNZBI33/qF//HAAs3MawDjRa0=
+github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad/go.mod h1:Hy8o65+MXnS6EwGElrSRjUzQDLXreJlzYLlWiHtt8hM=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE=
+golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.4.5 h1:YGD4H+SuIOOqsyoLOpZDWcieM28W47/zRO7f+9V3nvo=
+honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k=
diff --git a/keyring/fs_keyring.go b/keyring/fs_keyring.go
new file mode 100644 (file)
index 0000000..9b949b9
--- /dev/null
@@ -0,0 +1,326 @@
+/*
+ * fs_keyring.go - Add/remove encryption policy keys to/from filesystem
+ *
+ * Copyright 2019 Google LLC
+ * Author: Eric Biggers (ebiggers@google.com)
+ *
+ * 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.
+ */
+
+package keyring
+
+/*
+#include <string.h>
+*/
+import "C"
+
+import (
+       "encoding/hex"
+       "log"
+       "os"
+       "os/user"
+       "sync"
+       "unsafe"
+
+       "github.com/pkg/errors"
+       "golang.org/x/sys/unix"
+
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/security"
+       "github.com/google/fscrypt/util"
+)
+
+var (
+       fsKeyringSupported      bool
+       fsKeyringSupportedKnown bool
+       fsKeyringSupportedLock  sync.Mutex
+)
+
+func checkForFsKeyringSupport(mount *filesystem.Mount) bool {
+       dir, err := os.Open(mount.Path)
+       if err != nil {
+               log.Printf("Unexpected error opening %q. Assuming filesystem keyring is unsupported.",
+                       mount.Path)
+               return false
+       }
+       defer dir.Close()
+
+       // FS_IOC_ADD_ENCRYPTION_KEY with a NULL argument will fail with ENOTTY
+       // if the ioctl isn't supported.  Otherwise it should fail with EFAULT.
+       //
+       // Note that there's no need to check for FS_IOC_REMOVE_ENCRYPTION_KEY
+       // support separately, since it's guaranteed to be available if
+       // FS_IOC_ADD_ENCRYPTION_KEY is.  There's also no need to check for
+       // support on every filesystem separately, since either the kernel
+       // supports the ioctls on all fscrypt-capable filesystems or it doesn't.
+       _, _, errno := unix.Syscall(unix.SYS_IOCTL, dir.Fd(), unix.FS_IOC_ADD_ENCRYPTION_KEY, 0)
+       if errno == unix.ENOTTY {
+               log.Printf("Kernel doesn't support filesystem keyring. Falling back to user keyring.")
+               return false
+       }
+       if errno == unix.EFAULT {
+               log.Printf("Detected support for filesystem keyring")
+       } else {
+               // EFAULT is expected, but as long as we didn't get ENOTTY the
+               // ioctl should be available.
+               log.Printf("Unexpected error from FS_IOC_ADD_ENCRYPTION_KEY(%q, NULL): %v", mount.Path, errno)
+       }
+       return true
+}
+
+// IsFsKeyringSupported returns true if the kernel supports the ioctls to
+// add/remove fscrypt keys directly to/from the filesystem.  For support to be
+// detected, the given Mount must be for a filesystem that supports fscrypt.
+func IsFsKeyringSupported(mount *filesystem.Mount) bool {
+       fsKeyringSupportedLock.Lock()
+       defer fsKeyringSupportedLock.Unlock()
+       if !fsKeyringSupportedKnown {
+               fsKeyringSupported = checkForFsKeyringSupport(mount)
+               fsKeyringSupportedKnown = true
+       }
+       return fsKeyringSupported
+}
+
+// buildKeySpecifier converts the key descriptor string to an FscryptKeySpecifier.
+func buildKeySpecifier(spec *unix.FscryptKeySpecifier, descriptor string) error {
+       descriptorBytes, err := hex.DecodeString(descriptor)
+       if err != nil {
+               return errors.Errorf("key descriptor %q is invalid", descriptor)
+       }
+       switch len(descriptorBytes) {
+       case unix.FSCRYPT_KEY_DESCRIPTOR_SIZE:
+               spec.Type = unix.FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR
+       case unix.FSCRYPT_KEY_IDENTIFIER_SIZE:
+               spec.Type = unix.FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER
+       default:
+               return errors.Errorf("key descriptor %q has unknown length", descriptor)
+       }
+       copy(spec.U[:], descriptorBytes)
+       return nil
+}
+
+type savedPrivs struct {
+       ruid, euid, suid int
+}
+
+// dropPrivsIfNeeded drops privileges (UIDs only) to the given user if we're
+// working with a v2 policy key, and if the user is different from the user the
+// process is currently running as.
+//
+// This is needed to change the effective UID so that FS_IOC_ADD_ENCRYPTION_KEY
+// and FS_IOC_REMOVE_ENCRYPTION_KEY will add/remove a claim to the key for the
+// intended user, and so that FS_IOC_GET_ENCRYPTION_KEY_STATUS will return the
+// correct status flags for the user.
+func dropPrivsIfNeeded(user *user.User, spec *unix.FscryptKeySpecifier) (*savedPrivs, error) {
+       if spec.Type == unix.FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR {
+               // v1 policy keys don't have any concept of user claims.
+               return nil, nil
+       }
+       targetUID := util.AtoiOrPanic(user.Uid)
+       ruid, euid, suid := security.GetUids()
+       if euid == targetUID {
+               return nil, nil
+       }
+       if err := security.SetUids(targetUID, targetUID, euid); err != nil {
+               return nil, err
+       }
+       return &savedPrivs{ruid, euid, suid}, nil
+}
+
+// restorePrivs restores root privileges if needed.
+func restorePrivs(privs *savedPrivs) error {
+       if privs != nil {
+               return security.SetUids(privs.ruid, privs.euid, privs.suid)
+       }
+       return nil
+}
+
+// validateKeyDescriptor validates that the correct key descriptor was provided.
+// This isn't really necessary; this is just an extra sanity check.
+func validateKeyDescriptor(spec *unix.FscryptKeySpecifier, descriptor string) (string, error) {
+       if spec.Type != unix.FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER {
+               // v1 policy key: the descriptor is chosen arbitrarily by
+               // userspace, so there's nothing to validate.
+               return descriptor, nil
+       }
+       // v2 policy key.  The descriptor ("identifier" in the kernel UAPI) is
+       // calculated as a cryptographic hash of the key itself.  The kernel
+       // ignores the provided value, and calculates and returns it itself.  So
+       // verify that the returned value is as expected.  If it's not, the key
+       // doesn't actually match the encryption policy we thought it was for.
+       actual := hex.EncodeToString(spec.U[:unix.FSCRYPT_KEY_IDENTIFIER_SIZE])
+       if descriptor == actual {
+               return descriptor, nil
+       }
+       return actual,
+               errors.Errorf("provided and actual key descriptors differ (%q != %q)",
+                       descriptor, actual)
+}
+
+// fsAddEncryptionKey adds the specified encryption key to the specified filesystem.
+func fsAddEncryptionKey(key *crypto.Key, descriptor string,
+       mount *filesystem.Mount, user *user.User) error {
+
+       dir, err := os.Open(mount.Path)
+       if err != nil {
+               return err
+       }
+       defer dir.Close()
+
+       argKey, err := crypto.NewBlankKey(int(unsafe.Sizeof(unix.FscryptAddKeyArg{})) + key.Len())
+       if err != nil {
+               return err
+       }
+       defer argKey.Wipe()
+       arg := (*unix.FscryptAddKeyArg)(argKey.UnsafePtr())
+
+       if err = buildKeySpecifier(&arg.Key_spec, descriptor); err != nil {
+               return err
+       }
+
+       raw := unsafe.Pointer(uintptr(argKey.UnsafePtr()) + unsafe.Sizeof(*arg))
+       arg.Raw_size = uint32(key.Len())
+       C.memcpy(raw, key.UnsafePtr(), C.size_t(key.Len()))
+
+       savedPrivs, err := dropPrivsIfNeeded(user, &arg.Key_spec)
+       if err != nil {
+               return err
+       }
+       _, _, errno := unix.Syscall(unix.SYS_IOCTL, dir.Fd(),
+               unix.FS_IOC_ADD_ENCRYPTION_KEY, uintptr(argKey.UnsafePtr()))
+       restorePrivs(savedPrivs)
+
+       log.Printf("FS_IOC_ADD_ENCRYPTION_KEY(%q, %s, <raw>) = %v", mount.Path, descriptor, errno)
+       if errno != 0 {
+               return errors.Wrapf(errno,
+                       "error adding key with descriptor %s to filesystem %s",
+                       descriptor, mount.Path)
+       }
+       if descriptor, err = validateKeyDescriptor(&arg.Key_spec, descriptor); err != nil {
+               fsRemoveEncryptionKey(descriptor, mount, user)
+               return err
+       }
+       return nil
+}
+
+// fsRemoveEncryptionKey removes the specified encryption key from the specified
+// filesystem.
+func fsRemoveEncryptionKey(descriptor string, mount *filesystem.Mount,
+       user *user.User) error {
+
+       dir, err := os.Open(mount.Path)
+       if err != nil {
+               return err
+       }
+       defer dir.Close()
+
+       var arg unix.FscryptRemoveKeyArg
+       if err = buildKeySpecifier(&arg.Key_spec, descriptor); err != nil {
+               return err
+       }
+
+       ioc := uintptr(unix.FS_IOC_REMOVE_ENCRYPTION_KEY)
+       iocName := "FS_IOC_REMOVE_ENCRYPTION_KEY"
+       var savedPrivs *savedPrivs
+       if user == nil {
+               ioc = unix.FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS
+               iocName = "FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS"
+       } else {
+               savedPrivs, err = dropPrivsIfNeeded(user, &arg.Key_spec)
+               if err != nil {
+                       return err
+               }
+       }
+       _, _, errno := unix.Syscall(unix.SYS_IOCTL, dir.Fd(), ioc, uintptr(unsafe.Pointer(&arg)))
+       restorePrivs(savedPrivs)
+
+       log.Printf("%s(%q, %s) = %v, removal_status_flags=0x%x",
+               iocName, mount.Path, descriptor, errno, arg.Removal_status_flags)
+       switch errno {
+       case 0:
+               switch {
+               case arg.Removal_status_flags&unix.FSCRYPT_KEY_REMOVAL_STATUS_FLAG_OTHER_USERS != 0:
+                       return ErrKeyAddedByOtherUsers
+               case arg.Removal_status_flags&unix.FSCRYPT_KEY_REMOVAL_STATUS_FLAG_FILES_BUSY != 0:
+                       return ErrKeyFilesOpen
+               }
+               return nil
+       case unix.ENOKEY:
+               // ENOKEY means either the key is completely missing or that the
+               // current user doesn't have a claim to it.  Distinguish between
+               // these two cases by getting the key status.
+               if user != nil {
+                       status, _ := fsGetEncryptionKeyStatus(descriptor, mount, user)
+                       if status == KeyPresentButOnlyOtherUsers {
+                               return ErrKeyAddedByOtherUsers
+                       }
+               }
+               return ErrKeyNotPresent
+       default:
+               return errors.Wrapf(errno,
+                       "error removing key with descriptor %s from filesystem %s",
+                       descriptor, mount.Path)
+       }
+}
+
+// fsGetEncryptionKeyStatus gets the status of the specified encryption key on
+// the specified filesystem.
+func fsGetEncryptionKeyStatus(descriptor string, mount *filesystem.Mount,
+       user *user.User) (KeyStatus, error) {
+
+       dir, err := os.Open(mount.Path)
+       if err != nil {
+               return KeyStatusUnknown, err
+       }
+       defer dir.Close()
+
+       var arg unix.FscryptGetKeyStatusArg
+       err = buildKeySpecifier(&arg.Key_spec, descriptor)
+       if err != nil {
+               return KeyStatusUnknown, err
+       }
+
+       savedPrivs, err := dropPrivsIfNeeded(user, &arg.Key_spec)
+       if err != nil {
+               return KeyStatusUnknown, err
+       }
+       _, _, errno := unix.Syscall(unix.SYS_IOCTL, dir.Fd(),
+               unix.FS_IOC_GET_ENCRYPTION_KEY_STATUS, uintptr(unsafe.Pointer(&arg)))
+       restorePrivs(savedPrivs)
+
+       log.Printf("FS_IOC_GET_ENCRYPTION_KEY_STATUS(%q, %s) = %v, status=%d, status_flags=0x%x",
+               mount.Path, descriptor, errno, arg.Status, arg.Status_flags)
+       if errno != 0 {
+               return KeyStatusUnknown,
+                       errors.Wrapf(errno,
+                               "error getting status of key with descriptor %s on filesystem %s",
+                               descriptor, mount.Path)
+       }
+       switch arg.Status {
+       case unix.FSCRYPT_KEY_STATUS_ABSENT:
+               return KeyAbsent, nil
+       case unix.FSCRYPT_KEY_STATUS_PRESENT:
+               if arg.Key_spec.Type != unix.FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR &&
+                       (arg.Status_flags&unix.FSCRYPT_KEY_STATUS_FLAG_ADDED_BY_SELF) == 0 {
+                       return KeyPresentButOnlyOtherUsers, nil
+               }
+               return KeyPresent, nil
+       case unix.FSCRYPT_KEY_STATUS_INCOMPLETELY_REMOVED:
+               return KeyAbsentButFilesBusy, nil
+       default:
+               return KeyStatusUnknown,
+                       errors.Errorf("unknown key status (%d) for key with descriptor %s on filesystem %s",
+                               arg.Status, descriptor, mount.Path)
+       }
+}
diff --git a/keyring/keyring.go b/keyring/keyring.go
new file mode 100644 (file)
index 0000000..5ddceaf
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * keyring.go - Add/remove encryption policy keys to/from kernel
+ *
+ * Copyright 2019 Google LLC
+ * Author: Eric Biggers (ebiggers@google.com)
+ *
+ * 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.
+ */
+
+// Package keyring manages adding, removing, and getting the status of
+// encryption policy keys to/from the kernel.  Most public functions are in
+// keyring.go, and they delegate to either user_keyring.go or fs_keyring.go,
+// depending on whether a user keyring or a filesystem keyring is being used.
+//
+// v2 encryption policies always use the filesystem keyring.
+// v1 policies use the user keyring by default, but can be configured to use the
+// filesystem keyring instead (requires root and kernel v5.4+).
+package keyring
+
+import (
+       "encoding/hex"
+       "os/user"
+       "strconv"
+
+       "github.com/pkg/errors"
+       "golang.org/x/sys/unix"
+
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+// Keyring error values
+var (
+       ErrKeyAddedByOtherUsers  = errors.New("other users have added the key too")
+       ErrKeyFilesOpen          = errors.New("some files using the key are still open")
+       ErrKeyNotPresent         = errors.New("key not present or already removed")
+       ErrV2PoliciesUnsupported = errors.New("kernel is too old to support v2 encryption policies")
+)
+
+// Options are the options which specify *which* keyring the key should be
+// added/removed/gotten to, and how.
+type Options struct {
+       // Mount is the filesystem to which the key should be
+       // added/removed/gotten.
+       Mount *filesystem.Mount
+       // User is the user for whom the key should be added/removed/gotten.
+       User *user.User
+       // UseFsKeyringForV1Policies is true if keys for v1 encryption policies
+       // should be put in the filesystem's keyring (if supported) rather than
+       // in the user's keyring.  Note that this makes AddEncryptionKey and
+       // RemoveEncryptionKey require root privileges.
+       UseFsKeyringForV1Policies bool
+}
+
+func shouldUseFsKeyring(descriptor string, options *Options) (bool, error) {
+       // For v1 encryption policy keys, use the filesystem keyring if
+       // use_fs_keyring_for_v1_policies is set in /etc/fscrypt.conf and the
+       // kernel supports it.
+       if len(descriptor) == hex.EncodedLen(unix.FSCRYPT_KEY_DESCRIPTOR_SIZE) {
+               return options.UseFsKeyringForV1Policies && IsFsKeyringSupported(options.Mount), nil
+       }
+       // For v2 encryption policy keys, always use the filesystem keyring; the
+       // kernel doesn't support any other way.
+       if !IsFsKeyringSupported(options.Mount) {
+               return true, ErrV2PoliciesUnsupported
+       }
+       return true, nil
+}
+
+// buildKeyDescription builds the description for an fscrypt key of type
+// "logon". For ext4 and f2fs, it uses the legacy filesystem-specific prefixes
+// for compatibility with kernels before v4.8 and v4.6 respectively. For other
+// filesystems it uses the generic prefix "fscrypt".
+func buildKeyDescription(options *Options, descriptor string) string {
+       switch options.Mount.FilesystemType {
+       case "ext4", "f2fs":
+               return options.Mount.FilesystemType + ":" + descriptor
+       default:
+               return unix.FSCRYPT_KEY_DESC_PREFIX + descriptor
+       }
+}
+
+// AddEncryptionKey adds an encryption policy key to a kernel keyring.  It uses
+// either the filesystem keyring for the target Mount or the user keyring for
+// the target User.
+func AddEncryptionKey(key *crypto.Key, descriptor string, options *Options) error {
+       if err := util.CheckValidLength(metadata.PolicyKeyLen, key.Len()); err != nil {
+               return errors.Wrap(err, "policy key")
+       }
+       useFsKeyring, err := shouldUseFsKeyring(descriptor, options)
+       if err != nil {
+               return err
+       }
+       if useFsKeyring {
+               return fsAddEncryptionKey(key, descriptor, options.Mount, options.User)
+       }
+       return userAddKey(key, buildKeyDescription(options, descriptor), options.User)
+}
+
+// RemoveEncryptionKey removes an encryption policy key from a kernel keyring.
+// It uses either the filesystem keyring for the target Mount or the user
+// keyring for the target User.
+func RemoveEncryptionKey(descriptor string, options *Options, allUsers bool) error {
+       useFsKeyring, err := shouldUseFsKeyring(descriptor, options)
+       if err != nil {
+               return err
+       }
+       if useFsKeyring {
+               user := options.User
+               if allUsers {
+                       user = nil
+               }
+               return fsRemoveEncryptionKey(descriptor, options.Mount, user)
+       }
+       return userRemoveKey(buildKeyDescription(options, descriptor), options.User)
+}
+
+// KeyStatus is an enum that represents the status of a key in a kernel keyring.
+type KeyStatus int
+
+// The possible values of KeyStatus.
+const (
+       KeyStatusUnknown = 0 + iota
+       KeyAbsent
+       KeyAbsentButFilesBusy
+       KeyPresent
+       KeyPresentButOnlyOtherUsers
+)
+
+func (status KeyStatus) String() string {
+       switch status {
+       case KeyStatusUnknown:
+               return "Unknown"
+       case KeyAbsent:
+               return "Absent"
+       case KeyAbsentButFilesBusy:
+               return "AbsentButFilesBusy"
+       case KeyPresent:
+               return "Present"
+       case KeyPresentButOnlyOtherUsers:
+               return "PresentButOnlyOtherUsers"
+       default:
+               return strconv.Itoa(int(status))
+       }
+}
+
+// GetEncryptionKeyStatus gets the status of an encryption policy key in a
+// kernel keyring.  It uses either the filesystem keyring for the target Mount
+// or the user keyring for the target User.
+func GetEncryptionKeyStatus(descriptor string, options *Options) (KeyStatus, error) {
+       useFsKeyring, err := shouldUseFsKeyring(descriptor, options)
+       if err != nil {
+               return KeyStatusUnknown, err
+       }
+       if useFsKeyring {
+               return fsGetEncryptionKeyStatus(descriptor, options.Mount, options.User)
+       }
+       _, _, err = userFindKey(buildKeyDescription(options, descriptor), options.User)
+       if err != nil {
+               return KeyAbsent, nil
+       }
+       return KeyPresent, nil
+}
diff --git a/keyring/keyring_test.go b/keyring/keyring_test.go
new file mode 100644 (file)
index 0000000..26f6036
--- /dev/null
@@ -0,0 +1,330 @@
+/*
+ * keyring_test.go - tests for the keyring package
+ *
+ * Copyright 2017 Google Inc.
+ *
+ * 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.
+ */
+
+package keyring
+
+import (
+       "os/user"
+       "strconv"
+       "testing"
+
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/util"
+)
+
+// Reader that always returns the same byte
+type ConstReader byte
+
+func (r ConstReader) Read(b []byte) (n int, err error) {
+       for i := range b {
+               b[i] = byte(r)
+       }
+       return len(b), nil
+}
+
+// Makes a key of the same repeating byte
+func makeKey(b byte, n int) (*crypto.Key, error) {
+       return crypto.NewFixedLengthKeyFromReader(ConstReader(b), n)
+}
+
+var (
+       testUser, _             = util.EffectiveUser()
+       fakeValidPolicyKey, _   = makeKey(42, metadata.PolicyKeyLen)
+       fakeInvalidPolicyKey, _ = makeKey(42, metadata.PolicyKeyLen-1)
+       fakeV1Descriptor        = "0123456789abcdef"
+       fakeV2Descriptor, _     = crypto.ComputeKeyDescriptor(fakeValidPolicyKey, 2)
+)
+
+func assertKeyStatus(t *testing.T, descriptor string, options *Options,
+       expectedStatus KeyStatus) {
+       status, err := GetEncryptionKeyStatus(descriptor, options)
+       if err != nil {
+               t.Error(err)
+       }
+       if status != expectedStatus {
+               t.Errorf("Expected key status %v but got key status %v", expectedStatus, status)
+       }
+}
+
+// getTestMount retrieves the Mount for a test filesystem, or skips the test if
+// no test filesystem is available.
+func getTestMount(t *testing.T) *filesystem.Mount {
+       root, err := util.TestRoot()
+       if err != nil {
+               t.Skip(err)
+       }
+       mount, err := filesystem.GetMount(root)
+       if err != nil {
+               t.Skip(err)
+       }
+       return mount
+}
+
+// getTestMountV2 is like getTestMount, but it also checks that the
+// filesystem keyring and v2 encryption policies are supported.
+func getTestMountV2(t *testing.T) *filesystem.Mount {
+       mount := getTestMount(t)
+       if !IsFsKeyringSupported(mount) {
+               t.Skip("No support for fs keyring, skipping test.")
+       }
+       return mount
+}
+
+func requireRoot(t *testing.T) {
+       if !util.IsUserRoot() {
+               t.Skip("Not root, skipping test.")
+       }
+}
+
+// getNonRootUsers checks for root permission, then returns the users for uids
+// 1000...1000+count-1.  If this fails, the test is skipped.
+func getNonRootUsers(t *testing.T, count int) []*user.User {
+       requireRoot(t)
+       users := make([]*user.User, count)
+       for i := 0; i < count; i++ {
+               uid := 1000 + i
+               user, err := user.LookupId(strconv.Itoa(uid))
+               if err != nil {
+                       t.Skip(err)
+               }
+               users[i] = user
+       }
+       return users
+}
+
+func getOptionsForFsKeyringUsers(t *testing.T, numNonRootUsers int) (rootOptions *Options, userOptions []*Options) {
+       mount := getTestMountV2(t)
+       nonRootUsers := getNonRootUsers(t, numNonRootUsers)
+       rootOptions = &Options{
+               Mount: mount,
+               User:  testUser,
+       }
+       userOptions = make([]*Options, numNonRootUsers)
+       for i := 0; i < numNonRootUsers; i++ {
+               userOptions[i] = &Options{
+                       Mount: mount,
+                       User:  nonRootUsers[i],
+               }
+       }
+       return
+}
+
+// testAddAndRemoveKey does the common tests for adding+removing keys that are
+// run in multiple configurations (v1 policies with user keyring, v1 policies
+// with fs keyring, and v2 policies with fs keyring).
+func testAddAndRemoveKey(t *testing.T, descriptor string, options *Options) {
+
+       // Basic add, get status, and remove
+       if err := AddEncryptionKey(fakeValidPolicyKey, descriptor, options); err != nil {
+               t.Error(err)
+       }
+       assertKeyStatus(t, descriptor, options, KeyPresent)
+       if err := RemoveEncryptionKey(descriptor, options, false); err != nil {
+               t.Error(err)
+       }
+       assertKeyStatus(t, descriptor, options, KeyAbsent)
+       err := RemoveEncryptionKey(descriptor, options, false)
+       if err != ErrKeyNotPresent {
+               t.Error(err)
+       }
+
+       // Adding a key twice should succeed
+       if err := AddEncryptionKey(fakeValidPolicyKey, descriptor, options); err != nil {
+               t.Error(err)
+       }
+       if err := AddEncryptionKey(fakeValidPolicyKey, descriptor, options); err != nil {
+               t.Error("AddEncryptionKey should not fail if key already exists")
+       }
+       RemoveEncryptionKey(descriptor, options, false)
+       assertKeyStatus(t, descriptor, options, KeyAbsent)
+
+       // Adding a key with wrong length should fail
+       if err := AddEncryptionKey(fakeInvalidPolicyKey, descriptor, options); err == nil {
+               RemoveEncryptionKey(descriptor, options, false)
+               t.Error("AddEncryptionKey should fail with wrong-length key")
+       }
+       assertKeyStatus(t, descriptor, options, KeyAbsent)
+}
+
+func TestUserKeyring(t *testing.T) {
+       mount := getTestMount(t)
+       options := &Options{
+               Mount:                     mount,
+               User:                      testUser,
+               UseFsKeyringForV1Policies: false,
+       }
+       testAddAndRemoveKey(t, fakeV1Descriptor, options)
+}
+
+func TestFsKeyringV1PolicyKey(t *testing.T) {
+       requireRoot(t)
+       mount := getTestMountV2(t)
+       options := &Options{
+               Mount:                     mount,
+               User:                      testUser,
+               UseFsKeyringForV1Policies: true,
+       }
+       testAddAndRemoveKey(t, fakeV1Descriptor, options)
+}
+
+func TestV2PolicyKey(t *testing.T) {
+       mount := getTestMountV2(t)
+       options := &Options{
+               Mount: mount,
+               User:  testUser,
+       }
+       testAddAndRemoveKey(t, fakeV2Descriptor, options)
+}
+
+func TestV2PolicyKeyCannotBeRemovedByAnotherUser(t *testing.T) {
+       rootOptions, userOptions := getOptionsForFsKeyringUsers(t, 2)
+       user1Options := userOptions[0]
+       user2Options := userOptions[1]
+
+       // Add key as non-root user.
+       if err := AddEncryptionKey(fakeValidPolicyKey, fakeV2Descriptor, user1Options); err != nil {
+               t.Error(err)
+       }
+       assertKeyStatus(t, fakeV2Descriptor, user1Options, KeyPresent)
+       assertKeyStatus(t, fakeV2Descriptor, user2Options, KeyPresentButOnlyOtherUsers)
+       assertKeyStatus(t, fakeV2Descriptor, rootOptions, KeyPresentButOnlyOtherUsers)
+
+       // Key shouldn't be removable by another user, even root.
+       err := RemoveEncryptionKey(fakeV2Descriptor, user2Options, false)
+       if err != ErrKeyAddedByOtherUsers {
+               t.Error(err)
+       }
+       assertKeyStatus(t, fakeV2Descriptor, user1Options, KeyPresent)
+       assertKeyStatus(t, fakeV2Descriptor, user2Options, KeyPresentButOnlyOtherUsers)
+       assertKeyStatus(t, fakeV2Descriptor, rootOptions, KeyPresentButOnlyOtherUsers)
+       err = RemoveEncryptionKey(fakeV2Descriptor, rootOptions, false)
+       if err != ErrKeyAddedByOtherUsers {
+               t.Error(err)
+       }
+       assertKeyStatus(t, fakeV2Descriptor, user1Options, KeyPresent)
+       assertKeyStatus(t, fakeV2Descriptor, user2Options, KeyPresentButOnlyOtherUsers)
+       assertKeyStatus(t, fakeV2Descriptor, rootOptions, KeyPresentButOnlyOtherUsers)
+
+       if err := RemoveEncryptionKey(fakeV2Descriptor, user1Options, false); err != nil {
+               t.Error(err)
+       }
+       assertKeyStatus(t, fakeV2Descriptor, user1Options, KeyAbsent)
+       assertKeyStatus(t, fakeV2Descriptor, user2Options, KeyAbsent)
+       assertKeyStatus(t, fakeV2Descriptor, rootOptions, KeyAbsent)
+}
+
+func TestV2PolicyKeyMultipleUsers(t *testing.T) {
+       rootOptions, userOptions := getOptionsForFsKeyringUsers(t, 2)
+       user1Options := userOptions[0]
+       user2Options := userOptions[1]
+
+       // Add key as two non-root users.
+       if err := AddEncryptionKey(fakeValidPolicyKey, fakeV2Descriptor, user1Options); err != nil {
+               t.Error(err)
+       }
+       if err := AddEncryptionKey(fakeValidPolicyKey, fakeV2Descriptor, user2Options); err != nil {
+               t.Error(err)
+       }
+       assertKeyStatus(t, fakeV2Descriptor, user1Options, KeyPresent)
+       assertKeyStatus(t, fakeV2Descriptor, user2Options, KeyPresent)
+       assertKeyStatus(t, fakeV2Descriptor, rootOptions, KeyPresentButOnlyOtherUsers)
+
+       // Remove key as one user.
+       err := RemoveEncryptionKey(fakeV2Descriptor, user1Options, false)
+       if err != ErrKeyAddedByOtherUsers {
+               t.Error(err)
+       }
+       assertKeyStatus(t, fakeV2Descriptor, user1Options, KeyPresentButOnlyOtherUsers)
+       assertKeyStatus(t, fakeV2Descriptor, user2Options, KeyPresent)
+       assertKeyStatus(t, fakeV2Descriptor, rootOptions, KeyPresentButOnlyOtherUsers)
+
+       // Remove key as the other user.
+       err = RemoveEncryptionKey(fakeV2Descriptor, user2Options, false)
+       if err != nil {
+               t.Error(err)
+       }
+       assertKeyStatus(t, fakeV2Descriptor, user1Options, KeyAbsent)
+       assertKeyStatus(t, fakeV2Descriptor, user2Options, KeyAbsent)
+       assertKeyStatus(t, fakeV2Descriptor, rootOptions, KeyAbsent)
+}
+
+func TestV2PolicyKeyWrongDescriptor(t *testing.T) {
+       mount := getTestMountV2(t)
+       options := &Options{
+               Mount: mount,
+               User:  testUser,
+       }
+       // one wrong but valid hex, and one not valid hex
+       wrongV2Descriptors := []string{"abcdabcdabcdabcdabcdabcdabcdabcd", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}
+
+       for _, desc := range wrongV2Descriptors {
+               if err := AddEncryptionKey(fakeValidPolicyKey, desc, options); err == nil {
+                       RemoveEncryptionKey(desc, options, false)
+                       t.Error("For v2 policy keys, AddEncryptionKey should fail if the descriptor is wrong")
+               }
+       }
+}
+
+func TestV2PolicyKeyBadMount(t *testing.T) {
+       options := &Options{
+               Mount: &filesystem.Mount{Path: "/NONEXISTENT_MOUNT"},
+               User:  testUser,
+       }
+       if err := AddEncryptionKey(fakeValidPolicyKey, fakeV2Descriptor, options); err == nil {
+               RemoveEncryptionKey(fakeV2Descriptor, options, false)
+               t.Error("AddEncryptionKey should have failed with bad mount!")
+       }
+       if err := RemoveEncryptionKey(fakeV2Descriptor, options, false); err == nil {
+               t.Error("RemoveEncryptionKey should have failed with bad mount!")
+       }
+       status, err := GetEncryptionKeyStatus(fakeV2Descriptor, options)
+       if err == nil {
+               t.Error("GetEncryptionKeyStatus should have failed with bad mount!")
+       }
+       if status != KeyStatusUnknown {
+               t.Error("GetEncryptionKeyStatus should have returned unknown status!")
+       }
+}
+
+func TestV2PolicyKeyRemoveForAllUsers(t *testing.T) {
+       rootOptions, userOptions := getOptionsForFsKeyringUsers(t, 2)
+       user1Options := userOptions[0]
+       user2Options := userOptions[1]
+
+       // Add key as two non-root users.
+       if err := AddEncryptionKey(fakeValidPolicyKey, fakeV2Descriptor, user1Options); err != nil {
+               t.Error(err)
+       }
+       if err := AddEncryptionKey(fakeValidPolicyKey, fakeV2Descriptor, user2Options); err != nil {
+               t.Error(err)
+       }
+       assertKeyStatus(t, fakeV2Descriptor, user1Options, KeyPresent)
+       assertKeyStatus(t, fakeV2Descriptor, user2Options, KeyPresent)
+       assertKeyStatus(t, fakeV2Descriptor, rootOptions, KeyPresentButOnlyOtherUsers)
+
+       // Remove key for all users as root.
+       err := RemoveEncryptionKey(fakeV2Descriptor, rootOptions, true)
+       if err != nil {
+               t.Error(err)
+       }
+       assertKeyStatus(t, fakeV2Descriptor, user1Options, KeyAbsent)
+       assertKeyStatus(t, fakeV2Descriptor, user2Options, KeyAbsent)
+       assertKeyStatus(t, fakeV2Descriptor, rootOptions, KeyAbsent)
+}
diff --git a/keyring/user_keyring.go b/keyring/user_keyring.go
new file mode 100644 (file)
index 0000000..0ea4689
--- /dev/null
@@ -0,0 +1,251 @@
+/*
+ * user_keyring.go - Add/remove encryption policy keys to/from user keyrings.
+ * This is the deprecated mechanism; see fs_keyring.go for the new mechanism.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package keyring
+
+import (
+       "os/user"
+       "runtime"
+       "unsafe"
+
+       "github.com/pkg/errors"
+       "golang.org/x/sys/unix"
+
+       "fmt"
+       "log"
+
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/security"
+       "github.com/google/fscrypt/util"
+)
+
+// ErrAccessUserKeyring indicates that a user's keyring cannot be
+// accessed.
+type ErrAccessUserKeyring struct {
+       TargetUser      *user.User
+       UnderlyingError error
+}
+
+func (err *ErrAccessUserKeyring) Error() string {
+       return fmt.Sprintf("could not access user keyring for %q: %s",
+               err.TargetUser.Username, err.UnderlyingError)
+}
+
+// ErrSessionUserKeyring indicates that a user's keyring is not linked
+// into the session keyring.
+type ErrSessionUserKeyring struct {
+       TargetUser *user.User
+}
+
+func (err *ErrSessionUserKeyring) Error() string {
+       return fmt.Sprintf("user keyring for %q is not linked into the session keyring",
+               err.TargetUser.Username)
+}
+
+// KeyType is always logon as required by filesystem encryption.
+const KeyType = "logon"
+
+// userAddKey puts the provided policy key into the user keyring for the
+// specified user with the provided description, and type logon.
+func userAddKey(key *crypto.Key, description string, targetUser *user.User) error {
+       runtime.LockOSThread() // ensure target user keyring remains possessed in thread keyring
+       defer runtime.UnlockOSThread()
+
+       // Create our payload (containing an FscryptKey)
+       payload, err := crypto.NewBlankKey(int(unsafe.Sizeof(unix.FscryptKey{})))
+       if err != nil {
+               return err
+       }
+       defer payload.Wipe()
+
+       // Cast the payload to an FscryptKey so we can initialize the fields.
+       fscryptKey := (*unix.FscryptKey)(payload.UnsafePtr())
+       // Mode is ignored by the kernel
+       fscryptKey.Mode = 0
+       fscryptKey.Size = uint32(key.Len())
+       copy(fscryptKey.Raw[:], key.Data())
+
+       keyringID, err := UserKeyringID(targetUser, true)
+       if err != nil {
+               return err
+       }
+       keyID, err := unix.AddKey(KeyType, description, payload.Data(), keyringID)
+       log.Printf("KeyctlAddKey(%s, %s, <data>, %d) = %d, %v",
+               KeyType, description, keyringID, keyID, err)
+       if err != nil {
+               return errors.Wrapf(err,
+                       "error adding key with description %s to user keyring for %q",
+                       description, targetUser.Username)
+       }
+       return nil
+}
+
+// userRemoveKey tries to remove a policy key from the user keyring with the
+// provided description. An error is returned if the key does not exist.
+func userRemoveKey(description string, targetUser *user.User) error {
+       runtime.LockOSThread() // ensure target user keyring remains possessed in thread keyring
+       defer runtime.UnlockOSThread()
+
+       keyID, keyringID, err := userFindKey(description, targetUser)
+       if err != nil {
+               return ErrKeyNotPresent
+       }
+
+       _, err = unix.KeyctlInt(unix.KEYCTL_UNLINK, keyID, keyringID, 0, 0)
+       log.Printf("KeyctlUnlink(%d, %d) = %v", keyID, keyringID, err)
+       if err != nil {
+               return errors.Wrapf(err,
+                       "error removing key with description %s from user keyring for %q",
+                       description, targetUser.Username)
+       }
+       return nil
+}
+
+// userFindKey tries to locate a key with the provided description in the user
+// keyring for the target user. The key ID and keyring ID are returned if we can
+// find the key. An error is returned if the key does not exist.
+func userFindKey(description string, targetUser *user.User) (int, int, error) {
+       runtime.LockOSThread() // ensure target user keyring remains possessed in thread keyring
+       defer runtime.UnlockOSThread()
+
+       keyringID, err := UserKeyringID(targetUser, false)
+       if err != nil {
+               return 0, 0, err
+       }
+
+       keyID, err := unix.KeyctlSearch(keyringID, KeyType, description, 0)
+       log.Printf("KeyctlSearch(%d, %s, %s) = %d, %v", keyringID, KeyType, description, keyID, err)
+       if err != nil {
+               return 0, 0, errors.Wrapf(err,
+                       "error searching for key %s in user keyring for %q",
+                       description, targetUser.Username)
+       }
+       return keyID, keyringID, err
+}
+
+// UserKeyringID returns the key id of the target user's user keyring. We also
+// ensure that the keyring will be accessible by linking it into the thread
+// keyring and linking it into the root user keyring (permissions allowing). If
+// checkSession is true, an error is returned if a normal user requests their
+// user keyring, but it is not in the current session keyring.
+func UserKeyringID(targetUser *user.User, checkSession bool) (int, error) {
+       runtime.LockOSThread() // ensure target user keyring remains possessed in thread keyring
+       defer runtime.UnlockOSThread()
+
+       uid := util.AtoiOrPanic(targetUser.Uid)
+       targetKeyring, err := userKeyringIDLookup(uid)
+       if err != nil {
+               return 0, &ErrAccessUserKeyring{targetUser, err}
+       }
+
+       if !util.IsUserRoot() {
+               // Make sure the returned keyring will be accessible by checking
+               // that it is in the session keyring.
+               if checkSession && !isUserKeyringInSession(uid) {
+                       return 0, &ErrSessionUserKeyring{targetUser}
+               }
+               return targetKeyring, nil
+       }
+
+       // Make sure the returned keyring will be accessible by linking it into
+       // the root user's user keyring (which will not be garbage collected).
+       rootKeyring, err := userKeyringIDLookup(0)
+       if err != nil {
+               return 0, errors.Wrapf(err, "error looking up root's user keyring")
+       }
+
+       if rootKeyring != targetKeyring {
+               if err = keyringLink(targetKeyring, rootKeyring); err != nil {
+                       return 0, errors.Wrapf(err,
+                               "error linking user keyring for %q into root's user keyring",
+                               targetUser.Username)
+               }
+       }
+       return targetKeyring, nil
+}
+
+func userKeyringIDLookup(uid int) (keyringID int, err error) {
+
+       // Our goals here are to:
+       //    - Find the user keyring (for the provided uid)
+       //    - Link it into the current thread keyring (so we can use it)
+       //    - Make no permanent changes to the process privileges
+       // Complicating this are the facts that:
+       //    - The value of KEY_SPEC_USER_KEYRING is determined by the ruid
+       //    - Keyring linking permissions use the euid
+       // So we have to change both the ruid and euid to make this work,
+       // setting the suid to 0 so that we can later switch back.
+       ruid, euid, suid := security.GetUids()
+       if ruid != uid || euid != uid {
+               if err = security.SetUids(uid, uid, 0); err != nil {
+                       return
+               }
+               defer func() {
+                       resetErr := security.SetUids(ruid, euid, suid)
+                       if resetErr != nil {
+                               err = resetErr
+                       }
+               }()
+       }
+
+       // We get the value of KEY_SPEC_USER_KEYRING. Note that this will also
+       // trigger the creation of the uid keyring if it does not yet exist.
+       keyringID, err = unix.KeyctlGetKeyringID(unix.KEY_SPEC_USER_KEYRING, true)
+       log.Printf("keyringID(_uid.%d) = %d, %v", uid, keyringID, err)
+       if err != nil {
+               return 0, err
+       }
+
+       // We still want to use this keyring after our privileges are reset. So
+       // we link it into the thread keyring, preventing a loss of access.
+       //
+       // We must be under LockOSThread() for this to work reliably.  Note that
+       // we can't just use the process keyring, since it doesn't work reliably
+       // in Go programs, due to the Go runtime creating threads before the
+       // program starts and has a chance to create the process keyring.
+       if err = keyringLink(keyringID, unix.KEY_SPEC_THREAD_KEYRING); err != nil {
+               return 0, err
+       }
+
+       return keyringID, nil
+}
+
+// isUserKeyringInSession tells us if the user's uid keyring is in the current
+// session keyring.
+func isUserKeyringInSession(uid int) bool {
+       // We cannot use unix.KEY_SPEC_SESSION_KEYRING directly as that might
+       // create a session keyring if one does not exist.
+       sessionKeyring, err := unix.KeyctlGetKeyringID(unix.KEY_SPEC_SESSION_KEYRING, false)
+       log.Printf("keyringID(session) = %d, %v", sessionKeyring, err)
+       if err != nil {
+               return false
+       }
+
+       description := fmt.Sprintf("_uid.%d", uid)
+       id, err := unix.KeyctlSearch(sessionKeyring, "keyring", description, 0)
+       log.Printf("KeyctlSearch(%d, keyring, %s) = %d, %v", sessionKeyring, description, id, err)
+       return err == nil
+}
+
+func keyringLink(keyID int, keyringID int) error {
+       _, err := unix.KeyctlInt(unix.KEYCTL_LINK, keyID, keyringID, 0, 0)
+       log.Printf("KeyctlLink(%d, %d) = %v", keyID, keyringID, err)
+       return err
+}
diff --git a/metadata/checks.go b/metadata/checks.go
new file mode 100644 (file)
index 0000000..d7dea41
--- /dev/null
@@ -0,0 +1,241 @@
+/*
+ * checks.go - Some sanity check methods for our metadata structures
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package metadata
+
+import (
+       "log"
+       "math"
+
+       "github.com/pkg/errors"
+       "google.golang.org/protobuf/proto"
+
+       "github.com/google/fscrypt/util"
+)
+
+var errNotInitialized = errors.New("not initialized")
+
+// Metadata is the interface to all of the protobuf structures that can be
+// checked for validity.
+type Metadata interface {
+       CheckValidity() error
+       proto.Message
+}
+
+// CheckValidity ensures the mode has a name and isn't empty.
+func (m EncryptionOptions_Mode) CheckValidity() error {
+       if m == EncryptionOptions_default {
+               return errNotInitialized
+       }
+       if m.String() == "" {
+               return errors.Errorf("unknown %d", m)
+       }
+       return nil
+}
+
+// CheckValidity ensures the source has a name and isn't empty.
+func (s SourceType) CheckValidity() error {
+       if s == SourceType_default {
+               return errNotInitialized
+       }
+       if s.String() == "" {
+               return errors.Errorf("unknown %d", s)
+       }
+       return nil
+}
+
+// MaxParallelism is the maximum allowed value for HashingCosts.Parallelism.
+const MaxParallelism = math.MaxUint8
+
+// CheckValidity ensures the hash costs will be accepted by Argon2.
+func (h *HashingCosts) CheckValidity() error {
+       if h == nil {
+               return errNotInitialized
+       }
+
+       minP := int64(1)
+       p := uint8(h.Parallelism)
+       if h.Parallelism < minP || h.Parallelism > MaxParallelism {
+               if h.TruncationFixed || p == 0 {
+                       return errors.Errorf("parallelism cost %d is not in range [%d, %d]",
+                               h.Parallelism, minP, MaxParallelism)
+               }
+               // Previously we unconditionally casted costs.Parallelism to a uint8,
+               // so we replicate this behavior for backwards compatibility.
+               log.Printf("WARNING: Truncating parallelism cost of %d to %d", h.Parallelism, p)
+       }
+
+       minT := int64(1)
+       maxT := int64(math.MaxUint32)
+       if h.Time < minT || h.Time > maxT {
+               return errors.Errorf("time cost %d is not in range [%d, %d]", h.Time, minT, maxT)
+       }
+
+       minM := 8 * int64(p)
+       maxM := int64(math.MaxUint32)
+       if h.Memory < minM || h.Memory > maxM {
+               return errors.Errorf("memory cost %d KiB is not in range [%d, %d]", h.Memory, minM, maxM)
+       }
+       return nil
+}
+
+// CheckValidity ensures our buffers are the correct length.
+func (w *WrappedKeyData) CheckValidity() error {
+       if w == nil {
+               return errNotInitialized
+       }
+       if len(w.EncryptedKey) == 0 {
+               return errors.Wrap(errNotInitialized, "encrypted key")
+       }
+       if err := util.CheckValidLength(IVLen, len(w.IV)); err != nil {
+               return errors.Wrap(err, "IV")
+       }
+       return errors.Wrap(util.CheckValidLength(HMACLen, len(w.Hmac)), "HMAC")
+}
+
+// CheckValidity ensures our ProtectorData has the correct fields for its source.
+func (p *ProtectorData) CheckValidity() error {
+       if p == nil {
+               return errNotInitialized
+       }
+
+       if err := p.Source.CheckValidity(); err != nil {
+               return errors.Wrap(err, "protector source")
+       }
+
+       // Source specific checks
+       switch p.Source {
+       case SourceType_pam_passphrase:
+               if p.Uid < 0 {
+                       return errors.Errorf("UID=%d is negative", p.Uid)
+               }
+               fallthrough
+       case SourceType_custom_passphrase:
+               if err := p.Costs.CheckValidity(); err != nil {
+                       return errors.Wrap(err, "passphrase hashing costs")
+               }
+               if err := util.CheckValidLength(SaltLen, len(p.Salt)); err != nil {
+                       return errors.Wrap(err, "passphrase hashing salt")
+               }
+       }
+
+       // Generic checks
+       if err := p.WrappedKey.CheckValidity(); err != nil {
+               return errors.Wrap(err, "wrapped protector key")
+       }
+       if err := util.CheckValidLength(ProtectorDescriptorLen, len(p.ProtectorDescriptor)); err != nil {
+               return errors.Wrap(err, "protector descriptor")
+
+       }
+       err := util.CheckValidLength(InternalKeyLen, len(p.WrappedKey.EncryptedKey))
+       return errors.Wrap(err, "encrypted protector key")
+}
+
+// CheckValidity ensures each of the options is valid.
+func (e *EncryptionOptions) CheckValidity() error {
+       if e == nil {
+               return errNotInitialized
+       }
+       if _, ok := util.Index(e.Padding, paddingArray); !ok {
+               return errors.Errorf("padding of %d is invalid", e.Padding)
+       }
+       if err := e.Contents.CheckValidity(); err != nil {
+               return errors.Wrap(err, "contents encryption mode")
+       }
+       if err := e.Filenames.CheckValidity(); err != nil {
+               return errors.Wrap(err, "filenames encryption mode")
+       }
+       // If PolicyVersion is unset, treat it as 1.
+       if e.PolicyVersion == 0 {
+               e.PolicyVersion = 1
+       }
+       if e.PolicyVersion != 1 && e.PolicyVersion != 2 {
+               return errors.Errorf("policy version of %d is invalid", e.PolicyVersion)
+       }
+       return nil
+}
+
+// CheckValidity ensures the fields are valid and have the correct lengths.
+func (w *WrappedPolicyKey) CheckValidity() error {
+       if w == nil {
+               return errNotInitialized
+       }
+       if err := w.WrappedKey.CheckValidity(); err != nil {
+               return errors.Wrap(err, "wrapped key")
+       }
+       if err := util.CheckValidLength(PolicyKeyLen, len(w.WrappedKey.EncryptedKey)); err != nil {
+               return errors.Wrap(err, "encrypted key")
+       }
+       err := util.CheckValidLength(ProtectorDescriptorLen, len(w.ProtectorDescriptor))
+       return errors.Wrap(err, "wrapping protector descriptor")
+}
+
+// CheckValidity ensures the fields and each wrapped key are valid.
+func (p *PolicyData) CheckValidity() error {
+       if p == nil {
+               return errNotInitialized
+       }
+       // Check each wrapped key
+       for i, w := range p.WrappedPolicyKeys {
+               if err := w.CheckValidity(); err != nil {
+                       return errors.Wrapf(err, "policy key slot %d", i)
+               }
+       }
+
+       if err := p.Options.CheckValidity(); err != nil {
+               return errors.Wrap(err, "policy options")
+       }
+
+       var expectedLen int
+       switch p.Options.PolicyVersion {
+       case 1:
+               expectedLen = PolicyDescriptorLenV1
+       case 2:
+               expectedLen = PolicyDescriptorLenV2
+       default:
+               return errors.Errorf("policy version of %d is invalid", p.Options.PolicyVersion)
+       }
+
+       if err := util.CheckValidLength(expectedLen, len(p.KeyDescriptor)); err != nil {
+               return errors.Wrap(err, "policy key descriptor")
+       }
+
+       return nil
+}
+
+// CheckValidity ensures the Config has all the necessary info for its Source.
+func (c *Config) CheckValidity() error {
+       // General checks
+       if c == nil {
+               return errNotInitialized
+       }
+       if err := c.Source.CheckValidity(); err != nil {
+               return errors.Wrap(err, "default config source")
+       }
+
+       // Source specific checks
+       switch c.Source {
+       case SourceType_pam_passphrase, SourceType_custom_passphrase:
+               if err := c.HashCosts.CheckValidity(); err != nil {
+                       return errors.Wrap(err, "config hashing costs")
+               }
+       }
+
+       return errors.Wrap(c.Options.CheckValidity(), "config options")
+}
diff --git a/metadata/config.go b/metadata/config.go
new file mode 100644 (file)
index 0000000..65fd7b5
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * config.go - Parsing for our global config file. The file is simply the JSON
+ * output of the Config protocol buffer.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+// Package metadata contains all of the on disk structures.
+// These structures are defined in metadata.proto. The package also
+// contains functions for manipulating these structures, specifically:
+//   - Reading and Writing the Config file to disk
+//   - Getting and Setting Policies for directories
+//   - Reasonable defaults for a Policy's EncryptionOptions
+package metadata
+
+import (
+       "io"
+
+       "google.golang.org/protobuf/encoding/protojson"
+)
+
+// WriteConfig outputs the Config data as nicely formatted JSON
+func WriteConfig(config *Config, out io.Writer) error {
+       m := protojson.MarshalOptions{
+               Multiline:       true,
+               Indent:          "\t",
+               UseProtoNames:   true,
+               UseEnumNumbers:  false,
+               EmitUnpopulated: true,
+       }
+       bytes, err := m.Marshal(config)
+       if err != nil {
+               return err
+       }
+       if _, err = out.Write(bytes); err != nil {
+               return err
+       }
+       _, err = out.Write([]byte{'\n'})
+       return err
+}
+
+// ReadConfig writes the JSON data into the config structure
+func ReadConfig(in io.Reader) (*Config, error) {
+       bytes, err := io.ReadAll(in)
+       if err != nil {
+               return nil, err
+       }
+       config := new(Config)
+       // Discard unknown fields for forwards compatibility.
+       u := protojson.UnmarshalOptions{
+               DiscardUnknown: true,
+       }
+       return config, u.Unmarshal(bytes, config)
+}
diff --git a/metadata/config_test.go b/metadata/config_test.go
new file mode 100644 (file)
index 0000000..3048757
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * config_test.go - Tests the processing of the config file
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package metadata
+
+import (
+       "bytes"
+       "encoding/json"
+       "testing"
+
+       "google.golang.org/protobuf/proto"
+)
+
+var testConfig = &Config{
+       Source: SourceType_custom_passphrase,
+       HashCosts: &HashingCosts{
+               Time:            10,
+               Memory:          1 << 12,
+               Parallelism:     8,
+               TruncationFixed: true,
+       },
+       Options: DefaultOptions,
+}
+
+var testConfigString = `{
+       "source": "custom_passphrase",
+       "hash_costs": {
+               "time": "10",
+               "memory": "4096",
+               "parallelism": "8",
+               "truncation_fixed": true
+       },
+       "options": {
+               "padding": "32",
+               "contents": "AES_256_XTS",
+               "filenames": "AES_256_CTS",
+               "policy_version": "1"
+       },
+       "use_fs_keyring_for_v1_policies": false,
+       "allow_cross_user_metadata": false
+}
+`
+
+// Used for JSON string comparison while ignoring whitespace
+func compact(t testing.TB, s string) string {
+       var b bytes.Buffer
+       if err := json.Compact(&b, []byte(s)); err != nil {
+               t.Fatalf("failed to compact json: %v", err)
+       }
+       return b.String()
+}
+
+// Makes sure that writing a config and reading it back gives the same thing.
+func TestWrite(t *testing.T) {
+       var b bytes.Buffer
+       err := WriteConfig(testConfig, &b)
+       if err != nil {
+               t.Fatal(err)
+       }
+       t.Logf("json encoded config:\n%s", b.String())
+       if compact(t, b.String()) != compact(t, testConfigString) {
+               t.Errorf("did not match: %s", testConfigString)
+       }
+}
+
+func TestRead(t *testing.T) {
+       buf := bytes.NewBufferString(testConfigString)
+       cfg, err := ReadConfig(buf)
+       if err != nil {
+               t.Fatal(err)
+       }
+       t.Logf("decoded config:\n%s", cfg)
+       if !proto.Equal(cfg, testConfig) {
+               t.Errorf("did not match: %s", testConfig)
+       }
+}
+
+// Makes sure we can parse a legacy config file that doesn't have the fields
+// that were added later and that has the removed "compatibility" field.
+func TestOptionalFields(t *testing.T) {
+       contents := `{
+               "source": "custom_passphrase",
+               "hash_costs": {
+                       "time": "10",
+                       "memory": "4096",
+                       "parallelism": "8"
+               },
+               "compatibility": "legacy",
+               "options": {
+                       "padding": "32",
+                       "contents": "AES_256_XTS",
+                       "filenames": "AES_256_CTS"
+               }
+       }
+       `
+       buf := bytes.NewBufferString(contents)
+       cfg, err := ReadConfig(buf)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if cfg.GetUseFsKeyringForV1Policies() {
+               t.Error("use_fs_keyring_for_v1_policies should be false, but was true")
+       }
+       if cfg.Options.PolicyVersion != 0 {
+               t.Errorf("policy version should be 0, but was %d", cfg.Options.PolicyVersion)
+       }
+       if err = cfg.CheckValidity(); err != nil {
+               t.Error(err)
+       }
+       // CheckValidity() should change an unset policy version to 1.
+       if cfg.Options.PolicyVersion != 1 {
+               t.Errorf("policy version should be 1 now, but was %d", cfg.Options.PolicyVersion)
+       }
+}
diff --git a/metadata/constants.go b/metadata/constants.go
new file mode 100644 (file)
index 0000000..fa6b8a7
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * constants.go - Some metadata constants used throughout fscrypt
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package metadata
+
+import (
+       "crypto/sha256"
+
+       "golang.org/x/sys/unix"
+)
+
+// Lengths for our keys, buffers, and strings used in fscrypt.
+const (
+       // Length of policy descriptor (in hex chars) for v1 encryption policies
+       PolicyDescriptorLenV1 = 2 * unix.FSCRYPT_KEY_DESCRIPTOR_SIZE
+       // Length of protector descriptor (in hex chars)
+       ProtectorDescriptorLen = PolicyDescriptorLenV1
+       // Length of policy descriptor (in hex chars) for v2 encryption policies
+       PolicyDescriptorLenV2 = 2 * unix.FSCRYPT_KEY_IDENTIFIER_SIZE
+       // We always use 256-bit keys internally (compared to 512-bit policy keys).
+       InternalKeyLen = 32
+       IVLen          = 16
+       SaltLen        = 16
+       // We use SHA256 for the HMAC, and len(HMAC) == len(hash size).
+       HMACLen = sha256.Size
+       // PolicyKeyLen is the length of all keys passed directly to the Keyring
+       PolicyKeyLen = unix.FSCRYPT_MAX_KEY_SIZE
+)
+
+var (
+       // DefaultOptions use the supported encryption modes, max padding, and
+       // policy version 1.
+       DefaultOptions = &EncryptionOptions{
+               Padding:       32,
+               Contents:      EncryptionOptions_AES_256_XTS,
+               Filenames:     EncryptionOptions_AES_256_CTS,
+               PolicyVersion: 1,
+       }
+       // DefaultSource is the source we use if none is specified.
+       DefaultSource = SourceType_custom_passphrase
+)
diff --git a/metadata/metadata.pb.go b/metadata/metadata.pb.go
new file mode 100644 (file)
index 0000000..38e6476
--- /dev/null
@@ -0,0 +1,936 @@
+//
+// metadata.proto - File which contains all of the metadata structures which we
+// write to metadata files. Must be compiled with protoc to use the library.
+// Compilation can be invoked with go generate.
+//
+// Copyright 2017 Google Inc.
+// Author: Joe Richey (joerichey@google.com)
+//
+// 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.
+
+// If the *.proto file is modified, be sure to run "make gen" (at the project
+// root) to recreate the *.pb.go file.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+//     protoc-gen-go v1.31.0
+//     protoc        v3.6.1
+// source: metadata/metadata.proto
+
+package metadata
+
+import (
+       protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+       protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+       reflect "reflect"
+       sync "sync"
+)
+
+const (
+       // Verify that this generated code is sufficiently up-to-date.
+       _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+       // Verify that runtime/protoimpl is sufficiently up-to-date.
+       _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Specifies the method in which an outside secret is obtained for a Protector
+type SourceType int32
+
+const (
+       SourceType_default           SourceType = 0
+       SourceType_pam_passphrase    SourceType = 1
+       SourceType_custom_passphrase SourceType = 2
+       SourceType_raw_key           SourceType = 3
+)
+
+// Enum value maps for SourceType.
+var (
+       SourceType_name = map[int32]string{
+               0: "default",
+               1: "pam_passphrase",
+               2: "custom_passphrase",
+               3: "raw_key",
+       }
+       SourceType_value = map[string]int32{
+               "default":           0,
+               "pam_passphrase":    1,
+               "custom_passphrase": 2,
+               "raw_key":           3,
+       }
+)
+
+func (x SourceType) Enum() *SourceType {
+       p := new(SourceType)
+       *p = x
+       return p
+}
+
+func (x SourceType) String() string {
+       return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (SourceType) Descriptor() protoreflect.EnumDescriptor {
+       return file_metadata_metadata_proto_enumTypes[0].Descriptor()
+}
+
+func (SourceType) Type() protoreflect.EnumType {
+       return &file_metadata_metadata_proto_enumTypes[0]
+}
+
+func (x SourceType) Number() protoreflect.EnumNumber {
+       return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use SourceType.Descriptor instead.
+func (SourceType) EnumDescriptor() ([]byte, []int) {
+       return file_metadata_metadata_proto_rawDescGZIP(), []int{0}
+}
+
+// Type of encryption; should match declarations of unix.FSCRYPT_MODE
+type EncryptionOptions_Mode int32
+
+const (
+       EncryptionOptions_default       EncryptionOptions_Mode = 0
+       EncryptionOptions_AES_256_XTS   EncryptionOptions_Mode = 1
+       EncryptionOptions_AES_256_GCM   EncryptionOptions_Mode = 2
+       EncryptionOptions_AES_256_CBC   EncryptionOptions_Mode = 3
+       EncryptionOptions_AES_256_CTS   EncryptionOptions_Mode = 4
+       EncryptionOptions_AES_128_CBC   EncryptionOptions_Mode = 5
+       EncryptionOptions_AES_128_CTS   EncryptionOptions_Mode = 6
+       EncryptionOptions_Adiantum      EncryptionOptions_Mode = 9
+       EncryptionOptions_AES_256_HCTR2 EncryptionOptions_Mode = 10
+)
+
+// Enum value maps for EncryptionOptions_Mode.
+var (
+       EncryptionOptions_Mode_name = map[int32]string{
+               0:  "default",
+               1:  "AES_256_XTS",
+               2:  "AES_256_GCM",
+               3:  "AES_256_CBC",
+               4:  "AES_256_CTS",
+               5:  "AES_128_CBC",
+               6:  "AES_128_CTS",
+               9:  "Adiantum",
+               10: "AES_256_HCTR2",
+       }
+       EncryptionOptions_Mode_value = map[string]int32{
+               "default":       0,
+               "AES_256_XTS":   1,
+               "AES_256_GCM":   2,
+               "AES_256_CBC":   3,
+               "AES_256_CTS":   4,
+               "AES_128_CBC":   5,
+               "AES_128_CTS":   6,
+               "Adiantum":      9,
+               "AES_256_HCTR2": 10,
+       }
+)
+
+func (x EncryptionOptions_Mode) Enum() *EncryptionOptions_Mode {
+       p := new(EncryptionOptions_Mode)
+       *p = x
+       return p
+}
+
+func (x EncryptionOptions_Mode) String() string {
+       return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (EncryptionOptions_Mode) Descriptor() protoreflect.EnumDescriptor {
+       return file_metadata_metadata_proto_enumTypes[1].Descriptor()
+}
+
+func (EncryptionOptions_Mode) Type() protoreflect.EnumType {
+       return &file_metadata_metadata_proto_enumTypes[1]
+}
+
+func (x EncryptionOptions_Mode) Number() protoreflect.EnumNumber {
+       return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use EncryptionOptions_Mode.Descriptor instead.
+func (EncryptionOptions_Mode) EnumDescriptor() ([]byte, []int) {
+       return file_metadata_metadata_proto_rawDescGZIP(), []int{3, 0}
+}
+
+// Cost parameters to be used in our hashing functions.
+type HashingCosts struct {
+       state         protoimpl.MessageState
+       sizeCache     protoimpl.SizeCache
+       unknownFields protoimpl.UnknownFields
+
+       Time        int64 `protobuf:"varint,2,opt,name=time,proto3" json:"time,omitempty"`
+       Memory      int64 `protobuf:"varint,3,opt,name=memory,proto3" json:"memory,omitempty"`
+       Parallelism int64 `protobuf:"varint,4,opt,name=parallelism,proto3" json:"parallelism,omitempty"`
+       // If true, parallelism should no longer be truncated to 8 bits.
+       TruncationFixed bool `protobuf:"varint,5,opt,name=truncation_fixed,json=truncationFixed,proto3" json:"truncation_fixed,omitempty"`
+}
+
+func (x *HashingCosts) Reset() {
+       *x = HashingCosts{}
+       if protoimpl.UnsafeEnabled {
+               mi := &file_metadata_metadata_proto_msgTypes[0]
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               ms.StoreMessageInfo(mi)
+       }
+}
+
+func (x *HashingCosts) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HashingCosts) ProtoMessage() {}
+
+func (x *HashingCosts) ProtoReflect() protoreflect.Message {
+       mi := &file_metadata_metadata_proto_msgTypes[0]
+       if protoimpl.UnsafeEnabled && x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use HashingCosts.ProtoReflect.Descriptor instead.
+func (*HashingCosts) Descriptor() ([]byte, []int) {
+       return file_metadata_metadata_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *HashingCosts) GetTime() int64 {
+       if x != nil {
+               return x.Time
+       }
+       return 0
+}
+
+func (x *HashingCosts) GetMemory() int64 {
+       if x != nil {
+               return x.Memory
+       }
+       return 0
+}
+
+func (x *HashingCosts) GetParallelism() int64 {
+       if x != nil {
+               return x.Parallelism
+       }
+       return 0
+}
+
+func (x *HashingCosts) GetTruncationFixed() bool {
+       if x != nil {
+               return x.TruncationFixed
+       }
+       return false
+}
+
+// This structure is used for our authenticated wrapping/unwrapping of keys.
+type WrappedKeyData struct {
+       state         protoimpl.MessageState
+       sizeCache     protoimpl.SizeCache
+       unknownFields protoimpl.UnknownFields
+
+       IV           []byte `protobuf:"bytes,1,opt,name=IV,proto3" json:"IV,omitempty"`
+       EncryptedKey []byte `protobuf:"bytes,2,opt,name=encrypted_key,json=encryptedKey,proto3" json:"encrypted_key,omitempty"`
+       Hmac         []byte `protobuf:"bytes,3,opt,name=hmac,proto3" json:"hmac,omitempty"`
+}
+
+func (x *WrappedKeyData) Reset() {
+       *x = WrappedKeyData{}
+       if protoimpl.UnsafeEnabled {
+               mi := &file_metadata_metadata_proto_msgTypes[1]
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               ms.StoreMessageInfo(mi)
+       }
+}
+
+func (x *WrappedKeyData) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WrappedKeyData) ProtoMessage() {}
+
+func (x *WrappedKeyData) ProtoReflect() protoreflect.Message {
+       mi := &file_metadata_metadata_proto_msgTypes[1]
+       if protoimpl.UnsafeEnabled && x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use WrappedKeyData.ProtoReflect.Descriptor instead.
+func (*WrappedKeyData) Descriptor() ([]byte, []int) {
+       return file_metadata_metadata_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *WrappedKeyData) GetIV() []byte {
+       if x != nil {
+               return x.IV
+       }
+       return nil
+}
+
+func (x *WrappedKeyData) GetEncryptedKey() []byte {
+       if x != nil {
+               return x.EncryptedKey
+       }
+       return nil
+}
+
+func (x *WrappedKeyData) GetHmac() []byte {
+       if x != nil {
+               return x.Hmac
+       }
+       return nil
+}
+
+// The associated data for each protector
+type ProtectorData struct {
+       state         protoimpl.MessageState
+       sizeCache     protoimpl.SizeCache
+       unknownFields protoimpl.UnknownFields
+
+       ProtectorDescriptor string     `protobuf:"bytes,1,opt,name=protector_descriptor,json=protectorDescriptor,proto3" json:"protector_descriptor,omitempty"`
+       Source              SourceType `protobuf:"varint,2,opt,name=source,proto3,enum=metadata.SourceType" json:"source,omitempty"`
+       // These are only used by some of the protector types
+       Name       string          `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+       Costs      *HashingCosts   `protobuf:"bytes,4,opt,name=costs,proto3" json:"costs,omitempty"`
+       Salt       []byte          `protobuf:"bytes,5,opt,name=salt,proto3" json:"salt,omitempty"`
+       Uid        int64           `protobuf:"varint,6,opt,name=uid,proto3" json:"uid,omitempty"`
+       WrappedKey *WrappedKeyData `protobuf:"bytes,7,opt,name=wrapped_key,json=wrappedKey,proto3" json:"wrapped_key,omitempty"`
+}
+
+func (x *ProtectorData) Reset() {
+       *x = ProtectorData{}
+       if protoimpl.UnsafeEnabled {
+               mi := &file_metadata_metadata_proto_msgTypes[2]
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               ms.StoreMessageInfo(mi)
+       }
+}
+
+func (x *ProtectorData) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ProtectorData) ProtoMessage() {}
+
+func (x *ProtectorData) ProtoReflect() protoreflect.Message {
+       mi := &file_metadata_metadata_proto_msgTypes[2]
+       if protoimpl.UnsafeEnabled && x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use ProtectorData.ProtoReflect.Descriptor instead.
+func (*ProtectorData) Descriptor() ([]byte, []int) {
+       return file_metadata_metadata_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *ProtectorData) GetProtectorDescriptor() string {
+       if x != nil {
+               return x.ProtectorDescriptor
+       }
+       return ""
+}
+
+func (x *ProtectorData) GetSource() SourceType {
+       if x != nil {
+               return x.Source
+       }
+       return SourceType_default
+}
+
+func (x *ProtectorData) GetName() string {
+       if x != nil {
+               return x.Name
+       }
+       return ""
+}
+
+func (x *ProtectorData) GetCosts() *HashingCosts {
+       if x != nil {
+               return x.Costs
+       }
+       return nil
+}
+
+func (x *ProtectorData) GetSalt() []byte {
+       if x != nil {
+               return x.Salt
+       }
+       return nil
+}
+
+func (x *ProtectorData) GetUid() int64 {
+       if x != nil {
+               return x.Uid
+       }
+       return 0
+}
+
+func (x *ProtectorData) GetWrappedKey() *WrappedKeyData {
+       if x != nil {
+               return x.WrappedKey
+       }
+       return nil
+}
+
+// Encryption policy specifics, corresponds to the fscrypt_policy struct
+type EncryptionOptions struct {
+       state         protoimpl.MessageState
+       sizeCache     protoimpl.SizeCache
+       unknownFields protoimpl.UnknownFields
+
+       Padding       int64                  `protobuf:"varint,1,opt,name=padding,proto3" json:"padding,omitempty"`
+       Contents      EncryptionOptions_Mode `protobuf:"varint,2,opt,name=contents,proto3,enum=metadata.EncryptionOptions_Mode" json:"contents,omitempty"`
+       Filenames     EncryptionOptions_Mode `protobuf:"varint,3,opt,name=filenames,proto3,enum=metadata.EncryptionOptions_Mode" json:"filenames,omitempty"`
+       PolicyVersion int64                  `protobuf:"varint,4,opt,name=policy_version,json=policyVersion,proto3" json:"policy_version,omitempty"`
+}
+
+func (x *EncryptionOptions) Reset() {
+       *x = EncryptionOptions{}
+       if protoimpl.UnsafeEnabled {
+               mi := &file_metadata_metadata_proto_msgTypes[3]
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               ms.StoreMessageInfo(mi)
+       }
+}
+
+func (x *EncryptionOptions) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EncryptionOptions) ProtoMessage() {}
+
+func (x *EncryptionOptions) ProtoReflect() protoreflect.Message {
+       mi := &file_metadata_metadata_proto_msgTypes[3]
+       if protoimpl.UnsafeEnabled && x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use EncryptionOptions.ProtoReflect.Descriptor instead.
+func (*EncryptionOptions) Descriptor() ([]byte, []int) {
+       return file_metadata_metadata_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *EncryptionOptions) GetPadding() int64 {
+       if x != nil {
+               return x.Padding
+       }
+       return 0
+}
+
+func (x *EncryptionOptions) GetContents() EncryptionOptions_Mode {
+       if x != nil {
+               return x.Contents
+       }
+       return EncryptionOptions_default
+}
+
+func (x *EncryptionOptions) GetFilenames() EncryptionOptions_Mode {
+       if x != nil {
+               return x.Filenames
+       }
+       return EncryptionOptions_default
+}
+
+func (x *EncryptionOptions) GetPolicyVersion() int64 {
+       if x != nil {
+               return x.PolicyVersion
+       }
+       return 0
+}
+
+type WrappedPolicyKey struct {
+       state         protoimpl.MessageState
+       sizeCache     protoimpl.SizeCache
+       unknownFields protoimpl.UnknownFields
+
+       ProtectorDescriptor string          `protobuf:"bytes,1,opt,name=protector_descriptor,json=protectorDescriptor,proto3" json:"protector_descriptor,omitempty"`
+       WrappedKey          *WrappedKeyData `protobuf:"bytes,2,opt,name=wrapped_key,json=wrappedKey,proto3" json:"wrapped_key,omitempty"`
+}
+
+func (x *WrappedPolicyKey) Reset() {
+       *x = WrappedPolicyKey{}
+       if protoimpl.UnsafeEnabled {
+               mi := &file_metadata_metadata_proto_msgTypes[4]
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               ms.StoreMessageInfo(mi)
+       }
+}
+
+func (x *WrappedPolicyKey) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WrappedPolicyKey) ProtoMessage() {}
+
+func (x *WrappedPolicyKey) ProtoReflect() protoreflect.Message {
+       mi := &file_metadata_metadata_proto_msgTypes[4]
+       if protoimpl.UnsafeEnabled && x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use WrappedPolicyKey.ProtoReflect.Descriptor instead.
+func (*WrappedPolicyKey) Descriptor() ([]byte, []int) {
+       return file_metadata_metadata_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *WrappedPolicyKey) GetProtectorDescriptor() string {
+       if x != nil {
+               return x.ProtectorDescriptor
+       }
+       return ""
+}
+
+func (x *WrappedPolicyKey) GetWrappedKey() *WrappedKeyData {
+       if x != nil {
+               return x.WrappedKey
+       }
+       return nil
+}
+
+// The associated data for each policy
+type PolicyData struct {
+       state         protoimpl.MessageState
+       sizeCache     protoimpl.SizeCache
+       unknownFields protoimpl.UnknownFields
+
+       KeyDescriptor     string              `protobuf:"bytes,1,opt,name=key_descriptor,json=keyDescriptor,proto3" json:"key_descriptor,omitempty"`
+       Options           *EncryptionOptions  `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
+       WrappedPolicyKeys []*WrappedPolicyKey `protobuf:"bytes,3,rep,name=wrapped_policy_keys,json=wrappedPolicyKeys,proto3" json:"wrapped_policy_keys,omitempty"`
+}
+
+func (x *PolicyData) Reset() {
+       *x = PolicyData{}
+       if protoimpl.UnsafeEnabled {
+               mi := &file_metadata_metadata_proto_msgTypes[5]
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               ms.StoreMessageInfo(mi)
+       }
+}
+
+func (x *PolicyData) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PolicyData) ProtoMessage() {}
+
+func (x *PolicyData) ProtoReflect() protoreflect.Message {
+       mi := &file_metadata_metadata_proto_msgTypes[5]
+       if protoimpl.UnsafeEnabled && x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use PolicyData.ProtoReflect.Descriptor instead.
+func (*PolicyData) Descriptor() ([]byte, []int) {
+       return file_metadata_metadata_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *PolicyData) GetKeyDescriptor() string {
+       if x != nil {
+               return x.KeyDescriptor
+       }
+       return ""
+}
+
+func (x *PolicyData) GetOptions() *EncryptionOptions {
+       if x != nil {
+               return x.Options
+       }
+       return nil
+}
+
+func (x *PolicyData) GetWrappedPolicyKeys() []*WrappedPolicyKey {
+       if x != nil {
+               return x.WrappedPolicyKeys
+       }
+       return nil
+}
+
+// Data stored in the config file
+type Config struct {
+       state         protoimpl.MessageState
+       sizeCache     protoimpl.SizeCache
+       unknownFields protoimpl.UnknownFields
+
+       Source                    SourceType         `protobuf:"varint,1,opt,name=source,proto3,enum=metadata.SourceType" json:"source,omitempty"`
+       HashCosts                 *HashingCosts      `protobuf:"bytes,2,opt,name=hash_costs,json=hashCosts,proto3" json:"hash_costs,omitempty"`
+       Options                   *EncryptionOptions `protobuf:"bytes,4,opt,name=options,proto3" json:"options,omitempty"`
+       UseFsKeyringForV1Policies bool               `protobuf:"varint,5,opt,name=use_fs_keyring_for_v1_policies,json=useFsKeyringForV1Policies,proto3" json:"use_fs_keyring_for_v1_policies,omitempty"`
+       AllowCrossUserMetadata    bool               `protobuf:"varint,6,opt,name=allow_cross_user_metadata,json=allowCrossUserMetadata,proto3" json:"allow_cross_user_metadata,omitempty"`
+}
+
+func (x *Config) Reset() {
+       *x = Config{}
+       if protoimpl.UnsafeEnabled {
+               mi := &file_metadata_metadata_proto_msgTypes[6]
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               ms.StoreMessageInfo(mi)
+       }
+}
+
+func (x *Config) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Config) ProtoMessage() {}
+
+func (x *Config) ProtoReflect() protoreflect.Message {
+       mi := &file_metadata_metadata_proto_msgTypes[6]
+       if protoimpl.UnsafeEnabled && x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use Config.ProtoReflect.Descriptor instead.
+func (*Config) Descriptor() ([]byte, []int) {
+       return file_metadata_metadata_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *Config) GetSource() SourceType {
+       if x != nil {
+               return x.Source
+       }
+       return SourceType_default
+}
+
+func (x *Config) GetHashCosts() *HashingCosts {
+       if x != nil {
+               return x.HashCosts
+       }
+       return nil
+}
+
+func (x *Config) GetOptions() *EncryptionOptions {
+       if x != nil {
+               return x.Options
+       }
+       return nil
+}
+
+func (x *Config) GetUseFsKeyringForV1Policies() bool {
+       if x != nil {
+               return x.UseFsKeyringForV1Policies
+       }
+       return false
+}
+
+func (x *Config) GetAllowCrossUserMetadata() bool {
+       if x != nil {
+               return x.AllowCrossUserMetadata
+       }
+       return false
+}
+
+var File_metadata_metadata_proto protoreflect.FileDescriptor
+
+var file_metadata_metadata_proto_rawDesc = []byte{
+       0x0a, 0x17, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x6d, 0x65, 0x74, 0x61, 0x64,
+       0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
+       0x61, 0x74, 0x61, 0x22, 0x87, 0x01, 0x0a, 0x0c, 0x48, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x43,
+       0x6f, 0x73, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
+       0x28, 0x03, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f,
+       0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79,
+       0x12, 0x20, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x69, 0x73, 0x6d, 0x18,
+       0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x70, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x69,
+       0x73, 0x6d, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+       0x5f, 0x66, 0x69, 0x78, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x74, 0x72,
+       0x75, 0x6e, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x78, 0x65, 0x64, 0x22, 0x59, 0x0a,
+       0x0e, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x44, 0x61, 0x74, 0x61, 0x12,
+       0x0e, 0x0a, 0x02, 0x49, 0x56, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x49, 0x56, 0x12,
+       0x23, 0x0a, 0x0d, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79,
+       0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
+       0x64, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x03, 0x20, 0x01,
+       0x28, 0x0c, 0x52, 0x04, 0x68, 0x6d, 0x61, 0x63, 0x22, 0x93, 0x02, 0x0a, 0x0d, 0x50, 0x72, 0x6f,
+       0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x31, 0x0a, 0x14, 0x70, 0x72,
+       0x6f, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
+       0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63,
+       0x74, 0x6f, 0x72, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x2c, 0x0a,
+       0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e,
+       0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54,
+       0x79, 0x70, 0x65, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e,
+       0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
+       0x2c, 0x0a, 0x05, 0x63, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16,
+       0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x48, 0x61, 0x73, 0x68, 0x69, 0x6e,
+       0x67, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x05, 0x63, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x12, 0x0a,
+       0x04, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x73, 0x61, 0x6c,
+       0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03,
+       0x75, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x0b, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b,
+       0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64,
+       0x61, 0x74, 0x61, 0x2e, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x44, 0x61,
+       0x74, 0x61, 0x52, 0x0a, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x22, 0xef,
+       0x02, 0x0a, 0x11, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74,
+       0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x18,
+       0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x70, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x3c,
+       0x0a, 0x08, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e,
+       0x32, 0x20, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x45, 0x6e, 0x63, 0x72,
+       0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4d, 0x6f,
+       0x64, 0x65, 0x52, 0x08, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x3e, 0x0a, 0x09,
+       0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32,
+       0x20, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79,
+       0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4d, 0x6f, 0x64,
+       0x65, 0x52, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e,
+       0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04,
+       0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x56, 0x65, 0x72, 0x73,
+       0x69, 0x6f, 0x6e, 0x22, 0x9a, 0x01, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0b, 0x0a, 0x07,
+       0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x45, 0x53,
+       0x5f, 0x32, 0x35, 0x36, 0x5f, 0x58, 0x54, 0x53, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x45,
+       0x53, 0x5f, 0x32, 0x35, 0x36, 0x5f, 0x47, 0x43, 0x4d, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x41,
+       0x45, 0x53, 0x5f, 0x32, 0x35, 0x36, 0x5f, 0x43, 0x42, 0x43, 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b,
+       0x41, 0x45, 0x53, 0x5f, 0x32, 0x35, 0x36, 0x5f, 0x43, 0x54, 0x53, 0x10, 0x04, 0x12, 0x0f, 0x0a,
+       0x0b, 0x41, 0x45, 0x53, 0x5f, 0x31, 0x32, 0x38, 0x5f, 0x43, 0x42, 0x43, 0x10, 0x05, 0x12, 0x0f,
+       0x0a, 0x0b, 0x41, 0x45, 0x53, 0x5f, 0x31, 0x32, 0x38, 0x5f, 0x43, 0x54, 0x53, 0x10, 0x06, 0x12,
+       0x0c, 0x0a, 0x08, 0x41, 0x64, 0x69, 0x61, 0x6e, 0x74, 0x75, 0x6d, 0x10, 0x09, 0x12, 0x11, 0x0a,
+       0x0d, 0x41, 0x45, 0x53, 0x5f, 0x32, 0x35, 0x36, 0x5f, 0x48, 0x43, 0x54, 0x52, 0x32, 0x10, 0x0a,
+       0x22, 0x80, 0x01, 0x0a, 0x10, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69,
+       0x63, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x14, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74,
+       0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20,
+       0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x44, 0x65,
+       0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x77, 0x72, 0x61, 0x70,
+       0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e,
+       0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64,
+       0x4b, 0x65, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0a, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64,
+       0x4b, 0x65, 0x79, 0x22, 0xb6, 0x01, 0x0a, 0x0a, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x44, 0x61,
+       0x74, 0x61, 0x12, 0x25, 0x0a, 0x0e, 0x6b, 0x65, 0x79, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69,
+       0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44,
+       0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x35, 0x0a, 0x07, 0x6f, 0x70, 0x74,
+       0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x65, 0x74,
+       0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e,
+       0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
+       0x12, 0x4a, 0x0a, 0x13, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x70, 0x6f, 0x6c, 0x69,
+       0x63, 0x79, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
+       0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64,
+       0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4b, 0x65, 0x79, 0x52, 0x11, 0x77, 0x72, 0x61, 0x70, 0x70,
+       0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4b, 0x65, 0x79, 0x73, 0x22, 0xb7, 0x02, 0x0a,
+       0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63,
+       0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
+       0x74, 0x61, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06, 0x73,
+       0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x35, 0x0a, 0x0a, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x63, 0x6f,
+       0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x74, 0x61,
+       0x64, 0x61, 0x74, 0x61, 0x2e, 0x48, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x73, 0x74,
+       0x73, 0x52, 0x09, 0x68, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x35, 0x0a, 0x07,
+       0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e,
+       0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
+       0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69,
+       0x6f, 0x6e, 0x73, 0x12, 0x41, 0x0a, 0x1e, 0x75, 0x73, 0x65, 0x5f, 0x66, 0x73, 0x5f, 0x6b, 0x65,
+       0x79, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x76, 0x31, 0x5f, 0x70, 0x6f, 0x6c,
+       0x69, 0x63, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x19, 0x75, 0x73, 0x65,
+       0x46, 0x73, 0x4b, 0x65, 0x79, 0x72, 0x69, 0x6e, 0x67, 0x46, 0x6f, 0x72, 0x56, 0x31, 0x50, 0x6f,
+       0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x19, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f,
+       0x63, 0x72, 0x6f, 0x73, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64,
+       0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x61, 0x6c, 0x6c, 0x6f, 0x77,
+       0x43, 0x72, 0x6f, 0x73, 0x73, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
+       0x61, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x0d, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x69,
+       0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x2a, 0x51, 0x0a, 0x0a, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65,
+       0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10,
+       0x00, 0x12, 0x12, 0x0a, 0x0e, 0x70, 0x61, 0x6d, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72,
+       0x61, 0x73, 0x65, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f,
+       0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07,
+       0x72, 0x61, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x10, 0x03, 0x42, 0x24, 0x5a, 0x22, 0x67, 0x69, 0x74,
+       0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x66,
+       0x73, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x62,
+       0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+       file_metadata_metadata_proto_rawDescOnce sync.Once
+       file_metadata_metadata_proto_rawDescData = file_metadata_metadata_proto_rawDesc
+)
+
+func file_metadata_metadata_proto_rawDescGZIP() []byte {
+       file_metadata_metadata_proto_rawDescOnce.Do(func() {
+               file_metadata_metadata_proto_rawDescData = protoimpl.X.CompressGZIP(file_metadata_metadata_proto_rawDescData)
+       })
+       return file_metadata_metadata_proto_rawDescData
+}
+
+var file_metadata_metadata_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
+var file_metadata_metadata_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
+var file_metadata_metadata_proto_goTypes = []interface{}{
+       (SourceType)(0),             // 0: metadata.SourceType
+       (EncryptionOptions_Mode)(0), // 1: metadata.EncryptionOptions.Mode
+       (*HashingCosts)(nil),        // 2: metadata.HashingCosts
+       (*WrappedKeyData)(nil),      // 3: metadata.WrappedKeyData
+       (*ProtectorData)(nil),       // 4: metadata.ProtectorData
+       (*EncryptionOptions)(nil),   // 5: metadata.EncryptionOptions
+       (*WrappedPolicyKey)(nil),    // 6: metadata.WrappedPolicyKey
+       (*PolicyData)(nil),          // 7: metadata.PolicyData
+       (*Config)(nil),              // 8: metadata.Config
+}
+var file_metadata_metadata_proto_depIdxs = []int32{
+       0,  // 0: metadata.ProtectorData.source:type_name -> metadata.SourceType
+       2,  // 1: metadata.ProtectorData.costs:type_name -> metadata.HashingCosts
+       3,  // 2: metadata.ProtectorData.wrapped_key:type_name -> metadata.WrappedKeyData
+       1,  // 3: metadata.EncryptionOptions.contents:type_name -> metadata.EncryptionOptions.Mode
+       1,  // 4: metadata.EncryptionOptions.filenames:type_name -> metadata.EncryptionOptions.Mode
+       3,  // 5: metadata.WrappedPolicyKey.wrapped_key:type_name -> metadata.WrappedKeyData
+       5,  // 6: metadata.PolicyData.options:type_name -> metadata.EncryptionOptions
+       6,  // 7: metadata.PolicyData.wrapped_policy_keys:type_name -> metadata.WrappedPolicyKey
+       0,  // 8: metadata.Config.source:type_name -> metadata.SourceType
+       2,  // 9: metadata.Config.hash_costs:type_name -> metadata.HashingCosts
+       5,  // 10: metadata.Config.options:type_name -> metadata.EncryptionOptions
+       11, // [11:11] is the sub-list for method output_type
+       11, // [11:11] is the sub-list for method input_type
+       11, // [11:11] is the sub-list for extension type_name
+       11, // [11:11] is the sub-list for extension extendee
+       0,  // [0:11] is the sub-list for field type_name
+}
+
+func init() { file_metadata_metadata_proto_init() }
+func file_metadata_metadata_proto_init() {
+       if File_metadata_metadata_proto != nil {
+               return
+       }
+       if !protoimpl.UnsafeEnabled {
+               file_metadata_metadata_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+                       switch v := v.(*HashingCosts); i {
+                       case 0:
+                               return &v.state
+                       case 1:
+                               return &v.sizeCache
+                       case 2:
+                               return &v.unknownFields
+                       default:
+                               return nil
+                       }
+               }
+               file_metadata_metadata_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+                       switch v := v.(*WrappedKeyData); i {
+                       case 0:
+                               return &v.state
+                       case 1:
+                               return &v.sizeCache
+                       case 2:
+                               return &v.unknownFields
+                       default:
+                               return nil
+                       }
+               }
+               file_metadata_metadata_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+                       switch v := v.(*ProtectorData); i {
+                       case 0:
+                               return &v.state
+                       case 1:
+                               return &v.sizeCache
+                       case 2:
+                               return &v.unknownFields
+                       default:
+                               return nil
+                       }
+               }
+               file_metadata_metadata_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+                       switch v := v.(*EncryptionOptions); i {
+                       case 0:
+                               return &v.state
+                       case 1:
+                               return &v.sizeCache
+                       case 2:
+                               return &v.unknownFields
+                       default:
+                               return nil
+                       }
+               }
+               file_metadata_metadata_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+                       switch v := v.(*WrappedPolicyKey); i {
+                       case 0:
+                               return &v.state
+                       case 1:
+                               return &v.sizeCache
+                       case 2:
+                               return &v.unknownFields
+                       default:
+                               return nil
+                       }
+               }
+               file_metadata_metadata_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+                       switch v := v.(*PolicyData); i {
+                       case 0:
+                               return &v.state
+                       case 1:
+                               return &v.sizeCache
+                       case 2:
+                               return &v.unknownFields
+                       default:
+                               return nil
+                       }
+               }
+               file_metadata_metadata_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+                       switch v := v.(*Config); i {
+                       case 0:
+                               return &v.state
+                       case 1:
+                               return &v.sizeCache
+                       case 2:
+                               return &v.unknownFields
+                       default:
+                               return nil
+                       }
+               }
+       }
+       type x struct{}
+       out := protoimpl.TypeBuilder{
+               File: protoimpl.DescBuilder{
+                       GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+                       RawDescriptor: file_metadata_metadata_proto_rawDesc,
+                       NumEnums:      2,
+                       NumMessages:   7,
+                       NumExtensions: 0,
+                       NumServices:   0,
+               },
+               GoTypes:           file_metadata_metadata_proto_goTypes,
+               DependencyIndexes: file_metadata_metadata_proto_depIdxs,
+               EnumInfos:         file_metadata_metadata_proto_enumTypes,
+               MessageInfos:      file_metadata_metadata_proto_msgTypes,
+       }.Build()
+       File_metadata_metadata_proto = out.File
+       file_metadata_metadata_proto_rawDesc = nil
+       file_metadata_metadata_proto_goTypes = nil
+       file_metadata_metadata_proto_depIdxs = nil
+}
diff --git a/metadata/metadata.proto b/metadata/metadata.proto
new file mode 100644 (file)
index 0000000..f2dd78f
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * metadata.proto - File which contains all of the metadata structures which we
+ * write to metadata files. Must be compiled with protoc to use the library.
+ * Compilation can be invoked with go generate.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+// If the *.proto file is modified, be sure to run "make gen" (at the project
+// root) to recreate the *.pb.go file.
+syntax = "proto3";
+package metadata;
+
+option go_package = "github.com/google/fscrypt/metadata";
+
+// Cost parameters to be used in our hashing functions.
+message HashingCosts {
+  int64 time = 2;
+  int64 memory = 3;
+  int64 parallelism = 4;
+  // If true, parallelism should no longer be truncated to 8 bits.
+  bool truncation_fixed = 5;
+}
+
+// This structure is used for our authenticated wrapping/unwrapping of keys.
+message WrappedKeyData {
+  bytes IV = 1;
+  bytes encrypted_key = 2;
+  bytes hmac = 3;
+}
+
+// Specifies the method in which an outside secret is obtained for a Protector
+enum SourceType {
+  default = 0;
+  pam_passphrase = 1;
+  custom_passphrase = 2;
+  raw_key = 3;
+}
+
+// The associated data for each protector
+message ProtectorData {
+  string protector_descriptor = 1;
+  SourceType source = 2;
+
+  // These are only used by some of the protector types
+  string name = 3;
+  HashingCosts costs = 4;
+  bytes salt = 5;
+  int64 uid = 6;
+
+  WrappedKeyData wrapped_key = 7;
+}
+
+// Encryption policy specifics, corresponds to the fscrypt_policy struct
+message EncryptionOptions {
+  int64 padding = 1;
+
+  // Type of encryption; should match declarations of unix.FSCRYPT_MODE
+  enum Mode {
+    default = 0;
+    AES_256_XTS = 1;
+    AES_256_GCM = 2;
+    AES_256_CBC = 3;
+    AES_256_CTS = 4;
+    AES_128_CBC = 5;
+    AES_128_CTS = 6;
+    Adiantum = 9;
+    AES_256_HCTR2 = 10;
+  }
+
+  Mode contents = 2;
+  Mode filenames = 3;
+
+  int64 policy_version = 4;
+}
+
+message WrappedPolicyKey {
+  string protector_descriptor = 1;
+  WrappedKeyData wrapped_key = 2;
+}
+
+// The associated data for each policy
+message PolicyData {
+  string key_descriptor = 1;
+  EncryptionOptions options = 2;
+  repeated WrappedPolicyKey wrapped_policy_keys = 3;
+}
+
+// Data stored in the config file
+message Config {
+  SourceType source = 1;
+  HashingCosts hash_costs = 2;
+  EncryptionOptions options = 4;
+  bool use_fs_keyring_for_v1_policies = 5;
+  bool allow_cross_user_metadata = 6;
+
+  // reserve the removed field 'string compatibility = 3;'
+  reserved 3;
+  reserved "compatibility";
+}
diff --git a/metadata/policy.go b/metadata/policy.go
new file mode 100644 (file)
index 0000000..fe6c38f
--- /dev/null
@@ -0,0 +1,374 @@
+/*
+ * policy.go - Functions for getting and setting policies on a specified
+ * directory or file.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package metadata
+
+import (
+       "encoding/hex"
+       "fmt"
+       "log"
+       "math"
+       "os"
+       "os/user"
+       "strconv"
+       "syscall"
+       "unsafe"
+
+       "github.com/pkg/errors"
+       "golang.org/x/sys/unix"
+
+       "github.com/google/fscrypt/util"
+)
+
+var (
+       // ErrEncryptionNotSupported indicates that encryption is not supported
+       // on the given filesystem, and there is no way to enable it.
+       ErrEncryptionNotSupported = errors.New("encryption not supported")
+
+       // ErrEncryptionNotEnabled indicates that encryption is not supported on
+       // the given filesystem, but there is a way to enable it.
+       ErrEncryptionNotEnabled = errors.New("encryption not enabled")
+)
+
+// ErrAlreadyEncrypted indicates that the path is already encrypted.
+type ErrAlreadyEncrypted struct {
+       Path string
+}
+
+func (err *ErrAlreadyEncrypted) Error() string {
+       return fmt.Sprintf("file or directory %q is already encrypted", err.Path)
+}
+
+// ErrBadEncryptionOptions indicates that unsupported encryption options were given.
+type ErrBadEncryptionOptions struct {
+       Path    string
+       Options *EncryptionOptions
+}
+
+func (err *ErrBadEncryptionOptions) Error() string {
+       return fmt.Sprintf(`cannot encrypt %q because the kernel doesn't support the requested encryption options.
+
+       The options are %s`, err.Path, err.Options)
+}
+
+// ErrDirectoryNotOwned indicates a directory can't be encrypted because it's
+// owned by another user.
+type ErrDirectoryNotOwned struct {
+       Path  string
+       Owner uint32
+}
+
+func (err *ErrDirectoryNotOwned) Error() string {
+       owner := strconv.Itoa(int(err.Owner))
+       if u, e := user.LookupId(owner); e == nil && u.Username != "" {
+               owner = u.Username
+       }
+       return fmt.Sprintf(`cannot encrypt %q because it's owned by another user (%s).
+
+       Encryption can only be enabled on a directory you own, even if you have
+       write access to the directory.`, err.Path, owner)
+}
+
+// ErrLockedRegularFile indicates that the path is a locked regular file.
+type ErrLockedRegularFile struct {
+       Path string
+}
+
+func (err *ErrLockedRegularFile) Error() string {
+       return fmt.Sprintf("cannot operate on locked regular file %q", err.Path)
+}
+
+// ErrNotEncrypted indicates that the path is not encrypted.
+type ErrNotEncrypted struct {
+       Path string
+}
+
+func (err *ErrNotEncrypted) Error() string {
+       return fmt.Sprintf("file or directory %q is not encrypted", err.Path)
+}
+
+func getPolicyIoctl(file *os.File, request uintptr, arg unsafe.Pointer) error {
+       _, _, errno := unix.Syscall(unix.SYS_IOCTL, file.Fd(), request, uintptr(arg))
+       if errno == 0 {
+               return nil
+       }
+       return errno
+}
+
+func setPolicy(file *os.File, arg unsafe.Pointer) error {
+       _, _, errno := unix.Syscall(unix.SYS_IOCTL, file.Fd(), unix.FS_IOC_SET_ENCRYPTION_POLICY, uintptr(arg))
+       if errno != 0 {
+               return errno
+       }
+
+       if err := file.Sync(); err != nil {
+               return err
+       }
+
+       return nil
+}
+
+// Maps EncryptionOptions.Padding <-> FSCRYPT_POLICY_FLAGS
+var (
+       paddingArray = []int64{4, 8, 16, 32}
+       flagsArray   = []int64{unix.FSCRYPT_POLICY_FLAGS_PAD_4, unix.FSCRYPT_POLICY_FLAGS_PAD_8,
+               unix.FSCRYPT_POLICY_FLAGS_PAD_16, unix.FSCRYPT_POLICY_FLAGS_PAD_32}
+)
+
+// flagsToPadding returns the amount of padding specified in the policy flags.
+func flagsToPadding(flags uint8) int64 {
+       paddingFlag := int64(flags & unix.FS_POLICY_FLAGS_PAD_MASK)
+
+       // This lookup should always succeed
+       padding, ok := util.Lookup(paddingFlag, flagsArray, paddingArray)
+       if !ok {
+               log.Panicf("padding flag of %x not found", paddingFlag)
+       }
+       return padding
+}
+
+func buildV1PolicyData(policy *unix.FscryptPolicyV1) *PolicyData {
+       return &PolicyData{
+               KeyDescriptor: hex.EncodeToString(policy.Master_key_descriptor[:]),
+               Options: &EncryptionOptions{
+                       Padding:       flagsToPadding(policy.Flags),
+                       Contents:      EncryptionOptions_Mode(policy.Contents_encryption_mode),
+                       Filenames:     EncryptionOptions_Mode(policy.Filenames_encryption_mode),
+                       PolicyVersion: 1,
+               },
+       }
+}
+
+func buildV2PolicyData(policy *unix.FscryptPolicyV2) *PolicyData {
+       return &PolicyData{
+               KeyDescriptor: hex.EncodeToString(policy.Master_key_identifier[:]),
+               Options: &EncryptionOptions{
+                       Padding:       flagsToPadding(policy.Flags),
+                       Contents:      EncryptionOptions_Mode(policy.Contents_encryption_mode),
+                       Filenames:     EncryptionOptions_Mode(policy.Filenames_encryption_mode),
+                       PolicyVersion: 2,
+               },
+       }
+}
+
+// GetPolicy returns the Policy data for the given directory or file (includes
+// the KeyDescriptor and the encryption options). Returns an error if the
+// path is not encrypted or the policy couldn't be retrieved.
+func GetPolicy(path string) (*PolicyData, error) {
+       file, err := os.Open(path)
+       if err != nil {
+               if err.(*os.PathError).Err == syscall.ENOKEY {
+                       return nil, &ErrLockedRegularFile{path}
+               }
+               return nil, err
+       }
+       defer file.Close()
+
+       // First try the new version of the ioctl. This works for both v1 and v2 policies.
+       var arg unix.FscryptGetPolicyExArg
+       arg.Size = uint64(unsafe.Sizeof(arg.Policy))
+       policyPtr := util.Ptr(arg.Policy[:])
+       err = getPolicyIoctl(file, unix.FS_IOC_GET_ENCRYPTION_POLICY_EX, unsafe.Pointer(&arg))
+       if err == unix.ENOTTY {
+               // Fall back to the old version of the ioctl. This works for v1 policies only.
+               err = getPolicyIoctl(file, unix.FS_IOC_GET_ENCRYPTION_POLICY, policyPtr)
+               arg.Size = uint64(unsafe.Sizeof(unix.FscryptPolicyV1{}))
+       }
+       switch err {
+       case nil:
+               break
+       case unix.ENOTTY:
+               return nil, ErrEncryptionNotSupported
+       case unix.EOPNOTSUPP:
+               return nil, ErrEncryptionNotEnabled
+       case unix.ENODATA, unix.ENOENT:
+               // ENOENT was returned instead of ENODATA on some filesystems before v4.11.
+               return nil, &ErrNotEncrypted{path}
+       default:
+               return nil, errors.Wrapf(err, "failed to get encryption policy of %q", path)
+       }
+       switch arg.Policy[0] { // arg.policy.version
+       case unix.FSCRYPT_POLICY_V1:
+               if arg.Size != uint64(unsafe.Sizeof(unix.FscryptPolicyV1{})) {
+                       // should never happen
+                       return nil, errors.New("unexpected size for v1 policy")
+               }
+               return buildV1PolicyData((*unix.FscryptPolicyV1)(policyPtr)), nil
+       case unix.FSCRYPT_POLICY_V2:
+               if arg.Size != uint64(unsafe.Sizeof(unix.FscryptPolicyV2{})) {
+                       // should never happen
+                       return nil, errors.New("unexpected size for v2 policy")
+               }
+               return buildV2PolicyData((*unix.FscryptPolicyV2)(policyPtr)), nil
+       default:
+               return nil, errors.Errorf("unsupported encryption policy version [%d]",
+                       arg.Policy[0])
+       }
+}
+
+// For improved performance, use the DIRECT_KEY flag when using ciphers that
+// support it, e.g. Adiantum.  It is safe because fscrypt won't reuse the key
+// for any other policy.  (Multiple directories with same policy are okay.)
+func shouldUseDirectKeyFlag(options *EncryptionOptions) bool {
+       // Contents and filenames encryption modes must be the same
+       if options.Contents != options.Filenames {
+               return false
+       }
+       // Currently only Adiantum supports DIRECT_KEY.
+       return options.Contents == EncryptionOptions_Adiantum
+}
+
+func buildPolicyFlags(options *EncryptionOptions) uint8 {
+       // This lookup should always succeed (as policy is valid)
+       flags, ok := util.Lookup(options.Padding, paddingArray, flagsArray)
+       if !ok {
+               log.Panicf("padding of %d was not found", options.Padding)
+       }
+       if shouldUseDirectKeyFlag(options) {
+               flags |= unix.FSCRYPT_POLICY_FLAG_DIRECT_KEY
+       }
+       return uint8(flags)
+}
+
+func setV1Policy(file *os.File, options *EncryptionOptions, descriptorBytes []byte) error {
+       policy := unix.FscryptPolicyV1{
+               Version:                   unix.FSCRYPT_POLICY_V1,
+               Contents_encryption_mode:  uint8(options.Contents),
+               Filenames_encryption_mode: uint8(options.Filenames),
+               Flags:                     uint8(buildPolicyFlags(options)),
+       }
+
+       // The descriptor should always be the correct length (as policy is valid)
+       if len(descriptorBytes) != unix.FSCRYPT_KEY_DESCRIPTOR_SIZE {
+               log.Panic("wrong descriptor size for v1 policy")
+       }
+       copy(policy.Master_key_descriptor[:], descriptorBytes)
+
+       return setPolicy(file, unsafe.Pointer(&policy))
+}
+
+func setV2Policy(file *os.File, options *EncryptionOptions, descriptorBytes []byte) error {
+       policy := unix.FscryptPolicyV2{
+               Version:                   unix.FSCRYPT_POLICY_V2,
+               Contents_encryption_mode:  uint8(options.Contents),
+               Filenames_encryption_mode: uint8(options.Filenames),
+               Flags:                     uint8(buildPolicyFlags(options)),
+       }
+
+       // The descriptor should always be the correct length (as policy is valid)
+       if len(descriptorBytes) != unix.FSCRYPT_KEY_IDENTIFIER_SIZE {
+               log.Panic("wrong descriptor size for v2 policy")
+       }
+       copy(policy.Master_key_identifier[:], descriptorBytes)
+
+       return setPolicy(file, unsafe.Pointer(&policy))
+}
+
+// SetPolicy sets up the specified directory to be encrypted with the specified
+// policy. Returns an error if we cannot set the policy for any reason (not a
+// directory, invalid options or KeyDescriptor, etc).
+func SetPolicy(path string, data *PolicyData) error {
+       file, err := os.Open(path)
+       if err != nil {
+               return err
+       }
+       defer file.Close()
+
+       if err = data.CheckValidity(); err != nil {
+               return errors.Wrap(err, "invalid policy")
+       }
+
+       descriptorBytes, err := hex.DecodeString(data.KeyDescriptor)
+       if err != nil {
+               return errors.New("invalid key descriptor: " + data.KeyDescriptor)
+       }
+
+       switch data.Options.PolicyVersion {
+       case 1:
+               err = setV1Policy(file, data.Options, descriptorBytes)
+       case 2:
+               err = setV2Policy(file, data.Options, descriptorBytes)
+       default:
+               err = errors.Errorf("policy version of %d is invalid", data.Options.PolicyVersion)
+       }
+       if err == unix.EINVAL {
+               // Before kernel v4.11, many different errors all caused unix.EINVAL to be returned.
+               // We try to disambiguate this error here. This disambiguation will not always give
+               // the correct error due to a potential race condition on path.
+               if info, statErr := os.Stat(path); statErr != nil || !info.IsDir() {
+                       // Checking if the path is not a directory
+                       err = unix.ENOTDIR
+               } else if _, policyErr := GetPolicy(path); policyErr == nil {
+                       // Checking if a policy is already set on this directory
+                       err = unix.EEXIST
+               }
+       }
+       switch err {
+       case nil:
+               return nil
+       case unix.EACCES:
+               var stat unix.Stat_t
+               if statErr := unix.Stat(path, &stat); statErr == nil && stat.Uid != uint32(os.Geteuid()) {
+                       return &ErrDirectoryNotOwned{path, stat.Uid}
+               }
+       case unix.EEXIST:
+               return &ErrAlreadyEncrypted{path}
+       case unix.EINVAL:
+               return &ErrBadEncryptionOptions{path, data.Options}
+       case unix.ENOTTY:
+               return ErrEncryptionNotSupported
+       case unix.EOPNOTSUPP:
+               return ErrEncryptionNotEnabled
+       }
+       return errors.Wrapf(err, "failed to set encryption policy on %q", path)
+}
+
+// CheckSupport returns an error if the filesystem containing path does not
+// support filesystem encryption. This can be for many reasons including an
+// incompatible kernel or filesystem or not enabling the right feature flags.
+func CheckSupport(path string) error {
+       file, err := os.Open(path)
+       if err != nil {
+               return err
+       }
+       defer file.Close()
+
+       // On supported directories, giving a bad policy will return EINVAL
+       badPolicy := unix.FscryptPolicyV1{
+               Version:                   math.MaxUint8,
+               Contents_encryption_mode:  math.MaxUint8,
+               Filenames_encryption_mode: math.MaxUint8,
+               Flags:                     math.MaxUint8,
+       }
+
+       err = setPolicy(file, unsafe.Pointer(&badPolicy))
+       switch err {
+       case nil:
+               log.Panicf(`FS_IOC_SET_ENCRYPTION_POLICY succeeded when it should have failed.
+                       Please open an issue, filesystem %q may be corrupted.`, path)
+       case unix.EINVAL, unix.EACCES:
+               return nil
+       case unix.ENOTTY:
+               return ErrEncryptionNotSupported
+       case unix.EOPNOTSUPP:
+               return ErrEncryptionNotEnabled
+       }
+       return errors.Wrapf(err, "unexpected error checking for encryption support on filesystem %q", path)
+}
diff --git a/metadata/policy_test.go b/metadata/policy_test.go
new file mode 100644 (file)
index 0000000..7856ed3
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * policy_test.go - Tests the getting/setting of encryption policies
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package metadata
+
+import (
+       "fmt"
+       "os"
+       "path/filepath"
+       "testing"
+
+       "golang.org/x/sys/unix"
+       "google.golang.org/protobuf/proto"
+
+       "github.com/google/fscrypt/util"
+)
+
+const goodV1Descriptor = "0123456789abcdef"
+
+var goodV1Policy = &PolicyData{
+       KeyDescriptor: goodV1Descriptor,
+       Options:       DefaultOptions,
+}
+
+var goodV2EncryptionOptions = &EncryptionOptions{
+       Padding:       32,
+       Contents:      EncryptionOptions_AES_256_XTS,
+       Filenames:     EncryptionOptions_AES_256_CTS,
+       PolicyVersion: 2,
+}
+
+var goodV2Policy = &PolicyData{
+       KeyDescriptor: "0123456789abcdef0123456789abcdef",
+       Options:       goodV2EncryptionOptions,
+}
+
+// Creates a temporary directory for testing.
+func createTestDirectory(t *testing.T) (directory string, err error) {
+       baseDirectory, err := util.TestRoot()
+       if err != nil {
+               t.Skip(err)
+       }
+       if s, err := os.Stat(baseDirectory); err != nil || !s.IsDir() {
+               return "", fmt.Errorf("test directory %q is not valid", baseDirectory)
+       }
+
+       directoryPath := filepath.Join(baseDirectory, "test")
+       return directoryPath, os.MkdirAll(directoryPath, os.ModePerm)
+}
+
+// Makes a test directory, makes a file in the directory, and fills the file
+// with data. Returns the directory name, file name, and error (if one).
+func createTestFile(t *testing.T) (directory, file string, err error) {
+       if directory, err = createTestDirectory(t); err != nil {
+               return
+       }
+       // Cleanup if the file creation fails
+       defer func() {
+               if err != nil {
+                       os.RemoveAll(directory)
+               }
+       }()
+
+       filePath := filepath.Join(directory, "test.txt")
+       fileHandle, err := os.Create(filePath)
+       if err != nil {
+               return directory, filePath, err
+       }
+       defer fileHandle.Close()
+
+       _, err = fileHandle.Write([]byte("Here is some test data!\n"))
+       return directory, filePath, err
+}
+
+// Tests that we can set a policy on an empty directory
+func TestSetPolicyEmptyDirectory(t *testing.T) {
+       directory, err := createTestDirectory(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(directory)
+
+       if err = SetPolicy(directory, goodV1Policy); err != nil {
+               t.Error(err)
+       }
+}
+
+// Tests that we cannot set a policy on a nonempty directory
+func TestSetPolicyNonemptyDirectory(t *testing.T) {
+       directory, _, err := createTestFile(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(directory)
+
+       if err = SetPolicy(directory, goodV1Policy); err == nil {
+               t.Error("should have failed to set policy on a nonempty directory")
+       }
+}
+
+// Tests that we cannot set a policy on a file
+func TestSetPolicyFile(t *testing.T) {
+       directory, file, err := createTestFile(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(directory)
+
+       if err = SetPolicy(file, goodV1Policy); err == nil {
+               t.Error("should have failed to set policy on a file")
+       }
+}
+
+// Tests that we fail when using bad policies
+func TestSetPolicyBadDescriptors(t *testing.T) {
+       // Policies that are too short, have invalid chars, or are too long
+       badDescriptors := []string{"123456789abcde", "xxxxxxxxxxxxxxxx", "0123456789abcdef00"}
+       for _, badDescriptor := range badDescriptors {
+               badPolicy := &PolicyData{KeyDescriptor: badDescriptor, Options: DefaultOptions}
+               directory, err := createTestDirectory(t)
+               if err != nil {
+                       t.Fatal(err)
+               }
+
+               if err = SetPolicy(directory, badPolicy); err == nil {
+                       t.Errorf("descriptor %q should have made SetPolicy fail", badDescriptor)
+               }
+               os.RemoveAll(directory)
+       }
+}
+
+// Tests that we get back the same policy that we set on a directory
+func TestGetPolicyEmptyDirectory(t *testing.T) {
+       directory, err := createTestDirectory(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(directory)
+
+       var actualPolicy *PolicyData
+       if err = SetPolicy(directory, goodV1Policy); err != nil {
+               t.Fatal(err)
+       }
+       if actualPolicy, err = GetPolicy(directory); err != nil {
+               t.Fatal(err)
+       }
+
+       if !proto.Equal(actualPolicy, goodV1Policy) {
+               t.Errorf("policy %+v does not equal expected policy %+v", actualPolicy, goodV1Policy)
+       }
+}
+
+// Tests that we cannot get a policy on an unencrypted directory
+func TestGetPolicyUnencrypted(t *testing.T) {
+       directory, err := createTestDirectory(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(directory)
+
+       if _, err = GetPolicy(directory); err == nil {
+               t.Error("should have failed to set policy on a file")
+       }
+}
+
+func requireV2PolicySupport(t *testing.T, directory string) {
+       file, err := os.Open(directory)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer file.Close()
+
+       err = getPolicyIoctl(file, unix.FS_IOC_GET_ENCRYPTION_POLICY_EX, nil)
+       if err == ErrEncryptionNotSupported {
+               t.Skip("No support for v2 encryption policies, skipping test")
+       }
+}
+
+// Tests that a non-root user cannot set a v2 encryption policy unless the key
+// has been added.
+func TestSetV2PolicyNoKey(t *testing.T) {
+       if util.IsUserRoot() {
+               t.Skip("This test cannot be run as root")
+       }
+       directory, err := createTestDirectory(t)
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(directory)
+       requireV2PolicySupport(t, directory)
+
+       err = SetPolicy(directory, goodV2Policy)
+       if err == nil {
+               t.Error("shouldn't have been able to set v2 policy without key added")
+       }
+}
diff --git a/pam/constants.go b/pam/constants.go
new file mode 100644 (file)
index 0000000..d2d0cf3
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * constants.go - PAM flags and item types from github.com/msteinert/pam
+ *
+ * Modifications Copyright 2017 Google Inc.
+ * Modifications Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+/*
+ * Copyright 2011, krockot
+ * Copyright 2015, Michael Steinert <mike.steinert@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ *   list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package pam
+
+/*
+#cgo LDFLAGS: -lpam
+
+#include <security/pam_modules.h>
+*/
+import "C"
+
+// Item is a PAM information type.
+type Item int
+
+// PAM Item types.
+const (
+       // Service is the name which identifies the PAM stack.
+       Service Item = C.PAM_SERVICE
+       // User identifies the username identity used by a service.
+       User = C.PAM_USER
+       // Tty is the terminal name.
+       Tty = C.PAM_TTY
+       // Rhost is the requesting host name.
+       Rhost = C.PAM_RHOST
+       // Authtok is the currently active authentication token.
+       Authtok = C.PAM_AUTHTOK
+       // Oldauthtok is the old authentication token.
+       Oldauthtok = C.PAM_OLDAUTHTOK
+       // Ruser is the requesting user name.
+       Ruser = C.PAM_RUSER
+       // UserPrompt is the string use to prompt for a username.
+       UserPrompt = C.PAM_USER_PROMPT
+)
+
+// Flag is used as input to various PAM functions. Flags can be combined with a
+// bitwise or. Refer to the official PAM documentation for which flags are
+// accepted by which functions.
+type Flag int
+
+// PAM Flag types.
+const (
+       // Silent indicates that no messages should be emitted.
+       Silent Flag = C.PAM_SILENT
+       // DisallowNullAuthtok indicates that authorization should fail
+       // if the user does not have a registered authentication token.
+       DisallowNullAuthtok = C.PAM_DISALLOW_NULL_AUTHTOK
+       // EstablishCred indicates that credentials should be established
+       // for the user.
+       EstablishCred = C.PAM_ESTABLISH_CRED
+       // DeleteCred inidicates that credentials should be deleted.
+       DeleteCred = C.PAM_DELETE_CRED
+       // ReinitializeCred indicates that credentials should be fully
+       // reinitialized.
+       ReinitializeCred = C.PAM_REINITIALIZE_CRED
+       // RefreshCred indicates that the lifetime of existing credentials
+       // should be extended.
+       RefreshCred = C.PAM_REFRESH_CRED
+       // ChangeExpiredAuthtok indicates that the authentication token
+       // should be changed if it has expired.
+       ChangeExpiredAuthtok = C.PAM_CHANGE_EXPIRED_AUTHTOK
+       // PrelimCheck indicates that the modules are being probed as to their
+       // ready status for altering the user's authentication token.
+       PrelimCheck = C.PAM_PRELIM_CHECK
+       // UpdateAuthtok informs the module that this is the call it should
+       // change the authorization tokens.
+       UpdateAuthtok = C.PAM_UPDATE_AUTHTOK
+)
diff --git a/pam/login.go b/pam/login.go
new file mode 100644 (file)
index 0000000..e4f8f83
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * login.go - Checks the validity of a login token key against PAM.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+// Package pam contains all the functionality for interfacing with Linux
+// Pluggable Authentication Modules (PAM). Currently, all this package does is
+// check the validity of a user's login passphrase.
+// See http://www.linux-pam.org/Linux-PAM-html/ for more information.
+package pam
+
+import "C"
+
+import (
+       "fmt"
+       "log"
+       "sync"
+
+       "github.com/pkg/errors"
+
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/util"
+)
+
+// Pam error values
+var (
+       ErrPassphrase = errors.New("incorrect login passphrase")
+)
+
+// Global state is needed for the PAM callback, so we guard this function with a
+// lock. tokenToCheck is only ever non-nil when tokenLock is held.
+var (
+       tokenLock    sync.Mutex
+       tokenToCheck *crypto.Key
+)
+
+// userInput is run when the callback needs some input from the user. We prompt
+// the user for information and return their answer. A return value of nil
+// indicates an error occurred.
+//
+//export userInput
+func userInput(prompt *C.char) *C.char {
+       fmt.Print(C.GoString(prompt))
+       input, err := util.ReadLine()
+       if err != nil {
+               log.Printf("getting input for PAM: %s", err)
+               return nil
+       }
+       return C.CString(input)
+}
+
+// passphraseInput is run when the callback needs a passphrase from the user. We
+// pass along the tokenToCheck without prompting. A return value of nil
+// indicates an error occurred.
+//
+//export passphraseInput
+func passphraseInput(prompt *C.char) *C.char {
+       log.Printf("getting secret data for PAM: %q", C.GoString(prompt))
+       if tokenToCheck == nil {
+               log.Print("secret data requested multiple times")
+               return nil
+       }
+
+       // Subsequent calls to passphrase input should fail
+       input := (*C.char)(tokenToCheck.UnsafeToCString())
+       tokenToCheck = nil
+       return input
+}
+
+// IsUserLoginToken returns nil if the presented token is the user's login key,
+// and returns an error otherwise. Note that unless we are currently running as
+// root, this check will only work for the user running this process.
+func IsUserLoginToken(username string, token *crypto.Key, quiet bool) error {
+       log.Printf("Checking login token for %s", username)
+
+       // We require global state for the function. This function never takes
+       // ownership of the token, so it is not responsible for wiping it.
+       tokenLock.Lock()
+       tokenToCheck = token
+       defer func() {
+               tokenToCheck = nil
+               tokenLock.Unlock()
+       }()
+
+       transaction, err := Start("fscrypt", username)
+       if err != nil {
+               return err
+       }
+       defer transaction.End()
+
+       // Ask PAM to authenticate the token.
+       authenticated, err := transaction.Authenticate(quiet)
+       if err != nil {
+               return err
+       }
+
+       if !authenticated {
+               return ErrPassphrase
+       }
+       return nil
+}
diff --git a/pam/pam.c b/pam/pam.c
new file mode 100644 (file)
index 0000000..1d6aefe
--- /dev/null
+++ b/pam/pam.c
@@ -0,0 +1,114 @@
+/*
+ * pam.c - Functions to let us call into libpam from Go.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+#include "pam.h"
+
+#include <security/pam_appl.h>
+#include <security/pam_ext.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>  // mlock/munlock
+
+#include "_cgo_export.h"  // for input callbacks
+
+static int conversation(int num_msg, const struct pam_message** msg,
+                        struct pam_response** resp, void* appdata_ptr) {
+  if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) {
+    return PAM_CONV_ERR;
+  }
+
+  // Allocate the response table with num_msg entries.
+  *resp = calloc(num_msg, sizeof **resp);
+  if (!*resp) {
+    return PAM_BUF_ERR;
+  }
+
+  // Check each message to see if we need to run a callback.
+  char* callback_msg = NULL;
+  char* callback_resp = NULL;
+  int i;
+  for (i = 0; i < num_msg; ++i) {
+    callback_msg = (char*)msg[i]->msg;
+
+    // We run our input callback if the style tells us we need data. Otherwise,
+    // we just print the error messages or text info to standard output.
+    switch (msg[i]->msg_style) {
+      case PAM_PROMPT_ECHO_OFF:
+        callback_resp = passphraseInput(callback_msg);
+        break;
+      case PAM_PROMPT_ECHO_ON:
+        callback_resp = userInput(callback_msg);
+        break;
+      case PAM_ERROR_MSG:
+      case PAM_TEXT_INFO:
+        fprintf(stderr, "%s\n", callback_msg);
+        continue;
+    }
+
+    if (!callback_resp) {
+      // If the callback failed, free each nonempty response in the response
+      // table and the response table itself.
+      while (--i >= 0) {
+        free((*resp)[i].resp);
+      }
+      free(*resp);
+      *resp = NULL;
+      return PAM_CONV_ERR;
+    }
+
+    (*resp)[i].resp = callback_resp;
+  }
+
+  return PAM_SUCCESS;
+}
+
+static const struct pam_conv conv = {conversation, NULL};
+const struct pam_conv* goConv = &conv;
+
+void freeData(pam_handle_t* pamh, void* data, int error_status) { free(data); }
+
+void freeArray(pam_handle_t* pamh, void** array, int error_status) {
+  int i;
+  for (i = 0; array[i]; ++i) {
+    free(array[i]);
+  }
+  free(array);
+}
+
+void* copyIntoSecret(void* data) {
+  size_t size = strlen(data) + 1;  // include null terminator
+  void* copy = calloc(1, size);    // initialize to avoid a compiler warning
+  mlock(copy, size);
+  memcpy(copy, data, size);
+  return copy;
+}
+
+void freeSecret(pam_handle_t* pamh, char* data, int error_status) {
+  size_t size = strlen(data) + 1;  // Include null terminator
+  // Use volatile function pointer to actually clear the memory.
+  static void* (*const volatile memset_sec)(void*, int, size_t) = &memset;
+  memset_sec(data, 0, size);
+  munlock(data, size);
+  free(data);
+}
+
+void infoMessage(pam_handle_t* pamh, const char* message) {
+  pam_info(pamh, "%s", message);
+}
diff --git a/pam/pam.go b/pam/pam.go
new file mode 100644 (file)
index 0000000..ea1c34e
--- /dev/null
@@ -0,0 +1,226 @@
+/*
+ * pam.go - Utility functions for interfacing with the PAM libraries.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package pam
+
+/*
+#cgo LDFLAGS: -lpam
+#include "pam.h"
+
+#include <pwd.h>
+#include <stdlib.h>
+#include <security/pam_modules.h>
+*/
+import "C"
+import (
+       "errors"
+       "log"
+       "os/user"
+       "unsafe"
+
+       "github.com/google/fscrypt/security"
+)
+
+// Handle wraps the C pam_handle_t type. This is used from within modules.
+type Handle struct {
+       handle    *C.pam_handle_t
+       status    C.int
+       origPrivs *security.Privileges
+       // PamUser is the user for whom the PAM module is running.
+       PamUser *user.User
+}
+
+// NewHandle creates a Handle from a raw pointer.
+func NewHandle(pamh unsafe.Pointer) (*Handle, error) {
+       var err error
+       h := &Handle{
+               handle: (*C.pam_handle_t)(pamh),
+               status: C.PAM_SUCCESS,
+       }
+
+       var pamUsername *C.char
+       h.status = C.pam_get_user(h.handle, &pamUsername, nil)
+       if err = h.err(); err != nil {
+               return nil, err
+       }
+
+       h.PamUser, err = user.Lookup(C.GoString(pamUsername))
+       return h, err
+}
+
+func (h *Handle) setData(name string, data unsafe.Pointer, cleanup C.CleanupFunc) error {
+       cName := C.CString(name)
+       defer C.free(unsafe.Pointer(cName))
+       h.status = C.pam_set_data(h.handle, cName, data, cleanup)
+       return h.err()
+}
+
+func (h *Handle) getData(name string) (unsafe.Pointer, error) {
+       var data unsafe.Pointer
+       cName := C.CString(name)
+       defer C.free(unsafe.Pointer(cName))
+       h.status = C.pam_get_data(h.handle, cName, &data)
+       return data, h.err()
+}
+
+// ClearData removes the PAM data with the specified name.
+func (h *Handle) ClearData(name string) error {
+       return h.setData(name, unsafe.Pointer(C.CString("")), C.CleanupFunc(C.freeData))
+}
+
+// SetSecret sets a copy of the C string secret into the PAM data with the
+// specified name. This copy will be held in locked memory until this PAM data
+// is cleared.
+func (h *Handle) SetSecret(name string, secret unsafe.Pointer) error {
+       return h.setData(name, C.copyIntoSecret(secret), C.CleanupFunc(C.freeSecret))
+}
+
+// GetSecret returns a pointer to the C string PAM data with the specified name.
+// This is a pointer directly to the data, so it shouldn't be modified. It
+// should have been previously set with SetSecret().
+func (h *Handle) GetSecret(name string) (unsafe.Pointer, error) {
+       return h.getData(name)
+}
+
+// SetString sets a string value for the PAM data with the specified name.
+func (h *Handle) SetString(name string, s string) error {
+       return h.setData(name, unsafe.Pointer(C.CString(s)), C.CleanupFunc(C.freeData))
+}
+
+// GetString gets a string value for the PAM data with the specified name. It
+// should have been previously set with SetString().
+func (h *Handle) GetString(name string) (string, error) {
+       data, err := h.getData(name)
+       if err != nil {
+               return "", err
+       }
+       return C.GoString((*C.char)(data)), nil
+}
+
+// GetItem retrieves a PAM information item. This is a pointer directly to the
+// data, so it shouldn't be modified.
+func (h *Handle) GetItem(i Item) (unsafe.Pointer, error) {
+       var data unsafe.Pointer
+       h.status = C.pam_get_item(h.handle, C.int(i), &data)
+       if err := h.err(); err != nil {
+               return nil, err
+       }
+       if data == nil {
+               return nil, errors.New("item not found")
+       }
+       return data, nil
+}
+
+// GetServiceName retrieves the name of the application running the PAM transaction.
+func (h *Handle) GetServiceName() string {
+       data, err := h.GetItem(Service)
+       if err != nil {
+               return "[unknown service]"
+       }
+       return C.GoString((*C.char)(data))
+}
+
+// StartAsPamUser sets the effective privileges to that of the PAM user.
+func (h *Handle) StartAsPamUser() error {
+       userPrivs, err := security.UserPrivileges(h.PamUser)
+       if err != nil {
+               return err
+       }
+       origPrivs, err := security.ProcessPrivileges()
+       if err != nil {
+               return err
+       }
+       if err = security.SetProcessPrivileges(userPrivs); err != nil {
+               return err
+       }
+       h.origPrivs = origPrivs
+       return nil
+}
+
+// StopAsPamUser restores the original privileges that were running the
+// PAM module (this is usually root).
+func (h *Handle) StopAsPamUser() error {
+       if h.origPrivs == nil {
+               return nil
+       }
+       err := security.SetProcessPrivileges(h.origPrivs)
+       h.origPrivs = nil
+       if err != nil {
+               log.Print(err)
+       }
+       return err
+}
+
+func (h *Handle) err() error {
+       if h.status == C.PAM_SUCCESS {
+               return nil
+       }
+       s := C.GoString(C.pam_strerror(h.handle, C.int(h.status)))
+       return errors.New(s)
+}
+
+// InfoMessage sends a message to the application using pam_info().
+func (h *Handle) InfoMessage(message string) {
+       cMessage := C.CString(message)
+       defer C.free(unsafe.Pointer(cMessage))
+       C.infoMessage(h.handle, cMessage)
+}
+
+// Transaction represents a wrapped pam_handle_t type created with pam_start
+// from an application.
+type Transaction Handle
+
+// Start initializes a pam Transaction. End() should be called after the
+// Transaction is no longer needed.
+func Start(service, username string) (*Transaction, error) {
+       cService := C.CString(service)
+       defer C.free(unsafe.Pointer(cService))
+       cUsername := C.CString(username)
+       defer C.free(unsafe.Pointer(cUsername))
+
+       t := &Transaction{
+               handle: nil,
+               status: C.PAM_SUCCESS,
+       }
+       t.status = C.pam_start(
+               cService,
+               cUsername,
+               C.goConv,
+               &t.handle)
+       return t, (*Handle)(t).err()
+}
+
+// End finalizes a pam Transaction with pam_end().
+func (t *Transaction) End() {
+       C.pam_end(t.handle, t.status)
+}
+
+// Authenticate returns a boolean indicating if the user authenticated correctly
+// or not. If the authentication check did not complete, an error is returned.
+func (t *Transaction) Authenticate(quiet bool) (bool, error) {
+       var flags C.int = C.PAM_DISALLOW_NULL_AUTHTOK
+       if quiet {
+               flags |= C.PAM_SILENT
+       }
+       t.status = C.pam_authenticate(t.handle, flags)
+       if t.status == C.PAM_AUTH_ERR {
+               return false, nil
+       }
+       return true, (*Handle)(t).err()
+}
diff --git a/pam/pam.h b/pam/pam.h
new file mode 100644 (file)
index 0000000..3cb609a
--- /dev/null
+++ b/pam/pam.h
@@ -0,0 +1,47 @@
+/*
+ * pam.h - Functions to let us call into libpam from Go.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+#ifndef FSCRYPT_PAM_H
+#define FSCRYPT_PAM_H
+
+#include <security/pam_appl.h>
+
+// Conversation that will call back into Go code when appropriate.
+extern const struct pam_conv *goConv;
+
+// CleaupFuncs are used to cleanup specific PAM data.
+typedef void (*CleanupFunc)(pam_handle_t *pamh, void *data, int error_status);
+
+// CleaupFunc that calls free() on data.
+void freeData(pam_handle_t *pamh, void *data, int error_status);
+
+// CleaupFunc that frees each item in a null terminated array of pointers and
+// then frees the array itself.
+void freeArray(pam_handle_t *pamh, void **array, int error_status);
+
+// Creates a copy of a C string, which resides in a locked buffer.
+void *copyIntoSecret(void *data);
+
+// CleaupFunc that Zeros wipes a C string and unlocks and frees its memory.
+void freeSecret(pam_handle_t *pamh, char *data, int error_status);
+
+// Sends a message to the application using pam_info().
+void infoMessage(pam_handle_t *pamh, const char *message);
+
+#endif  // FSCRYPT_PAM_H
diff --git a/pam/pam_test.go b/pam/pam_test.go
new file mode 100644 (file)
index 0000000..97cc792
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * pam_test.go - Stub test file that has one test that always passes.
+ *
+ * Copyright 2018 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package pam
+
+import "testing"
+
+func TestTrivial(t *testing.T) {}
diff --git a/pam_fscrypt/config b/pam_fscrypt/config
new file mode 100644 (file)
index 0000000..f83dab2
--- /dev/null
@@ -0,0 +1,13 @@
+Name: fscrypt PAM passphrase support
+Default: yes
+Priority: 100
+Auth-Type: Additional
+Auth-Final:
+       optional        PAM_INSTALL_PATH
+Session-Type: Additional
+Session-Interactive-Only: yes
+Session-Final:
+       optional        PAM_INSTALL_PATH
+Password-Type: Additional
+Password-Final:
+       optional        PAM_INSTALL_PATH
diff --git a/pam_fscrypt/pam_fscrypt.go b/pam_fscrypt/pam_fscrypt.go
new file mode 100644 (file)
index 0000000..15066c1
--- /dev/null
@@ -0,0 +1,432 @@
+/*
+ * pam_fscrypt.go - Checks the validity of a login token key against PAM.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+/*
+#cgo LDFLAGS: -lpam -fPIC
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <security/pam_appl.h>
+*/
+import "C"
+import (
+       "fmt"
+       "log"
+       "log/syslog"
+       "os"
+       "strconv"
+       "unsafe"
+
+       "github.com/pkg/errors"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/crypto"
+       "github.com/google/fscrypt/keyring"
+       "github.com/google/fscrypt/pam"
+       "github.com/google/fscrypt/security"
+)
+
+const (
+       moduleName = "pam_fscrypt"
+       // authtokLabel tags the AUTHTOK in the PAM data.
+       authtokLabel = "fscrypt_authtok"
+       // pidLabel tags the pid in the PAM data.
+       pidLabel = "fscrypt_pid"
+       // These flags are used to toggle behavior of the PAM module.
+       debugFlag = "debug"
+
+       // This option is accepted for compatibility with existing config files,
+       // but now we lock policies by default and this option is a no-op.
+       lockPoliciesFlag = "lock_policies"
+
+       // Only unlock directories, don't lock them.
+       unlockOnlyFlag = "unlock_only"
+
+       // This option is accepted for compatibility with existing config files,
+       // but it no longer does anything.  pam_fscrypt now drops caches if and
+       // only if it is needed.  (Usually it is not needed anymore, as the
+       // FS_IOC_REMOVE_ENCRYPTION_KEY ioctl handles this automatically.)
+       dropCachesFlag = "drop_caches"
+)
+
+var (
+       // PamFuncs for our 4 provided methods
+       authenticateFunc = PamFunc{"Authenticate", Authenticate}
+       openSessionFunc  = PamFunc{"OpenSession", OpenSession}
+       closeSessionFunc = PamFunc{"CloseSession", CloseSession}
+       chauthtokFunc    = PamFunc{"Chauthtok", Chauthtok}
+)
+
+// Authenticate copies the AUTHTOK (if necessary) into the PAM data so it can be
+// used in pam_sm_open_session.
+func Authenticate(handle *pam.Handle, _ map[string]bool) error {
+       if err := handle.StartAsPamUser(); err != nil {
+               return err
+       }
+       defer handle.StopAsPamUser()
+
+       // Save the PID in the PAM data so that the Session hook can try to
+       // detect the unsupported situation where the process was forked.
+       if err := handle.SetString(pidLabel, strconv.Itoa(os.Getpid())); err != nil {
+               return errors.Wrap(err, "could not save pid in PAM data")
+       }
+
+       // If this user doesn't have a login protector, no unlocking is needed.
+       if _, err := loginProtector(handle); err != nil {
+               log.Printf("no protector, no need for AUTHTOK: %s", err)
+               return nil
+       }
+
+       log.Print("copying AUTHTOK for use in the session open")
+       authtok, err := handle.GetItem(pam.Authtok)
+       if err != nil {
+               return errors.Wrap(err, "could not get AUTHTOK")
+       }
+       err = handle.SetSecret(authtokLabel, authtok)
+       return errors.Wrap(err, "could not set AUTHTOK data")
+}
+
+func beginProvisioningOp(handle *pam.Handle, policy *actions.Policy) error {
+       if policy.NeedsRootToProvision() {
+               return handle.StopAsPamUser()
+       }
+       return nil
+}
+
+func endProvisioningOp(handle *pam.Handle, policy *actions.Policy) error {
+       if policy.NeedsRootToProvision() {
+               return handle.StartAsPamUser()
+       }
+       return nil
+}
+
+// Set up the PAM user's keyring if needed by any encryption policies.
+func setupUserKeyringIfNeeded(handle *pam.Handle, policies []*actions.Policy) error {
+       needed := false
+       for _, policy := range policies {
+               if policy.NeedsUserKeyring() {
+                       needed = true
+                       break
+               }
+       }
+       if !needed {
+               return nil
+       }
+       err := handle.StopAsPamUser()
+       if err != nil {
+               return err
+       }
+       _, err = keyring.UserKeyringID(handle.PamUser, true)
+       if err != nil {
+               log.Printf("Setting up keyrings in PAM: %v", err)
+       }
+       return handle.StartAsPamUser()
+}
+
+// The Go runtime doesn't support being forked, as it is multithreaded but
+// fork() deletes all threads except one.  Some programs, such as xrdp, misuse
+// libpam by fork()-ing the process between pam_authenticate() and
+// pam_open_session().  Try to detect such unsupported cases and bail out early
+// rather than deadlocking the Go runtime, which would prevent the user from
+// logging in entirely.  This isn't guaranteed to work, as we are already
+// running Go code here, so we may have already deadlocked.  But in practice the
+// deadlock doesn't occur until hashing the login passphrase is attempted.
+func isUnsupportedFork(handle *pam.Handle) bool {
+       pidString, err := handle.GetString(pidLabel)
+       if err != nil {
+               return false
+       }
+       expectedPid, err := strconv.Atoi(pidString)
+       if err != nil {
+               log.Printf("%s parse error: %v", pidLabel, err)
+               return false
+       }
+       if os.Getpid() == expectedPid {
+               return false
+       }
+       handle.InfoMessage(fmt.Sprintf("%s couldn't automatically unlock directories, see syslog", moduleName))
+       if logger, err := syslog.New(syslog.LOG_WARNING, moduleName); err == nil {
+               fmt.Fprintf(logger,
+                       "not unlocking directories because %s forked the process between authenticating the user and opening the session, which is incompatible with %s.  See https://github.com/google/fscrypt/issues/350",
+                       handle.GetServiceName(), moduleName)
+               logger.Close()
+       }
+       return true
+}
+
+// OpenSession provisions any policies protected with the login protector.
+func OpenSession(handle *pam.Handle, _ map[string]bool) error {
+       // We will always clear the AUTHTOK data
+       defer handle.ClearData(authtokLabel)
+       // Increment the count as we add a session
+       if _, err := AdjustCount(handle, +1); err != nil {
+               return err
+       }
+
+       if err := handle.StartAsPamUser(); err != nil {
+               return err
+       }
+       defer handle.StopAsPamUser()
+
+       // If there are no polices for the login protector, no unlocking needed.
+       protector, err := loginProtector(handle)
+       if err != nil {
+               log.Printf("no protector to unlock: %s", err)
+               return nil
+       }
+       policies := policiesUsingProtector(protector, false)
+       if len(policies) == 0 {
+               log.Print("no policies to unlock")
+               return nil
+       }
+
+       if isUnsupportedFork(handle) {
+               return nil
+       }
+
+       if err = setupUserKeyringIfNeeded(handle, policies); err != nil {
+               return errors.Wrapf(err, "setting up user keyring")
+       }
+
+       log.Printf("unlocking %d policies protected with AUTHTOK", len(policies))
+       keyFn := func(_ actions.ProtectorInfo, retry bool) (*crypto.Key, error) {
+               if retry {
+                       // Login passphrase and login protector have diverged.
+                       // We could prompt the user for the old passphrase and
+                       // rewrap, but we currently don't.
+                       return nil, pam.ErrPassphrase
+               }
+
+               authtok, err := handle.GetSecret(authtokLabel)
+               if err != nil {
+                       // pam_sm_authenticate was not run before the session is
+                       // opened. This can happen when a user does something
+                       // like "sudo su <user>". We could prompt for the
+                       // login passphrase here, but we currently don't.
+                       return nil, errors.Wrap(err, "AUTHTOK data missing")
+               }
+
+               return crypto.NewKeyFromCString(authtok)
+       }
+       if err := protector.Unlock(keyFn); err != nil {
+               return errors.Wrapf(err, "unlocking protector %s", protector.Descriptor())
+       }
+       defer protector.Lock()
+
+       // We don't stop provisioning polices on error, we try all of them.
+       for _, policy := range policies {
+               if err := policy.UnlockWithProtector(protector); err != nil {
+                       log.Printf("unlocking policy %s: %s", policy.Descriptor(), err)
+                       continue
+               }
+               defer policy.Lock()
+
+               if err := beginProvisioningOp(handle, policy); err != nil {
+                       return err
+               }
+               provisionErr := policy.Provision()
+               if err := endProvisioningOp(handle, policy); err != nil {
+                       return err
+               }
+               if provisionErr != nil {
+                       log.Printf("provisioning policy %s: %s", policy.Descriptor(), provisionErr)
+                       continue
+               }
+               log.Printf("policy %s provisioned by %v", policy.Descriptor(),
+                       handle.PamUser.Username)
+       }
+       return nil
+}
+
+// CloseSession can deprovision all keys provisioned at the start of the
+// session. It can also clear the cache so these changes take effect.
+func CloseSession(handle *pam.Handle, args map[string]bool) error {
+       // Only do stuff on session close when we are the last session
+       if count, err := AdjustCount(handle, -1); err != nil || count != 0 {
+               log.Printf("count is %d and we are not locking", count)
+               return err
+       }
+
+       if args[lockPoliciesFlag] {
+               log.Print("ignoring deprecated 'lock_policies' option (now the default)")
+       }
+
+       if args[dropCachesFlag] {
+               log.Print("ignoring deprecated 'drop_caches' option (now auto-detected)")
+       }
+
+       // Don't automatically drop privileges, since we may need them to
+       // deprovision policies or to drop caches.
+
+       if !args[unlockOnlyFlag] {
+               log.Print("locking policies protected with login protector")
+               needDropCaches, errLock := lockLoginPolicies(handle)
+
+               var errCache error
+               if needDropCaches {
+                       log.Print("dropping appropriate filesystem caches at session close")
+                       errCache = security.DropFilesystemCache()
+               }
+               if errLock != nil {
+                       return errLock
+               }
+               return errCache
+       }
+       return nil
+}
+
+// lockLoginPolicies deprovisions all policy keys that are protected by the
+// user's login protector.  It returns true if dropping filesystem caches will
+// be needed afterwards to completely lock the relevant directories.
+func lockLoginPolicies(handle *pam.Handle) (bool, error) {
+       needDropCaches := false
+
+       if err := handle.StartAsPamUser(); err != nil {
+               return needDropCaches, err
+       }
+       defer handle.StopAsPamUser()
+
+       // If there are no polices for the login protector, no locking needed.
+       protector, err := loginProtector(handle)
+       if err != nil {
+               log.Printf("nothing to lock: %s", err)
+               return needDropCaches, nil
+       }
+       policies := policiesUsingProtector(protector, true)
+       if len(policies) == 0 {
+               log.Print("no policies to lock")
+               return needDropCaches, nil
+       }
+
+       if err = setupUserKeyringIfNeeded(handle, policies); err != nil {
+               return needDropCaches, errors.Wrapf(err, "setting up user keyring")
+       }
+
+       // We will try to deprovision all of the policies.
+       for _, policy := range policies {
+               if policy.NeedsUserKeyring() {
+                       needDropCaches = true
+               }
+               if err := beginProvisioningOp(handle, policy); err != nil {
+                       return needDropCaches, err
+               }
+               deprovisionErr := policy.Deprovision(false)
+               if err := endProvisioningOp(handle, policy); err != nil {
+                       return needDropCaches, err
+               }
+               if deprovisionErr != nil {
+                       log.Printf("deprovisioning policy %s: %s", policy.Descriptor(), deprovisionErr)
+                       continue
+               }
+               log.Printf("policy %s deprovisioned by %v", policy.Descriptor(), handle.PamUser.Username)
+       }
+       return needDropCaches, nil
+}
+
+var noOldAuthTokMessage string = `
+pam_fscrypt: cannot update login protector for '%s' because old passphrase
+was not given.  This is expected when changing a user's passphrase as root.
+You'll need to manually update the protector's passphrase using:
+
+   fscrypt metadata change-passphrase --protector=%s:%s
+`
+
+// Chauthtok rewraps the login protector when the passphrase changes.
+func Chauthtok(handle *pam.Handle, _ map[string]bool) error {
+       if err := handle.StartAsPamUser(); err != nil {
+               return err
+       }
+       defer handle.StopAsPamUser()
+
+       protector, err := loginProtector(handle)
+       if err != nil {
+               log.Printf("no login protector to rewrap: %s", err)
+               return nil
+       }
+
+       oldKeyFn := func(_ actions.ProtectorInfo, retry bool) (*crypto.Key, error) {
+               if retry {
+                       // If the OLDAUTHTOK disagrees with the login protector,
+                       // we do nothing, as the protector will (probably) still
+                       // disagree after the login passphrase changes.
+                       return nil, pam.ErrPassphrase
+               }
+               authtok, err := handle.GetItem(pam.Oldauthtok)
+               if err != nil {
+                       handle.InfoMessage(fmt.Sprintf(noOldAuthTokMessage,
+                               handle.PamUser.Username,
+                               protector.Context.Mount.Path, protector.Descriptor()))
+                       return nil, errors.Wrap(err, "could not get OLDAUTHTOK")
+               }
+               return crypto.NewKeyFromCString(authtok)
+       }
+
+       newKeyFn := func(_ actions.ProtectorInfo, _ bool) (*crypto.Key, error) {
+               authtok, err := handle.GetItem(pam.Authtok)
+               if err != nil {
+                       return nil, errors.Wrap(err, "could not get AUTHTOK")
+               }
+               return crypto.NewKeyFromCString(authtok)
+       }
+
+       log.Print("rewrapping login protector")
+       if err = protector.Unlock(oldKeyFn); err != nil {
+               return err
+       }
+       defer protector.Lock()
+
+       return protector.Rewrap(newKeyFn)
+}
+
+//export pam_sm_authenticate
+func pam_sm_authenticate(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+       return authenticateFunc.Run(pamh, argc, argv)
+}
+
+// pam_sm_setcred needed because we use pam_sm_authenticate.
+//
+//export pam_sm_setcred
+func pam_sm_setcred(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+       return C.PAM_SUCCESS
+}
+
+//export pam_sm_open_session
+func pam_sm_open_session(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+       return openSessionFunc.Run(pamh, argc, argv)
+}
+
+//export pam_sm_close_session
+func pam_sm_close_session(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+       return closeSessionFunc.Run(pamh, argc, argv)
+}
+
+//export pam_sm_chauthtok
+func pam_sm_chauthtok(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+       // Only do rewrapping if we have both AUTHTOKs and a login protector.
+       if pam.Flag(flags)&pam.PrelimCheck != 0 {
+               return C.PAM_SUCCESS
+       }
+       return chauthtokFunc.Run(pamh, argc, argv)
+}
+
+// main() is needed to make a shared library compile
+func main() {}
diff --git a/pam_fscrypt/run_fscrypt.go b/pam_fscrypt/run_fscrypt.go
new file mode 100644 (file)
index 0000000..af9537f
--- /dev/null
@@ -0,0 +1,278 @@
+/*
+ * run_fscrypt.go - Helpers for running functions in the PAM module.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+/*
+#cgo LDFLAGS: -lpam -fPIC
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <security/pam_appl.h>
+*/
+import "C"
+import (
+       "fmt"
+       "io"
+       "log"
+       "log/syslog"
+       "os"
+       "os/user"
+       "path/filepath"
+       "runtime/debug"
+       "unsafe"
+
+       "golang.org/x/sys/unix"
+
+       "github.com/pkg/errors"
+
+       "github.com/google/fscrypt/actions"
+       "github.com/google/fscrypt/filesystem"
+       "github.com/google/fscrypt/metadata"
+       "github.com/google/fscrypt/pam"
+       "github.com/google/fscrypt/util"
+)
+
+const (
+       // countDirectory is in a tmpfs filesystem so it will reset on reboot.
+       countDirectory = "/run/fscrypt"
+       // count files should only be readable and writable by root
+       countDirectoryPermissions = 0700
+       countFilePermissions      = 0600
+       countFileFormat           = "%d\n"
+       // uidMin is the first UID that can be used for a regular user (as
+       // opposed to a system user or root).  This value is fairly standard
+       // across Linux distros, but it can be adjusted if needed.
+       uidMin = 1000
+)
+
+// PamFunc is used to define the various actions in the PAM module.
+type PamFunc struct {
+       // Name of the function being executed
+       name string
+       // Go implementation of this function
+       impl func(handle *pam.Handle, args map[string]bool) error
+}
+
+// isSystemUser checks if a user is a system user.  pam_fscrypt should never
+// need to do anything for system users since they should never have login
+// protectors.  Therefore, we detect them early to avoid wasting resources.
+func isSystemUser(user *user.User) bool {
+       uid := util.AtoiOrPanic(user.Uid)
+       return uid < uidMin && uid != 0
+}
+
+// Run is used to convert between the Go functions and exported C funcs.
+func (f *PamFunc) Run(pamh unsafe.Pointer, argc C.int, argv **C.char) (ret C.int) {
+       args := parseArgs(argc, argv)
+       errorWriter := setupLogging(args)
+
+       // Log any panics to the errorWriter
+       defer func() {
+               if r := recover(); r != nil {
+                       ret = C.PAM_SERVICE_ERR
+                       fmt.Fprintf(errorWriter,
+                               "%s(%v) panicked: %s\nPlease open a bug.\n%s",
+                               f.name, args, r, debug.Stack())
+               }
+       }()
+
+       log.Printf("%s(%v) starting", f.name, args)
+       handle, err := pam.NewHandle(pamh)
+       if err == nil {
+               if isSystemUser(handle.PamUser) {
+                       log.Printf("invoked for system user %q (%s), doing nothing",
+                               handle.PamUser.Username, handle.PamUser.Uid)
+                       err = nil
+               } else {
+                       err = f.impl(handle, args)
+               }
+       }
+       if err != nil {
+               fmt.Fprintf(errorWriter, "%s(%v) failed: %s", f.name, args, err)
+               return C.PAM_SERVICE_ERR
+       }
+       log.Printf("%s(%v) succeeded", f.name, args)
+       return C.PAM_SUCCESS
+}
+
+// parseArgs takes a list of C arguments into a PAM function and returns a map
+// where a key has a value of true if it appears in the argument list.
+func parseArgs(argc C.int, argv **C.char) map[string]bool {
+       args := make(map[string]bool)
+       if argc == 0 || argv == nil {
+               return args
+       }
+       for _, cString := range util.PointerSlice(unsafe.Pointer(argv))[:argc] {
+               args[C.GoString((*C.char)(cString))] = true
+       }
+       return args
+}
+
+// setupLogging directs turns off standard logging (or redirects it to debug
+// syslog if the "debug" argument is passed) and returns a writer to the error
+// syslog.
+func setupLogging(args map[string]bool) io.Writer {
+       log.SetFlags(0) // Syslog already includes time data itself
+       log.SetOutput(io.Discard)
+       if args[debugFlag] {
+               debugWriter, err := syslog.New(syslog.LOG_DEBUG, moduleName)
+               if err == nil {
+                       log.SetOutput(debugWriter)
+               }
+       }
+
+       errorWriter, err := syslog.New(syslog.LOG_ERR, moduleName)
+       if err != nil {
+               return io.Discard
+       }
+       return errorWriter
+}
+
+// loginProtector returns the login protector corresponding to the PAM_USER if
+// one exists. This protector descriptor (if found) will be cached in the pam
+// data, under descriptorLabel.
+func loginProtector(handle *pam.Handle) (*actions.Protector, error) {
+       ctx, err := actions.NewContextFromMountpoint(actions.LoginProtectorMountpoint,
+               handle.PamUser)
+       if err != nil {
+               return nil, err
+       }
+       // Ensure that pam_fscrypt only processes metadata files owned by the
+       // user or root, even if the user is root themselves.  (Normally, when
+       // fscrypt is run as root it is allowed to process all metadata files.
+       // This implements stricter behavior for pam_fscrypt.)
+       if !ctx.Config.GetAllowCrossUserMetadata() {
+               ctx.TrustedUser = handle.PamUser
+       }
+
+       // Find the user's PAM protector.
+       options, err := ctx.ProtectorOptions()
+       if err != nil {
+               return nil, err
+       }
+       uid := int64(util.AtoiOrPanic(handle.PamUser.Uid))
+       for _, option := range options {
+               if option.Source() == metadata.SourceType_pam_passphrase && option.UID() == uid {
+                       return actions.GetProtectorFromOption(ctx, option)
+               }
+       }
+       return nil, errors.Errorf("no PAM protector for UID=%d on %q", uid, ctx.Mount.Path)
+}
+
+// policiesUsingProtector searches all the mountpoints for any policies
+// protected with the specified protector.  If provisioned is true, then only
+// policies provisioned by the target user are returned; otherwise only policies
+// *not* provisioned by the target user are returned.
+func policiesUsingProtector(protector *actions.Protector, provisioned bool) []*actions.Policy {
+       mounts, err := filesystem.AllFilesystems()
+       if err != nil {
+               log.Print(err)
+               return nil
+       }
+
+       var policies []*actions.Policy
+       for _, mount := range mounts {
+               // Skip mountpoints that do not use the protector.
+               if _, _, err := mount.GetProtector(protector.Descriptor(),
+                       protector.Context.TrustedUser); err != nil {
+                       if _, ok := err.(*filesystem.ErrNotSetup); !ok {
+                               log.Print(err)
+                       }
+                       continue
+               }
+               policyDescriptors, err := mount.ListPolicies(protector.Context.TrustedUser)
+               if err != nil {
+                       log.Printf("listing policies: %s", err)
+                       continue
+               }
+
+               // Clone context but modify the mountpoint
+               ctx := *protector.Context
+               ctx.Mount = mount
+               for _, policyDescriptor := range policyDescriptors {
+                       policy, err := actions.GetPolicy(&ctx, policyDescriptor)
+                       if err != nil {
+                               log.Printf("reading policy: %s", err)
+                               continue
+                       }
+
+                       if !policy.UsesProtector(protector) {
+                               continue
+                       }
+                       if provisioned {
+                               if !policy.IsProvisionedByTargetUser() {
+                                       log.Printf("policy %s not provisioned by %v",
+                                               policy.Descriptor(), ctx.TargetUser.Username)
+                                       continue
+                               }
+                       } else {
+                               if policy.IsProvisionedByTargetUser() {
+                                       log.Printf("policy %s already provisioned by %v",
+                                               policy.Descriptor(), ctx.TargetUser.Username)
+                                       continue
+                               }
+                       }
+                       policies = append(policies, policy)
+               }
+       }
+       return policies
+}
+
+// AdjustCount changes the session count for the pam user by the specified
+// amount. If the count file does not exist, create it as if it had a count of
+// zero. If the adjustment would bring the count below zero, the count is set to
+// zero. The value of the new count is returned. Requires root privileges.
+func AdjustCount(handle *pam.Handle, delta int) (int, error) {
+       // Make sure the directory exists
+       if err := os.MkdirAll(countDirectory, countDirectoryPermissions); err != nil {
+               return 0, err
+       }
+
+       path := filepath.Join(countDirectory, handle.PamUser.Uid+".count")
+       file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, countFilePermissions)
+       if err != nil {
+               return 0, err
+       }
+       if err = unix.Flock(int(file.Fd()), unix.LOCK_EX); err != nil {
+               return 0, err
+       }
+       defer file.Close()
+
+       newCount := util.MaxInt(getCount(file)+delta, 0)
+       if _, err = file.Seek(0, io.SeekStart); err != nil {
+               return 0, err
+       }
+       if _, err = fmt.Fprintf(file, countFileFormat, newCount); err != nil {
+               return 0, err
+       }
+
+       log.Printf("Session count for UID=%s updated to %d", handle.PamUser.Uid, newCount)
+       return newCount, nil
+}
+
+// Returns the count in the file (or zero if the count cannot be read).
+func getCount(file *os.File) int {
+       var count int
+       if _, err := fmt.Fscanf(file, countFileFormat, &count); err != nil {
+               return 0
+       }
+       return count
+}
diff --git a/pam_fscrypt/run_test.go b/pam_fscrypt/run_test.go
new file mode 100644 (file)
index 0000000..40ace4c
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * run_test.go - tests that the PAM helper functions work properly
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package main
+
+import (
+       "testing"
+)
+
+func TestParseArgsEmpty(t *testing.T) {
+       // An empty argv should create a map with no entries
+       args := parseArgs(0, nil)
+       if args == nil {
+               t.Fatal("args map should not be nil")
+       }
+       if len(args) > 0 {
+               t.Fatal("args map should not have any entries")
+       }
+}
diff --git a/security/cache.go b/security/cache.go
new file mode 100644 (file)
index 0000000..f11248d
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * cache.go - Handles cache clearing and management.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package security
+
+import (
+       "log"
+       "os"
+
+       "golang.org/x/sys/unix"
+)
+
+// DropFilesystemCache instructs the kernel to free the reclaimable inodes and
+// dentries. This has the effect of making encrypted directories whose keys are
+// not present no longer accessible. Requires root privileges.
+func DropFilesystemCache() error {
+       // Dirty reclaimable inodes must be synced so that they will be freed.
+       log.Print("syncing changes to filesystem")
+       unix.Sync()
+
+       // See: https://www.kernel.org/doc/Documentation/sysctl/vm.txt
+       log.Print("freeing reclaimable inodes and dentries")
+       file, err := os.OpenFile("/proc/sys/vm/drop_caches", os.O_WRONLY|os.O_SYNC, 0)
+       if err != nil {
+               return err
+       }
+       defer file.Close()
+       // "2" just frees the reclaimable inodes and dentries. The associated
+       // pages to these inodes will be freed. We do not need to free the
+       // entire pagecache (as this will severely impact performance).
+       _, err = file.WriteString("2")
+       return err
+}
diff --git a/security/privileges.go b/security/privileges.go
new file mode 100644 (file)
index 0000000..fe8668d
--- /dev/null
@@ -0,0 +1,156 @@
+/*
+ * privileges.go - Functions for managing users and privileges.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+// Package security manages:
+//   - Cache clearing (cache.go)
+//   - Privilege manipulation (privileges.go)
+package security
+
+// Use the libc versions of setreuid, setregid, and setgroups instead of the
+// "sys/unix" versions.  The "sys/unix" versions use the raw syscalls which
+// operate on the calling thread only, whereas the libc versions operate on the
+// whole process.  And we need to operate on the whole process, firstly for
+// pam_fscrypt to prevent the privileges of Go worker threads from diverging
+// from the PAM stack's "main" thread, violating libc's assumption and causing
+// an abort() later in the PAM stack; and secondly because Go code may migrate
+// between OS-level threads while it's running.
+//
+// See also: https://github.com/golang/go/issues/1435
+
+/*
+#define _GNU_SOURCE    // for getresuid and setresuid
+#include <sys/types.h>
+#include <unistd.h>    // getting and setting uids and gids
+#include <grp.h>       // setgroups
+*/
+import "C"
+
+import (
+       "log"
+       "os/user"
+       "syscall"
+
+       "github.com/pkg/errors"
+
+       "github.com/google/fscrypt/util"
+)
+
+// Privileges encapsulate the effective uid/gid and groups of a process.
+type Privileges struct {
+       euid   C.uid_t
+       egid   C.gid_t
+       groups []C.gid_t
+}
+
+// ProcessPrivileges returns the process's current effective privileges.
+func ProcessPrivileges() (*Privileges, error) {
+       ruid := C.getuid()
+       euid := C.geteuid()
+       rgid := C.getgid()
+       egid := C.getegid()
+
+       var groups []C.gid_t
+       n, err := C.getgroups(0, nil)
+       if n < 0 {
+               return nil, err
+       }
+       // If n == 0, the user isn't in any groups, so groups == nil is fine.
+       if n > 0 {
+               groups = make([]C.gid_t, n)
+               n, err = C.getgroups(n, &groups[0])
+               if n < 0 {
+                       return nil, err
+               }
+               groups = groups[:n]
+       }
+       log.Printf("Current privs (real, effective): uid=(%d,%d) gid=(%d,%d) groups=%v",
+               ruid, euid, rgid, egid, groups)
+       return &Privileges{euid, egid, groups}, nil
+}
+
+// UserPrivileges returns the default privileges for the specified user.
+func UserPrivileges(user *user.User) (*Privileges, error) {
+       privs := &Privileges{
+               euid: C.uid_t(util.AtoiOrPanic(user.Uid)),
+               egid: C.gid_t(util.AtoiOrPanic(user.Gid)),
+       }
+       userGroups, err := user.GroupIds()
+       if err != nil {
+               return nil, util.SystemError(err.Error())
+       }
+       privs.groups = make([]C.gid_t, len(userGroups))
+       for i, group := range userGroups {
+               privs.groups[i] = C.gid_t(util.AtoiOrPanic(group))
+       }
+       return privs, nil
+}
+
+// SetProcessPrivileges sets the privileges of the current process to have those
+// specified by privs. The original privileges can be obtained by first saving
+// the output of ProcessPrivileges, calling SetProcessPrivileges with the
+// desired privs, then calling SetProcessPrivileges with the saved privs.
+func SetProcessPrivileges(privs *Privileges) error {
+       log.Printf("Setting euid=%d egid=%d groups=%v", privs.euid, privs.egid, privs.groups)
+
+       // If setting privs as root, we need to set the euid to 0 first, so that
+       // we will have the necessary permissions to make the other changes to
+       // the groups/egid/euid, regardless of our original euid.
+       C.seteuid(0)
+
+       // Separately handle the case where the user is in no groups.
+       numGroups := C.size_t(len(privs.groups))
+       groupsPtr := (*C.gid_t)(nil)
+       if numGroups > 0 {
+               groupsPtr = &privs.groups[0]
+       }
+
+       if res, err := C.setgroups(numGroups, groupsPtr); res < 0 {
+               return errors.Wrapf(err.(syscall.Errno), "setting groups")
+       }
+       if res, err := C.setegid(privs.egid); res < 0 {
+               return errors.Wrapf(err.(syscall.Errno), "setting egid")
+       }
+       if res, err := C.seteuid(privs.euid); res < 0 {
+               return errors.Wrapf(err.(syscall.Errno), "setting euid")
+       }
+       ProcessPrivileges()
+       return nil
+}
+
+// SetUids sets the process's real, effective, and saved UIDs.
+func SetUids(ruid, euid, suid int) error {
+       log.Printf("Setting ruid=%d euid=%d suid=%d", ruid, euid, suid)
+       // We elevate all the privs before setting them. This prevents issues
+       // with (ruid=1000,euid=1000,suid=0), where just a single call to
+       // setresuid might fail with permission denied.
+       if res, err := C.setresuid(0, 0, 0); res < 0 {
+               return errors.Wrapf(err.(syscall.Errno), "setting uids")
+       }
+       if res, err := C.setresuid(C.uid_t(ruid), C.uid_t(euid), C.uid_t(suid)); res < 0 {
+               return errors.Wrapf(err.(syscall.Errno), "setting uids")
+       }
+       return nil
+}
+
+// GetUids gets the process's real, effective, and saved UIDs.
+func GetUids() (int, int, int) {
+       var ruid, euid, suid C.uid_t
+       C.getresuid(&ruid, &euid, &suid)
+       return int(ruid), int(euid), int(suid)
+}
diff --git a/security/security_test.go b/security/security_test.go
new file mode 100644 (file)
index 0000000..45e4f63
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * security_test.go - Stub test file that has one test that always passes.
+ *
+ * Copyright 2018 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package security
+
+import "testing"
+
+func TestTrivial(t *testing.T) {}
diff --git a/tools.go b/tools.go
new file mode 100644 (file)
index 0000000..7a5072b
--- /dev/null
+++ b/tools.go
@@ -0,0 +1,14 @@
+//go:build tools
+// +build tools
+
+// Never compiled, just used to manage tool dependencies
+
+package tools
+
+import (
+       _ "github.com/client9/misspell/cmd/misspell"
+       _ "github.com/wadey/gocovmerge"
+       _ "golang.org/x/tools/cmd/goimports"
+       _ "google.golang.org/protobuf/cmd/protoc-gen-go"
+       _ "honnef.co/go/tools/cmd/staticcheck"
+)
diff --git a/util/errors.go b/util/errors.go
new file mode 100644 (file)
index 0000000..3c87a2c
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * errors.go - Custom errors and error functions used by fscrypt
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package util
+
+import (
+       "fmt"
+       "io"
+       "log"
+       "os"
+
+       "github.com/pkg/errors"
+)
+
+// ErrReader wraps an io.Reader, passing along calls to Read() until a read
+// fails. Then, the error is stored, and all subsequent calls to Read() do
+// nothing. This allows you to write code which has many subsequent reads and
+// do all of the error checking at the end. For example:
+//
+//     r := NewErrReader(reader)
+//     r.Read(foo)
+//     r.Read(bar)
+//     r.Read(baz)
+//     if r.Err() != nil {
+//             // Handle error
+//     }
+//
+// Taken from https://blog.golang.org/errors-are-values by Rob Pike.
+type ErrReader struct {
+       r   io.Reader
+       err error
+}
+
+// NewErrReader creates an ErrReader which wraps the provided reader.
+func NewErrReader(reader io.Reader) *ErrReader {
+       return &ErrReader{r: reader, err: nil}
+}
+
+// Read runs ReadFull on the wrapped reader if no errors have occurred.
+// Otherwise, the previous error is just returned and no reads are attempted.
+func (e *ErrReader) Read(p []byte) (n int, err error) {
+       if e.err == nil {
+               n, e.err = io.ReadFull(e.r, p)
+       }
+       return n, e.err
+}
+
+// Err returns the first encountered err (or nil if no errors occurred).
+func (e *ErrReader) Err() error {
+       return e.err
+}
+
+// ErrWriter works exactly like ErrReader, except with io.Writer.
+type ErrWriter struct {
+       w   io.Writer
+       err error
+}
+
+// NewErrWriter creates an ErrWriter which wraps the provided writer.
+func NewErrWriter(writer io.Writer) *ErrWriter {
+       return &ErrWriter{w: writer, err: nil}
+}
+
+// Write runs the wrapped writer's Write if no errors have occurred. Otherwise,
+// the previous error is just returned and no writes are attempted.
+func (e *ErrWriter) Write(p []byte) (n int, err error) {
+       if e.err == nil {
+               n, e.err = e.w.Write(p)
+       }
+       return n, e.err
+}
+
+// Err returns the first encountered err (or nil if no errors occurred).
+func (e *ErrWriter) Err() error {
+       return e.err
+}
+
+// CheckValidLength returns an invalid length error if expected != actual
+func CheckValidLength(expected, actual int) error {
+       if expected == actual {
+               return nil
+       }
+       return fmt.Errorf("expected length of %d, got %d", expected, actual)
+}
+
+// SystemError is an error that should indicate something has gone wrong in the
+// underlying system (syscall failure, bad ioctl, etc...).
+type SystemError string
+
+func (s SystemError) Error() string {
+       return "system error: " + string(s)
+}
+
+// NeverError panics if a non-nil error is passed in. It should be used to check
+// for logic errors, not to handle recoverable errors.
+func NeverError(err error) {
+       if err != nil {
+               log.Panicf("NeverError() check failed: %v", err)
+       }
+}
+
+var (
+       // testEnvVarName is the name of an environment variable that should be
+       // set to an empty mountpoint. This is only used for integration tests.
+       // If not set, integration tests are skipped.
+       testEnvVarName = "TEST_FILESYSTEM_ROOT"
+       // ErrSkipIntegration indicates integration tests shouldn't be run.
+       ErrSkipIntegration = errors.New("skipping integration test")
+)
+
+// TestRoot returns a the root of a filesystem specified by testEnvVarName. This
+// function is only used for integration tests.
+func TestRoot() (string, error) {
+       path := os.Getenv(testEnvVarName)
+       if path == "" {
+               return "", ErrSkipIntegration
+       }
+       return path, nil
+}
diff --git a/util/util.go b/util/util.go
new file mode 100644 (file)
index 0000000..1dab335
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * util.go - Various helpers used throughout fscrypt
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+// Package util contains useful components for simplifying Go code.
+//
+// The package contains common error types (errors.go) and functions for
+// converting arrays to pointers.
+package util
+
+import (
+       "bufio"
+       "fmt"
+       "log"
+       "os"
+       "os/user"
+       "strconv"
+       "unsafe"
+
+       "golang.org/x/sys/unix"
+)
+
+// Ptr converts a Go byte array to a pointer to the start of the array.
+func Ptr(slice []byte) unsafe.Pointer {
+       if len(slice) == 0 {
+               return nil
+       }
+       return unsafe.Pointer(&slice[0])
+}
+
+// ByteSlice takes a pointer to some data and views it as a slice of bytes.
+// Note, indexing into this slice is unsafe.
+func ByteSlice(ptr unsafe.Pointer) []byte {
+       // Slice must fit in the smallest address space go supports.
+       return (*[1 << 30]byte)(ptr)[:]
+}
+
+// PointerSlice takes a pointer to an array of pointers and views it as a slice
+// of pointers. Note, indexing into this slice is unsafe.
+func PointerSlice(ptr unsafe.Pointer) []unsafe.Pointer {
+       // Slice must fit in the smallest address space go supports.
+       return (*[1 << 28]unsafe.Pointer)(ptr)[:]
+}
+
+// Index returns the first index i such that inVal == inArray[i].
+// ok is true if we find a match, false otherwise.
+func Index(inVal int64, inArray []int64) (index int, ok bool) {
+       for index, val := range inArray {
+               if val == inVal {
+                       return index, true
+               }
+       }
+       return 0, false
+}
+
+// Lookup finds inVal in inArray and returns the corresponding element in
+// outArray. Specifically, if inVal == inArray[i], outVal == outArray[i].
+// ok is true if we find a match, false otherwise.
+func Lookup(inVal int64, inArray, outArray []int64) (outVal int64, ok bool) {
+       index, ok := Index(inVal, inArray)
+       if !ok {
+               return 0, false
+       }
+       return outArray[index], true
+}
+
+// MinInt returns the lesser of a and b.
+func MinInt(a, b int) int {
+       if a < b {
+               return a
+       }
+       return b
+}
+
+// MaxInt returns the greater of a and b.
+func MaxInt(a, b int) int {
+       if a > b {
+               return a
+       }
+       return b
+}
+
+// MinInt64 returns the lesser of a and b.
+func MinInt64(a, b int64) int64 {
+       if a < b {
+               return a
+       }
+       return b
+}
+
+// ReadLine returns a line of input from standard input. An empty string is
+// returned if the user didn't insert anything or on error.
+func ReadLine() (string, error) {
+       scanner := bufio.NewScanner(os.Stdin)
+       scanner.Scan()
+       return scanner.Text(), scanner.Err()
+}
+
+// AtoiOrPanic converts a string to an int or it panics. Should only be used in
+// situations where the input MUST be a decimal number.
+func AtoiOrPanic(input string) int {
+       i, err := strconv.Atoi(input)
+       if err != nil {
+               panic(err)
+       }
+       return i
+}
+
+// UserFromUID returns the User corresponding to the given user id.
+func UserFromUID(uid int64) (*user.User, error) {
+       return user.LookupId(strconv.FormatInt(uid, 10))
+}
+
+// EffectiveUser returns the user entry corresponding to the effective user.
+func EffectiveUser() (*user.User, error) {
+       return UserFromUID(int64(os.Geteuid()))
+}
+
+// IsUserRoot checks if the effective user is root.
+func IsUserRoot() bool {
+       return os.Geteuid() == 0
+}
+
+// Chown changes the owner of a File to a User.
+func Chown(file *os.File, user *user.User) error {
+       uid := AtoiOrPanic(user.Uid)
+       gid := AtoiOrPanic(user.Gid)
+       return file.Chown(uid, gid)
+}
+
+// IsKernelVersionAtLeast returns true if the Linux kernel version is at least
+// major.minor. If something goes wrong it assumes false.
+func IsKernelVersionAtLeast(major, minor int) bool {
+       var uname unix.Utsname
+       if err := unix.Uname(&uname); err != nil {
+               log.Printf("Uname failed [%v], assuming old kernel", err)
+               return false
+       }
+       release := string(uname.Release[:])
+       log.Printf("Kernel version is %s", release)
+       var actualMajor, actualMinor int
+       if n, _ := fmt.Sscanf(release, "%d.%d", &actualMajor, &actualMinor); n != 2 {
+               log.Printf("Unrecognized uname format %q, assuming old kernel", release)
+               return false
+       }
+       return actualMajor > major ||
+               (actualMajor == major && actualMinor >= minor)
+}
diff --git a/util/util_test.go b/util/util_test.go
new file mode 100644 (file)
index 0000000..70e7070
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * util_test.go - Tests the util package
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * 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.
+ */
+
+package util
+
+import (
+       "bytes"
+       "testing"
+       "unsafe"
+)
+
+const offset = 3
+
+var (
+       byteArr = []byte{'a', 'b', 'c', 'd'}
+       ptrArr  = []*int{&a, &b, &c, &d}
+       a       = 1
+       b       = 2
+       c       = 3
+       d       = 4
+)
+
+// Make sure the address behaves well under slicing
+func TestPtrOffset(t *testing.T) {
+       i1 := uintptr(Ptr(byteArr[offset:]))
+       i2 := uintptr(Ptr(byteArr))
+
+       if i1 != i2+offset {
+               t.Errorf("pointers %v and %v do not have an offset of %v", i1, i2, offset)
+       }
+}
+
+// Tests that the ByteSlice method essentially reverses the Ptr method
+func TestByteSlice(t *testing.T) {
+       ptr := Ptr(byteArr)
+       generatedArr := ByteSlice(ptr)[:len(byteArr)]
+
+       if !bytes.Equal(byteArr, generatedArr) {
+               t.Errorf("generated array (%v) and original array (%v) do not agree",
+                       generatedArr, byteArr)
+       }
+}
+
+// Tests that the PointerSlice method correctly handles Go Pointers
+func TestPointerSlice(t *testing.T) {
+       arrPtr := unsafe.Pointer(&ptrArr[0])
+
+       // Convert an array of unsafe pointers to int pointers.
+       for i, ptr := range PointerSlice(arrPtr)[:len(ptrArr)] {
+               if ptrArr[i] != (*int)(ptr) {
+                       t.Errorf("generated array and original array disagree at %d", i)
+               }
+       }
+}
+
+// Make sure NeverError actually panics
+func TestNeverErrorPanic(t *testing.T) {
+       defer func() {
+               if r := recover(); r == nil {
+                       t.Errorf("NeverError did not panic")
+               }
+       }()
+
+       err := SystemError("Hello")
+       NeverError(err)
+}
+
+// Make sure NeverError doesn't panic on nil
+func TestNeverErrorNoPanic(t *testing.T) {
+       defer func() {
+               if r := recover(); r != nil {
+                       t.Errorf("NeverError panicked")
+               }
+       }()
+
+       NeverError(nil)
+}
+
+func TestIsKernelVersionAtLeast(t *testing.T) {
+       // Even just running Go requires at least v2.6.23, so...
+       if !IsKernelVersionAtLeast(2, 6) {
+               t.Error("IsKernelVersionAtLeast() is broken")
+       }
+}