From 8c12cd64ab471d0a73ef4c300d7c40077cad5d5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 23:20:51 +0000 Subject: [PATCH] build(deps): bump golang.org/x/crypto from 0.13.0 to 0.17.0 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] --- .github/workflows/ci.yml | 143 +++ .gitignore | 13 + CODE_OF_CONDUCT.md | 93 ++ CONTRIBUTING.md | 163 ++++ LICENSE | 202 ++++ Makefile | 237 +++++ NEWS.md | 295 ++++++ README.md | 1282 ++++++++++++++++++++++++++ actions/callback.go | 132 +++ actions/config.go | 301 ++++++ actions/config_test.go | 78 ++ actions/context.go | 184 ++++ actions/context_test.go | 103 +++ actions/hashing_test.go | 76 ++ actions/policy.go | 622 +++++++++++++ actions/policy_test.go | 214 +++++ actions/protector.go | 300 ++++++ actions/protector_test.go | 65 ++ actions/recovery.go | 133 +++ actions/recovery_test.go | 90 ++ bin/files-changed | 25 + cli-tests/README.md | 67 ++ cli-tests/common.sh | 187 ++++ cli-tests/run.sh | 307 ++++++ cli-tests/t_change_passphrase.out | 32 + cli-tests/t_change_passphrase.sh | 60 ++ cli-tests/t_encrypt.out | 106 +++ cli-tests/t_encrypt.sh | 54 ++ cli-tests/t_encrypt_custom.out | 58 ++ cli-tests/t_encrypt_custom.sh | 50 + cli-tests/t_encrypt_login.out | 209 +++++ cli-tests/t_encrypt_login.sh | 104 +++ cli-tests/t_encrypt_raw_key.out | 74 ++ cli-tests/t_encrypt_raw_key.sh | 53 ++ cli-tests/t_lock.out | 102 ++ cli-tests/t_lock.sh | 65 ++ cli-tests/t_metadata.out | 19 + cli-tests/t_metadata.sh | 36 + cli-tests/t_not_enabled.out | 63 ++ cli-tests/t_not_enabled.sh | 39 + cli-tests/t_not_supported.out | 9 + cli-tests/t_not_supported.sh | 17 + cli-tests/t_passphrase_hashing.out | 0 cli-tests/t_passphrase_hashing.sh | 34 + cli-tests/t_setup.out | 51 + cli-tests/t_setup.sh | 52 ++ cli-tests/t_single_user.out | 30 + cli-tests/t_single_user.sh | 55 ++ cli-tests/t_status.out | 50 + cli-tests/t_status.sh | 56 ++ cli-tests/t_unlock.out | 116 +++ cli-tests/t_unlock.sh | 82 ++ cli-tests/t_v1_policy.out | 144 +++ cli-tests/t_v1_policy.sh | 72 ++ cli-tests/t_v1_policy_fs_keyring.out | 75 ++ cli-tests/t_v1_policy_fs_keyring.sh | 49 + cmd/fscrypt/commands.go | 1151 +++++++++++++++++++++++ cmd/fscrypt/errors.go | 382 ++++++++ cmd/fscrypt/flags.go | 309 +++++++ cmd/fscrypt/format.go | 178 ++++ cmd/fscrypt/fscrypt.go | 141 +++ cmd/fscrypt/fscrypt_bash_completion | 332 +++++++ cmd/fscrypt/fscrypt_test.go | 24 + cmd/fscrypt/keys.go | 216 +++++ cmd/fscrypt/prompt.go | 328 +++++++ cmd/fscrypt/protector.go | 147 +++ cmd/fscrypt/setup.go | 127 +++ cmd/fscrypt/status.go | 230 +++++ cmd/fscrypt/strings.go | 107 +++ crypto/crypto.go | 228 +++++ crypto/crypto_test.go | 656 +++++++++++++ crypto/key.go | 354 +++++++ crypto/rand.go | 99 ++ crypto/recovery_test.go | 246 +++++ filesystem/filesystem.go | 1089 ++++++++++++++++++++++ filesystem/filesystem_test.go | 610 ++++++++++++ filesystem/mountpoint.go | 582 ++++++++++++ filesystem/mountpoint_test.go | 546 +++++++++++ filesystem/path.go | 128 +++ filesystem/path_test.go | 85 ++ go.mod | 24 + go.sum | 54 ++ keyring/fs_keyring.go | 326 +++++++ keyring/keyring.go | 175 ++++ keyring/keyring_test.go | 330 +++++++ keyring/user_keyring.go | 251 +++++ metadata/checks.go | 241 +++++ metadata/config.go | 67 ++ metadata/config_test.go | 130 +++ metadata/constants.go | 57 ++ metadata/metadata.pb.go | 936 +++++++++++++++++++ metadata/metadata.proto | 113 +++ metadata/policy.go | 374 ++++++++ metadata/policy_test.go | 212 +++++ pam/constants.go | 110 +++ pam/login.go | 115 +++ pam/pam.c | 114 +++ pam/pam.go | 226 +++++ pam/pam.h | 47 + pam/pam_test.go | 24 + pam_fscrypt/config | 13 + pam_fscrypt/pam_fscrypt.go | 432 +++++++++ pam_fscrypt/run_fscrypt.go | 278 ++++++ pam_fscrypt/run_test.go | 35 + security/cache.go | 49 + security/privileges.go | 156 ++++ security/security_test.go | 24 + tools.go | 14 + util/errors.go | 135 +++ util/util.go | 163 ++++ util/util_test.go | 100 ++ 111 files changed, 20948 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 NEWS.md create mode 100644 README.md create mode 100644 actions/callback.go create mode 100644 actions/config.go create mode 100644 actions/config_test.go create mode 100644 actions/context.go create mode 100644 actions/context_test.go create mode 100644 actions/hashing_test.go create mode 100644 actions/policy.go create mode 100644 actions/policy_test.go create mode 100644 actions/protector.go create mode 100644 actions/protector_test.go create mode 100644 actions/recovery.go create mode 100644 actions/recovery_test.go create mode 100755 bin/files-changed create mode 100644 cli-tests/README.md create mode 100644 cli-tests/common.sh create mode 100755 cli-tests/run.sh create mode 100644 cli-tests/t_change_passphrase.out create mode 100755 cli-tests/t_change_passphrase.sh create mode 100644 cli-tests/t_encrypt.out create mode 100755 cli-tests/t_encrypt.sh create mode 100644 cli-tests/t_encrypt_custom.out create mode 100755 cli-tests/t_encrypt_custom.sh create mode 100644 cli-tests/t_encrypt_login.out create mode 100755 cli-tests/t_encrypt_login.sh create mode 100644 cli-tests/t_encrypt_raw_key.out create mode 100755 cli-tests/t_encrypt_raw_key.sh create mode 100644 cli-tests/t_lock.out create mode 100755 cli-tests/t_lock.sh create mode 100644 cli-tests/t_metadata.out create mode 100755 cli-tests/t_metadata.sh create mode 100644 cli-tests/t_not_enabled.out create mode 100755 cli-tests/t_not_enabled.sh create mode 100644 cli-tests/t_not_supported.out create mode 100755 cli-tests/t_not_supported.sh create mode 100644 cli-tests/t_passphrase_hashing.out create mode 100755 cli-tests/t_passphrase_hashing.sh create mode 100644 cli-tests/t_setup.out create mode 100755 cli-tests/t_setup.sh create mode 100644 cli-tests/t_single_user.out create mode 100755 cli-tests/t_single_user.sh create mode 100644 cli-tests/t_status.out create mode 100755 cli-tests/t_status.sh create mode 100644 cli-tests/t_unlock.out create mode 100755 cli-tests/t_unlock.sh create mode 100644 cli-tests/t_v1_policy.out create mode 100755 cli-tests/t_v1_policy.sh create mode 100644 cli-tests/t_v1_policy_fs_keyring.out create mode 100755 cli-tests/t_v1_policy_fs_keyring.sh create mode 100644 cmd/fscrypt/commands.go create mode 100644 cmd/fscrypt/errors.go create mode 100644 cmd/fscrypt/flags.go create mode 100644 cmd/fscrypt/format.go create mode 100644 cmd/fscrypt/fscrypt.go create mode 100644 cmd/fscrypt/fscrypt_bash_completion create mode 100644 cmd/fscrypt/fscrypt_test.go create mode 100644 cmd/fscrypt/keys.go create mode 100644 cmd/fscrypt/prompt.go create mode 100644 cmd/fscrypt/protector.go create mode 100644 cmd/fscrypt/setup.go create mode 100644 cmd/fscrypt/status.go create mode 100644 cmd/fscrypt/strings.go create mode 100644 crypto/crypto.go create mode 100644 crypto/crypto_test.go create mode 100644 crypto/key.go create mode 100644 crypto/rand.go create mode 100644 crypto/recovery_test.go create mode 100644 filesystem/filesystem.go create mode 100644 filesystem/filesystem_test.go create mode 100644 filesystem/mountpoint.go create mode 100644 filesystem/mountpoint_test.go create mode 100644 filesystem/path.go create mode 100644 filesystem/path_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 keyring/fs_keyring.go create mode 100644 keyring/keyring.go create mode 100644 keyring/keyring_test.go create mode 100644 keyring/user_keyring.go create mode 100644 metadata/checks.go create mode 100644 metadata/config.go create mode 100644 metadata/config_test.go create mode 100644 metadata/constants.go create mode 100644 metadata/metadata.pb.go create mode 100644 metadata/metadata.proto create mode 100644 metadata/policy.go create mode 100644 metadata/policy_test.go create mode 100644 pam/constants.go create mode 100644 pam/login.go create mode 100644 pam/pam.c create mode 100644 pam/pam.go create mode 100644 pam/pam.h create mode 100644 pam/pam_test.go create mode 100644 pam_fscrypt/config create mode 100644 pam_fscrypt/pam_fscrypt.go create mode 100644 pam_fscrypt/run_fscrypt.go create mode 100644 pam_fscrypt/run_test.go create mode 100644 security/cache.go create mode 100644 security/privileges.go create mode 100644 security/security_test.go create mode 100644 tools.go create mode 100644 util/errors.go create mode 100644 util/util.go create mode 100644 util/util_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1377df2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 index 0000000..0d8b56f --- /dev/null +++ b/.gitignore @@ -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 index 0000000..8bca305 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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 index 0000000..a547427 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 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 + and 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 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 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 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 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 + +This is not an official Google product. diff --git a/actions/callback.go b/actions/callback.go new file mode 100644 index 0000000..f15893d --- /dev/null +++ b/actions/callback.go @@ -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 index 0000000..7c7c0e6 --- /dev/null +++ b/actions/config.go @@ -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 index 0000000..49838e3 --- /dev/null +++ b/actions/config_test.go @@ -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 index 0000000..4253de2 --- /dev/null +++ b/actions/context.go @@ -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 index 0000000..6e28857 --- /dev/null +++ b/actions/context_test.go @@ -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 index 0000000..26f627b --- /dev/null +++ b/actions/hashing_test.go @@ -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 index 0000000..c621725 --- /dev/null +++ b/actions/policy.go @@ -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 index 0000000..8248862 --- /dev/null +++ b/actions/policy_test.go @@ -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 index 0000000..b986eb0 --- /dev/null +++ b/actions/protector.go @@ -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 index 0000000..f20dbcf --- /dev/null +++ b/actions/protector_test.go @@ -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 index 0000000..2bb8a23 --- /dev/null +++ b/actions/recovery.go @@ -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 index 0000000..35ade0e --- /dev/null +++ b/actions/recovery_test.go @@ -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 index 0000000..3ffbad6 --- /dev/null +++ b/bin/files-changed @@ -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 index 0000000..dfcc1d0 --- /dev/null +++ b/cli-tests/README.md @@ -0,0 +1,67 @@ +# fscrypt command-line interface tests + +## Usage + +To run the command-line interface (CLI) tests for `fscrypt`, ensure +that your kernel is v5.4 or later and has `CONFIG_FS_ENCRYPTION=y`. +Also ensure that you have the following packages installed: + +* e2fsprogs +* expect +* keyutils + +Then, run: + +```shell +make cli-test +``` + +You'll need to enter your `sudo` password, as the tests require root. + +If you only want to run specific tests, run a command like: + +```shell +make && sudo cli-tests/run.sh t_encrypt t_unlock +``` + +## Updating the expected output + +When the output of `fscrypt` has intentionally changed, the test +`.out` files need to be updated. This can be done automatically by +the following command, but be sure to review the changes: + +```shell +make cli-test-update +``` + +## Writing CLI tests + +The fscrypt CLI tests are `bash` scripts named like `t_*.sh`. + +The test scripts must be executable and begin by sourcing `common.sh`. +They all run in bash "extra-strict mode" (`-e -u -o pipefail`). They +run as root and have access to the following environment: + +* `$DEV`, `$DEV_ROOT`: ext4 filesystem images with encryption enabled + +* `$MNT`, `$MNT_ROOT`: the mountpoints of the above filesystems. + Initially all filesystems are mounted and are setup for fscrypt. + Login protectors will be stored on `$MNT_ROOT`. + +* `$TMPDIR`: a temporary directory that the test may use + +* `$FSCRYPT_CONF`: location of the fscrypt.conf file. Initially this + file exists and specifies to use v2 policies with the default + settings, except password hashing is configured to be extra fast. + +* `$TEST_USER`: a non-root user that the test may use. Their password + is `TEST_USER_PASS`. + +Any output (stdout and stderr) the test prints is compared to the +corresponding `.out` file. If a difference is detected then the test +is considered to have failed. The output is first sent through some +standard filters; see `run.sh`. + +The test is also failed if it exits with nonzero status. + +See `common.sh` for utility functions the tests may use. diff --git a/cli-tests/common.sh b/cli-tests/common.sh new file mode 100644 index 0000000..1d7b17b --- /dev/null +++ b/cli-tests/common.sh @@ -0,0 +1,187 @@ +#!/bin/bash +# +# common.sh - helper functions for fscrypt command-line interface tests +# +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# + +# Use extra-strict mode. +set -e -u -o pipefail + +# Don't allow running the test scripts directly. They need to be run via +# run.sh, to set up everything correctly. +if [ -z "${MNT:-}" ] || [ -z "${MNT_ROOT:-}" ]; then + echo 1>&2 "ERROR: This script can only be run via run.sh, not on its own." + exit 1 +fi + +# Prints an error message, then fails the test by exiting with failure status. +_fail() +{ + echo 1>&2 "ERROR: $1" + exit 1 +} + +# Runs a shell command and expects that it fails. +_expect_failure() +{ + if eval "$1"; then + _fail "command unexpectedly succeeded: \"$1\"" + fi +} + +# Prints a message to mark the beginning of the next part of the test. +_print_header() +{ + echo + echo "# $1" +} + +# Deletes all files on the test filesystems, including all policies and +# protectors. Leaves the fscrypt metadata directories themselves. +_reset_filesystems() +{ + local mnt + + for mnt in "$MNT" "$MNT_ROOT"; do + rm -rf "${mnt:?}"/* "${mnt:?}"/.fscrypt/{policies,protectors}/* + done +} + +# Prints the number of filesystems that have encryption support enabled. +_get_enabled_fs_count() +{ + local count + + count=$(fscrypt status | awk '/filesystems supporting encryption/ { print $4 }') + if [ -z "$count" ]; then + _fail "encryption support status line not found" + fi + echo "$count" +} + +# Gets the descriptor of the given protector. +_get_protector_descriptor() +{ + local mnt=$1 + local source=$2 + + case $source in + custom) + local name=$3 + local description="custom protector \\\"$name\\\"" + ;; + login) + local user=$3 + local description="login protector for $user" + ;; + *) + _fail "Unknown protector source $source" + esac + + local descriptor + descriptor=$(fscrypt status "$mnt" | + awk -F ' *' '{ if ($3 == "'"$description"'") print $1 }') + if [ -z "$descriptor" ]; then + _fail "Can't find $description on $mnt" + fi + echo "$descriptor" +} + +# Gets the descriptor of the login protector for $TEST_USER. +_get_login_descriptor() +{ + _get_protector_descriptor "$MNT_ROOT" login "$TEST_USER" +} + +# Prints the number of filesystems that have fscrypt metadata. +_get_setup_fs_count() +{ + local count + + count=$(fscrypt status | awk '/filesystems with fscrypt metadata/ { print $5 }') + if [ -z "$count" ]; then + _fail "fscrypt metadata status line not found" + fi + echo "$count" +} + +# Removes all fscrypt metadata from the given filesystem. +_rm_metadata() +{ + rm -r "${1:?}/.fscrypt" +} + +# Runs a shell command, ignoring its output (stdout and stderr) if it succeeds. +# If the command fails, prints its output and fails the test. +_run_noisy_command() +{ + if ! eval "$1" &> "$TMPDIR/out"; then + _fail "Command failed: '$1'. Output was: $(cat "$TMPDIR/out")" + fi +} + +# Runs the given shell command as the test user. +_user_do() +{ + su "$TEST_USER" --shell=/bin/bash --command="export PATH='$PATH'; $1" +} + +# Runs the given shell command as the test user and expects it to fail. +_user_do_and_expect_failure() +{ + _expect_failure "_user_do '$1'" +} + +# Clear the test user's user keyring and unlink it from root's user keyring, if +# it is linked into it. +_cleanup_user_keyrings() +{ + local ringid + + ringid=$(_user_do "keyctl show @u" | awk '/keyring: _uid/{print $1}') + + _user_do "keyctl clear $ringid" + keyctl unlink "$ringid" @u &> /dev/null || true +} + +# Gives the test a new session keyring which contains the test user's keyring +# but not root's keyring. Also clears the test user's keyring. This must be +# called at the beginning of the test script as it may re-execute the script. +_setup_session_keyring() +{ + # This *should* just use 'keyctl new_session', but that doesn't work if + # the session keyring is owned by a user other than root. So instead we + # have to use 'keyctl session' and re-execute the script. + if [ -z "${FSCRYPT_SESSION_KEYRING_SET:-}" ]; then + export FSCRYPT_SESSION_KEYRING_SET=1 + set +e + keyctl session - "$0" |& grep -v '^Joined session keyring' + exit "${PIPESTATUS[0]}" + fi + + # Link the test user's keyring into the new session keyring. + keyctl setperm @s 0x3f000000 # all possessor permissions + _user_do "keyctl link @u @s" + + # Clear the test user's keyring. + _user_do "keyctl clear @u" +} + +# Wraps the 'expect' command to force subprocesses to have 80-column output. +expect() +{ + command expect -c 'set stty_init "cols 80"' -f - +} diff --git a/cli-tests/run.sh b/cli-tests/run.sh new file mode 100755 index 0000000..9ab5b78 --- /dev/null +++ b/cli-tests/run.sh @@ -0,0 +1,307 @@ +#!/bin/bash +# +# run.sh - run the fscrypt command-line interface tests +# +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# + +# Use extra-strict mode. +set -e -u -o pipefail + +# Ensure we're in the cli-tests/ directory. +cd "$(dirname "$0")" + +# Names of the test devices. +# Variables with these names are exported to the tests. +DEVICES=(DEV DEV_ROOT) + +# Names of the mountpoint of each test device. +# Variables with these names are exported to the tests. +MOUNTS=(MNT MNT_ROOT) + +# Name of the test user. This user will be created and deleted by this script. +# This variable is exported to the tests. +TEST_USER=fscrypt-test-user + +# The temporary directory to use. +# This variable is exported to the tests. +TMPDIR=$(mktemp -d /tmp/fscrypt.XXXXXX) + +# The loopback devices that correspond to each test device. +LOOPS=() + +# Update the expected output files to match the actual output? +UPDATE_OUTPUT=false + +LONGOPTS_ARRAY=( +'help' +'update-output' +) +LONGOPTS=$(echo "${LONGOPTS_ARRAY[*]}" | tr ' ' ,) + +cleanup() +{ + local mnt loop + + # Unmount all the test filesystems. + for mnt in "${MOUNTS[@]}"; do + mnt="$TMPDIR/$mnt" + if mountpoint "$mnt" &> /dev/null; then + umount "$mnt" + fi + done + + # Delete the loopback device of each test device. + for loop in "${LOOPS[@]}"; do + losetup -d "$loop" + done + + # Delete all temporary files. + rm -rf "${TMPDIR:?}"/* +} + +cleanup_full() +{ + cleanup + rm -rf "$TMPDIR" + userdel "$TEST_USER" || true +} + +# Filters the output of the test script to make the output consistent on every +# run of the test. For example, references to the mountpoint like +# /tmp/fscrypt.4OTb6y/MNT will be replaced with simply MNT, since the name of +# the temporary directory is different every time. +filter_test_output() +{ + local sedscript="" + local raw_output=$TMPDIR/raw-test-output + local i + + cat > "$raw_output" + + # Filter mountpoint and device names. + for i in "${!DEVICES[@]}"; do + sedscript+="s@$TMPDIR/${MOUNTS[$i]}@${MOUNTS[$i]}@g;" + sedscript+="s@${LOOPS[$i]}@${DEVICES[$i]}@g;" + done + + # Filter the path to fscrypt.conf. + sedscript+="s@$FSCRYPT_CONF@FSCRYPT_CONF@g;" + + # Filter policy and protector descriptors. + sedscript+=$(grep -E -o '\<([a-f0-9]{16})|([a-f0-9]{32})\>' \ + "$raw_output" \ + | awk '{ printf "s@\\<" $1 "\\>@desc" NR "@g;" }') + + # Filter any other paths in TMPDIR. + sedscript+="s@$TMPDIR@TMPDIR@g;" + + # At some point, 'bash -c COMMAND' started showing error messages as + # "bash: line 1: " instead of just "bash: ". Filter out the "line 1: ". + sedscript+="s@^bash: line 1: @bash: @;" + + # Work around protojson whitespace randomization. + sedscript+="/^Options: /s@ @ @g;" + sedscript+="s@^Options: @Options: @;" + + sed -e "$sedscript" "$raw_output" +} + +# Prepares to run a test script. +setup_for_test() +{ + local i dev_var mnt_var img mnt loop + + # Start with a clean state. + cleanup + + # ../bin/fscrypt might not be accessible to $TEST_USER. Copy it into + # $TMPDIR so that $TEST_USER is guaranteed to have access to it. + mkdir "$TMPDIR/bin" + cp ../bin/fscrypt "$TMPDIR/bin/" + chmod 755 "$TMPDIR" "$TMPDIR/bin" "$TMPDIR/bin/fscrypt" + + # Create the test filesystems and mountpoints. + LOOPS=() + for i in "${!DEVICES[@]}"; do + dev_var=${DEVICES[$i]} + mnt_var=${MOUNTS[$i]} + img="$TMPDIR/$dev_var" + if ! mkfs.ext4 -O encrypt -F -b 4096 -I 256 "$img" $((1<<15)) \ + &> "$TMPDIR/mkfs.out" + then + cat 1>&2 "$TMPDIR/mkfs.out" + exit 1 + fi + loop=$(losetup --find --show "$img") + LOOPS+=("$loop") + export "$dev_var=$loop" + mnt="$TMPDIR/$mnt_var" + export "$mnt_var=$mnt" + mkdir -p "$mnt" + mount "$loop" "$mnt" + done + + # Give the tests their own "root" mount for storing login protectors, so + # they don't use the real "/". + export FSCRYPT_ROOT_MNT="$MNT_ROOT" + + # Enable consistent output mode. + export FSCRYPT_CONSISTENT_OUTPUT="1" + + # Give the tests their own fscrypt.conf. + export FSCRYPT_CONF="$TMPDIR/fscrypt.conf" + fscrypt setup --time=1ms --quiet --all-users > /dev/null + + # The tests assume kernel support for v2 policies. + if ! grep -E -q '"policy_version": +"2"' "$FSCRYPT_CONF"; then + cat 1>&2 << EOF +ERROR: Can't run these tests because your kernel doesn't support v2 policies. +You need kernel v5.4 or later. +EOF + exit 1 + fi + + # Set up the test filesystems that aren't already set up. + fscrypt setup --quiet --all-users "$MNT" > /dev/null +} + +run_test() +{ + local t=$1 + + # Run the test script. + set +e + "./$1.sh" |& filter_test_output > "$t.out.actual" + status=${PIPESTATUS[0]} + set -e + + # Check for failure status. + if [ "$status" != 0 ]; then + echo 1>&2 "FAILED: $t [exited with failure status $status]" + if [ -s "$t.out.actual" ]; then + if (( $(wc -l "$t.out.actual" | cut -f1 -d' ') > 10 )); then + echo 1>&2 "Last 10 lines of test output:" + tail -n10 "$t.out.actual" | sed 1>&2 's/^/ /' + echo 1>&2 + echo 1>&2 "See $t.out.actual for the full output." + else + echo 1>&2 "Test output:" + sed 1>&2 's/^/ /' < "$t.out.actual" + fi + fi + exit 1 + fi + + # Check for output mismatch. + if ! cmp "$t.out" "$t.out.actual" &> /dev/null; then + if $UPDATE_OUTPUT; then + cp "$t.out.actual" "$t.out" + echo "Updated $t.out" + else + echo 1>&2 "FAILED: $t [output mismatch]" + echo 1>&2 "Differences between $t.out and $t.out.actual:" + echo 1>&2 + diff 1>&2 "$t.out" "$t.out.actual" + exit 1 + fi + fi + rm -f "$t.out.actual" +} + +usage() +{ + cat << EOF +Usage: run.sh [--update-output] [TEST_SCRIPT_NAME]..." +EOF + exit 1 +} + +if ! options=$(getopt -o "" -l "$LONGOPTS" -- "$@"); then + usage +fi +eval set -- "$options" +while (( $# >= 1 )); do + case "$1" in + --update-output) + UPDATE_OUTPUT=true + ;; + --) + shift + break + ;; + --help|*) + usage + ;; + esac + shift +done + +if [ "$(id -u)" != 0 ]; then + echo 1>&2 "ERROR: You must be root to run these tests." + exit 1 +fi + +# Check for prerequisites. +PREREQ_CMDS=(mkfs.ext4 expect keyctl) +PREREQ_PKGS=(e2fsprogs expect keyutils) +for i in ${!PREREQ_CMDS[*]}; do + if ! type -P "${PREREQ_CMDS[$i]}" > /dev/null; then + cat 1>&2 << EOF +ERROR: You must install the '${PREREQ_PKGS[$i]}' package to run these tests. + Try a command like 'sudo apt-get install ${PREREQ_PKGS[$i]}'. +EOF + exit 1 + fi +done + +# Use a consistent umask. +umask 022 + +# Use a consistent locale, to prevent output mismatches. +export LANG=C +export LC_ALL=C + +# Always cleanup fully on exit. +trap cleanup_full EXIT + +# Create a test user, so that we can test non-root use of fscrypt. Give them a +# password, so that we can test creating login passphrase protected directories. +userdel "$TEST_USER" &> /dev/null || true +useradd "$TEST_USER" +echo "$TEST_USER:TEST_USER_PASS" | chpasswd +export TEST_USER + +# Let the tests use $TMPDIR if they need it. +export TMPDIR + +# Make it so that running 'fscrypt' in the tests runs the correct binary. +export PATH="$TMPDIR/bin:$PATH" + +if (( $# >= 1 )); then + # Tests specified on command line. + tests=("$@") +else + # No tests specified on command line. Just run everything. + tests=(t_*.sh) +fi +for t in "${tests[@]}"; do + t=${t%.sh} + echo "Running $t" + setup_for_test + run_test "$t" +done + +echo "All tests passed!" diff --git a/cli-tests/t_change_passphrase.out b/cli-tests/t_change_passphrase.out new file mode 100644 index 0000000..747ed89 --- /dev/null +++ b/cli-tests/t_change_passphrase.out @@ -0,0 +1,32 @@ + +# Create encrypted directory + +# Try to unlock with wrong passphrase +[ERROR] fscrypt unlock: incorrect key provided +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# Change passphrase + +# Try to unlock with old passphrase +[ERROR] fscrypt unlock: incorrect key provided +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# Unlock with new passphrase + +# Try to change passphrase (interactively, mismatch) +spawn fscrypt metadata change-passphrase --protector=MNT:desc1 +Enter old custom passphrase for protector "prot": +Enter new custom passphrase for protector "prot": +Confirm passphrase: +[ERROR] fscrypt metadata change-passphrase: entered passphrases do not match + +# Change passphrase (interactively) +spawn fscrypt metadata change-passphrase --protector=MNT:desc1 +Enter old custom passphrase for protector "prot": +Enter new custom passphrase for protector "prot": +Confirm passphrase: +Passphrase for protector desc1 successfully changed. + +# Lock, then unlock with new passphrase +"MNT/dir" is now locked. +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available diff --git a/cli-tests/t_change_passphrase.sh b/cli-tests/t_change_passphrase.sh new file mode 100755 index 0000000..1360bc2 --- /dev/null +++ b/cli-tests/t_change_passphrase.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Test changing the passphrase of a custom_passphrase protector. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" + +_print_header "Create encrypted directory" +mkdir "$dir" +echo pass1 | fscrypt encrypt --quiet --name=prot --skip-unlock "$dir" + +_print_header "Try to unlock with wrong passphrase" +_expect_failure "echo pass2 | fscrypt unlock --quiet '$dir'" +_expect_failure "mkdir '$dir/subdir'" +protector=$(_get_protector_descriptor "$dir" custom prot) + +_print_header "Change passphrase" +echo $'pass1\npass2' | \ + fscrypt metadata change-passphrase --protector="$MNT:$protector" --quiet + +_print_header "Try to unlock with old passphrase" +_expect_failure "echo pass1 | fscrypt unlock --quiet '$dir'" +_expect_failure "mkdir '$dir/subdir'" + +_print_header "Unlock with new passphrase" +echo pass2 | fscrypt unlock --quiet "$dir" +mkdir "$dir/subdir" +rmdir "$dir/subdir" + +_print_header "Try to change passphrase (interactively, mismatch)" +expect << EOF +spawn fscrypt metadata change-passphrase --protector=$MNT:$protector +expect "Enter old custom passphrase" +send "pass2\r" +expect "Enter new custom passphrase" +send "pass3\r" +expect "Confirm passphrase" +send "bad\r" +expect eof +EOF + +_print_header "Change passphrase (interactively)" +expect << EOF +spawn fscrypt metadata change-passphrase --protector=$MNT:$protector +expect "Enter old custom passphrase" +send "pass2\r" +expect "Enter new custom passphrase" +send "pass3\r" +expect "Confirm passphrase" +send "pass3\r" +expect eof +EOF + +_print_header "Lock, then unlock with new passphrase" +fscrypt lock "$dir" +_expect_failure "mkdir '$dir/subdir'" +echo pass3 | fscrypt unlock --quiet "$dir" +mkdir "$dir/subdir" diff --git a/cli-tests/t_encrypt.out b/cli-tests/t_encrypt.out new file mode 100644 index 0000000..4de05e4 --- /dev/null +++ b/cli-tests/t_encrypt.out @@ -0,0 +1,106 @@ + +# Try to encrypt a nonexistent directory +[ERROR] fscrypt encrypt: no such file or directory +ext4 filesystem "MNT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted + +# Try to encrypt a nonempty directory +[ERROR] fscrypt encrypt: Directory "MNT/dir" cannot be + encrypted because it is non-empty. + +Files cannot be encrypted in-place. Instead, encrypt a new directory, copy the +files into it, and securely delete the original directory. For example: + + mkdir "MNT/dir.new" + fscrypt encrypt "MNT/dir.new" + cp -a -T "MNT/dir" "MNT/dir.new" + find "MNT/dir" -type f -print0 | xargs -0 shred -n1 --remove=unlink + rm -rf "MNT/dir" + mv "MNT/dir.new" "MNT/dir" + +Caution: due to the nature of modern storage devices and filesystems, the +original data may still be recoverable from disk. It's much better to encrypt +your files from the start. +ext4 filesystem "MNT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted + +# => with trailing slash +[ERROR] fscrypt encrypt: Directory "MNT/dir/" cannot be + encrypted because it is non-empty. + +Files cannot be encrypted in-place. Instead, encrypt a new directory, copy the +files into it, and securely delete the original directory. For example: + + mkdir "MNT/dir.new" + fscrypt encrypt "MNT/dir.new" + cp -a -T "MNT/dir" "MNT/dir.new" + find "MNT/dir" -type f -print0 | xargs -0 shred -n1 --remove=unlink + rm -rf "MNT/dir" + mv "MNT/dir.new" "MNT/dir" + +Caution: due to the nature of modern storage devices and filesystems, the +original data may still be recoverable from disk. It's much better to encrypt +your files from the start. +ext4 filesystem "MNT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted + +# Encrypt a directory as non-root user +ext4 filesystem "MNT" has 1 protector and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" + +POLICY UNLOCKED PROTECTORS +desc2 Yes desc1 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc2 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" +ext4 filesystem "MNT" has 1 protector and 1 policy (only including ones owned by fscrypt-test-user or root). +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" + +POLICY UNLOCKED PROTECTORS +desc2 Yes desc1 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc2 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" + +# Try to encrypt an already-encrypted directory +[ERROR] fscrypt encrypt: file or directory "MNT/dir" is + already encrypted + +# Try to encrypt another user's directory as a non-root user +[ERROR] fscrypt encrypt: cannot encrypt "MNT/dir" because + it's owned by another user (root). + + Encryption can only be enabled on a directory you own, + even if you have write access to the directory. +ext4 filesystem "MNT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted diff --git a/cli-tests/t_encrypt.sh b/cli-tests/t_encrypt.sh new file mode 100755 index 0000000..ffd6165 --- /dev/null +++ b/cli-tests/t_encrypt.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# General tests for 'fscrypt encrypt'. For protector-specific tests, see +# t_encrypt_custom, t_encrypt_login, and t_encrypt_raw_key. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" + +begin() +{ + _reset_filesystems + mkdir "$dir" + _print_header "$@" +} + +show_status() +{ + local encrypted=$1 + + fscrypt status "$MNT" + if $encrypted; then + fscrypt status "$dir" + else + _expect_failure "fscrypt status '$dir'" + fi +} + +begin "Try to encrypt a nonexistent directory" +_expect_failure "echo hunter2 | fscrypt encrypt --quiet '$MNT/nonexistent'" +show_status false + +begin "Try to encrypt a nonempty directory" +touch "$dir/file" +_expect_failure "echo hunter2 | fscrypt encrypt --quiet '$dir'" +show_status false +_print_header "=> with trailing slash" +_expect_failure "echo hunter2 | fscrypt encrypt --quiet '$dir/'" +show_status false + +begin "Encrypt a directory as non-root user" +chown "$TEST_USER" "$dir" +_user_do "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" +show_status true +_user_do "fscrypt status '$MNT'" +_user_do "fscrypt status '$dir'" + +_print_header "Try to encrypt an already-encrypted directory" +_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" + +begin "Try to encrypt another user's directory as a non-root user" +_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" +show_status false diff --git a/cli-tests/t_encrypt_custom.out b/cli-tests/t_encrypt_custom.out new file mode 100644 index 0000000..2f1c03c --- /dev/null +++ b/cli-tests/t_encrypt_custom.out @@ -0,0 +1,58 @@ + +# Encrypt with custom passphrase protector +ext4 filesystem "MNT" has 1 protector and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" + +POLICY UNLOCKED PROTECTORS +desc2 Yes desc1 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc2 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "prot" + +# Encrypt with custom passphrase protector, interactively +spawn fscrypt encrypt MNT/dir +The following protector sources are available: +1 - Your login passphrase (pam_passphrase) +2 - A custom passphrase (custom_passphrase) +3 - A raw 256-bit key (raw_key) +Enter the source number for the new protector [2 - custom_passphrase]: 2 +Enter a name for the new protector: prot +Enter custom passphrase for protector "prot": +Confirm passphrase: +"MNT/dir" is now encrypted, unlocked, and ready for use. +ext4 filesystem "MNT" has 1 protector and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc6 No custom protector "prot" + +POLICY UNLOCKED PROTECTORS +desc7 Yes desc6 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc7 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc6 No custom protector "prot" + +# Try to use a custom protector without a name +[ERROR] fscrypt encrypt: custom_passphrase protectors must be named + +Use --name=PROTECTOR_NAME to specify a protector name. +ext4 filesystem "MNT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted diff --git a/cli-tests/t_encrypt_custom.sh b/cli-tests/t_encrypt_custom.sh new file mode 100755 index 0000000..48cbe25 --- /dev/null +++ b/cli-tests/t_encrypt_custom.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# Test encrypting a directory using a custom_passphrase protector. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" + +begin() +{ + _reset_filesystems + mkdir "$dir" + _print_header "$1" +} + +show_status() +{ + local encrypted=$1 + + fscrypt status "$MNT" + if $encrypted; then + fscrypt status "$dir" + else + _expect_failure "fscrypt status '$dir'" + fi +} + +begin "Encrypt with custom passphrase protector" +echo hunter2 | fscrypt encrypt --quiet --name=prot "$dir" +show_status true + +begin "Encrypt with custom passphrase protector, interactively" +expect << EOF +spawn fscrypt encrypt "$dir" +expect "Enter the source number for the new protector" +send "2\r" +expect "Enter a name for the new protector:" +send "prot\r" +expect "Enter custom passphrase" +send "hunter2\r" +expect "Confirm passphrase" +send "hunter2\r" +expect eof +EOF +show_status true + +begin "Try to use a custom protector without a name" +_expect_failure "echo hunter2 | fscrypt encrypt --quiet '$dir'" +show_status false diff --git a/cli-tests/t_encrypt_login.out b/cli-tests/t_encrypt_login.out new file mode 100644 index 0000000..b1f6c82 --- /dev/null +++ b/cli-tests/t_encrypt_login.out @@ -0,0 +1,209 @@ + +# Encrypt with login protector + +IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for + important recovery instructions. It is *strongly recommended* to + record the recovery passphrase in a secure location; otherwise you + will lose access to this directory if you reinstall the operating + system or move this filesystem to another system. + +ext4 filesystem "MNT" has 2 protectors and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc1 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc2 No custom protector "Recovery passphrase for dir" + +POLICY UNLOCKED PROTECTORS +desc3 Yes desc1, desc2 +ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc1 No login protector for fscrypt-test-user +"MNT/dir" is encrypted with fscrypt. + +Policy: desc3 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 2 protectors: +PROTECTOR LINKED DESCRIPTION +desc1 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc2 No custom protector "Recovery passphrase for dir" + +# => Lock, then unlock with login passphrase +"MNT/dir" is now locked. + +# => Lock, then unlock with recovery passphrase +"MNT/dir" is now locked. + +# Encrypt with login protector, interactively +spawn fscrypt encrypt MNT/dir +The following protector sources are available: +1 - Your login passphrase (pam_passphrase) +2 - A custom passphrase (custom_passphrase) +3 - A raw 256-bit key (raw_key) +Enter the source number for the new protector [2 - custom_passphrase]: 1 + +IMPORTANT: Before continuing, ensure you have properly set up your system for + login protectors. See + https://github.com/google/fscrypt#setting-up-for-login-protectors + +Enter login passphrase for fscrypt-test-user: + +IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for + important recovery instructions. It is *strongly recommended* to + record the recovery passphrase in a secure location; otherwise you + will lose access to this directory if you reinstall the operating + system or move this filesystem to another system. + +"MNT/dir" is now encrypted, unlocked, and ready for use. +ext4 filesystem "MNT" has 2 protectors and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc10 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc11 No custom protector "Recovery passphrase for dir" + +POLICY UNLOCKED PROTECTORS +desc12 Yes desc10, desc11 +ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc10 No login protector for fscrypt-test-user +"MNT/dir" is encrypted with fscrypt. + +Policy: desc12 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 2 protectors: +PROTECTOR LINKED DESCRIPTION +desc10 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc11 No custom protector "Recovery passphrase for dir" + +# Encrypt with login protector as root + +IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for + important recovery instructions. It is *strongly recommended* to + record the recovery passphrase in a secure location; otherwise you + will lose access to this directory if you reinstall the operating + system or move this filesystem to another system. + +ext4 filesystem "MNT" has 2 protectors and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc19 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc20 No custom protector "Recovery passphrase for dir" + +POLICY UNLOCKED PROTECTORS +desc21 Yes desc19, desc20 +ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc19 No login protector for fscrypt-test-user +"MNT/dir" is encrypted with fscrypt. + +Policy: desc21 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 2 protectors: +PROTECTOR LINKED DESCRIPTION +desc19 Yes (MNT_ROOT) login protector for fscrypt-test-user +desc20 No custom protector "Recovery passphrase for dir" + +Protector is owned by fscrypt-test-user:fscrypt-test-user +"MNT/dir" is now locked. +"MNT/dir" is now locked. + +# Encrypt with login protector with --no-recovery +ext4 filesystem "MNT" has 1 protector and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc28 Yes (MNT_ROOT) login protector for fscrypt-test-user + +POLICY UNLOCKED PROTECTORS +desc29 Yes desc28 +ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc28 No login protector for fscrypt-test-user +"MNT/dir" is encrypted with fscrypt. + +Policy: desc29 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc28 Yes (MNT_ROOT) login protector for fscrypt-test-user + +# Encrypt with login protector on root fs (shouldn't generate a recovery passphrase) +"MNT_ROOT/dir" is encrypted with fscrypt. + +Policy: desc34 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc35 No login protector for fscrypt-test-user +ext4 filesystem "MNT_ROOT" has 1 protector and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc35 No login protector for fscrypt-test-user + +POLICY UNLOCKED PROTECTORS +desc34 Yes desc35 + +# Try to give a login protector a name +[ERROR] fscrypt encrypt: cannot assign name "prot" to new login protector for + user "fscrypt-test-user" because login protectors are + identified by user, not by name. + +To fix this, don't specify the --name=PROTECTOR_NAME option. +ext4 filesystem "MNT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +ext4 filesystem "MNT_ROOT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted + +# Try to use the wrong login passphrase +[ERROR] fscrypt encrypt: incorrect login passphrase +ext4 filesystem "MNT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +ext4 filesystem "MNT_ROOT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted + +# Test that linked protector works even if UUID link is broken + +IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for + important recovery instructions. It is *strongly recommended* to + record the recovery passphrase in a secure location; otherwise you + will lose access to this directory if you reinstall the operating + system or move this filesystem to another system. + +ext4 filesystem "MNT" has 2 protectors and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc39 No custom protector "Recovery passphrase for dir" +desc40 Yes (MNT_ROOT) login protector for fscrypt-test-user + +POLICY UNLOCKED PROTECTORS +desc41 Yes desc40, desc39 diff --git a/cli-tests/t_encrypt_login.sh b/cli-tests/t_encrypt_login.sh new file mode 100755 index 0000000..b6ae2d8 --- /dev/null +++ b/cli-tests/t_encrypt_login.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +# Test encrypting a directory using a login (pam_passphrase) protector. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" + +begin() +{ + _reset_filesystems + mkdir "$dir" + _print_header "$1" +} + +show_status() +{ + local encrypted=$1 + + fscrypt status "$MNT" + fscrypt status "$MNT_ROOT" + if $encrypted; then + fscrypt status "$dir" + else + _expect_failure "fscrypt status '$dir'" + fi +} + +begin "Encrypt with login protector" +chown "$TEST_USER" "$dir" +_user_do "echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase '$dir'" +show_status true +recovery_passphrase=$(grep -E '^ +[a-z]{20}$' "$dir/fscrypt_recovery_readme.txt" | sed 's/^ +//') +recovery_protector=$(_get_protector_descriptor "$MNT" custom 'Recovery passphrase for dir') +login_protector=$(_get_login_descriptor) +_print_header "=> Lock, then unlock with login passphrase" +_user_do "fscrypt lock '$dir'" +# FIXME: should we be able to use $MNT:$login_protector here? +_user_do "echo TEST_USER_PASS | fscrypt unlock --quiet --unlock-with=$MNT_ROOT:$login_protector '$dir'" +_print_header "=> Lock, then unlock with recovery passphrase" +_user_do "fscrypt lock '$dir'" +_user_do "echo $recovery_passphrase | fscrypt unlock --quiet --unlock-with=$MNT:$recovery_protector '$dir'" + +begin "Encrypt with login protector, interactively" +chown "$TEST_USER" "$dir" +_user_do expect << EOF +spawn fscrypt encrypt "$dir" +expect "Enter the source number for the new protector" +send "1\r" +expect "Enter login passphrase" +send "TEST_USER_PASS\r" +expect eof +EOF +show_status true + +begin "Encrypt with login protector as root" +echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --user="$TEST_USER" "$dir" +show_status true +# The newly-created login protector should be owned by the user, not root. +# This is partly redundant with the below check, but we might as well test both. +login_protector=$(_get_login_descriptor) +owner=$(stat -c "%U:%G" "$MNT_ROOT/.fscrypt/protectors/$login_protector") +echo -e "\nProtector is owned by $owner" +# The user should be able to lock and unlock the directory themselves. This +# tests that the fscrypt metadata file permissions got set appropriately when +# root set up the encryption on the user's behalf. +chown "$TEST_USER" "$dir" +_user_do "fscrypt lock $dir" +_user_do "echo TEST_USER_PASS | fscrypt unlock $dir --quiet --unlock-with=$MNT_ROOT:$login_protector" +_user_do "fscrypt lock $dir" + +begin "Encrypt with login protector with --no-recovery" +chown "$TEST_USER" "$dir" +_user_do "echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --no-recovery '$dir'" +show_status true + +begin "Encrypt with login protector on root fs (shouldn't generate a recovery passphrase)" +mkdir "$MNT_ROOT/dir" +chown "$TEST_USER" "$MNT_ROOT/dir" +_user_do "echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --no-recovery '$MNT_ROOT/dir'" +fscrypt status "$MNT_ROOT/dir" +fscrypt status "$MNT_ROOT" +rmdir "$MNT_ROOT/dir" + +begin "Try to give a login protector a name" +chown "$TEST_USER" "$dir" +_user_do_and_expect_failure \ + "echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --name=prot '$dir'" +show_status false + +begin "Try to use the wrong login passphrase" +chown "$TEST_USER" "$dir" +_user_do_and_expect_failure \ + "echo wrong_passphrase | fscrypt encrypt --quiet --source=pam_passphrase '$dir'" +show_status false + +begin "Test that linked protector works even if UUID link is broken" +echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --user="$TEST_USER" "$dir" +protector=$(_get_login_descriptor) +link_file=$MNT/.fscrypt/protectors/$protector.link +[ -e "$link_file" ] || _fail "$link_file does not exist" +sed -i 's/UUID=.*/UUID=00000000-0000-0000-0000-000000000000/' "$link_file" +fscrypt status "$MNT" diff --git a/cli-tests/t_encrypt_raw_key.out b/cli-tests/t_encrypt_raw_key.out new file mode 100644 index 0000000..78aa0b7 --- /dev/null +++ b/cli-tests/t_encrypt_raw_key.out @@ -0,0 +1,74 @@ + +# Encrypt with raw_key protector from file +ext4 filesystem "MNT" has 1 protector and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc1 No raw key protector "prot" + +POLICY UNLOCKED PROTECTORS +desc2 Yes desc1 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc2 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc1 No raw key protector "prot" + +# Encrypt with raw_key protector from stdin +ext4 filesystem "MNT" has 1 protector and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc6 No raw key protector "prot" + +POLICY UNLOCKED PROTECTORS +desc7 Yes desc6 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc7 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc6 No raw key protector "prot" + +# Try to encrypt with raw_key protector from file, using wrong key length +[ERROR] fscrypt encrypt: TMPDIR/raw_key: key file must be 32 bytes +ext4 filesystem "MNT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted + +# Try to encrypt with raw_key protector from stdin, using wrong key length +[ERROR] fscrypt encrypt: unexpected EOF +ext4 filesystem "MNT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted + +# Encrypt with raw_key protector from file, unlock from stdin +"MNT/dir" is now locked. +ext4 filesystem "MNT" has 1 protector and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc11 No raw key protector "prot" + +POLICY UNLOCKED PROTECTORS +desc12 Yes desc11 +"MNT/dir" is encrypted with fscrypt. + +Policy: desc12 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc11 No raw key protector "prot" diff --git a/cli-tests/t_encrypt_raw_key.sh b/cli-tests/t_encrypt_raw_key.sh new file mode 100755 index 0000000..e5c6d20 --- /dev/null +++ b/cli-tests/t_encrypt_raw_key.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Test encrypting a directory using a raw_key protector. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" +raw_key_file="$TMPDIR/raw_key" + +begin() +{ + _reset_filesystems + mkdir "$dir" + _print_header "$1" +} + +show_status() +{ + local encrypted=$1 + + fscrypt status "$MNT" + if $encrypted; then + fscrypt status "$dir" + else + _expect_failure "fscrypt status '$dir'" + fi +} + +begin "Encrypt with raw_key protector from file" +head -c 32 /dev/urandom > "$raw_key_file" +fscrypt encrypt --quiet --name=prot --source=raw_key --key="$raw_key_file" "$dir" +show_status true + +begin "Encrypt with raw_key protector from stdin" +head -c 32 /dev/urandom | fscrypt encrypt --quiet --name=prot --source=raw_key "$dir" +show_status true + +begin "Try to encrypt with raw_key protector from file, using wrong key length" +head -c 16 /dev/urandom > "$raw_key_file" +_expect_failure "fscrypt encrypt --quiet --name=prot --source=raw_key --key='$raw_key_file' '$dir'" +show_status false + +begin "Try to encrypt with raw_key protector from stdin, using wrong key length" +_expect_failure "head -c 16 /dev/urandom | fscrypt encrypt --quiet --name=prot --source=raw_key '$dir'" +show_status false + +begin "Encrypt with raw_key protector from file, unlock from stdin" +head -c 32 /dev/urandom > "$raw_key_file" +fscrypt encrypt --quiet --name=prot --source=raw_key --key="$raw_key_file" "$dir" +fscrypt lock "$dir" +fscrypt unlock --quiet "$dir" < "$raw_key_file" +show_status true diff --git a/cli-tests/t_lock.out b/cli-tests/t_lock.out new file mode 100644 index 0000000..ce27713 --- /dev/null +++ b/cli-tests/t_lock.out @@ -0,0 +1,102 @@ + +# Encrypt directory +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Lock directory +"MNT/dir" is now locked. + +# => filenames should be in encrypted form +cat: MNT/dir/file: No such file or directory + +# => shouldn't be able to create a subdirectory +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# Unlock directory +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +contents + +# Try to lock directory while files busy +[ERROR] fscrypt lock: Directory was incompletely locked because some files are + still open. These files remain accessible. + +Try killing any processes using files in the directory, for example using: + + find "MNT/dir" -print0 | xargs -0 fuser -k + +Then re-run: + + fscrypt lock "MNT/dir" + +# => status should be incompletely locked +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Partially (incompletely locked) + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# => open file should still be readable +contents + +# => shouldn't be able to create a new file +bash: MNT/dir/file2: Required key not available + +# Finish locking directory +"MNT/dir" is now locked. +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +cat: MNT/dir/file: No such file or directory +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# Try to lock directory while other user has unlocked +[ERROR] fscrypt lock: Directory "MNT/dir" couldn't be fully + locked because other user(s) have unlocked it. + +If you want to force the directory to be locked, use: + + sudo fscrypt lock --all-users "MNT/dir" +contents +"MNT/dir" is now locked. +cat: MNT/dir/file: No such file or directory + +# Try to operate on locked regular file +"MNT/dir" is now locked. +[ERROR] fscrypt status: cannot operate on locked regular file + "MNT/file" + +It is not possible to operate directly on a locked regular file, since the +kernel does not support this. Specify the parent directory instead. (For loose +files, any directory with the file's policy works.) +[ERROR] fscrypt unlock: cannot operate on locked regular file + "MNT/file" + +It is not possible to operate directly on a locked regular file, since the +kernel does not support this. Specify the parent directory instead. (For loose +files, any directory with the file's policy works.) diff --git a/cli-tests/t_lock.sh b/cli-tests/t_lock.sh new file mode 100755 index 0000000..e5df4df --- /dev/null +++ b/cli-tests/t_lock.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Test locking a directory. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" +mkdir "$dir" + +_print_header "Encrypt directory" +echo hunter2 | fscrypt encrypt --quiet --name=prot "$dir" +fscrypt status "$dir" +echo contents > "$dir/file" + +_print_header "Lock directory" +fscrypt lock "$dir" +_print_header "=> filenames should be in encrypted form" +_expect_failure "cat '$dir/file'" +_print_header "=> shouldn't be able to create a subdirectory" +_expect_failure "mkdir '$dir/subdir'" + +_print_header "Unlock directory" +echo hunter2 | fscrypt unlock "$dir" +fscrypt status "$dir" +cat "$dir/file" + +_print_header "Try to lock directory while files busy" +exec 3<"$dir/file" +_expect_failure "fscrypt lock '$dir'" +_print_header "=> status should be incompletely locked" +fscrypt status "$dir" +_print_header "=> open file should still be readable" +cat "$dir/file" +_print_header "=> shouldn't be able to create a new file" +_expect_failure "bash -c \"echo contents > '$dir/file2'\"" + +_print_header "Finish locking directory" +exec 3<&- +fscrypt lock "$dir" +fscrypt status "$dir" +_expect_failure "cat '$dir/file'" +_expect_failure "mkdir '$dir/subdir'" + +_print_header "Try to lock directory while other user has unlocked" +rm -rf "$dir" +mkdir "$dir" +chown "$TEST_USER" "$dir" +_user_do "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" +_user_do "echo contents > $dir/file" +_expect_failure "fscrypt lock '$dir'" +cat "$dir/file" +fscrypt lock --all-users "$dir" +_expect_failure "cat '$dir/file'" + +_print_header "Try to operate on locked regular file" +_reset_filesystems +rm -rf "$dir" +mkdir "$dir" +echo hunter2 | fscrypt encrypt --quiet --name=prot "$dir" +echo contents > "$dir/file" +mv "$dir/file" "$MNT/file" # Make it a loose encrypted file. +fscrypt lock "$dir" +_expect_failure "fscrypt status '$MNT/file'" +_expect_failure "fscrypt unlock '$MNT/file'" diff --git a/cli-tests/t_metadata.out b/cli-tests/t_metadata.out new file mode 100644 index 0000000..bbcc0f2 --- /dev/null +++ b/cli-tests/t_metadata.out @@ -0,0 +1,19 @@ +ext4 filesystem "MNT" has 3 protectors and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "foo" +desc2 No custom protector "bar" +desc3 No custom protector "baz" + +POLICY UNLOCKED PROTECTORS +desc4 No desc1, desc2, desc3 +ext4 filesystem "MNT" has 2 protectors and 1 policy. +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc1 No custom protector "foo" +desc2 No custom protector "bar" + +POLICY UNLOCKED PROTECTORS +desc4 No desc1 diff --git a/cli-tests/t_metadata.sh b/cli-tests/t_metadata.sh new file mode 100755 index 0000000..e688eda --- /dev/null +++ b/cli-tests/t_metadata.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Test 'fscrypt metadata'. + +cd "$(dirname "$0")" +. common.sh + +# Create three protectors, and a policy protected by them. +echo foo | fscrypt metadata create protector "$MNT" \ + --quiet --name=foo --source=custom_passphrase +echo bar | fscrypt metadata create protector "$MNT" \ + --quiet --name=bar --source=custom_passphrase +echo baz | fscrypt metadata create protector "$MNT" \ + --quiet --name=baz --source=custom_passphrase +prot_foo=$MNT:$(_get_protector_descriptor "$MNT" custom foo) +prot_bar=$MNT:$(_get_protector_descriptor "$MNT" custom bar) +desc_baz=$(_get_protector_descriptor "$MNT" custom baz) +prot_baz=$MNT:$desc_baz +echo foo | fscrypt metadata create policy "$MNT" --quiet \ + --protector="$prot_foo" +policy=$MNT:$(fscrypt status "$MNT" | grep -A10 "^POLICY" | \ + tail -1 | awk '{print $1}') +echo -e "bar\nfoo" | fscrypt metadata add-protector-to-policy --quiet \ + --policy="$policy" --protector="$prot_bar" +echo -e "baz\nfoo" | fscrypt metadata add-protector-to-policy --quiet \ + --policy="$policy" --protector="$prot_baz" --unlock-with="$prot_foo" +fscrypt status "$MNT" + +# Remove two of the protectors from the policy. +# Make sure that this works even if the protector was already deleted. +fscrypt metadata remove-protector-from-policy --quiet --force \ + --policy="$policy" --protector="$prot_bar" +rm "$MNT/.fscrypt/protectors/$desc_baz" +fscrypt metadata remove-protector-from-policy --quiet --force \ + --policy="$policy" --protector="$prot_baz" +fscrypt status "$MNT" diff --git a/cli-tests/t_not_enabled.out b/cli-tests/t_not_enabled.out new file mode 100644 index 0000000..07c9aa3 --- /dev/null +++ b/cli-tests/t_not_enabled.out @@ -0,0 +1,63 @@ + +# Disable encryption on DEV + +# Try to encrypt a directory when encryption is disabled +[ERROR] fscrypt encrypt: encryption not enabled on filesystem + MNT (DEV). + +To enable encryption support on this filesystem, run: + + sudo tune2fs -O encrypt "DEV" + +Also ensure that your kernel has CONFIG_FS_ENCRYPTION=y. See the documentation +for more details. + +# Try to unlock a directory when encryption is disabled +[ERROR] fscrypt unlock: encryption not enabled on filesystem + MNT (DEV). + +To enable encryption support on this filesystem, run: + + sudo tune2fs -O encrypt "DEV" + +Also ensure that your kernel has CONFIG_FS_ENCRYPTION=y. See the documentation +for more details. + +# Try to lock a directory when encryption is disabled +[ERROR] fscrypt lock: encryption not enabled on filesystem + MNT (DEV). + +To enable encryption support on this filesystem, run: + + sudo tune2fs -O encrypt "DEV" + +Also ensure that your kernel has CONFIG_FS_ENCRYPTION=y. See the documentation +for more details. + +# Check for additional message when GRUB appears to be installed +[ERROR] fscrypt encrypt: encryption not enabled on filesystem + MNT (DEV). + +To enable encryption support on this filesystem, run: + + sudo tune2fs -O encrypt "DEV" + +WARNING: you seem to have GRUB installed on this filesystem. Before doing the +above, make sure you are using GRUB v2.04 or later; otherwise your system will +become unbootable. + +Also ensure that your kernel has CONFIG_FS_ENCRYPTION=y. See the documentation +for more details. + +# Enable encryption on DEV + +# Encrypt a directory when encryption was just enabled +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" diff --git a/cli-tests/t_not_enabled.sh b/cli-tests/t_not_enabled.sh new file mode 100755 index 0000000..fae1094 --- /dev/null +++ b/cli-tests/t_not_enabled.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Test that fscrypt fails when the filesystem doesn't have the encrypt feature +# enabled. Then test enabling it. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" +mkdir "$dir" + +_print_header "Disable encryption on $DEV" +count_before=$(_get_enabled_fs_count) +umount "$MNT" +_run_noisy_command "debugfs -w -R 'feature -encrypt' '$DEV'" +mount "$DEV" "$MNT" +count_after=$(_get_enabled_fs_count) +(( count_after == count_before - 1 )) || _fail "wrong enabled count" + +_print_header "Try to encrypt a directory when encryption is disabled" +_expect_failure "fscrypt encrypt '$dir'" + +_print_header "Try to unlock a directory when encryption is disabled" +_expect_failure "fscrypt unlock '$dir'" + +_print_header "Try to lock a directory when encryption is disabled" +_expect_failure "fscrypt lock '$dir'" + +_print_header "Check for additional message when GRUB appears to be installed" +mkdir -p "$MNT/boot/grub" +_expect_failure "fscrypt encrypt '$dir'" +rm -r "${MNT:?}/boot" + +_print_header "Enable encryption on $DEV" +_run_noisy_command "tune2fs -O encrypt '$DEV'" + +_print_header "Encrypt a directory when encryption was just enabled" +echo hunter2 | fscrypt encrypt --quiet --source=custom_passphrase --name=prot "$dir" +fscrypt status "$dir" diff --git a/cli-tests/t_not_supported.out b/cli-tests/t_not_supported.out new file mode 100644 index 0000000..68e0897 --- /dev/null +++ b/cli-tests/t_not_supported.out @@ -0,0 +1,9 @@ + +# Mount tmpfs + +# Try to create fscrypt metadata on tmpfs +[ERROR] fscrypt setup: filesystem type tmpfs is not supported for fscrypt setup + +# Try to encrypt a directory on tmpfs +[ERROR] fscrypt encrypt: This kernel doesn't support encryption on tmpfs + filesystems. diff --git a/cli-tests/t_not_supported.sh b/cli-tests/t_not_supported.sh new file mode 100755 index 0000000..8b52392 --- /dev/null +++ b/cli-tests/t_not_supported.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Test that fscrypt fails when the filesystem doesn't support encryption. + +cd "$(dirname "$0")" +. common.sh + +_print_header "Mount tmpfs" +umount "$MNT" +mount tmpfs -t tmpfs -o size=128m "$MNT" + +_print_header "Try to create fscrypt metadata on tmpfs" +_expect_failure "fscrypt setup --quiet '$MNT'" + +_print_header "Try to encrypt a directory on tmpfs" +mkdir "$MNT/dir" +_expect_failure "fscrypt encrypt '$MNT/dir'" diff --git a/cli-tests/t_passphrase_hashing.out b/cli-tests/t_passphrase_hashing.out new file mode 100644 index 0000000..e69de29 diff --git a/cli-tests/t_passphrase_hashing.sh b/cli-tests/t_passphrase_hashing.sh new file mode 100755 index 0000000..a67dd7c --- /dev/null +++ b/cli-tests/t_passphrase_hashing.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Test that the passphrase hashing seems to take long enough. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" + +# Test encrypting 5 dirs with default of 1s. +fscrypt setup --force --quiet +start_time=$(date +%s) +for i in $(seq 5); do + rm -rf "$dir" + mkdir "$dir" + echo hunter2 | fscrypt encrypt --quiet --name="prot$i" "$dir" +done +end_time=$(date +%s) +elapsed=$((end_time - start_time)) +if (( elapsed <= 3 )); then + _fail "Passphrase hashing was much faster than expected! (expected about 5 x 1 == 5s, got ${elapsed}s)" +fi + +# Test encrypting 1 dir with difficulty overridden to 5s. +fscrypt setup --force --quiet --time=5s +start_time=$(date +%s) +rm -rf "$dir" +mkdir "$dir" +echo hunter2 | fscrypt encrypt --quiet --name=prot6 "$dir" +end_time=$(date +%s) +elapsed=$((end_time - start_time)) +if (( elapsed <= 3 )); then + _fail "Passphrase hashing was much faster than expected! (expected about 5s, got ${elapsed}s)" +fi diff --git a/cli-tests/t_setup.out b/cli-tests/t_setup.out new file mode 100644 index 0000000..ab0052c --- /dev/null +++ b/cli-tests/t_setup.out @@ -0,0 +1,51 @@ + +# fscrypt setup creates fscrypt.conf +Defaulting to policy_version 2 because kernel supports it. +Customizing passphrase hashing difficulty for this system... +Created global config file at "FSCRYPT_CONF". +Skipping creating MNT_ROOT/.fscrypt because it already exists. + +# fscrypt setup creates fscrypt.conf and /.fscrypt +Defaulting to policy_version 2 because kernel supports it. +Customizing passphrase hashing difficulty for this system... +Created global config file at "FSCRYPT_CONF". +Allow users other than root to create fscrypt metadata on this filesystem? (See +https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem) [y/N] Metadata directories created at "MNT_ROOT/.fscrypt", writable by everyone. + +# fscrypt setup when fscrypt.conf already exists (cancel) +Replace "FSCRYPT_CONF"? [y/N] [ERROR] fscrypt setup: operation canceled + +# fscrypt setup when fscrypt.conf already exists (cancel 2) +Replace "FSCRYPT_CONF"? [y/N] [ERROR] fscrypt setup: operation canceled + +# fscrypt setup when fscrypt.conf already exists (accept) +Replace "FSCRYPT_CONF"? [y/N] Defaulting to policy_version 2 because kernel supports it. +Customizing passphrase hashing difficulty for this system... +Created global config file at "FSCRYPT_CONF". +Skipping creating MNT_ROOT/.fscrypt because it already exists. + +# fscrypt setup --quiet when fscrypt.conf already exists +[ERROR] fscrypt setup: operation would be destructive + +If desired, use --force to automatically run destructive operations. + +# fscrypt setup --quiet --force when fscrypt.conf already exists + +# fscrypt setup filesystem +Allow users other than root to create fscrypt metadata on this filesystem? (See +https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem) [y/N] Metadata directories created at "MNT/.fscrypt", writable by everyone. + +# fscrypt setup filesystem (already set up) +[ERROR] fscrypt setup: filesystem MNT is already setup for + use with fscrypt + +# no config file +[ERROR] fscrypt setup: "FSCRYPT_CONF" doesn't exist + +Run "sudo fscrypt setup" to create this file. + +# bad config file +[ERROR] fscrypt setup: "FSCRYPT_CONF" is invalid: proto: + syntax error (line 1:1): invalid value bad + +Either fix this file manually, or run "sudo fscrypt setup" to recreate it. diff --git a/cli-tests/t_setup.sh b/cli-tests/t_setup.sh new file mode 100755 index 0000000..f7e302d --- /dev/null +++ b/cli-tests/t_setup.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Test 'fscrypt setup'. + +cd "$(dirname "$0")" +. common.sh + +# global setup + +_print_header "fscrypt setup creates fscrypt.conf" +rm -f "$FSCRYPT_CONF" +fscrypt setup --time=1ms + +_print_header "fscrypt setup creates fscrypt.conf and /.fscrypt" +_rm_metadata "$MNT_ROOT" +rm -f "$FSCRYPT_CONF" +echo y | fscrypt setup --time=1ms +[ -e "$MNT_ROOT/.fscrypt" ] + +_print_header "fscrypt setup when fscrypt.conf already exists (cancel)" +_expect_failure "echo | fscrypt setup --time=1ms" + +_print_header "fscrypt setup when fscrypt.conf already exists (cancel 2)" +_expect_failure "echo N | fscrypt setup --time=1ms" + +_print_header "fscrypt setup when fscrypt.conf already exists (accept)" +echo y | fscrypt setup --time=1ms + +_print_header "fscrypt setup --quiet when fscrypt.conf already exists" +_expect_failure "fscrypt setup --quiet --time=1ms" + +_print_header "fscrypt setup --quiet --force when fscrypt.conf already exists" +fscrypt setup --quiet --force --time=1ms + + +# filesystem setup + +_print_header "fscrypt setup filesystem" +_rm_metadata "$MNT" +echo y | fscrypt setup "$MNT" +[ -e "$MNT/.fscrypt" ] + +_print_header "fscrypt setup filesystem (already set up)" +_expect_failure "fscrypt setup '$MNT'" + +_print_header "no config file" +rm -f "$FSCRYPT_CONF" +_expect_failure "fscrypt setup '$MNT'" + +_print_header "bad config file" +echo bad > "$FSCRYPT_CONF" +_expect_failure "fscrypt setup '$MNT'" diff --git a/cli-tests/t_single_user.out b/cli-tests/t_single_user.out new file mode 100644 index 0000000..d038d52 --- /dev/null +++ b/cli-tests/t_single_user.out @@ -0,0 +1,30 @@ +ext4 filesystem "MNT" has 0 protectors and 0 policies. +Only root can create fscrypt metadata on this filesystem. + +ext4 filesystem "MNT" has 0 protectors and 0 policies (only including ones owned by fscrypt-test-user or root). +Only root can create fscrypt metadata on this filesystem. + + +# Encrypt, lock, and unlock as root +"MNT/dir" is now locked. + +# Encrypt as root with user's login protector + +IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for + important recovery instructions. It is *strongly recommended* to + record the recovery passphrase in a secure location; otherwise you + will lose access to this directory if you reinstall the operating + system or move this filesystem to another system. + +Protector desc1 no longer protecting policy desc2. +"MNT/dir" is now locked. +Enter login passphrase for fscrypt-test-user: "MNT/dir" is now unlocked and ready for use. + +# Encrypt as user (should fail) +[ERROR] fscrypt encrypt: user lacks permission to create fscrypt metadata on + MNT + +For how to allow users to create fscrypt metadata on a filesystem, refer to +https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem + +# Encrypt as user if they set up filesystem (should succeed) diff --git a/cli-tests/t_single_user.sh b/cli-tests/t_single_user.sh new file mode 100755 index 0000000..c569f20 --- /dev/null +++ b/cli-tests/t_single_user.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# Test 'fscrypt setup' without --all-users. + +cd "$(dirname "$0")" +. common.sh + +_rm_metadata "$MNT_ROOT" +_rm_metadata "$MNT" +rm "$FSCRYPT_CONF" +fscrypt setup --time=1ms --quiet +fscrypt setup --time=1ms --quiet "$MNT" +fscrypt status "$MNT" +_user_do "fscrypt status \"$MNT\"" + +dir=$MNT/dir + +begin() +{ + _reset_filesystems + mkdir "$dir" + _print_header "$1" +} + +begin "Encrypt, lock, and unlock as root" +echo hunter2 | fscrypt encrypt --quiet --name=dir --skip-unlock "$dir" +echo hunter2 | fscrypt unlock --quiet "$dir" +fscrypt lock "$dir" + +begin "Encrypt as root with user's login protector" +echo TEST_USER_PASS | fscrypt encrypt --quiet --source=pam_passphrase --user="$TEST_USER" "$dir" +# The user should be able to update the policy and protectors created by the +# above command themselves. The easiest way to test this is by updating the +# policy to remove the auto-generated recovery protector. This verifies that +# (a) the policy was made owned by the user, and that (b) policy updates fall +# back to overwrites when the process cannot write to the containing directory. +# (It would be better to test updating the protectors too, but this is the +# easiest test to do here.) +policy=$(fscrypt status "$dir" | awk '/Policy/{print $2}') +recovery_protector=$(_get_protector_descriptor "$MNT" custom 'Recovery passphrase for dir') +_user_do "fscrypt metadata remove-protector-from-policy --force --protector=$MNT:$recovery_protector --policy=$MNT:$policy" +chown "$TEST_USER" "$dir" +_user_do "fscrypt lock $dir" +_user_do "echo TEST_USER_PASS | fscrypt unlock $dir" + +begin "Encrypt as user (should fail)" +chown "$TEST_USER" "$dir" +_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=dir --skip-unlock \"$dir\"" + +begin "Encrypt as user if they set up filesystem (should succeed)" +_rm_metadata "$MNT" +chown "$TEST_USER" "$MNT" +chown "$TEST_USER" "$dir" +_user_do "fscrypt setup --time=1ms --quiet $MNT" +_user_do "echo hunter2 | fscrypt encrypt --quiet --name=dir3 --skip-unlock \"$dir\"" diff --git a/cli-tests/t_status.out b/cli-tests/t_status.out new file mode 100644 index 0000000..058c62c --- /dev/null +++ b/cli-tests/t_status.out @@ -0,0 +1,50 @@ + +# Get status of setup mountpoint via global status +ext4 supported Yes +ext4 supported Yes + +# Get status of setup mountpoint +ext4 filesystem "MNT" has 0 protectors and 0 policies. +All users can create fscrypt metadata on this filesystem. + +ext4 filesystem "MNT" has 0 protectors and 0 policies (only including ones owned by fscrypt-test-user or root). +All users can create fscrypt metadata on this filesystem. + + +# Get status of unencrypted directory on setup mountpoint +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted + +# Remove fscrypt metadata from MNT + +# Check enabled / setup count again + +# Get status of not-setup mounntpoint via global status +ext4 supported No +ext4 supported No + +# Get status of not-setup mountpoint +[ERROR] fscrypt status: filesystem MNT is not setup for use + with fscrypt + +Run "sudo fscrypt setup MNT" to use fscrypt on this +filesystem. +[ERROR] fscrypt status: filesystem MNT is not setup for use + with fscrypt + +Run "sudo fscrypt setup MNT" to use fscrypt on this +filesystem. + +# Get status of unencrypted directory on not-setup mountpoint +[ERROR] fscrypt status: filesystem MNT is not setup for use + with fscrypt + +Run "sudo fscrypt setup MNT" to use fscrypt on this +filesystem. +[ERROR] fscrypt status: filesystem MNT is not setup for use + with fscrypt + +Run "sudo fscrypt setup MNT" to use fscrypt on this +filesystem. diff --git a/cli-tests/t_status.sh b/cli-tests/t_status.sh new file mode 100755 index 0000000..cfc3616 --- /dev/null +++ b/cli-tests/t_status.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Test getting global, filesystem, and unencrypted directory status +# when the filesystem is or isn't set up for fscrypt. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" +mkdir "$dir" + +filter_mnt_status() +{ + awk '$1 == "'"$MNT"'" { print $3, $4, $5 }' +} + +# Initially, $MNT has encryption enabled and fscrypt setup. + +enabled_count1=$(_get_enabled_fs_count) +setup_count1=$(_get_setup_fs_count) + + +_print_header "Get status of setup mountpoint via global status" +fscrypt status | filter_mnt_status +_user_do "fscrypt status" | filter_mnt_status + +_print_header "Get status of setup mountpoint" +fscrypt status "$MNT" +_user_do "fscrypt status '$MNT'" + +_print_header "Get status of unencrypted directory on setup mountpoint" +_expect_failure "fscrypt status '$dir'" +_user_do_and_expect_failure "fscrypt status '$dir'" + +_print_header "Remove fscrypt metadata from $MNT" +_rm_metadata "$MNT" + +# Now, $MNT has encryption enabled but fscrypt *not* setup. + +_print_header "Check enabled / setup count again" +enabled_count2=$(_get_enabled_fs_count) +setup_count2=$(_get_setup_fs_count) +(( enabled_count2 == enabled_count1 )) || _fail "wrong enabled count" +(( setup_count2 == setup_count1 - 1 )) || _fail "wrong setup count" + +_print_header "Get status of not-setup mounntpoint via global status" +fscrypt status | filter_mnt_status +_user_do "fscrypt status" | filter_mnt_status + +_print_header "Get status of not-setup mountpoint" +_expect_failure "fscrypt status '$MNT'" +_user_do_and_expect_failure "fscrypt status '$MNT'" + +_print_header "Get status of unencrypted directory on not-setup mountpoint" +_expect_failure "fscrypt status '$dir'" +_user_do_and_expect_failure "fscrypt status '$dir'" diff --git a/cli-tests/t_unlock.out b/cli-tests/t_unlock.out new file mode 100644 index 0000000..b3c9b2a --- /dev/null +++ b/cli-tests/t_unlock.out @@ -0,0 +1,116 @@ + +# Encrypt directory with --skip-unlock + +# => Check dir status +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +touch: cannot touch 'MNT/dir/file': Required key not available + +# => Get policy status via mount: +desc1 No desc2 + +# Unlock directory +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. + +# => Check dir status +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# => Get policy status via mount: +desc1 Yes desc2 + +# Lock by cycling mount + +# => Check dir status +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# => Get policy status via mount: +desc1 No desc2 + +# Try to unlock with wrong passphrase +[ERROR] fscrypt unlock: incorrect key provided +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Unlock directory +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. + +# => Check dir status +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +contents + +# => Get policy status via mount: +desc1 Yes desc2 + +# Try to unlock with corrupt policy metadata +[ERROR] fscrypt unlock: fscrypt metadata file at + "MNT/.fscrypt/policies/desc1" + is corrupt: proto: cannot parse invalid wire-format data + +# Try to unlock with missing policy metadata +[ERROR] fscrypt unlock: filesystem "MNT" does not contain + the policy metadata for "MNT/dir". + This directory has either been encrypted with another + tool (such as e4crypt), or the file + "MNT/.fscrypt/policies/desc20" + has been deleted. + +# Try to unlock with missing protector metadata +[ERROR] fscrypt unlock: could not load any protectors + +You may need to mount a linked filesystem. Run with --verbose for more +information. + +# Try to unlock with wrong policy metadata +[ERROR] fscrypt unlock: inconsistent metadata between encrypted directory + "MNT/dir1" and its corresponding + metadata file + "MNT/.fscrypt/policies/desc21". + + Directory has + descriptor:desc21 padding:32 + contents:AES_256_XTS filenames:AES_256_CTS + policy_version:2 + + Metadata file has + descriptor:desc23 padding:32 + contents:AES_256_XTS filenames:AES_256_CTS + policy_version:2 diff --git a/cli-tests/t_unlock.sh b/cli-tests/t_unlock.sh new file mode 100755 index 0000000..e32b0f7 --- /dev/null +++ b/cli-tests/t_unlock.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# Test unlocking a directory. + +cd "$(dirname "$0")" +. common.sh + +dir="$MNT/dir" +mkdir "$dir" + +_print_header "Encrypt directory with --skip-unlock" +echo hunter2 | fscrypt encrypt --quiet --name=prot --skip-unlock "$dir" +_print_header "=> Check dir status" +fscrypt status "$dir" +_expect_failure "touch '$dir/file'" +policy=$(fscrypt status "$dir" | awk '/Policy:/{print $2}') +_print_header "=> Get policy status via mount:" +fscrypt status "$MNT" | grep "^$policy" + +_print_header "Unlock directory" +echo hunter2 | fscrypt unlock "$dir" +_print_header "=> Check dir status" +fscrypt status "$dir" +echo contents > "$dir/file" +_print_header "=> Get policy status via mount:" +fscrypt status "$MNT" | grep "^$policy" + +_print_header "Lock by cycling mount" +umount "$MNT" +mount "$DEV" "$MNT" +_print_header "=> Check dir status" +fscrypt status "$dir" +_expect_failure "mkdir '$dir/subdir'" +_print_header "=> Get policy status via mount:" +fscrypt status "$MNT" | grep "^$policy" + +_print_header "Try to unlock with wrong passphrase" +_expect_failure "echo bad | fscrypt unlock --quiet '$dir'" +fscrypt status "$dir" + +_print_header "Unlock directory" +echo hunter2 | fscrypt unlock "$dir" +_print_header "=> Check dir status" +fscrypt status "$dir" +cat "$dir/file" +_print_header "=> Get policy status via mount:" +fscrypt status "$MNT" | grep "^$policy" + +_print_header "Try to unlock with corrupt policy metadata" +umount "$MNT" +mount "$DEV" "$MNT" +echo bad > "$MNT/.fscrypt/policies/$policy" +_expect_failure "echo hunter2 | fscrypt unlock '$dir'" + +_reset_filesystems + +_print_header "Try to unlock with missing policy metadata" +mkdir "$dir" +echo hunter2 | fscrypt encrypt --quiet --name=prot --skip-unlock "$dir" +rm "$MNT"/.fscrypt/policies/* +_expect_failure "echo hunter2 | fscrypt unlock '$dir'" + +_reset_filesystems + +_print_header "Try to unlock with missing protector metadata" +mkdir "$dir" +echo hunter2 | fscrypt encrypt --quiet --name=prot --skip-unlock "$dir" +rm "$MNT"/.fscrypt/protectors/* +_expect_failure "echo hunter2 | fscrypt unlock '$dir'" + +_print_header "Try to unlock with wrong policy metadata" +_reset_filesystems +mkdir "$MNT/dir1" +mkdir "$MNT/dir2" +echo hunter2 | fscrypt encrypt --quiet --name=dir1 --skip-unlock "$MNT/dir1" +echo hunter2 | fscrypt encrypt --quiet --name=dir2 --skip-unlock "$MNT/dir2" +policy1=$(find "$MNT/.fscrypt/policies/" -type f | head -1) +policy2=$(find "$MNT/.fscrypt/policies/" -type f | tail -1) +mv "$policy1" "$TMPDIR/policy" +mv "$policy2" "$policy1" +mv "$TMPDIR/policy" "$policy2" +_expect_failure "echo hunter2 | fscrypt unlock '$MNT/dir1'" diff --git a/cli-tests/t_v1_policy.out b/cli-tests/t_v1_policy.out new file mode 100644 index 0000000..f14f357 --- /dev/null +++ b/cli-tests/t_v1_policy.out @@ -0,0 +1,144 @@ + +# Set policy_version 1 + +# Try to encrypt as root +[ERROR] fscrypt encrypt: user must be specified when run as root + +When running this command as root, you usually still want to provision/remove +keys for a normal user's keyring and use a normal user's login passphrase as a +protector (so the corresponding files will be accessible for that user). This +can be done with --user=USERNAME. To use the root user's keyring or passphrase, +use --user=root. + +# Try to use --user=root as user +[ERROR] fscrypt encrypt: could not access user keyring for "root": setting uids: + operation not permitted + +You can only use --user=USERNAME to access the user keyring of another user if +you are running as root. + +# Try to encrypt without user keyring in session keyring +[ERROR] fscrypt encrypt: user keyring for "fscrypt-test-user" is not linked into + the session keyring + +This is usually the result of a bad PAM configuration. Either correct the +problem in your PAM stack, enable pam_keyinit.so, or run "keyctl link @u @s". + +# Encrypt a directory + +# Get dir status as user +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Get dir status as root +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: Partially (incompletely locked, or unlocked by another user) + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Create files in v1-encrypted directory + +# Try to lock v1-encrypted directory as user +[ERROR] fscrypt lock: inode cache can only be dropped as root + +Either this command should be run as root to properly clear the inode cache, or +it should be run with --drop-caches=false (this may leave encrypted files and +directories in an accessible state). +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Try to lock v1-encrypted directory as root without --user +[ERROR] fscrypt lock: user must be specified when run as root + +When running this command as root, you usually still want to provision/remove +keys for a normal user's keyring and use a normal user's login passphrase as a +protector (so the corresponding files will be accessible for that user). This +can be done with --user=USERNAME. To use the root user's keyring or passphrase, +use --user=root. +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Lock v1-encrypted directory +Encrypted data removed from filesystem cache. +"MNT/dir" is now locked. +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +cat: MNT/dir/file: No such file or directory + +# Testing incompletely locking v1-encrypted directory +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. +Encrypted data removed from filesystem cache. +[ERROR] fscrypt lock: Directory was incompletely locked because some files are + still open. These files remain accessible. + +Try killing any processes using files in the directory, for example using: + + find "MNT/dir" -print0 | xargs -0 fuser -k + +Then re-run: + + fscrypt lock "MNT/dir" +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: Partially (incompletely locked, or unlocked by another user) + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +ext4 filesystem "MNT" has 1 protector and 1 policy (only including ones owned by fscrypt-test-user or root). +All users can create fscrypt metadata on this filesystem. + +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +POLICY UNLOCKED PROTECTORS +desc1 No desc2 + +# Finishing locking v1-encrypted directory +Encrypted data removed from filesystem cache. +"MNT/dir" is now locked. +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +cat: MNT/dir/file: No such file or directory diff --git a/cli-tests/t_v1_policy.sh b/cli-tests/t_v1_policy.sh new file mode 100755 index 0000000..46ccdaf --- /dev/null +++ b/cli-tests/t_v1_policy.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# Test using v1 encryption policies (deprecated). + +cd "$(dirname "$0")" +. common.sh + +_setup_session_keyring +trap _cleanup_user_keyrings EXIT + +dir="$MNT/dir" +mkdir "$dir" +chown "$TEST_USER" "$dir" + +_print_header "Set policy_version 1" +sed -E -i 's/"policy_version": +"2"/"policy_version": "1"/' "$FSCRYPT_CONF" + +_print_header "Try to encrypt as root" +_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" + +_print_header "Try to use --user=root as user" +_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot --user=root '$dir'" + +_print_header "Try to encrypt without user keyring in session keyring" +_user_do "keyctl unlink @u @s" +_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" +_user_do "keyctl link @u @s" + +_print_header "Encrypt a directory" +_user_do "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" + +_print_header "Get dir status as user" +_user_do "fscrypt status '$dir'" + +_print_header "Get dir status as root" +fscrypt status "$dir" + +_print_header "Create files in v1-encrypted directory" +echo contents > "$dir/file" +mkdir "$dir/subdir" +ln -s target "$dir/symlink" + +# Due to the limitations of the v1 key management mechanism, 'fscrypt lock' only +# works when run as root and with the --user argument. + +_print_header "Try to lock v1-encrypted directory as user" +_user_do_and_expect_failure "fscrypt lock '$dir'" +_user_do "fscrypt status '$dir'" + +_print_header "Try to lock v1-encrypted directory as root without --user" +_expect_failure "fscrypt lock '$dir'" +_user_do "fscrypt status '$dir'" + +_print_header "Lock v1-encrypted directory" +fscrypt lock "$dir" --user="$TEST_USER" +_user_do "fscrypt status '$dir'" +_expect_failure "cat '$dir/file'" + +# 'fscrypt lock' and 'fscrypt status' implement a heuristic that should detect +# the "files busy" case with v1. +_print_header "Testing incompletely locking v1-encrypted directory" +_user_do "echo hunter2 | fscrypt unlock '$dir'" +exec 3<"$dir/file" +_expect_failure "fscrypt lock '$dir' --user='$TEST_USER'" +_user_do "fscrypt status '$dir'" +# ... except in this case, because we can't detect it without a directory path. +_user_do "fscrypt status '$MNT'" +exec 3<&- +_print_header "Finishing locking v1-encrypted directory" +fscrypt lock "$dir" --user="$TEST_USER" +_user_do "fscrypt status '$dir'" +_expect_failure "cat '$dir/file'" diff --git a/cli-tests/t_v1_policy_fs_keyring.out b/cli-tests/t_v1_policy_fs_keyring.out new file mode 100644 index 0000000..9f0f0ab --- /dev/null +++ b/cli-tests/t_v1_policy_fs_keyring.out @@ -0,0 +1,75 @@ + +# Enable v1 policies with fs keyring + +# Try to encrypt directory as user +[ERROR] fscrypt encrypt: root is required to add/remove v1 encryption policy + keys to/from filesystem + +Either this command should be run as root, or you should set +'"use_fs_keyring_for_v1_policies": false' in /etc/fscrypt.conf, or you should +re-create your encrypted directories using v2 encryption policies rather than v1 +(this requires setting '"policy_version": "2"' in the "options" section of +/etc/fscrypt.conf). +[ERROR] fscrypt status: file or directory "MNT/dir" is not + encrypted + +# Encrypt directory as user with --skip-unlock +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" +mkdir: cannot create directory 'MNT/dir/subdir': Required key not available + +# Try to unlock directory as user +[ERROR] fscrypt unlock: root is required to add/remove v1 encryption policy keys + to/from filesystem + +Either this command should be run as root, or you should set +'"use_fs_keyring_for_v1_policies": false' in /etc/fscrypt.conf, or you should +re-create your encrypted directories using v2 encryption policies rather than v1 +(this requires setting '"policy_version": "2"' in the "options" section of +/etc/fscrypt.conf). + +# Unlock directory as root +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: Yes + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Try to lock directory as user +[ERROR] fscrypt lock: root is required to add/remove v1 encryption policy keys + to/from filesystem + +Either this command should be run as root, or you should set +'"use_fs_keyring_for_v1_policies": false' in /etc/fscrypt.conf, or you should +re-create your encrypted directories using v2 encryption policies rather than v1 +(this requires setting '"policy_version": "2"' in the "options" section of +/etc/fscrypt.conf). + +# Lock directory as root +"MNT/dir" is now locked. +cat: MNT/dir/file: No such file or directory +"MNT/dir" is encrypted with fscrypt. + +Policy: desc1 +Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:1 +Unlocked: No + +Protected with 1 protector: +PROTECTOR LINKED DESCRIPTION +desc2 No custom protector "prot" + +# Check that user can access file when directory is unlocked by root +Enter custom passphrase for protector "prot": "MNT/dir" is now unlocked and ready for use. +contents diff --git a/cli-tests/t_v1_policy_fs_keyring.sh b/cli-tests/t_v1_policy_fs_keyring.sh new file mode 100755 index 0000000..a8fd333 --- /dev/null +++ b/cli-tests/t_v1_policy_fs_keyring.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Test using v1 encryption policies (deprecated) with +# use_fs_keyring_for_v1_policies = true. + +# This works similar to v2 policies, except locking and unlocking (including +# 'fscrypt encrypt' without --skip-unlock) will only work as root. + +cd "$(dirname "$0")" +. common.sh + +_print_header "Enable v1 policies with fs keyring" +sed -E -e 's/"use_fs_keyring_for_v1_policies": +false/"use_fs_keyring_for_v1_policies": true/' \ + -e 's/"policy_version": +"2"/"policy_version": "1"/' \ + -i "$FSCRYPT_CONF" + +dir="$MNT/dir" +mkdir "$dir" +chown "$TEST_USER" "$dir" + +_print_header "Try to encrypt directory as user" +_user_do_and_expect_failure "echo hunter2 | fscrypt encrypt --quiet --name=prot '$dir'" +_expect_failure "fscrypt status '$dir'" + +_print_header "Encrypt directory as user with --skip-unlock" +_user_do "echo hunter2 | fscrypt encrypt --quiet --name=prot --skip-unlock '$dir'" +fscrypt status "$dir" +_expect_failure "mkdir '$dir/subdir'" + +_print_header "Try to unlock directory as user" +_user_do_and_expect_failure "echo hunter2 | fscrypt unlock '$dir'" + +_print_header "Unlock directory as root" +echo hunter2 | fscrypt unlock "$dir" +mkdir "$dir/subdir" +echo contents > "$dir/file" +fscrypt status "$dir" + +_print_header "Try to lock directory as user" +_user_do_and_expect_failure "fscrypt lock '$dir'" + +_print_header "Lock directory as root" +fscrypt lock "$dir" +_expect_failure "cat '$dir/file'" +fscrypt status "$dir" + +_print_header "Check that user can access file when directory is unlocked by root" +echo hunter2 | fscrypt unlock "$dir" +_user_do "cat '$dir/file'" diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go new file mode 100644 index 0000000..30aa3a7 --- /dev/null +++ b/cmd/fscrypt/commands.go @@ -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 index 0000000..c4814f4 --- /dev/null +++ b/cmd/fscrypt/errors.go @@ -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 index 0000000..7285133 --- /dev/null +++ b/cmd/fscrypt/flags.go @@ -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 index 0000000..21253ad --- /dev/null +++ b/cmd/fscrypt/format.go @@ -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 index 0000000..93f97de --- /dev/null +++ b/cmd/fscrypt/fscrypt.go @@ -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 --help" works + // and "fscrypt 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 index 0000000..110d2d4 --- /dev/null +++ b/cmd/fscrypt/fscrypt_bash_completion @@ -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 index 0000000..1d09bf8 --- /dev/null +++ b/cmd/fscrypt/fscrypt_test.go @@ -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 index 0000000..b57c01d --- /dev/null +++ b/cmd/fscrypt/keys.go @@ -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 index 0000000..d34a18a --- /dev/null +++ b/cmd/fscrypt/prompt.go @@ -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 index 0000000..186ca7a --- /dev/null +++ b/cmd/fscrypt/protector.go @@ -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 index 0000000..b9a16e8 --- /dev/null +++ b/cmd/fscrypt/setup.go @@ -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 index 0000000..bc8f1ee --- /dev/null +++ b/cmd/fscrypt/status.go @@ -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 index 0000000..cd51968 --- /dev/null +++ b/cmd/fscrypt/strings.go @@ -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 index 0000000..6a719dd --- /dev/null +++ b/crypto/crypto.go @@ -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 index 0000000..1fa5a0c --- /dev/null +++ b/crypto/crypto_test.go @@ -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 -m -p

-l 32 +// +// where costs.Time = , costs.Memory = 2^, and costs.Parallelism =

. +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 index 0000000..2e57443 --- /dev/null +++ b/crypto/key.go @@ -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 +#include +*/ +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 index 0000000..527f841 --- /dev/null +++ b/crypto/rand.go @@ -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 index 0000000..4a89e6d --- /dev/null +++ b/crypto/recovery_test.go @@ -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 index 0000000..9829435 --- /dev/null +++ b/filesystem/filesystem.go @@ -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: +// +// └── .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 index 0000000..f9c34ae --- /dev/null +++ b/filesystem/filesystem_test.go @@ -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 index 0000000..ae432bf --- /dev/null +++ b/filesystem/mountpoint.go @@ -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 +// (=), 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=\nPATH=\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 index 0000000..f06219c --- /dev/null +++ b/filesystem/mountpoint_test.go @@ -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 index 0000000..8cfb235 --- /dev/null +++ b/filesystem/path.go @@ -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 index 0000000..d325054 --- /dev/null +++ b/filesystem/path_test.go @@ -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 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 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 index 0000000..9b949b9 --- /dev/null +++ b/keyring/fs_keyring.go @@ -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 +*/ +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, ) = %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 index 0000000..5ddceaf --- /dev/null +++ b/keyring/keyring.go @@ -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 index 0000000..26f6036 --- /dev/null +++ b/keyring/keyring_test.go @@ -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 index 0000000..0ea4689 --- /dev/null +++ b/keyring/user_keyring.go @@ -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, , %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 index 0000000..d7dea41 --- /dev/null +++ b/metadata/checks.go @@ -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 index 0000000..65fd7b5 --- /dev/null +++ b/metadata/config.go @@ -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 index 0000000..3048757 --- /dev/null +++ b/metadata/config_test.go @@ -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 index 0000000..fa6b8a7 --- /dev/null +++ b/metadata/constants.go @@ -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 index 0000000..38e6476 --- /dev/null +++ b/metadata/metadata.pb.go @@ -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 index 0000000..f2dd78f --- /dev/null +++ b/metadata/metadata.proto @@ -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 index 0000000..fe6c38f --- /dev/null +++ b/metadata/policy.go @@ -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 index 0000000..7856ed3 --- /dev/null +++ b/metadata/policy_test.go @@ -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 index 0000000..d2d0cf3 --- /dev/null +++ b/pam/constants.go @@ -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 + * 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 +*/ +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 index 0000000..e4f8f83 --- /dev/null +++ b/pam/login.go @@ -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 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 +#include +#include +#include +#include +#include // 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 index 0000000..ea1c34e --- /dev/null +++ b/pam/pam.go @@ -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 +#include +#include +*/ +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 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 + +// 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 index 0000000..97cc792 --- /dev/null +++ b/pam/pam_test.go @@ -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 index 0000000..f83dab2 --- /dev/null +++ b/pam_fscrypt/config @@ -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 index 0000000..15066c1 --- /dev/null +++ b/pam_fscrypt/pam_fscrypt.go @@ -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 +#include + +#include +*/ +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 ". 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 index 0000000..af9537f --- /dev/null +++ b/pam_fscrypt/run_fscrypt.go @@ -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 +#include + +#include +*/ +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 index 0000000..40ace4c --- /dev/null +++ b/pam_fscrypt/run_test.go @@ -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 index 0000000..f11248d --- /dev/null +++ b/security/cache.go @@ -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 index 0000000..fe8668d --- /dev/null +++ b/security/privileges.go @@ -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 +#include // getting and setting uids and gids +#include // 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 index 0000000..45e4f63 --- /dev/null +++ b/security/security_test.go @@ -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 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 index 0000000..3c87a2c --- /dev/null +++ b/util/errors.go @@ -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 index 0000000..1dab335 --- /dev/null +++ b/util/util.go @@ -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 index 0000000..70e7070 --- /dev/null +++ b/util/util_test.go @@ -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") + } +} -- 2.47.3