Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.13.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.13.0...v0.17.0)
---
updated-dependencies:
- dependency-name: golang.org/x/crypto
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
--- /dev/null
+#
+# 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
--- /dev/null
+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
--- /dev/null
+# 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
--- /dev/null
+# How to Contribute to fscrypt
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines we ask you to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Reporting an Issue, Discussing Design, or Asking a Question
+
+__IMPORTANT__: Any significant security issues should __NOT__ be reported in
+the public issue tracker. Practice responsible disclosure by emailing
+<joerichey@google.com> and <ebiggers@google.com> directly.
+
+Any bugs, problems, or design discussion relating to fscrypt should be raised
+in the [Github Issue Tracker](https://github.com/google/fscrypt/issues/new).
+
+When reporting an issue or problem, be sure to give as much information as
+possible. Also, make sure you are running the `fscrypt` and `pam_fscrypt.so`
+built from the current `master` branch.
+
+If reporting an issue around the fscrypt command-line tool, post the
+relevant output from fscrypt, running with the `--verbose` flag. For the
+`pam_fscrypt` module, use the `debug` option with the module and post the
+relevant parts of the syslog (usually at `/var/log/syslog`).
+
+Be sure to correctly tag your issue. The usage for the tags is as follows:
+* `bug` - General problems with the program's behavior
+ * The program crashes or hangs
+ * Directories cannot be locked/unlocked
+ * Metadata corruption
+ * Data loss/corruption
+* `documentation`
+ * Typos or unclear explanations in `README.md` or man pages.
+ * Outdated example output
+ * Unclear or ambiguous error messages
+* `enhancement` - Things you want in fscrypt
+* `question` - You don't know how something works with fscrypt
+ * This usually turns into a `documentation` issue.
+* `testing` - Strange test failures or missing tests
+
+## Submitting a Change to fscrypt
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+On every pull request, [GitHub
+Actions](https://github.com/google/fscrypt/actions) runs tests, code formatters,
+and linters. To pass these checks you should make sure that in your submission:
+- `make` properly builds `fscrypt` and `pam_fscrypt.so`.
+- All tests, including [integration tests](#running-integration-tests) and
+ [command-line interface (CLI)
+ tests](https://github.com/google/fscrypt/blob/master/cli-tests/README.md),
+ should pass. If the CLI tests fail due to an expected change in output, you
+ can use `make cli-test-update`.
+- `make format` has been run.
+- If you made any changes to files ending in `.proto`, the corresponding
+ `.pb.go` files should be regenerated with `make gen`.
+- Any issues found by `make lint` have been addressed.
+- If any dependencies have changed, run `go mod tidy`.
+- `make coverage.out` can be used to generate a coverage report for all of the
+ tests, but isn't required for submission
+ (ideally most code would be tested, we are far from that ideal).
+
+Essentially, if you run:
+```
+make test-setup
+make all
+make test-teardown
+make cli-test
+go mod tidy
+```
+and everything succeeds, and no files are changed, you're good to submit.
+
+The `Makefile` will automatically download and build any needed Go dependencies.
+However, you'll also need to install some non-Go dependencies:
+ - `make format` requires
+ [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html).
+ - `make lint` requires [`shellcheck`](https://github.com/koalaman/shellcheck).
+ - `make test-setup` and `make cli-test` require
+ [`e2fsprogs`](https://en.wikipedia.org/wiki/E2fsprogs) version 1.43 or
+ later.
+ - `make cli-test` requires [`expect`](https://en.wikipedia.org/wiki/Expect)
+ and
+ [`keyutils`](https://manpages.debian.org/testing/keyutils/keyctl.1.en.html).
+
+On Ubuntu, the following command installs the needed packages:
+```
+sudo apt-get install clang-format shellcheck e2fsprogs expect keyutils
+```
+
+### Running Integration Tests
+
+Running `make test` will build each package and run the unit tests, but will
+skip the integration tests. To run the integration tests, you will need a
+filesystem that supports encryption. If you already have some empty filesystem
+at `/foo/bar` that supports filesystem encryption, just run:
+```bash
+make test MOUNT=/foo/bar
+```
+
+Otherwise, you can use the `make test-setup`/`make test-teardown` commands to
+create/destroy a test filesystem for running integration tests. By default, a
+filesystem will be created (then destroyed) at `/tmp/fscrypt-mount` (using an
+image file at `/tmp/fscrypt-image`). To create/test/destroy a filesystem at a
+custom mountpoint `/foo/bar`, run:
+```bash
+make test-setup MOUNT=/foo/bar
+make test MOUNT=/foo/bar
+make test-teardown MOUNT=/foo/bar
+```
+Running the commands without `MOUNT=/foo/bar` uses the default locations.
+
+Note that the setup/teardown commands require `sudo` to mount/unmount the
+test filesystem.
+
+### Changing dependencies
+
+fscrypt's dependencies are managed using the
+[Go module system](https://github.com/golang/go/wiki/Modules).
+If you add or remove a dependency, be sure to update `go.mod` and `go.sum`.
+
+Also, when adding a dependency, the license of the package must be compatible
+with [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). See the
+[FSF's article](https://www.gnu.org/licenses/license-list.html) for more
+information. This (unfortunately) means we cannot use external packages under
+the [GPL](https://choosealicense.com/licenses/gpl-3.0) or
+[LGPL](https://choosealicense.com/licenses/lgpl-3.0/). We also cannot use
+packages with missing, misleading, or joke licenses (e.g.
+[Unlicense](http://unlicense.org/), [WTFPL](http://www.wtfpl.net/),
+[CC0](https://creativecommons.org/publicdomain/zero/1.0/)).
+
+### Build System Details ###
+
+Under the hood, the Makefile uses many go tools to generate, format, and lint
+your code.
+
+`make gen`:
+ - Downloads [`protoc`](https://github.com/google/protobuf) to compile the
+ `.proto` files.
+ - Turns each `.proto` file into a matching `.pb.go` file using
+ [`protoc-gen-go`](https://go.googlesource.com/protobuf/+/refs/heads/master/cmd/protoc-gen-go).
+
+`make format` runs:
+ - [`goimports`](https://godoc.org/golang.org/x/tools/cmd/goimports)
+ on the `.go` files.
+ - [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html)
+ on the `.c` and `.h` files.
+
+`make lint` runs:
+ - [`go vet`](https://golang.org/cmd/vet/)
+ - [`staticcheck`](https://github.com/dominikh/go-tools/tree/master/cmd/staticcheck)
--- /dev/null
+
+ 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.
--- /dev/null
+# 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
--- /dev/null
+# `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.
--- /dev/null
+# fscrypt [](https://github.com/google/fscrypt/releases)
+
+[](https://github.com/google/fscrypt/actions?query=workflow%3ACI+branch%3Amaster)
+[](https://godoc.org/github.com/google/fscrypt)
+[](https://goreportcard.com/report/github.com/google/fscrypt)
+[](http://www.apache.org/licenses/LICENSE-2.0.html)
+
+`fscrypt` is a high-level tool for the management of [Linux native filesystem
+encryption](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html).
+`fscrypt` manages metadata, key generation, key wrapping, PAM integration, and
+provides a uniform interface for creating and modifying encrypted directories.
+For a small low-level tool that directly sets policies, see
+[`fscryptctl`](https://github.com/google/fscryptctl).
+
+To use `fscrypt`, you must have a filesystem that supports the Linux native
+filesystem encryption API (which is also sometimes called "fscrypt"; this
+documentation calls it "Linux native filesystem encryption" to avoid confusion).
+Only certain filesystems, such as [ext4](https://en.wikipedia.org/wiki/Ext4) and
+[f2fs](https://en.wikipedia.org/wiki/F2FS), support this API. For a full list
+of supported filesystems and how to enable encryption support on each one, see
+[Runtime dependencies](#runtime-dependencies).
+
+For the release notes, see the [NEWS file](NEWS.md).
+
+## Table of contents
+
+- [Alternatives to consider](#alternatives-to-consider)
+- [Threat model](#threat-model)
+- [Features](#features)
+- [Building and installing](#building-and-installing)
+- [Runtime dependencies](#runtime-dependencies)
+- [Configuration file](#configuration-file)
+- [Setting up `fscrypt` on a filesystem](#setting-up-fscrypt-on-a-filesystem)
+- [Setting up for login protectors](#setting-up-for-login-protectors)
+ - [Securing your login passphrase](#securing-your-login-passphrase)
+ - [Enabling the PAM module](#enabling-the-pam-module)
+ - [Enabling the PAM module on Debian or Ubuntu](#enabling-the-pam-module-on-debian-or-ubuntu)
+ - [Enabling the PAM module on Arch Linux](#enabling-the-pam-module-on-arch-linux)
+ - [Enabling the PAM module on other Linux distros](#enabling-the-pam-module-on-other-linux-distros)
+ - [Allowing `fscrypt` to check your login passphrase](#allowing-fscrypt-to-check-your-login-passphrase)
+- [Backup, restore, and recovery](#backup-restore-and-recovery)
+- [Encrypting existing files](#encrypting-existing-files)
+- [Example usage](#example-usage)
+ - [Setting up fscrypt on a directory](#setting-up-fscrypt-on-a-directory)
+ - [Locking and unlocking a directory](#locking-and-unlocking-a-directory)
+ - [Protecting a directory with your login passphrase](#protecting-a-directory-with-your-login-passphrase)
+ - [Changing a custom passphrase](#changing-a-custom-passphrase)
+ - [Using a raw key protector](#using-a-raw-key-protector)
+ - [Using multiple protectors for a policy](#using-multiple-protectors-for-a-policy)
+- [Contributing](#contributing)
+- [Troubleshooting](#troubleshooting)
+ - [I changed my login passphrase, now all my directories are inaccessible](#i-changed-my-login-passphrase-now-all-my-directories-are-inaccessible)
+ - [Directories using my login passphrase are not automatically unlocking](#directories-using-my-login-passphrase-are-not-automatically-unlocking)
+ - [Getting "encryption not enabled" on an ext4 filesystem](#getting-encryption-not-enabled-on-an-ext4-filesystem)
+ - [Getting "user keyring not linked into session keyring"](#getting-user-keyring-not-linked-into-session-keyring)
+ - [Getting "Operation not permitted" when moving files into an encrypted directory](#getting-operation-not-permitted-when-moving-files-into-an-encrypted-directory)
+ - [Getting "Package not installed" when trying to use an encrypted directory](#getting-package-not-installed-when-trying-to-use-an-encrypted-directory)
+ - [Some processes can't access unlocked encrypted files](#some-processes-cant-access-unlocked-encrypted-files)
+ - [Users can access other users' unlocked encrypted files](#users-can-access-other-users-unlocked-encrypted-files)
+ - [Getting "Required key not available" when backing up locked encrypted files](#getting-required-key-not-available-when-backing-up-locked-encrypted-files)
+ - [The reported size of encrypted symlinks is wrong](#the-reported-size-of-encrypted-symlinks-is-wrong)
+- [Legal](#legal)
+
+## Alternatives to consider
+
+Operating-system level storage encryption solutions work at either the
+filesystem or block device level. [Linux native filesystem
+encryption](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html)
+(the solution configured by `fscrypt`) is filesystem-level; it encrypts
+individual directories. Only file contents and filenames are encrypted;
+non-filename metadata, such as timestamps, the sizes and number of files, and
+extended attributes, is **not** encrypted. Users choose which directories will
+be encrypted, and with what keys.
+
+Before using `fscrypt`, you should consider other solutions:
+
+* [**dm-crypt/LUKS**](https://en.wikipedia.org/wiki/Dm-crypt) is block device
+ level encryption: it encrypts an entire block device (and hence an entire
+ filesystem) with one key. Unlocking this key will unlock the entire block
+ device. dm-crypt/LUKS is usually configured using
+ [cryptsetup](https://gitlab.com/cryptsetup/cryptsetup/-/wikis/home).
+
+* [**eCryptfs**](https://en.wikipedia.org/wiki/ECryptfs) is an alternative
+ filesystem-level encryption solution. It is a stacked filesystem, which means
+ it sits on top of a real filesystem, rather than being directly integrated
+ into the real filesystem. Stacked filesystems have a couple advantages (such
+ as working on almost any real filesystem), but also some significant
+ disadvantages. eCryptfs is usually configured using
+ [ecryptfs-utils](https://packages.debian.org/stretch/ecryptfs-utils).
+
+* The [**ZFS**](https://en.wikipedia.org/wiki/ZFS) filesystem supports
+ encryption in its own way (not compatible with `fscrypt`). ZFS encryption has
+ some advantages; however, ZFS isn't part of the upstream Linux kernel and is
+ less common than other filesystems, so this solution usually isn't an option.
+
+Which solution to use? Here are our recommendations:
+
+* eCryptfs shouldn't be used, if at all possible. eCryptfs's use of filesystem
+ stacking causes a number of issues, and eCryptfs is no longer actively
+ maintained. The original author of eCryptfs recommends using Linux native
+ filesystem encryption instead. The largest users of eCryptfs (Ubuntu and
+ Chrome OS) have switched to dm-crypt or Linux native filesystem encryption.
+
+* If you need fine-grained control of encryption within a filesystem, then use
+ `fscrypt`, or `fscrypt` together with dm-crypt/LUKS. If you don't need this,
+ then use dm-crypt/LUKS.
+
+ To understand this recommendation: consider that the main advantage of
+ `fscrypt` is to allow different files on the same filesystem to be encrypted
+ by different keys, and thus be unlockable, lockable, and securely deletable
+ independently from each other. Therefore, `fscrypt` is useful in cases such
+ as:
+
+ * Multi-user systems, since each user's files can be encrypted with their
+ own key that is unlocked by their own passphrase.
+
+ * Single-user systems where it's not possible for all files to have the
+ strongest level of protection. For example, it might be necessary for the
+ system to boot up without user interaction. Any files that are needed to
+ do so can only be encrypted by a hardware-protected (e.g. TPM-bound) key
+ at best. If the user's personal files are located on the same filesystem,
+ then with dm-crypt/LUKS the user's personal files would be limited to this
+ weak level of protection. With `fscrypt`, the user's personal files could
+ be fully protected using the user's passphrase.
+
+ `fscrypt` isn't very useful in the following cases:
+
+ * Single-user systems where the user is willing to enter a strong passphrase
+ at boot time to unlock the entire filesystem. In this case, the main
+ advantage of `fscrypt` would go unused, so dm-crypt/LUKS would be better
+ as it would provide better security (due to ensuring that all files and
+ all filesystem metadata are encrypted).
+
+ * Any case where it is feasible to create a separate filesystem for every
+ encryption key you want to use.
+
+ Note: dm-crypt/LUKS and `fscrypt` aren't mutually exclusive; they can be used
+ together when the performance hit of double encryption is tolerable. It only
+ makes sense to do this when the keys for each encryption layer are protected
+ in different ways, such that each layer serves a different purpose. A
+ reasonable set-up would be to encrypt the whole filesystem with dm-crypt/LUKS
+ using a TPM-bound key that is automatically unlocked at boot time, and also
+ encrypt users' home directories with `fscrypt` using their login passphrases.
+
+## Threat model
+
+Like other storage encryption solutions (including dm-crypt/LUKS and eCryptfs),
+Linux native filesystem encryption is primarily intended to protect the
+confidentiality of data from a single point-in-time permanent offline compromise
+of the disk. For a detailed description of the threat model, see the [kernel
+documentation](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html#threat-model).
+
+It's worth emphasizing that none of these encryption solutions protect unlocked
+encrypted files from other users on the same system (that's the job of OS-level
+access control, such as UNIX file permissions), or from the cloud provider you
+may be running a virtual machine on. By themselves, they also do not protect
+from "evil maid" attacks, i.e. non-permanent offline compromises of the disk.
+
+## Features
+
+`fscrypt` is intended to improve upon the work in
+[e4crypt](http://man7.org/linux/man-pages/man8/e4crypt.8.html) by providing a
+more managed environment and handling more functionality in the background.
+`fscrypt` has a [design document](https://goo.gl/55cCrI) specifying its full
+architecture. See also the [kernel documentation for Linux native filesystem
+encryption](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html).
+
+Briefly, `fscrypt` deals with protectors and policies. Protectors represent some
+secret or information used to protect the confidentiality of your data. The
+three currently supported protector types are:
+
+1. Your login passphrase, through [PAM](http://www.linux-pam.org/Linux-PAM-html).
+ The included PAM module (`pam_fscrypt.so`) can automatically unlock
+ directories protected by your login passphrase when you log in, and lock them
+ when you log out. __IMPORTANT:__ before using a login protector, follow
+ [Setting up for login protectors](#setting-up-for-login-protectors).
+
+2. A custom passphrase. This passphrase is hashed with
+ [Argon2id](https://en.wikipedia.org/wiki/Argon2), by default calibrated to
+ use all CPUs and take about 1 second.
+
+3. A raw key file. See [Using a raw key protector](#using-a-raw-key-protector).
+
+These protectors are mutable, so the information can change without needing to
+update any of your encrypted directories.
+
+Policies represent the actual key passed to the kernel. This "policy key" is
+immutable and policies are (usually) applied to a single directory. Protectors
+then protect policies, so that having one of the protectors for a policy is
+enough to get the policy key and access the data. Which protectors protect a
+policy can also be changed. This allows a user to change how a directory is
+protected without needing to reencrypt the directory's contents.
+
+Concretely, `fscrypt` contains the following functionality:
+* `fscrypt setup` - Creates `/etc/fscrypt.conf` and the `/.fscrypt` directory
+ * This is the only functionality which always requires root privileges
+* `fscrypt setup MOUNTPOINT` - Gets a filesystem ready for use with fscrypt
+* `fscrypt encrypt DIRECTORY` - Encrypts an empty directory
+* `fscrypt unlock DIRECTORY` - Unlocks an encrypted directory
+* `fscrypt lock DIRECTORY` - Locks an encrypted directory
+* `fscrypt purge MOUNTPOINT` - Locks all encrypted directories on a filesystem
+* `fscrypt status [PATH]` - Gets detailed info about filesystems or paths
+* `fscrypt metadata` - Manages policies or protectors directly
+
+See the example usage section below or run `fscrypt COMMAND --help` for more
+information about each of the commands.
+
+## Building and installing
+
+`fscrypt` has a minimal set of build dependencies:
+* [Go](https://golang.org/doc/install) 1.18 or higher. Older versions may work
+ but they are not tested or supported.
+* A C compiler (`gcc` or `clang`)
+* `make`
+* Headers for [`libpam`](http://www.linux-pam.org/).
+ Install them with the appropriate package manager:
+ - Debian/Ubuntu: `sudo apt install libpam0g-dev`
+ - Red Hat: `sudo yum install pam-devel`
+ - Arch: [`pam`](https://www.archlinux.org/packages/core/x86_64/pam/)
+ package (usually installed by default)
+
+Once all the dependencies are installed, clone the repository by running:
+```shell
+git clone https://github.com/google/fscrypt
+```
+Running `make` builds the binary (`fscrypt`) and PAM module (`pam_fscrypt.so`)
+in the `bin/` directory.
+
+Running `sudo make install` installs `fscrypt` into `/usr/local/bin`,
+`pam_fscrypt.so` into `/usr/local/lib/security`, and `pam_fscrypt/config` into
+`/usr/local/share/pam-configs`.
+
+On Debian (and Debian derivatives such as Ubuntu), use `sudo make install
+PREFIX=/usr` to install into `/usr` instead of the default of `/usr/local`.
+Ordinarily you shouldn't manually install software into `/usr`, since `/usr` is
+reserved for Debian's own packages. However, Debian's PAM configuration
+framework only recognizes configuration files in `/usr`, not in `/usr/local`.
+Therefore, the PAM module will only work if you install into `/usr`. Note: if
+you later decide to switch to using the Debian package `libpam-fscrypt`, you'll
+have to first manually run `sudo make uninstall PREFIX=/usr`.
+
+It is also possible to use `make install-bin` to only install the `fscrypt`
+binary, or `make install-pam` to only install the PAM files.
+
+Alternatively, if you only want to install the `fscrypt` binary to
+`$GOPATH/bin`, simply run:
+```shell
+go install github.com/google/fscrypt/cmd/fscrypt@latest
+```
+
+See the `Makefile` for instructions on how to further customize the build.
+
+## Runtime dependencies
+
+To run, `fscrypt` needs the following libraries:
+* `libpam.so` (almost certainly already on your system)
+
+In addition, `fscrypt` requires a filesystem that supports the Linux native
+filesystem encryption API. Currently, the filesystems that support this are:
+
+* ext4, with upstream kernel v4.1 or later. The kernel configuration must
+ contain `CONFIG_FS_ENCRYPTION=y` (for kernels v5.1+) or
+ `CONFIG_EXT4_ENCRYPTION=y` or `=m` (for older kernels). The filesystem must
+ also have the `encrypt` feature flag enabled; to enable this flag, see
+ [here](#getting-encryption-not-enabled-on-an-ext4-filesystem).
+
+* f2fs, with upstream kernel v4.2 or later. The kernel configuration must
+ contain `CONFIG_FS_ENCRYPTION=y` (for kernels v5.1+) or
+ `CONFIG_F2FS_FS_ENCRYPTION=y` (for older kernels). The filesystem must also
+ have the `encrypt` feature flag enabled; this flag can be enabled at format
+ time by `mkfs.f2fs -O encrypt` or later by `fsck.f2fs -O encrypt`.
+
+* UBIFS, with upstream kernel v4.10 or later. The kernel configuration must
+ contain `CONFIG_FS_ENCRYPTION=y` (for kernels v5.1+) or
+ `CONFIG_UBIFS_FS_ENCRYPTION=y` (for older kernels).
+
+* CephFS, with upstream kernel v6.6 or later. The kernel configuration must
+ contain `CONFIG_FS_ENCRYPTION=y`.
+
+* [Lustre](https://www.lustre.org/), with Lustre v2.14.0 or later. For details,
+ see the Lustre documentation. Please note that Lustre is not part of the
+ upstream Linux kernel, and its encryption implementation has not been reviewed
+ by the authors of `fscrypt`. Questions/issues about Lustre encryption should
+ be directed to the Lustre developers. Lustre version 2.14 does not encrypt
+ filenames, even though it claims to, so v2.15.0 or later should be used.
+
+To check whether the needed option is enabled in your kernel, run:
+```shell
+zgrep -h ENCRYPTION /proc/config.gz /boot/config-$(uname -r) | sort | uniq
+```
+
+It is also recommended to use Linux kernel v5.4 or later, since this
+allows the use of v2 encryption policies. v2 policies have several
+security and usability improvements over v1 policies.
+
+Be careful when using ext4 encryption on removable media, since ext4 filesystems
+with the `encrypt` feature cannot be mounted on systems with kernel versions
+older than the minimums listed above -- even to access unencrypted files!
+
+If you configure `fscrypt` to use non-default features, other kernel
+prerequisites may be needed too. See [Configuration
+file](#configuration-file).
+
+## Configuration file
+
+Running `sudo fscrypt setup` will create the configuration file
+`/etc/fscrypt.conf` if it doesn't already exist. It's a JSON file
+that looks like the following:
+
+```
+{
+ "source": "custom_passphrase",
+ "hash_costs": {
+ "time": "52",
+ "memory": "131072",
+ "parallelism": "32"
+ },
+ "options": {
+ "padding": "32",
+ "contents": "AES_256_XTS",
+ "filenames": "AES_256_CTS",
+ "policy_version": "2"
+ },
+ "use_fs_keyring_for_v1_policies": false,
+ "allow_cross_user_metadata": false
+}
+```
+
+The fields are:
+
+* "source" is the default source for new protectors. The choices are
+ "pam\_passphrase", "custom\_passphrase", and "raw\_key".
+
+* "hash\_costs" describes how difficult the passphrase hashing is.
+ By default, `fscrypt setup` calibrates the hashing to use all CPUs
+ and take about 1 second. The `--time` option to `fscrypt setup` can
+ be used to customize this time when creating the configuration file.
+
+* "options" are the encryption options to use for new encrypted
+ directories:
+
+ * "padding" is the number of bytes by which filenames are padded
+ before being encrypted. The choices are "32", "16", "8", and
+ "4". "32" is recommended.
+
+ * "contents" is the algorithm used to encrypt file contents. The
+ choices are "AES_256_XTS", "AES_128_CBC", and "Adiantum".
+ Normally, "AES_256_XTS" is recommended.
+
+ * "filenames" is the algorithm used to encrypt file names. The
+ choices are "AES_256_CTS", "AES_128_CTS", "Adiantum", and
+ "AES_256_HCTR2". Normally, "AES_256_CTS" is recommended.
+
+ To use algorithms other than "AES_256_XTS" for contents and
+ "AES_256_CTS" for filenames, the needed algorithm(s) may need to
+ be enabled in the Linux kernel's cryptography API. For example,
+ to use Adiantum, `CONFIG_CRYPTO_ADIANTUM` must be set. Also,
+ not all combinations of algorithms are allowed; for example,
+ "Adiantum" for contents can only be paired with "Adiantum" for
+ filenames. See the [kernel
+ documentation](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html#encryption-modes-and-usage)
+ for more details about the supported algorithms.
+
+ * "policy\_version" is the version of encryption policy to use.
+ The choices are "1" and "2". If unset, "1" is assumed.
+ Directories created with policy version "2" are only usable on
+ kernel v5.4 or later, but are preferable to version "1" if you
+ don't mind this restriction.
+
+* "use\_fs\_keyring\_for\_v1\_policies" specifies whether to add keys for v1
+ encryption policies to the filesystem keyrings, rather than to user keyrings.
+ This can solve [issues with processes being unable to access unlocked
+ encrypted files](#some-processes-cant-access-unlocked-encrypted-files).
+ However, it requires kernel v5.4 or later, and it makes unlocking and locking
+ encrypted directories require root. (The PAM module will still work.)
+
+ The purpose of this setting is to allow people to take advantage of some of
+ the improvements in Linux v5.4 on encrypted directories that are also
+ compatible with older kernels. If you don't need compatibility with older
+ kernels, it's better to not use this setting and instead (re-)create your
+ encrypted directories with `"policy_version": "2"`.
+
+* "allow\_cross\_user\_metadata" specifies whether `fscrypt` will allow
+ protectors and policies from other non-root users to be read, e.g. to be
+ offered as options by `fscrypt encrypt`. The default value is `false`, since
+ other users might be untrusted and could create malicious files. This can be
+ set to `true` to restore the old behavior on systems where `fscrypt` metadata
+ needs to be shared between multiple users. Note that this option is
+ independent from the permissions on the metadata files themselves, which are
+ set to 0600 by default; users who wish to share their metadata files with
+ other users would also need to explicitly change their mode to 0644.
+
+## Setting up `fscrypt` on a filesystem
+
+`fscrypt` needs some directories to exist on the filesystem on which encryption
+will be used:
+
+* `MOUNTPOINT/.fscrypt/policies`
+* `MOUNTPOINT/.fscrypt/protectors`
+
+(If login protectors are used, these must also exist on the root filesystem.)
+
+To create these directories, run `fscrypt setup MOUNTPOINT`. If MOUNTPOINT is
+owned by root, as is usually the case, then this command will require root.
+
+There will be one decision you'll need to make: whether non-root users will be
+allowed to create `fscrypt` metadata (policies and protectors).
+
+If you say `y`, then these directories will be made world-writable, with the
+sticky bit set so that users can't delete each other's files -- just like
+`/tmp`. If you say `N`, then these directories will be writable only by root.
+
+Saying `y` maximizes the usability of `fscrypt`, and on most systems it's fine
+to say `y`. However, on some systems this may be inappropriate, as it will
+allow malicious users to fill the entire filesystem unless filesystem quotas
+have been configured -- similar to problems that have historically existed with
+other world-writable directories, e.g. `/tmp`. If you are concerned about this,
+say `N`. If you say `N`, then you'll only be able to run `fscrypt` as root to
+set up encryption on users' behalf, unless you manually set custom permissions
+on the metadata directories to grant write access to specific users or groups.
+
+If you chose the wrong mode at `fscrypt setup` time, you can change the
+directory permissions at any time. To enable single-user writable mode, run:
+
+ sudo chmod 0755 MOUNTPOINT/.fscrypt/*
+
+To enable world-writable mode, run:
+
+ sudo chmod 1777 MOUNTPOINT/.fscrypt/*
+
+## Setting up for login protectors
+
+If you want any encrypted directories to be protected by your login passphrase,
+you'll need to:
+
+1. Secure your login passphrase (optional, but strongly recommended)
+2. Enable the PAM module (`pam_fscrypt.so`)
+
+If you installed `fscrypt` from source rather than from your distro's package
+manager, you may also need to allow `fscrypt` to check your login passphrase.
+
+### Securing your login passphrase
+
+Although `fscrypt` uses a strong passphrase hash algorithm, the security of
+login protectors is also limited by the strength of your system's passphrase
+hashing in `/etc/shadow`. On most Linux distributions, `/etc/shadow` by default
+uses SHA-512 with 5000 rounds, which is much weaker than what `fscrypt` uses.
+
+To mitigate this, you should use a strong login passphrase.
+
+If using a strong login passphrase is annoying because it needs to be entered
+frequently to run `sudo`, consider increasing the `sudo` timeout. That can be
+done by adding the following to `/etc/sudoers`:
+```
+Defaults timestamp_timeout=60
+```
+
+You should also increase the number of rounds that your system's passphrase
+hashing uses (though this doesn't increase security as much as choosing a strong
+passphrase). To do this, find the line in `/etc/pam.d/passwd` that looks like:
+```
+password required pam_unix.so sha512 shadow nullok
+```
+
+Append `rounds=1000000` (or another number of your choice; the goal is to make
+the passphrase hashing take about 1 second, similar to `fscrypt`'s default):
+```
+password required pam_unix.so sha512 shadow nullok rounds=1000000
+```
+
+Then, change your login passphrase to a new, strong passphrase:
+```
+passwd
+```
+
+If you'd like to keep the same login passphrase (not recommended, as the old
+passphrase hash may still be recoverable from disk), then instead run
+`sudo passwd $USER` and enter your existing passphrase. This re-hashes your
+existing passphrase with the new `rounds`.
+
+### Enabling the PAM module
+
+To enable the PAM module `pam_fscrypt.so`, follow the directions for your Linux
+distro below. Enabling the PAM module is needed for login passphrase-protected
+directories to be automatically unlocked when you log in (and be automatically
+locked when you log out), and for login passphrase-protected directories to
+remain accessible when you change your login passphrase.
+
+#### Enabling the PAM module on Debian or Ubuntu
+
+The official `libpam-fscrypt` package for Debian (and Debian derivatives such as
+Ubuntu) will install a configuration file for [Debian's PAM configuration
+framework](https://wiki.ubuntu.com/PAMConfigFrameworkSpec) to
+`/usr/share/pam-configs/fscrypt`. This file contains reasonable defaults for
+the PAM module. To automatically apply these defaults, run
+`sudo pam-auth-update` and follow the on-screen instructions.
+
+This file also gets installed if you build and install `fscrypt` from source,
+but it is only installed to the correct location if you use `make install
+PREFIX=/usr` to install into `/usr` instead of the default of `/usr/local`.
+
+#### Enabling the PAM module on Arch Linux
+
+On Arch Linux, follow the recommendations at the [Arch Linux
+Wiki](https://wiki.archlinux.org/index.php/Fscrypt#Auto-unlocking_directories).
+
+We recommend using the Arch Linux package, either `fscrypt` (official) or
+`fscrypt-git` (AUR). If you instead install `fscrypt` manually using `sudo make
+install`, then in addition to the steps on the Wiki you'll also need to [create
+`/etc/pam.d/fscrypt`](#allowing-fscrypt-to-check-your-login-passphrase).
+
+#### Enabling the PAM module on other Linux distros
+
+On all other Linux distros, follow the general guidance below to edit
+your PAM configuration files.
+
+The `fscrypt` PAM module implements the Auth, Session, and Password
+[types](http://www.linux-pam.org/Linux-PAM-html/sag-configuration-file.html).
+
+The Password functionality of `pam_fscrypt.so` is used to automatically rewrap
+a user's login protector when their unix passphrase changes. An easy way to get
+the working is to add the line:
+```
+password optional pam_fscrypt.so
+```
+after `pam_unix.so` in `/etc/pam.d/common-password` or similar.
+
+The Auth and Session functionality of `pam_fscrypt.so` are used to automatically
+unlock directories when logging in as a user, and lock them when logging out.
+An easy way to get this working is to add the line:
+```
+auth optional pam_fscrypt.so
+```
+after `pam_unix.so` in `/etc/pam.d/common-auth` or similar, and to add the
+line:
+```
+session optional pam_fscrypt.so
+```
+after `pam_unix.so` in `/etc/pam.d/common-session` or similar, but before
+`pam_systemd.so` or any other module that accesses the user's home directory or
+which starts processes that access the user's home directory during their
+session.
+
+`pam_fscrypt.so` accepts several options:
+
+* `debug`: print additional debug messages to the syslog. All hook types accept
+ this option.
+
+* `unlock_only`: only unlock directories (at log-in); don't also lock them (at
+ log-out). This is only relevant for the "session" hook. Note that in
+ `fscrypt` v0.2.9 and earlier, unlock-only was the default behavior, and
+ `lock_policies` needed to be specified to enable locking.
+
+### Allowing `fscrypt` to check your login passphrase
+
+This step is only needed if you installed `fscrypt` from source code.
+
+Some Linux distros use restrictive settings in `/etc/pam.d/other` that prevent
+programs from checking your login passphrase unless a per-program PAM
+configuration file grants access. This prevents `fscrypt` from creating any
+login passphrase-protected directories, even without auto-unlocking. To ensure
+that `fscrypt` will work properly (if you didn't install an official `fscrypt`
+package from your distro, which should have already handled this), also create a
+file `/etc/pam.d/fscrypt` containing:
+```
+auth required pam_unix.so
+```
+
+## Backup, restore, and recovery
+
+Encrypted files and directories can't be backed up while they are "locked", i.e.
+while they appear in encrypted form. They can only be backed up while they are
+unlocked, in which case they can be backed up like any other files. Note that
+since the encryption is transparent, the files won't be encrypted in the backup
+(unless the backup applies its own encryption).
+
+For the same reason (and several others), an encrypted directory can't be
+directly "moved" to another filesystem. However, it is possible to create a new
+encrypted directory on the destination filesystem using `fscrypt encrypt`, then
+copy the contents of the source directory into it.
+
+For directories protected by a `custom_passphrase` or `raw_key` protector, all
+metadata needed to unlock the directory (excluding the actual passphrase or raw
+key, of course) is located in the `.fscrypt` directory at the root of the
+filesystem that contains the encrypted directory. For example, if you have an
+encrypted directory `/home/$USER/private` that is protected by a custom
+passphrase, all `fscrypt` metadata needed to unlock the directory with that
+custom passphrase will be located in `/home/.fscrypt` if you are using a
+dedicated `/home` filesystem or in `/.fscrypt` if you aren't. If desired, you
+can back up the `fscrypt` metadata by making a copy of this directory, although
+this isn't too important since this metadata is located on the same filesystem
+as the encrypted directory(s).
+
+`pam_passphrase` (login passphrase) protectors are a bit different as they are
+always stored on the root filesystem, in `/.fscrypt`. This ties them to the
+specific system and ensures that each user has only a single login protector.
+Therefore, encrypted directories on a non-root filesystem **can't be unlocked
+via a login protector if the operating system is reinstalled or if the disk is
+connected to another system** -- even if the new system uses the same login
+passphrase for the user.
+
+Because of this, `fscrypt encrypt` will automatically generate a recovery
+passphrase when creating a login passphrase-protected directory on a non-root
+filesystem. The recovery passphrase is simply a `custom_passphrase` protector
+with a randomly generated high-entropy passphrase. Initially, the recovery
+passphrase is stored in a file in the encrypted directory itself; therefore, to
+use it you **must** record it in another secure location. It is strongly
+recommended to do this. Then, if ever needed, you can use `fscrypt unlock` to
+unlock the directory with the recovery passphrase (by choosing the recovery
+protector instead of the login protector).
+
+If you really want to disable the generation of a recovery passphrase, use the
+`--no-recovery` option. Only do this if you really know what you are doing and
+are prepared for potential data loss.
+
+Alternative approaches to supporting recovery of login passphrase-protected
+directories include the following:
+
+* Manually adding your own recovery protector, using
+ `fscrypt metadata add-protector-to-policy`.
+
+* Backing up and restoring the `/.fscrypt` directory on the root filesystem.
+ Note that after restoring the `/.fscrypt` directory, unlocking the login
+ protectors will require the passphrases they had at the time the backup was
+ made **even if they were changed later**, so make sure to remember these
+ passphrase(s) or record them in a secure location. Also note that if the UUID
+ of the root filesystem changed, you will need to manually fix the UUID in any
+ `.fscrypt/protectors/*.link` files on other filesystems.
+
+The auto-generated recovery passphrases should be enough for most users, though.
+
+## Encrypting existing files
+
+`fscrypt` isn't designed to encrypt existing files, as this presents significant
+technical challenges and usually is impossible to do securely. Therefore,
+`fscrypt encrypt` only works on empty directories.
+
+Of course, it is still possible to create an encrypted directory, copy files
+into it, and delete the original files. The `mv` command will even work, as it
+will fall back to a copy and delete ([except on older
+kernels](#getting-operation-not-permitted-when-moving-files-into-an-encrypted-directory)).
+However, beware that due to the characteristics of filesystems and storage
+devices, this may not properly protect the files, as their original contents may
+still be forensically recoverable from disk even after being deleted. It's
+**much** better to encrypt files from the very beginning.
+
+There are only a few cases where copying files into an encrypted directory can
+really make sense, such as:
+
+* The source files are located on an in-memory filesystem such as `tmpfs`.
+
+* The confidentiality of the source files isn't important, e.g. they are
+ system default files and the user hasn't added any personal files yet.
+
+* The source files are protected by a different `fscrypt` policy, the old and
+ new policies are protected by only the same protector(s), and the old policy
+ uses similar strength encryption.
+
+If one of the above doesn't apply, then it's probably too late to securely
+encrypt your existing files.
+
+As a best-effort attempt, you can use the `shred` program to try to erase the
+original files. Here are the recommended commands for "best-effort" encryption
+of an existing directory named "dir":
+
+```bash
+mkdir dir.new
+fscrypt encrypt dir.new
+cp -a -T dir dir.new
+find dir -type f -print0 | xargs -0 shred -n1 --remove=unlink
+rm -rf dir
+mv dir.new dir
+```
+
+However, beware that `shred` isn't guaranteed to be effective on all storage
+devices and filesystems. For example, if you're using an SSD, "overwrites" of
+data typically go to new flash blocks, so they aren't really overwrites.
+
+Note: for reasons similar to the above, changed or removed `fscrypt` protectors
+aren't guaranteed to be forensically unrecoverable from disk either. Thus, the
+use of weak or default passphrases should be avoided, even if changed later.
+
+## Example usage
+
+All these examples assume there is an ext4 filesystem which supports
+encryption mounted at `/mnt/disk`. See
+[here](#getting-encryption-not-enabled-on-an-ext4-filesystem) for how
+to enable encryption support on an ext4 filesystem.
+
+### Setting up fscrypt on a directory
+
+```bash
+# Check which directories on our system support encryption
+>>>>> fscrypt status
+filesystems supporting encryption: 1
+filesystems with fscrypt metadata: 0
+
+MOUNTPOINT DEVICE FILESYSTEM ENCRYPTION FSCRYPT
+/ /dev/sda1 ext4 not enabled No
+/mnt/disk /dev/sdb ext4 supported No
+
+# Create the global configuration file. Nothing else necessarily needs root.
+>>>>> sudo fscrypt setup
+Defaulting to policy_version 2 because kernel supports it.
+Customizing passphrase hashing difficulty for this system...
+Created global config file at "/etc/fscrypt.conf".
+Allow users other than root to create fscrypt metadata on the root filesystem?
+(See https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem) [y/N] y
+Metadata directories created at "/.fscrypt", writable by everyone.
+
+# Start using fscrypt with our filesystem
+>>>>> sudo fscrypt setup /mnt/disk
+Allow users other than root to create fscrypt metadata on this filesystem? (See
+https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem) [y/N] y
+Metadata directories created at "/mnt/disk/.fscrypt", writable by everyone.
+
+# Initialize encryption on a new empty directory
+>>>>> mkdir /mnt/disk/dir1
+>>>>> fscrypt encrypt /mnt/disk/dir1
+The following protector sources are available:
+1 - Your login passphrase (pam_passphrase)
+2 - A custom passphrase (custom_passphrase)
+3 - A raw 256-bit key (raw_key)
+Enter the source number for the new protector [2 - custom_passphrase]: 2
+Enter a name for the new protector: Super Secret
+Enter custom passphrase for protector "Super Secret":
+Confirm passphrase:
+"/mnt/disk/dir1" is now encrypted, unlocked, and ready for use.
+
+# We can see this created one policy and one protector for this directory
+>>>>> fscrypt status /mnt/disk
+ext4 filesystem "/mnt/disk" has 1 protector and 1 policy
+
+PROTECTOR LINKED DESCRIPTION
+7626382168311a9d No custom protector "Super Secret"
+
+POLICY UNLOCKED PROTECTORS
+16382f282d7b29ee27e6460151d03382 Yes 7626382168311a9d
+```
+
+#### Quiet version
+```bash
+>>>>> sudo fscrypt setup --quiet --force --all-users
+>>>>> sudo fscrypt setup /mnt/disk --quiet --all-users
+>>>>> echo "hunter2" | fscrypt encrypt /mnt/disk/dir1 --quiet --source=custom_passphrase --name="Super Secret"
+```
+
+### Locking and unlocking a directory
+
+```bash
+# Write a file to our encrypted directory.
+>>>>> echo "Hello World" > /mnt/disk/dir1/secret.txt
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy: 16382f282d7b29ee27e6460151d03382
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR LINKED DESCRIPTION
+7626382168311a9d No custom protector "Super Secret"
+
+# Lock the directory. Note: if using a v1 encryption policy instead
+# of v2, you'll need 'sudo fscrypt lock /mnt/disk/dir1 --user=$USER'.
+>>>>> fscrypt lock /mnt/disk/dir1
+"/mnt/disk/dir1" is now locked.
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy: 16382f282d7b29ee27e6460151d03382
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: No
+
+Protected with 1 protector:
+PROTECTOR LINKED DESCRIPTION
+7626382168311a9d No custom protector "Super Secret"
+
+# Now the filenames and file contents are inaccessible
+>>>>> ls /mnt/disk/dir1
+u,k20l9HrtrizDjh0zGkw2dTfBkX4T0ZDUlsOhBLl4P
+>>>>> cat /mnt/disk/dir1/u,k20l9HrtrizDjh0zGkw2dTfBkX4T0ZDUlsOhBLl4P
+cat: /mnt/disk/dir1/u,k20l9HrtrizDjh0zGkw2dTfBkX4T0ZDUlsOhBLl4P: Required key not available
+
+# Unlocking the directory makes the contents available
+>>>>> fscrypt unlock /mnt/disk/dir1
+Enter custom passphrase for protector "Super Secret":
+"/mnt/disk/dir1" is now unlocked and ready for use.
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy: 16382f282d7b29ee27e6460151d03382
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR LINKED DESCRIPTION
+7626382168311a9d No custom protector "Super Secret"
+>>>>> cat /mnt/disk/dir1/secret.txt
+Hello World
+```
+
+#### Quiet version
+```bash
+>>>>> fscrypt lock /mnt/disk/dir1 --quiet
+>>>>> echo "hunter2" | fscrypt unlock /mnt/disk/dir1 --quiet
+```
+
+### Protecting a directory with your login passphrase
+
+First, ensure that you have properly [set up your system for login
+protectors](#setting-up-for-login-protectors).
+
+Then, you can protect directories with your login passphrase as follows:
+
+```bash
+# Select your login passphrase as the desired source.
+>>>>> mkdir /mnt/disk/dir2
+>>>>> fscrypt encrypt /mnt/disk/dir2
+Should we create a new protector? [y/N] y
+The following protector sources are available:
+1 - Your login passphrase (pam_passphrase)
+2 - A custom passphrase (custom_passphrase)
+3 - A raw 256-bit key (raw_key)
+Enter the source number for the new protector [2 - custom_passphrase]: 1
+Enter login passphrase for joerichey:
+"/mnt/disk/dir2" is now encrypted, unlocked, and ready for use.
+
+# Note that the login protector actually sits on the root filesystem
+>>>>> fscrypt status /mnt/disk/dir2
+"/mnt/disk/dir2" is encrypted with fscrypt.
+
+Policy: fe1c92009abc1cff4f3257c77e8134e3
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR LINKED DESCRIPTION
+6891f0a901f0065e Yes (/) login protector for joerichey
+>>>>> fscrypt status /mnt/disk
+ext4 filesystem "/mnt/disk" has 2 protectors and 2 policies
+
+PROTECTOR LINKED DESCRIPTION
+7626382168311a9d No custom protector "Super Secret"
+6891f0a901f0065e Yes (/) login protector for joerichey
+
+POLICY UNLOCKED PROTECTORS
+16382f282d7b29ee27e6460151d03382 Yes 7626382168311a9d
+fe1c92009abc1cff4f3257c77e8134e3 Yes 6891f0a901f0065e
+>>>>> fscrypt status /
+ext4 filesystem "/" has 1 protector(s) and 0 policy(ies)
+
+PROTECTOR LINKED DESCRIPTION
+6891f0a901f0065e No login protector for joerichey
+```
+
+#### Quiet version
+```bash
+>>>>> mkdir /mnt/disk/dir2
+>>>>> echo "password" | fscrypt encrypt /mnt/disk/dir2 --source=pam_passphrase --quiet
+```
+
+### Changing a custom passphrase
+```bash
+# First we have to figure out which protector we wish to change.
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy: 16382f282d7b29ee27e6460151d03382
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR LINKED DESCRIPTION
+7626382168311a9d No custom protector "Super Secret"
+
+# Now specify the protector directly to the metadata command
+>>>>> fscrypt metadata change-passphrase --protector=/mnt/disk:7626382168311a9d
+Enter old custom passphrase for protector "Super Secret":
+Enter new custom passphrase for protector "Super Secret":
+Confirm passphrase:
+Passphrase for protector 7626382168311a9d successfully changed.
+```
+
+#### Quiet version
+```bash
+>>>>> printf "hunter2\nhunter3" | fscrypt metadata change-passphrase --protector=/mnt/disk:7626382168311a9d --quiet
+```
+
+### Using a raw key protector
+
+`fscrypt` also supports protectors which use raw key files as the user-provided
+secret. These key files must be exactly 32 bytes long and contain the raw binary
+data of the key. Obviously, make sure to store the key file securely (and not in
+the directory you are encrypting with it). If generating the keys on Linux make
+sure you are aware of
+[how randomness works](http://man7.org/linux/man-pages/man7/random.7.html) and
+[some common myths](https://www.2uo.de/myths-about-urandom/).
+
+```bash
+# Generate a 256-bit key file
+>>>>> head --bytes=32 /dev/urandom > secret.key
+
+# Now create a key file protector without using it on a directory. Note that we
+# could also use `fscrypt encrypt --key=secret.key` to achieve the same thing.
+>>>>> fscrypt metadata create protector /mnt/disk
+Create new protector on "/mnt/disk" [Y/n] y
+The following protector sources are available:
+1 - Your login passphrase (pam_passphrase)
+2 - A custom passphrase (custom_passphrase)
+3 - A raw 256-bit key (raw_key)
+Enter the source number for the new protector [2 - custom_passphrase]: 3
+Enter a name for the new protector: Skeleton
+Enter key file for protector "Skeleton": secret.key
+Protector 2c75f519b9c9959d created on filesystem "/mnt/disk".
+>>>>> fscrypt status /mnt/disk
+ext4 filesystem "/mnt/disk" has 3 protectors and 2 policies
+
+PROTECTOR LINKED DESCRIPTION
+7626382168311a9d No custom protector "Super Secret"
+2c75f519b9c9959d No raw key protector "Skeleton"
+6891f0a901f0065e Yes (/) login protector for joerichey
+
+POLICY UNLOCKED PROTECTORS
+16382f282d7b29ee27e6460151d03382 Yes 7626382168311a9d
+fe1c92009abc1cff4f3257c77e8134e3 Yes 6891f0a901f0065e
+
+# Finally, we could apply this key to a directory
+>>>>> mkdir /mnt/disk/dir3
+>>>>> fscrypt encrypt /mnt/disk/dir3 --protector=/mnt/disk:2c75f519b9c9959d
+Enter key file for protector "Skeleton": secret.key
+"/mnt/disk/dir3" is now encrypted, unlocked, and ready for use.
+```
+
+#### Quiet version
+```bash
+>>>>> head --bytes=32 /dev/urandom > secret.key
+>>>>> fscrypt encrypt /mnt/disk/dir3 --key=secret.key --source=raw_key --name=Skeleton
+```
+
+### Using multiple protectors for a policy
+
+`fscrypt` supports the idea of protecting a single directory with multiple
+protectors. This means having access to any of the protectors is sufficient to
+decrypt the directory. This is useful for sharing data or setting up access
+control systems.
+
+```bash
+# Add an existing protector to the policy for some directory
+>>>>> fscrypt status /mnt/disk
+ext4 filesystem "/mnt/disk" has 3 protectors and 3 policies
+
+PROTECTOR LINKED DESCRIPTION
+7626382168311a9d No custom protector "Super Secret"
+2c75f519b9c9959d No raw key protector "Skeleton"
+6891f0a901f0065e Yes (/) login protector for joerichey
+
+POLICY UNLOCKED PROTECTORS
+d03fb894584a4318d1780e9a7b0b47eb No 2c75f519b9c9959d
+16382f282d7b29ee27e6460151d03382 No 7626382168311a9d
+fe1c92009abc1cff4f3257c77e8134e3 No 6891f0a901f0065e
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy: 16382f282d7b29ee27e6460151d03382
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: No
+
+Protected with 1 protector:
+PROTECTOR LINKED DESCRIPTION
+7626382168311a9d No custom protector "Super Secret"
+>>>>> fscrypt metadata add-protector-to-policy --protector=/mnt/disk:2c75f519b9c9959d --policy=/mnt/disk:16382f282d7b29ee27e6460151d03382
+WARNING: All files using this policy will be accessible with this protector!!
+Protect policy 16382f282d7b29ee27e6460151d03382 with protector 2c75f519b9c9959d? [Y/n]
+Enter key file for protector "Skeleton": secret.key
+Enter custom passphrase for protector "Super Secret":
+Protector 2c75f519b9c9959d now protecting policy 16382f282d7b29ee27e6460151d03382.
+>>>>> fscrypt status /mnt/disk/dir1
+"/mnt/disk/dir1" is encrypted with fscrypt.
+
+Policy: 16382f282d7b29ee27e6460151d03382
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: No
+
+Protected with 2 protectors:
+PROTECTOR LINKED DESCRIPTION
+7626382168311a9d No custom protector "Super Secret"
+2c75f519b9c9959d No raw key protector "Skeleton"
+
+# Now the unlock command will prompt for which protector we want to use
+>>>>> fscrypt unlock /mnt/disk/dir1
+The available protectors are:
+0 - custom protector "Super Secret"
+1 - raw key protector "Skeleton"
+Enter the number of protector to use: 1
+Enter key file for protector "Skeleton": secret.key
+"/mnt/disk/dir1" is now unlocked and ready for use.
+
+# The protector can also be removed from the policy (if it is not the only one)
+>>>>> fscrypt metadata remove-protector-from-policy --protector=/mnt/disk:2c75f519b9c9959d --policy=/mnt/disk:16382f282d7b29ee27e6460151d03382
+WARNING: All files using this policy will NO LONGER be accessible with this protector!!
+Stop protecting policy 16382f282d7b29ee27e6460151d03382 with protector 2c75f519b9c9959d? [y/N] y
+Protector 2c75f519b9c9959d no longer protecting policy 16382f282d7b29ee27e6460151d03382.
+```
+
+#### Quiet version
+```bash
+>>>>> echo "hunter2" | fscrypt metadata add-protector-to-policy --protector=/mnt/disk:2c75f519b9c9959d --policy=/mnt/disk:16382f282d7b29ee27e6460151d03382 --key=secret.key --quiet
+>>>>> fscrypt metadata remove-protector-from-policy --protector=/mnt/disk:2c75f519b9c9959d --policy=/mnt/disk:16382f282d7b29ee27e6460151d03382 --quiet --force
+```
+
+## Contributing
+
+We would love to accept your contributions to `fscrypt`. See the
+`CONTRIBUTING.md` file for more information about signing the CLA and submitting
+a pull request.
+
+## Troubleshooting
+
+In general, if you are encountering issues with `fscrypt`,
+[open an issue](https://github.com/google/fscrypt/issues/new), following the
+guidelines in `CONTRIBUTING.md`. We will try our best to help.
+
+#### I changed my login passphrase, now all my directories are inaccessible
+
+Usually, the PAM module `pam_fscrypt.so` will automatically detect changes to a
+user's login passphrase and update the user's `fscrypt` login protector so that
+they retain access their login-passphrase protected directories. However,
+sometimes a user's login passphrase can become desynchronized from their
+`fscrypt` login protector. This can happen if `root` assigns the user a new
+passphrase without providing the old one, if the user's login passphrase is
+managed by an external system such as LDAP, if the PAM module is not installed,
+or if the PAM module is not properly configured. See [Enabling the PAM
+module](#enabling-the-pam-module) for how to configure the PAM module.
+
+To fix a user's login protector, find the corresponding protector ID by running
+`fscrypt status "/"`. Then, change this protector's passphrase by running:
+```
+fscrypt metadata change-passphrase --protector=/:ID
+```
+
+#### Directories using my login passphrase are not automatically unlocking
+
+First, directories won't unlock if your session starts without password
+authentication. The most common case of this is public-key ssh login. To
+trigger a password authentication event, run `su $USER -c exit`.
+
+If your session did start with password authentication, then the following may
+be causing the issue:
+
+* The PAM module might not be configured correctly. Ensure you have correctly
+ [configured the PAM module](#enabling-the-pam-module).
+
+* If your login passphrase recently changed, then it might have gotten out of
+ sync with your login protector. To fix this, [manually change your login
+ protector's
+ passphrase](#i-changed-my-login-passphrase-now-all-my-directories-are-inaccessible)
+ to get it back in sync with your actual login passphrase.
+
+* Due to a [bug in sshd](https://bugzilla.mindrot.org/show_bug.cgi?id=2548),
+ encrypted directories won't auto-unlock when logging in with ssh using the
+ `ChallengeResponseAuthentication` ssh authentication method, which is also
+ called `KbdInteractiveAuthentication`. This ssh authentication method
+ implements password authentication by default, so it might appear similar to
+ `PasswordAuthentication`. However, only `PasswordAuthentication` works with
+ `fscrypt`. To avoid this issue, make sure that your `/etc/ssh/sshd_config`
+ file contains `PasswordAuthentication yes`, `UsePAM yes`, and either
+ `ChallengeResponseAuthentication no` or `KbdInteractiveAuthentication no`.
+
+#### Getting "encryption not enabled" on an ext4 filesystem
+
+This is usually caused by your ext4 filesystem not having the `encrypt` feature
+flag enabled. The `encrypt` feature flag allows the filesystem to contain
+encrypted files. (It doesn't actually encrypt anything by itself.)
+
+Before enabling `encrypt` on your ext4 filesystem, first ensure that all of the
+following are true for you:
+
+* You only need to use your filesystem on kernels v4.1 and later.
+
+ (Kernels v4.0 and earlier can't mount ext4 filesystems that have the `encrypt`
+ feature flag.)
+
+* Either you only need to use your filesystem on kernels v5.5 and later, or your
+ kernel page size (run `getconf PAGE_SIZE`) and filesystem block size (run
+ `tune2fs -l /dev/device | grep 'Block size'`) are the same.
+
+ (Both values will almost always be 4096, but they may differ if your
+ filesystem is very small, if your system uses the PowerPC CPU architecture, or
+ if you overrode the default block size when you created the filesystem. Only
+ kernels v5.5 and later support ext4 encryption in such cases.)
+
+* Either you aren't using GRUB to boot directly off the filesystem in question,
+ or you are using GRUB 2.04 or later.
+
+ (Old versions of GRUB can't boot from ext4 filesystems that have `encrypt`
+ enabled. If, like most people, you have a separate `/boot` partition, you are
+ fine. You are also fine if you are using the GRUB Debian package `2.02-2` or
+ later [*not* `2.02_beta*`], including the version in Ubuntu 18.04 and later,
+ since the patch to support `encrypt` was backported.)
+
+After verifying all of the above, enable `encrypt` by running:
+```
+tune2fs -O encrypt /dev/device
+```
+
+If you need to undo this, first delete all encrypted files and directories on
+the filesystem. Then, run:
+```
+fsck -fn /dev/device
+debugfs -w -R "feature -encrypt" /dev/device
+fsck -fn /dev/device
+```
+
+If you've enabled `encrypt` but you still get the "encryption not enabled"
+error, then the problem is that ext4 encryption isn't enabled in your kernel
+config. See [Runtime dependencies](#runtime-dependencies) for how to enable it.
+
+#### Getting "user keyring not linked into session keyring"
+
+Some older versions of Ubuntu didn't link the user keyring into the session
+keyring, which caused problems with `fscrypt`.
+
+To avoid this issue, upgrade to Ubuntu 20.04 or later.
+
+#### Getting "Operation not permitted" when moving files into an encrypted directory
+
+Originally, filesystems didn't return the correct error code when attempting to
+rename unencrypted files (or files with a different encryption policy) into an
+encrypted directory. Specifically, they returned `EPERM` instead of `EXDEV`,
+which caused `mv` to fail rather than fall back to a copy as expected.
+
+This bug was fixed in version 5.1 of the mainline Linux kernel, as well as in
+versions 4.4 and later of the LTS (Long Term Support) branches of the Linux
+kernel; specifically v4.19.155, 4.14.204, v4.9.242, and v4.4.242.
+
+If the kernel can't be upgraded, this bug can be worked around by explicitly
+copying the files instead, e.g. with `cp`.
+
+__IMPORTANT:__ Encrypting existing files can be insecure. Before doing so, read
+[Encrypting existing files](#encrypting-existing-files).
+
+#### Getting "Package not installed" when trying to use an encrypted directory
+
+Trying to create or open an encrypted file will fail with `ENOPKG` ("Package not
+installed") when the kernel doesn't support one or more of the cryptographic
+algorithms used by the file or its directory. Note that `fscrypt encrypt` and
+`fscrypt unlock` will still succeed; it's only using the directory afterwards
+that will fail.
+
+The kernel will always support the algorithms that `fscrypt` uses by default.
+However, if you changed the contents and/or filenames encryption algorithms in
+[`/etc/fscrypt.conf`](#configuration-file), then you may run into this issue.
+To fix it, enable the needed `CONFIG_CRYPTO_*` options in your Linux kernel
+configuration. See the [kernel
+documentation](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html#encryption-modes-and-usage)
+for details about which option(s) are required for each encryption mode.
+
+#### Some processes can't access unlocked encrypted files
+
+This issue is caused by a limitation in the original design of Linux native
+filesystem encryption which made it difficult to ensure that all processes can
+access unlocked encrypted files. This issue can manifest in many ways, such as:
+
+* SSH to a user with an encrypted home directory not working, even when that
+ directory is already unlocked
+
+* Docker containers being unable to access encrypted files that were unlocked
+ from outside the container
+
+* NetworkManager being unable to access certificates stored in the user's
+ already-unlocked encrypted home directory
+
+* Other system services being unable to access already-unlocked encrypted files
+
+* `sudo` sessions being unable to access already-unlocked encrypted files
+
+* A user being unable to access encrypted files that were unlocked by root
+
+If an OS-level error is shown, it is `ENOKEY` ("Required key not available").
+
+To fix this issue, first run `fscrypt status $dir`, where `$dir` is your
+encrypted directory. If the output contains `policy_version:2`, then your issue
+is something else, so stop reading now. If the output contains
+`policy_version:1` or doesn't contain any mention of `policy_version`, then
+you'll need to upgrade your directory(s) to policy version 2. To do this:
+
+1. Upgrade to Linux kernel v5.4 or later.
+
+2. Upgrade to `fscrypt` v0.2.7 or later.
+
+3. Run `sudo fscrypt setup --force`.
+
+4. Re-encrypt your encrypted directory(s). Since files cannot be (re-)encrypted
+ in-place, this requires replacing them with new directories. For example:
+ ```
+ fscrypt unlock dir # if not already unlocked
+ mkdir dir.new
+ fscrypt encrypt dir.new
+ cp -a -T dir dir.new
+ find dir -type f -print0 | xargs -0 shred -n1 --remove=unlink
+ rm -rf dir
+ mv dir.new dir
+ ```
+
+ You don't need to create a new protector. I.e., when `fscrypt encrypt` asks
+ for a protector, just choose the one you were using before.
+
+5. `fscrypt status` on your directory(s) should now show `policy_version:2`,
+ and the issue should be gone.
+
+Note that once your directories are using policy version 2, they will only be
+usable with Linux kernel v5.4 and later and `fscrypt` v0.2.6 and later. So be
+careful not to downgrade your software past those versions.
+
+This issue can also be fixed by setting `"use_fs_keyring_for_v1_policies": true`
+in `/etc/fscrypt.conf`, as described in [Configuration
+file](#configuration-file). This avoids needing to upgrade directories to
+policy version 2. However, this has some limitations, and the same kernel and
+`fscrypt` prerequisites still apply for this option to take effect. It is
+recommended to upgrade your directories to policy version 2 instead.
+
+#### Users can access other users' unlocked encrypted files
+
+This is working as intended. When an encrypted directory is unlocked (or
+locked), it is unlocked (or locked) for all users. Encryption is not access
+control; the Linux kernel already has many access control mechanisms, such as
+the standard UNIX file permissions, that can be used to control access to files.
+
+Setting the mode of your encrypted directory to `0700` will prevent users other
+than the directory's owner and `root` from accessing it while it is unlocked.
+In `fscrypt` v0.2.5 and later, `fscrypt encrypt` sets this mode automatically.
+
+Having the locked/unlocked status of directories be global instead of per-user
+may seem unintuitive, but it is actually the only logical way. The encryption
+is done by the filesystem, so in reality the filesystem either has the key or it
+doesn't. And once it has the key, any additional checks of whether particular
+users "have" the key would be OS-level access control checks (not cryptography)
+that are redundant with existing OS-level access control mechanisms.
+
+Similarly, any attempt of the filesystem encryption feature to prevent `root`
+from accessing unlocked encrypted files would be pointless. On Linux systems,
+`root` is usually all-powerful and can always get access to files in ways that
+cannot be prevented, e.g. `setuid()` and `ptrace()`. The only reliable way to
+limit what `root` can do is via a mandatory access control system, e.g. SELinux.
+
+The original design of Linux native filesystem encryption actually did put the
+keys into per-user keyrings. However, this caused a [massive number of
+problems](#some-processes-cant-access-unlocked-encrypted-files), as it's
+actually very common that encrypted files need to be accessed by processes
+running under different user IDs -- even if it may not be immediately apparent.
+
+#### Getting "Required key not available" when backing up locked encrypted files
+
+Encrypted files can't be backed up while locked; you need to unlock them first.
+For details, see [Backup, restore, and recovery](#backup-restore-and-recovery).
+
+#### The reported size of encrypted symlinks is wrong
+
+Originally, filesystems didn't conform to POSIX when reporting the size of
+encrypted symlinks, as they gave the size of the ciphertext symlink target
+rather than the size of the plaintext target. This would make the reported size
+of symlinks appear to be slightly too large when queried using ``lstat()`` or
+similar system calls. Most programs don't care about this, but in rare cases
+programs can depend on the filesystem reporting symlink sizes correctly.
+
+This bug was fixed in version 5.15 of the mainline Linux kernel, as well as in
+versions 4.19 and later of the LTS (Long Term Support) branches of the Linux
+kernel; specifically v5.10.63, v5.4.145, and v4.19.207.
+
+If the kernel can't be upgraded, the only workaround for this bug is to update
+any affected programs to not depend on symlink sizes being reported correctly.
+
+## Legal
+
+Copyright 2017 Google Inc. under the
+[Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0); see the
+`LICENSE` file for more information.
+
+Author: Joe Richey <joerichey@google.com>
+
+This is not an official Google product.
--- /dev/null
+/*
+ * 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)
--- /dev/null
+/*
+ * 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)
+}
--- /dev/null
+/*
+ * 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")
+ }
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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)
+}
--- /dev/null
+/*
+ * 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)
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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()
+ }
+ }
+}
--- /dev/null
+/*
+ * 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)
+}
--- /dev/null
+/*
+ * 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")
+ }
+}
--- /dev/null
+/*
+ * 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()
+}
--- /dev/null
+/*
+ * 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)")
+ }
+}
--- /dev/null
+#!/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
--- /dev/null
+# 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.
--- /dev/null
+#!/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 -
+}
--- /dev/null
+#!/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!"
--- /dev/null
+
+# Create encrypted directory
+
+# Try to unlock with wrong passphrase
+[ERROR] fscrypt unlock: incorrect key provided
+mkdir: cannot create directory 'MNT/dir/subdir': Required key not available
+
+# Change passphrase
+
+# Try to unlock with old passphrase
+[ERROR] fscrypt unlock: incorrect key provided
+mkdir: cannot create directory 'MNT/dir/subdir': Required key not available
+
+# Unlock with new passphrase
+
+# Try to change passphrase (interactively, mismatch)
+spawn fscrypt metadata change-passphrase --protector=MNT:desc1\r
+Enter old custom passphrase for protector "prot": \r
+Enter new custom passphrase for protector "prot": \r
+Confirm passphrase: \r
+[ERROR] fscrypt metadata change-passphrase: entered passphrases do not match\r
+
+# Change passphrase (interactively)
+spawn fscrypt metadata change-passphrase --protector=MNT:desc1\r
+Enter old custom passphrase for protector "prot": \r
+Enter new custom passphrase for protector "prot": \r
+Confirm passphrase: \r
+Passphrase for protector desc1 successfully changed.\r
+
+# Lock, then unlock with new passphrase
+"MNT/dir" is now locked.
+mkdir: cannot create directory 'MNT/dir/subdir': Required key not available
--- /dev/null
+#!/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"
--- /dev/null
+
+# 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
--- /dev/null
+#!/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
--- /dev/null
+
+# Encrypt with custom passphrase protector
+ext4 filesystem "MNT" has 1 protector and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc1 No custom protector "prot"
+
+POLICY UNLOCKED PROTECTORS
+desc2 Yes desc1
+"MNT/dir" is encrypted with fscrypt.
+
+Policy: desc2
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR LINKED DESCRIPTION
+desc1 No custom protector "prot"
+
+# Encrypt with custom passphrase protector, interactively
+spawn fscrypt encrypt MNT/dir\r
+The following protector sources are available:\r
+1 - Your login passphrase (pam_passphrase)\r
+2 - A custom passphrase (custom_passphrase)\r
+3 - A raw 256-bit key (raw_key)\r
+Enter the source number for the new protector [2 - custom_passphrase]: 2\r
+Enter a name for the new protector: prot\r
+Enter custom passphrase for protector "prot": \r
+Confirm passphrase: \r
+"MNT/dir" is now encrypted, unlocked, and ready for use.\r
+ext4 filesystem "MNT" has 1 protector and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc6 No custom protector "prot"
+
+POLICY UNLOCKED PROTECTORS
+desc7 Yes desc6
+"MNT/dir" is encrypted with fscrypt.
+
+Policy: desc7
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR LINKED DESCRIPTION
+desc6 No custom protector "prot"
+
+# Try to use a custom protector without a name
+[ERROR] fscrypt encrypt: custom_passphrase protectors must be named
+
+Use --name=PROTECTOR_NAME to specify a protector name.
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
--- /dev/null
+#!/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
--- /dev/null
+
+# Encrypt with login protector
+
+IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for
+ important recovery instructions. It is *strongly recommended* to
+ record the recovery passphrase in a secure location; otherwise you
+ will lose access to this directory if you reinstall the operating
+ system or move this filesystem to another system.
+
+ext4 filesystem "MNT" has 2 protectors and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc1 Yes (MNT_ROOT) login protector for fscrypt-test-user
+desc2 No custom protector "Recovery passphrase for dir"
+
+POLICY UNLOCKED PROTECTORS
+desc3 Yes desc1, desc2
+ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc1 No login protector for fscrypt-test-user
+"MNT/dir" is encrypted with fscrypt.
+
+Policy: desc3
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 2 protectors:
+PROTECTOR LINKED DESCRIPTION
+desc1 Yes (MNT_ROOT) login protector for fscrypt-test-user
+desc2 No custom protector "Recovery passphrase for dir"
+
+# => Lock, then unlock with login passphrase
+"MNT/dir" is now locked.
+
+# => Lock, then unlock with recovery passphrase
+"MNT/dir" is now locked.
+
+# Encrypt with login protector, interactively
+spawn fscrypt encrypt MNT/dir\r
+The following protector sources are available:\r
+1 - Your login passphrase (pam_passphrase)\r
+2 - A custom passphrase (custom_passphrase)\r
+3 - A raw 256-bit key (raw_key)\r
+Enter the source number for the new protector [2 - custom_passphrase]: 1\r
+\r
+IMPORTANT: Before continuing, ensure you have properly set up your system for\r
+ login protectors. See\r
+ https://github.com/google/fscrypt#setting-up-for-login-protectors\r
+\r
+Enter login passphrase for fscrypt-test-user: \r
+\r
+IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for\r
+ important recovery instructions. It is *strongly recommended* to\r
+ record the recovery passphrase in a secure location; otherwise you\r
+ will lose access to this directory if you reinstall the operating\r
+ system or move this filesystem to another system.\r
+\r
+"MNT/dir" is now encrypted, unlocked, and ready for use.\r
+ext4 filesystem "MNT" has 2 protectors and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc10 Yes (MNT_ROOT) login protector for fscrypt-test-user
+desc11 No custom protector "Recovery passphrase for dir"
+
+POLICY UNLOCKED PROTECTORS
+desc12 Yes desc10, desc11
+ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc10 No login protector for fscrypt-test-user
+"MNT/dir" is encrypted with fscrypt.
+
+Policy: desc12
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 2 protectors:
+PROTECTOR LINKED DESCRIPTION
+desc10 Yes (MNT_ROOT) login protector for fscrypt-test-user
+desc11 No custom protector "Recovery passphrase for dir"
+
+# Encrypt with login protector as root
+
+IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for
+ important recovery instructions. It is *strongly recommended* to
+ record the recovery passphrase in a secure location; otherwise you
+ will lose access to this directory if you reinstall the operating
+ system or move this filesystem to another system.
+
+ext4 filesystem "MNT" has 2 protectors and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc19 Yes (MNT_ROOT) login protector for fscrypt-test-user
+desc20 No custom protector "Recovery passphrase for dir"
+
+POLICY UNLOCKED PROTECTORS
+desc21 Yes desc19, desc20
+ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc19 No login protector for fscrypt-test-user
+"MNT/dir" is encrypted with fscrypt.
+
+Policy: desc21
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 2 protectors:
+PROTECTOR LINKED DESCRIPTION
+desc19 Yes (MNT_ROOT) login protector for fscrypt-test-user
+desc20 No custom protector "Recovery passphrase for dir"
+
+Protector is owned by fscrypt-test-user:fscrypt-test-user
+"MNT/dir" is now locked.
+"MNT/dir" is now locked.
+
+# Encrypt with login protector with --no-recovery
+ext4 filesystem "MNT" has 1 protector and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc28 Yes (MNT_ROOT) login protector for fscrypt-test-user
+
+POLICY UNLOCKED PROTECTORS
+desc29 Yes desc28
+ext4 filesystem "MNT_ROOT" has 1 protector and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc28 No login protector for fscrypt-test-user
+"MNT/dir" is encrypted with fscrypt.
+
+Policy: desc29
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR LINKED DESCRIPTION
+desc28 Yes (MNT_ROOT) login protector for fscrypt-test-user
+
+# Encrypt with login protector on root fs (shouldn't generate a recovery passphrase)
+"MNT_ROOT/dir" is encrypted with fscrypt.
+
+Policy: desc34
+Options: padding:32 contents:AES_256_XTS filenames:AES_256_CTS policy_version:2
+Unlocked: Yes
+
+Protected with 1 protector:
+PROTECTOR LINKED DESCRIPTION
+desc35 No login protector for fscrypt-test-user
+ext4 filesystem "MNT_ROOT" has 1 protector and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc35 No login protector for fscrypt-test-user
+
+POLICY UNLOCKED PROTECTORS
+desc34 Yes desc35
+
+# Try to give a login protector a name
+[ERROR] fscrypt encrypt: cannot assign name "prot" to new login protector for
+ user "fscrypt-test-user" because login protectors are
+ identified by user, not by name.
+
+To fix this, don't specify the --name=PROTECTOR_NAME option.
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+ext4 filesystem "MNT_ROOT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
+
+# Try to use the wrong login passphrase
+[ERROR] fscrypt encrypt: incorrect login passphrase
+ext4 filesystem "MNT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+ext4 filesystem "MNT_ROOT" has 0 protectors and 0 policies.
+All users can create fscrypt metadata on this filesystem.
+
+[ERROR] fscrypt status: file or directory "MNT/dir" is not
+ encrypted
+
+# Test that linked protector works even if UUID link is broken
+
+IMPORTANT: See "MNT/dir/fscrypt_recovery_readme.txt" for
+ important recovery instructions. It is *strongly recommended* to
+ record the recovery passphrase in a secure location; otherwise you
+ will lose access to this directory if you reinstall the operating
+ system or move this filesystem to another system.
+
+ext4 filesystem "MNT" has 2 protectors and 1 policy.
+All users can create fscrypt metadata on this filesystem.
+
+PROTECTOR LINKED DESCRIPTION
+desc39 No custom protector "Recovery passphrase for dir"
+desc40 Yes (MNT_ROOT) login protector for fscrypt-test-user
+
+POLICY UNLOCKED PROTECTORS
+desc41 Yes desc40, desc39
--- /dev/null
+#!/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"
--- /dev/null
+
+# 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"
--- /dev/null
+#!/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
--- /dev/null
+
+# 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.)
--- /dev/null
+#!/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'"
--- /dev/null
+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
--- /dev/null
+#!/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"
--- /dev/null
+
+# 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"
--- /dev/null
+#!/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"
--- /dev/null
+
+# 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.
--- /dev/null
+#!/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'"
--- /dev/null
+#!/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
--- /dev/null
+
+# 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.
--- /dev/null
+#!/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'"
--- /dev/null
+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)
--- /dev/null
+#!/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\""
--- /dev/null
+
+# 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.
--- /dev/null
+#!/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'"
--- /dev/null
+
+# 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
--- /dev/null
+#!/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'"
--- /dev/null
+
+# 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
--- /dev/null
+#!/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'"
--- /dev/null
+
+# 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
--- /dev/null
+#!/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'"
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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()
+}
--- /dev/null
+/*
+ * 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()
+}
--- /dev/null
+/*
+ * fscrypt.go - File which starts up and runs the application. Initializes
+ * information about the application like the name, version, author, etc...
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+/*
+fscrypt is a command line tool for managing linux filesystem encryption.
+*/
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "os"
+
+ "github.com/urfave/cli"
+
+ "github.com/google/fscrypt/actions"
+ "github.com/google/fscrypt/filesystem"
+)
+
+// Current version of the program (set by Makefile)
+var version string
+
+func main() {
+ cli.AppHelpTemplate = appHelpTemplate
+ cli.CommandHelpTemplate = commandHelpTemplate
+ cli.SubcommandHelpTemplate = subcommandHelpTemplate
+
+ if conffile := os.Getenv("FSCRYPT_CONF"); conffile != "" {
+ actions.ConfigFileLocation = conffile
+ }
+ if rootmnt := os.Getenv("FSCRYPT_ROOT_MNT"); rootmnt != "" {
+ actions.LoginProtectorMountpoint = rootmnt
+ }
+ if consistent := os.Getenv("FSCRYPT_CONSISTENT_OUTPUT"); consistent == "1" {
+ filesystem.SortDescriptorsByLastMtime = true
+ }
+
+ // Create our command line application
+ app := cli.NewApp()
+ app.Usage = shortUsage
+
+ // Grab the version passed in from the Makefile.
+ app.Version = version
+ app.OnUsageError = onUsageError
+
+ // Setup global flags
+ cli.HelpFlag = helpFlag
+ cli.VersionFlag = versionFlag
+ app.Flags = universalFlags
+
+ // We hide the help subcommand so that "fscrypt <command> --help" works
+ // and "fscrypt <command> help" does not.
+ app.HideHelp = true
+
+ // Initialize command list and setup all of the commands.
+ app.Action = defaultAction
+ app.Commands = []cli.Command{Setup, Encrypt, Unlock, Lock, Purge, Status, Metadata}
+ for i := range app.Commands {
+ setupCommand(&app.Commands[i])
+ }
+
+ app.Run(os.Args)
+}
+
+// setupCommand performs some common setup for each command. This includes
+// hiding the help, formatting the description, adding in the necessary
+// flags, setting up error handlers, etc... Note that the command is modified
+// in place and its subcommands are also setup.
+func setupCommand(command *cli.Command) {
+ command.Description = wrapText(command.Description, indentLength)
+ command.HideHelp = true
+ command.Flags = append(command.Flags, universalFlags...)
+
+ if command.Action == nil {
+ command.Action = defaultAction
+ }
+
+ // Setup function handlers
+ command.OnUsageError = onUsageError
+ if len(command.Subcommands) == 0 {
+ command.Before = setupBefore
+ } else {
+ // Setup subcommands (if applicable)
+ for i := range command.Subcommands {
+ setupCommand(&command.Subcommands[i])
+ }
+ }
+}
+
+// setupBefore makes sure our logs, errors, and output are going to the correct
+// io.Writers and that we haven't over-specified our flags. We only print the
+// logs when using verbose, and only print normal stuff when not using quiet.
+func setupBefore(c *cli.Context) error {
+ log.SetOutput(io.Discard)
+ c.App.Writer = io.Discard
+
+ if verboseFlag.Value {
+ log.SetOutput(os.Stdout)
+ }
+ if !quietFlag.Value {
+ c.App.Writer = os.Stdout
+ }
+ return nil
+}
+
+// defaultAction will be run when no command is specified.
+func defaultAction(c *cli.Context) error {
+ // Always default to showing the help
+ if helpFlag.Value {
+ cli.ShowAppHelp(c)
+ return nil
+ }
+
+ // Only exit when not calling with the help command
+ var message string
+ if args := c.Args(); args.Present() {
+ message = fmt.Sprintf("command \"%s\" not found", args.First())
+ } else {
+ message = "no command was specified"
+ }
+ return &usageError{c, message}
+}
--- /dev/null
+# 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
--- /dev/null
+/*
+ * 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) {}
--- /dev/null
+/*
+ * 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
+ }
+ }
+}
--- /dev/null
+/*
+ * 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)
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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)
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * crypto_test.go - tests for the crypto package
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package crypto
+
+import (
+ "bytes"
+ "compress/zlib"
+ "crypto/aes"
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "os"
+ "testing"
+
+ "github.com/google/fscrypt/metadata"
+)
+
+// Reader that always returns the same byte
+type ConstReader byte
+
+func (r ConstReader) Read(b []byte) (n int, err error) {
+ for i := range b {
+ b[i] = byte(r)
+ }
+ return len(b), nil
+}
+
+// Makes a key of the same repeating byte
+func makeKey(b byte, n int) (*Key, error) {
+ return NewFixedLengthKeyFromReader(ConstReader(b), n)
+}
+
+var (
+ fakeSalt = bytes.Repeat([]byte{'a'}, metadata.SaltLen)
+ fakePassword = []byte("password")
+
+ fakeValidPolicyKey, _ = makeKey(42, metadata.PolicyKeyLen)
+ fakeWrappingKey, _ = makeKey(17, metadata.InternalKeyLen)
+)
+
+// As the passphrase hashing function clears the passphrase, we need to make
+// a new passphrase key for each test
+func fakePassphraseKey() (*Key, error) {
+ return NewFixedLengthKeyFromReader(bytes.NewReader(fakePassword), len(fakePassword))
+}
+
+// Values for test cases pulled from argon2 command line tool.
+// To generate run:
+//
+// echo "password" | argon2 "aaaaaaaaaaaaaaaa" -id -t <t> -m <m> -p <p> -l 32
+//
+// where costs.Time = <t>, costs.Memory = 2^<m>, and costs.Parallelism = <p>.
+type hashTestCase struct {
+ costs *metadata.HashingCosts
+ hexHash string
+}
+
+var hashTestCases = []hashTestCase{
+ {
+ costs: &metadata.HashingCosts{Time: 1, Memory: 1 << 10, Parallelism: 1},
+ hexHash: "a66f5398e33761bf161fdf1273e99b148f07d88d12d85b7673fddd723f95ec34",
+ },
+ // Make sure we maintain our backwards compatible behavior, where
+ // Parallelism is truncated to 8-bits unless TruncationFixed is true.
+ {
+ costs: &metadata.HashingCosts{Time: 1, Memory: 1 << 10, Parallelism: 257},
+ hexHash: "a66f5398e33761bf161fdf1273e99b148f07d88d12d85b7673fddd723f95ec34",
+ },
+ {
+ costs: &metadata.HashingCosts{Time: 10, Memory: 1 << 10, Parallelism: 1},
+ hexHash: "5fa2cb89db1f7413ba1776258b7c8ee8c377d122078d28fe1fd645c353787f50",
+ },
+ {
+ costs: &metadata.HashingCosts{Time: 1, Memory: 1 << 15, Parallelism: 1},
+ hexHash: "f474a213ed14d16ead619568000939b938ddfbd2ac4a82d253afa81b5ebaef84",
+ },
+ {
+ costs: &metadata.HashingCosts{Time: 1, Memory: 1 << 10, Parallelism: 10},
+ hexHash: "b7c3d7a0be222680b5ea3af3fb1a0b7b02b92cbd7007821dc8b84800c86c7783",
+ },
+ {
+ costs: &metadata.HashingCosts{Time: 1, Memory: 1 << 11, Parallelism: 255},
+ hexHash: "d51af3775bbdd0cba31d96fd6d921d9de27f521ceffe667618cd7624f6643071",
+ },
+ // Adding TruncationFixed shouldn't matter if Parallelism < 256.
+ {
+ costs: &metadata.HashingCosts{Time: 1, Memory: 1 << 11, Parallelism: 255, TruncationFixed: true},
+ hexHash: "d51af3775bbdd0cba31d96fd6d921d9de27f521ceffe667618cd7624f6643071",
+ },
+}
+
+// Checks that len(array) == expected
+func lengthCheck(name string, array []byte, expected int) error {
+ if len(array) != expected {
+ return fmt.Errorf("length of %s should be %d", name, expected)
+ }
+ return nil
+}
+
+// Tests the two ways of making keys
+func TestMakeKeys(t *testing.T) {
+ data := []byte("1234\n6789")
+
+ key1, err := NewKeyFromReader(bytes.NewReader(data))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer key1.Wipe()
+ if !bytes.Equal(data, key1.data) {
+ t.Error("Key from reader contained incorrect data")
+ }
+
+ key2, err := NewFixedLengthKeyFromReader(bytes.NewReader(data), 6)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer key2.Wipe()
+ if !bytes.Equal([]byte("1234\n6"), key2.data) {
+ t.Error("Fixed length key from reader contained incorrect data")
+ }
+}
+
+// Tests that wipe succeeds
+func TestWipe(t *testing.T) {
+ key, err := makeKey(1, 1000)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := key.Wipe(); err != nil {
+ t.Error(err)
+ }
+}
+
+// Making keys with negative length should fail
+func TestInvalidLength(t *testing.T) {
+ key, err := NewFixedLengthKeyFromReader(ConstReader(1), -1)
+ if err == nil {
+ key.Wipe()
+ t.Error("Negative lengths should cause failure")
+ }
+}
+
+// Test making keys of zero length
+func TestZeroLength(t *testing.T) {
+ key1, err := NewFixedLengthKeyFromReader(os.Stdin, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer key1.Wipe()
+ if key1.data != nil {
+ t.Error("Fixed length key from reader contained data")
+ }
+
+ key2, err := NewKeyFromReader(bytes.NewReader(nil))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer key2.Wipe()
+ if key2.data != nil {
+ t.Error("Key from empty reader contained data")
+ }
+}
+
+// Test that enabling then disabling memory locking succeeds even if a key is
+// active when the variable changes.
+func TestEnableDisableMemoryLocking(t *testing.T) {
+ // Mlock on for creation, off for wiping
+ key, err := NewRandomKey(metadata.InternalKeyLen)
+ UseMlock = false
+ defer func() {
+ UseMlock = true
+ }()
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := key.Wipe(); err != nil {
+ t.Error(err)
+ }
+}
+
+// Test that disabling then enabling memory locking succeeds even if a key is
+// active when the variable changes.
+func TestDisableEnableMemoryLocking(t *testing.T) {
+ // Mlock off for creation, on for wiping
+ UseMlock = false
+ key2, err := NewRandomKey(metadata.InternalKeyLen)
+ UseMlock = true
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := key2.Wipe(); err != nil {
+ t.Error(err)
+ }
+}
+
+// Test making keys long enough that the keys will have to resize
+func TestKeyResize(t *testing.T) {
+ // Key will have to resize once
+ r := io.LimitReader(ConstReader(1), int64(os.Getpagesize())+1)
+ key, err := NewKeyFromReader(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer key.Wipe()
+ for i, b := range key.data {
+ if b != 1 {
+ t.Fatalf("Byte %d contained invalid data %q", i, b)
+ }
+ }
+}
+
+// Test making keys so long that many resizes are necessary
+func TestKeyLargeResize(t *testing.T) {
+ // Key will have to resize 7 times
+ r := io.LimitReader(ConstReader(1), int64(os.Getpagesize())*65)
+
+ // Turn off Mlocking as the key will exceed the limit on some systems.
+ UseMlock = false
+ key, err := NewKeyFromReader(r)
+ UseMlock = true
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer key.Wipe()
+ for i, b := range key.data {
+ if b != 1 {
+ t.Fatalf("Byte %d contained invalid data %q", i, b)
+ }
+ }
+}
+
+// Check that we can create random keys. All this test does to test the
+// "randomness" is generate a page of random bytes and attempts compression.
+// If the data can be compressed it is probably not very random. This isn't
+// intended to be a sufficient test for randomness (which is impossible), but a
+// way to catch simple regressions (key is all zeros or contains a repeating
+// pattern).
+func TestRandomKeyGen(t *testing.T) {
+ key, err := NewRandomKey(os.Getpagesize())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer key.Wipe()
+
+ if didCompress(key.data) {
+ t.Errorf("Random key (%d bytes) should not be compressible", key.Len())
+ }
+}
+
+func TestBigKeyGen(t *testing.T) {
+ key, err := NewRandomKey(4096 * 4096)
+ switch err {
+ case nil:
+ key.Wipe()
+ return
+ case ErrMlockUlimit:
+ // Don't fail just because "ulimit -l" is too low.
+ return
+ default:
+ t.Fatal(err)
+ }
+}
+
+// didCompress checks if the given data can be compressed. Specifically, it
+// returns true if running zlib on the provided input produces a shorter output.
+func didCompress(input []byte) bool {
+ var output bytes.Buffer
+
+ w := zlib.NewWriter(&output)
+ _, err := w.Write(input)
+ w.Close()
+
+ return err == nil && len(input) > output.Len()
+}
+
+// Checks that the input arrays are all distinct
+func buffersDistinct(buffers ...[]byte) bool {
+ for i := 0; i < len(buffers); i++ {
+ for j := i + 1; j < len(buffers); j++ {
+ if bytes.Equal(buffers[i], buffers[j]) {
+ // Different entry, but equal arrays
+ return false
+ }
+ }
+ }
+ return true
+}
+
+// Checks that our cryptographic operations all produce distinct data
+func TestKeysAndOutputsDistinct(t *testing.T) {
+ data, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ encKey, authKey := stretchKey(fakeWrappingKey)
+ defer encKey.Wipe()
+ defer authKey.Wipe()
+
+ if !buffersDistinct(fakeWrappingKey.data, fakeValidPolicyKey.data,
+ encKey.data, authKey.data, data.IV, data.EncryptedKey, data.Hmac) {
+ t.Error("Key wrapping produced duplicate data")
+ }
+}
+
+// Check that Wrap() works with fixed keys
+func TestWrapSucceeds(t *testing.T) {
+ data, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err = lengthCheck("IV", data.IV, aes.BlockSize); err != nil {
+ t.Error(err)
+ }
+ if err = lengthCheck("Encrypted Key", data.EncryptedKey, metadata.PolicyKeyLen); err != nil {
+ t.Error(err)
+ }
+ if err = lengthCheck("HMAC", data.Hmac, sha256.Size); err != nil {
+ t.Error(err)
+ }
+}
+
+// Checks that applying Wrap then Unwrap gives the original data
+func testWrapUnwrapEqual(wrappingKey *Key, secretKey *Key) error {
+ data, err := Wrap(wrappingKey, secretKey)
+ if err != nil {
+ return err
+ }
+
+ secret, err := Unwrap(wrappingKey, data)
+ if err != nil {
+ return err
+ }
+ defer secret.Wipe()
+
+ if !bytes.Equal(secretKey.data, secret.data) {
+ return fmt.Errorf("Got %x after wrap/unwrap with w=%x and s=%x",
+ secret.data, wrappingKey.data, secretKey.data)
+ }
+ return nil
+}
+
+// Check that Unwrap(Wrap(x)) == x with fixed keys
+func TestWrapUnwrapEqual(t *testing.T) {
+ if err := testWrapUnwrapEqual(fakeWrappingKey, fakeValidPolicyKey); err != nil {
+ t.Error(err)
+ }
+}
+
+// Check that Unwrap(Wrap(x)) == x with random keys
+func TestRandomWrapUnwrapEqual(t *testing.T) {
+ for i := 0; i < 10; i++ {
+ wk, err := NewRandomKey(metadata.InternalKeyLen)
+ if err != nil {
+ t.Fatal(err)
+ }
+ sk, err := NewRandomKey(metadata.InternalKeyLen)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = testWrapUnwrapEqual(wk, sk); err != nil {
+ t.Error(err)
+ }
+ wk.Wipe()
+ sk.Wipe()
+ }
+}
+
+// Check that Unwrap(Wrap(x)) == x with differing lengths of secret key
+func TestDifferentLengthSecretKey(t *testing.T) {
+ wk, err := makeKey(1, metadata.InternalKeyLen)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer wk.Wipe()
+ for i := 0; i < 100; i++ {
+ sk, err := makeKey(2, i)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = testWrapUnwrapEqual(wk, sk); err != nil {
+ t.Error(err)
+ }
+ sk.Wipe()
+ }
+}
+
+// Wrong length of wrapping key should fail
+func TestWrongWrappingKeyLength(t *testing.T) {
+ _, err := Wrap(fakeValidPolicyKey, fakeWrappingKey)
+ if err == nil {
+ t.Fatal("using a policy key for wrapping should fail")
+ }
+}
+
+// Wrong length of unwrapping key should fail
+func TestWrongUnwrappingKeyLength(t *testing.T) {
+ data, err := Wrap(fakeWrappingKey, fakeWrappingKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if k, err := Unwrap(fakeValidPolicyKey, data); err == nil {
+ k.Wipe()
+ t.Fatal("using a policy key for unwrapping should fail")
+ }
+}
+
+// Wrapping twice with the same keys should give different components
+func TestWrapTwiceDistinct(t *testing.T) {
+ data1, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ data2, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !buffersDistinct(data1.IV, data1.EncryptedKey, data1.Hmac,
+ data2.IV, data2.EncryptedKey, data2.Hmac) {
+ t.Error("Wrapping same keys twice should give distinct results")
+ }
+}
+
+// Attempts to Unwrap data with key after altering tweak, should fail
+func testFailWithTweak(key *Key, data *metadata.WrappedKeyData, tweak []byte) error {
+ tweak[0]++
+ key, err := Unwrap(key, data)
+ if err == nil {
+ key.Wipe()
+ }
+ tweak[0]--
+ return err
+}
+
+// Wrapping then unwrapping with different components altered
+func TestUnwrapWrongKey(t *testing.T) {
+ data, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if testFailWithTweak(fakeWrappingKey, data, fakeWrappingKey.data) == nil {
+ t.Error("using a different wrapping key should make unwrap fail")
+ }
+}
+
+func TestUnwrapWrongData(t *testing.T) {
+ data, err := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if testFailWithTweak(fakeWrappingKey, data, data.EncryptedKey) == nil {
+ t.Error("changing encryption key should make unwrap fail")
+ }
+ if testFailWithTweak(fakeWrappingKey, data, data.IV) == nil {
+ t.Error("changing IV should make unwrap fail")
+ }
+ if testFailWithTweak(fakeWrappingKey, data, data.Hmac) == nil {
+ t.Error("changing HMAC should make unwrap fail")
+ }
+}
+
+func TestComputeKeyDescriptorV1(t *testing.T) {
+ descriptor, err := ComputeKeyDescriptor(fakeValidPolicyKey, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if descriptor != "8290608a029c5aae" {
+ t.Errorf("wrong v1 descriptor: %s", descriptor)
+ }
+}
+
+func TestComputeKeyDescriptorV2(t *testing.T) {
+ descriptor, err := ComputeKeyDescriptor(fakeValidPolicyKey, 2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if descriptor != "2139f52bf8386ee99845818ac7e91c4a" {
+ t.Errorf("wrong v2 descriptor: %s", descriptor)
+ }
+}
+
+func TestComputeKeyDescriptorBadVersion(t *testing.T) {
+ _, err := ComputeKeyDescriptor(fakeValidPolicyKey, 0)
+ if err == nil {
+ t.Error("computing key descriptor with bad version should fail")
+ }
+}
+
+// Run our test cases for passphrase hashing
+func TestPassphraseHashing(t *testing.T) {
+ pk, err := fakePassphraseKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer pk.Wipe()
+
+ for i, testCase := range hashTestCases {
+ if err := testCase.costs.CheckValidity(); err != nil {
+ t.Errorf("Hash test %d: for costs=%+v hashing failed: %v", i, testCase.costs, err)
+ continue
+ }
+ hash, err := PassphraseHash(pk, fakeSalt, testCase.costs)
+ if err != nil {
+ t.Errorf("Hash test %d: for costs=%+v hashing failed: %v", i, testCase.costs, err)
+ continue
+ }
+ defer hash.Wipe()
+
+ actual := hex.EncodeToString(hash.data)
+ if actual != testCase.hexHash {
+ t.Errorf("Hash test %d: for costs=%+v expected hash of %q got %q",
+ i, testCase.costs, testCase.hexHash, actual)
+ }
+ }
+}
+
+var badCosts = []*metadata.HashingCosts{
+ // Bad Time costs
+ {Time: 0, Memory: 1 << 11, Parallelism: 1},
+ {Time: 1 << 33, Memory: 1 << 11, Parallelism: 1},
+ // Bad Memory costs
+ {Time: 1, Memory: 5, Parallelism: 1},
+ {Time: 1, Memory: 1 << 33, Parallelism: 1},
+ // Bad Parallelism costs
+ {Time: 1, Memory: 1 << 11, Parallelism: 0, TruncationFixed: false},
+ {Time: 1, Memory: 1 << 11, Parallelism: 0, TruncationFixed: true},
+ {Time: 1, Memory: 1 << 11, Parallelism: 256, TruncationFixed: false},
+ {Time: 1, Memory: 1 << 11, Parallelism: 256, TruncationFixed: true},
+ {Time: 1, Memory: 1 << 11, Parallelism: 257, TruncationFixed: true},
+}
+
+func TestBadParameters(t *testing.T) {
+ for i, costs := range badCosts {
+ if costs.CheckValidity() == nil {
+ t.Errorf("Hash test %d: expected error for costs=%+v", i, costs)
+ }
+ }
+}
+
+func BenchmarkWrap(b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ Wrap(fakeWrappingKey, fakeValidPolicyKey)
+ }
+}
+
+func BenchmarkUnwrap(b *testing.B) {
+ b.StopTimer()
+
+ data, _ := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+
+ b.StartTimer()
+ for n := 0; n < b.N; n++ {
+ key, err := Unwrap(fakeWrappingKey, data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ key.Wipe()
+ }
+}
+
+func BenchmarkUnwrapNolock(b *testing.B) {
+ b.StopTimer()
+
+ UseMlock = false
+ defer func() {
+ UseMlock = true
+ }()
+ data, _ := Wrap(fakeWrappingKey, fakeValidPolicyKey)
+
+ b.StartTimer()
+ for n := 0; n < b.N; n++ {
+ key, err := Unwrap(fakeWrappingKey, data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ key.Wipe()
+ }
+}
+
+func BenchmarkRandomWrapUnwrap(b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ wk, _ := NewRandomKey(metadata.InternalKeyLen)
+ sk, _ := NewRandomKey(metadata.InternalKeyLen)
+
+ testWrapUnwrapEqual(wk, sk)
+ // Must manually call wipe here, or test will use too much memory.
+ wk.Wipe()
+ sk.Wipe()
+ }
+}
+
+func benchmarkPassphraseHashing(b *testing.B, costs *metadata.HashingCosts) {
+ b.StopTimer()
+
+ pk, err := fakePassphraseKey()
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer pk.Wipe()
+
+ b.StartTimer()
+ for n := 0; n < b.N; n++ {
+ hash, err := PassphraseHash(pk, fakeSalt, costs)
+ hash.Wipe()
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkPassphraseHashing_1MB_1Thread(b *testing.B) {
+ benchmarkPassphraseHashing(b,
+ &metadata.HashingCosts{Time: 1, Memory: 1 << 10, Parallelism: 1})
+}
+
+func BenchmarkPassphraseHashing_1GB_1Thread(b *testing.B) {
+ benchmarkPassphraseHashing(b,
+ &metadata.HashingCosts{Time: 1, Memory: 1 << 20, Parallelism: 1})
+}
+
+func BenchmarkPassphraseHashing_128MB_1Thread(b *testing.B) {
+ benchmarkPassphraseHashing(b,
+ &metadata.HashingCosts{Time: 1, Memory: 1 << 17, Parallelism: 1})
+}
+
+func BenchmarkPassphraseHashing_128MB_8Thread(b *testing.B) {
+ benchmarkPassphraseHashing(b,
+ &metadata.HashingCosts{Time: 1, Memory: 1 << 17, Parallelism: 8})
+}
+
+func BenchmarkPassphraseHashing_128MB_8Pass(b *testing.B) {
+ benchmarkPassphraseHashing(b,
+ &metadata.HashingCosts{Time: 8, Memory: 1 << 17, Parallelism: 1})
+}
--- /dev/null
+/*
+ * key.go - Cryptographic key management for fscrypt. Ensures that sensitive
+ * material is properly handled throughout the program.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package crypto
+
+/*
+#include <stdlib.h>
+#include <string.h>
+*/
+import "C"
+
+import (
+ "bytes"
+ "crypto/subtle"
+ "encoding/base32"
+ "io"
+ "log"
+ "os"
+ "runtime"
+ "unsafe"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sys/unix"
+
+ "github.com/google/fscrypt/metadata"
+ "github.com/google/fscrypt/util"
+)
+
+const (
+ // Keys need to readable and writable, but hidden from other processes.
+ keyProtection = unix.PROT_READ | unix.PROT_WRITE
+ keyMmapFlags = unix.MAP_PRIVATE | unix.MAP_ANONYMOUS
+)
+
+/*
+UseMlock determines whether we should use the mlock/munlock syscalls to
+prevent sensitive data like keys and passphrases from being paged to disk.
+UseMlock defaults to true, but can be set to false if the application calling
+into this library has insufficient privileges to lock memory. Code using this
+package could also bind this setting to a flag by using:
+
+ flag.BoolVar(&crypto.UseMlock, "lock-memory", true, "lock keys in memory")
+*/
+var UseMlock = true
+
+/*
+Key protects some arbitrary buffer of cryptographic material. Its methods
+ensure that the Key's data is locked in memory before being used (if
+UseMlock is set to true), and is wiped and unlocked after use (via the Wipe()
+method). This data is never accessed outside of the fscrypt/crypto package
+(except for the UnsafeData method). If a key is successfully created, the
+Wipe() method should be called after it's use. For example:
+
+ func UseKeyFromStdin() error {
+ key, err := NewKeyFromReader(os.Stdin)
+ if err != nil {
+ return err
+ }
+ defer key.Wipe()
+
+ // Do stuff with key
+
+ return nil
+ }
+
+The Wipe() method will also be called when a key is garbage collected; however,
+it is best practice to clear the key as soon as possible, so it spends a minimal
+amount of time in memory.
+
+Note that Key is not thread safe, as a key could be wiped while another thread
+is using it. Also, calling Wipe() from two threads could cause an error as
+memory could be freed twice.
+*/
+type Key struct {
+ data []byte
+}
+
+// NewBlankKey constructs a blank key of a specified length and returns an error
+// if we are unable to allocate or lock the necessary memory.
+func NewBlankKey(length int) (*Key, error) {
+ if length == 0 {
+ return &Key{data: nil}, nil
+ } else if length < 0 {
+ return nil, errors.Errorf("requested key length %d is negative", length)
+ }
+
+ flags := keyMmapFlags
+ if UseMlock {
+ flags |= unix.MAP_LOCKED
+ }
+
+ // See MAP_ANONYMOUS in http://man7.org/linux/man-pages/man2/mmap.2.html
+ data, err := unix.Mmap(-1, 0, length, keyProtection, flags)
+ if err == unix.EAGAIN {
+ return nil, ErrMlockUlimit
+ }
+ if err != nil {
+ return nil, errors.Wrapf(err,
+ "failed to allocate (mmap) key buffer of length %d", length)
+ }
+
+ key := &Key{data: data}
+
+ // Backup finalizer in case user forgets to "defer key.Wipe()"
+ runtime.SetFinalizer(key, (*Key).Wipe)
+ return key, nil
+}
+
+// Wipe destroys a Key by zeroing and freeing the memory. The data is zeroed
+// even if Wipe returns an error, which occurs if we are unable to unlock or
+// free the key memory. Wipe does nothing if the key is already wiped or is nil.
+func (key *Key) Wipe() error {
+ // We do nothing if key or key.data is nil so that Wipe() is idempotent
+ // and so Wipe() can be called on keys which have already been cleared.
+ if key != nil && key.data != nil {
+ data := key.data
+ key.data = nil
+
+ for i := range data {
+ data[i] = 0
+ }
+
+ if err := unix.Munmap(data); err != nil {
+ log.Printf("unix.Munmap() failed: %v", err)
+ return errors.Wrapf(err, "failed to free (munmap) key buffer")
+ }
+ }
+ return nil
+}
+
+// Len is the underlying data buffer's length.
+func (key *Key) Len() int {
+ return len(key.data)
+}
+
+// Equals compares the contents of two keys, returning true if they have the same
+// key data. This function runs in constant time.
+func (key *Key) Equals(key2 *Key) bool {
+ return subtle.ConstantTimeCompare(key.data, key2.data) == 1
+}
+
+// resize returns a new key with size requestedSize and the appropriate data
+// copied over. The original data is wiped. This method does nothing and returns
+// itself if the key's length equals requestedSize.
+func (key *Key) resize(requestedSize int) (*Key, error) {
+ if key.Len() == requestedSize {
+ return key, nil
+ }
+ defer key.Wipe()
+
+ resizedKey, err := NewBlankKey(requestedSize)
+ if err != nil {
+ return nil, err
+ }
+ copy(resizedKey.data, key.data)
+ return resizedKey, nil
+}
+
+// Data returns a slice of the key's underlying data. Note that this may become
+// outdated if the key is resized.
+func (key *Key) Data() []byte {
+ return key.data
+}
+
+// UnsafePtr returns an unsafe pointer to the key's underlying data. Note that
+// this will only be valid as long as the key is not resized.
+func (key *Key) UnsafePtr() unsafe.Pointer {
+ return util.Ptr(key.data)
+}
+
+// UnsafeToCString makes a copy of the string's data into a null-terminated C
+// string allocated by C. Note that this method is unsafe as this C copy has no
+// locking or wiping functionality. The key shouldn't contain any `\0` bytes.
+func (key *Key) UnsafeToCString() unsafe.Pointer {
+ size := C.size_t(key.Len())
+ data := C.calloc(size+1, 1)
+ C.memcpy(data, util.Ptr(key.data), size)
+ return data
+}
+
+// Clone creates a key as a copy of another one.
+func (key *Key) Clone() (*Key, error) {
+ newKey, err := NewBlankKey(key.Len())
+ if err != nil {
+ return nil, err
+ }
+ copy(newKey.data, key.data)
+ return newKey, nil
+}
+
+// NewKeyFromCString creates of a copy of some C string's data in a key. Note
+// that the original C string is not modified at all, so steps must be taken to
+// ensure that this original copy is secured.
+func NewKeyFromCString(str unsafe.Pointer) (*Key, error) {
+ size := C.strlen((*C.char)(str))
+ key, err := NewBlankKey(int(size))
+ if err != nil {
+ return nil, err
+ }
+ C.memcpy(util.Ptr(key.data), str, size)
+ return key, nil
+}
+
+// NewKeyFromReader constructs a key of arbitrary length by reading from reader
+// until hitting EOF.
+func NewKeyFromReader(reader io.Reader) (*Key, error) {
+ // Use an initial key size of a page. As Mmap allocates a page anyway,
+ // there isn't much additional overhead from starting with a whole page.
+ key, err := NewBlankKey(os.Getpagesize())
+ if err != nil {
+ return nil, err
+ }
+
+ totalBytesRead := 0
+ for {
+ bytesRead, err := reader.Read(key.data[totalBytesRead:])
+ totalBytesRead += bytesRead
+
+ switch err {
+ case nil:
+ // Need to continue reading. Grow key if necessary
+ if key.Len() == totalBytesRead {
+ if key, err = key.resize(2 * key.Len()); err != nil {
+ return nil, err
+ }
+ }
+ case io.EOF:
+ // Getting the EOF error means we are done
+ return key.resize(totalBytesRead)
+ default:
+ // Fail if Read() has a failure
+ key.Wipe()
+ return nil, err
+ }
+ }
+}
+
+// NewFixedLengthKeyFromReader constructs a key with a specified length by
+// reading exactly length bytes from reader.
+func NewFixedLengthKeyFromReader(reader io.Reader, length int) (*Key, error) {
+ key, err := NewBlankKey(length)
+ if err != nil {
+ return nil, err
+ }
+ if _, err := io.ReadFull(reader, key.data); err != nil {
+ key.Wipe()
+ return nil, err
+ }
+ return key, nil
+}
+
+var (
+ // The recovery code is base32 with a dash between each block of 8 characters.
+ encoding = base32.StdEncoding
+ blockSize = 8
+ separator = []byte("-")
+ encodedLength = encoding.EncodedLen(metadata.PolicyKeyLen)
+ decodedLength = encoding.DecodedLen(encodedLength)
+ // RecoveryCodeLength is the number of bytes in every recovery code
+ RecoveryCodeLength = (encodedLength/blockSize)*(blockSize+len(separator)) - len(separator)
+)
+
+// WriteRecoveryCode outputs key's recovery code to the provided writer.
+// WARNING: This recovery key is enough to derive the original key, so it must
+// be given the same level of protection as a raw cryptographic key.
+func WriteRecoveryCode(key *Key, writer io.Writer) error {
+ if err := util.CheckValidLength(metadata.PolicyKeyLen, key.Len()); err != nil {
+ return errors.Wrap(err, "recovery key")
+ }
+
+ // We store the base32 encoded data (without separators) in a temp key
+ encodedKey, err := NewBlankKey(encodedLength)
+ if err != nil {
+ return err
+ }
+ defer encodedKey.Wipe()
+ encoding.Encode(encodedKey.data, key.data)
+
+ w := util.NewErrWriter(writer)
+
+ // Write the blocks with separators between them
+ w.Write(encodedKey.data[:blockSize])
+ for blockStart := blockSize; blockStart < encodedLength; blockStart += blockSize {
+ w.Write(separator)
+
+ blockEnd := util.MinInt(blockStart+blockSize, encodedLength)
+ w.Write(encodedKey.data[blockStart:blockEnd])
+ }
+
+ // If any writes have failed, return the error
+ return w.Err()
+}
+
+// ReadRecoveryCode gets the recovery code from the provided reader and returns
+// the corresponding cryptographic key.
+// WARNING: This recovery key is enough to derive the original key, so it must
+// be given the same level of protection as a raw cryptographic key.
+func ReadRecoveryCode(reader io.Reader) (*Key, error) {
+ // We store the base32 encoded data (without separators) in a temp key
+ encodedKey, err := NewBlankKey(encodedLength)
+ if err != nil {
+ return nil, err
+ }
+ defer encodedKey.Wipe()
+
+ r := util.NewErrReader(reader)
+
+ // Read the other blocks, checking the separators between them
+ r.Read(encodedKey.data[:blockSize])
+ inputSeparator := make([]byte, len(separator))
+
+ for blockStart := blockSize; blockStart < encodedLength; blockStart += blockSize {
+ r.Read(inputSeparator)
+ if r.Err() == nil && !bytes.Equal(separator, inputSeparator) {
+ err = errors.Wrapf(ErrRecoveryCode, "invalid separator %q", inputSeparator)
+ return nil, err
+ }
+
+ blockEnd := util.MinInt(blockStart+blockSize, encodedLength)
+ r.Read(encodedKey.data[blockStart:blockEnd])
+ }
+
+ // If any reads have failed, return the error
+ if r.Err() != nil {
+ return nil, errors.Wrapf(ErrRecoveryCode, "read error %v", r.Err())
+ }
+
+ // Now we decode the key, resizing if necessary
+ decodedKey, err := NewBlankKey(decodedLength)
+ if err != nil {
+ return nil, err
+ }
+ if _, err = encoding.Decode(decodedKey.data, encodedKey.data); err != nil {
+ return nil, errors.Wrap(ErrRecoveryCode, err.Error())
+ }
+ return decodedKey.resize(metadata.PolicyKeyLen)
+}
--- /dev/null
+/*
+ * 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")
+}
--- /dev/null
+/*
+ * 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)
+ }
+ }
+}
--- /dev/null
+/*
+ * filesystem.go - Contains the functionality for a specific filesystem. This
+ * includes the commands to setup the filesystem, apply policies, and locate
+ * metadata.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+// Package filesystem deals with the structure of the files on disk used to
+// store the metadata for fscrypt. Specifically, this package includes:
+// 1. mountpoint management (mountpoint.go)
+// - querying existing mounted filesystems
+// - getting filesystems from a UUID
+// - finding the filesystem for a specific path
+// 2. metadata organization (filesystem.go)
+// - setting up a mounted filesystem for use with fscrypt
+// - adding/querying/deleting metadata
+// - making links to other filesystems' metadata
+// - following links to get data from other filesystems
+package filesystem
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "os/user"
+ "path/filepath"
+ "sort"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sys/unix"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/google/fscrypt/metadata"
+ "github.com/google/fscrypt/util"
+)
+
+// ErrAlreadySetup indicates that a filesystem is already setup for fscrypt.
+type ErrAlreadySetup struct {
+ Mount *Mount
+}
+
+func (err *ErrAlreadySetup) Error() string {
+ return fmt.Sprintf("filesystem %s is already setup for use with fscrypt",
+ err.Mount.Path)
+}
+
+// ErrCorruptMetadata indicates that an fscrypt metadata file is corrupt.
+type ErrCorruptMetadata struct {
+ Path string
+ UnderlyingError error
+}
+
+func (err *ErrCorruptMetadata) Error() string {
+ return fmt.Sprintf("fscrypt metadata file at %q is corrupt: %s",
+ err.Path, err.UnderlyingError)
+}
+
+// ErrFollowLink indicates that a protector link can't be followed.
+type ErrFollowLink struct {
+ Link string
+ UnderlyingError error
+}
+
+func (err *ErrFollowLink) Error() string {
+ return fmt.Sprintf("cannot follow filesystem link %q: %s",
+ err.Link, err.UnderlyingError)
+}
+
+// ErrInsecurePermissions indicates that a filesystem is not considered to be
+// setup for fscrypt because a metadata directory has insecure permissions.
+type ErrInsecurePermissions struct {
+ Path string
+}
+
+func (err *ErrInsecurePermissions) Error() string {
+ return fmt.Sprintf("%q has insecure permissions (world-writable without sticky bit)",
+ err.Path)
+}
+
+// ErrMakeLink indicates that a protector link can't be created.
+type ErrMakeLink struct {
+ Target *Mount
+ UnderlyingError error
+}
+
+func (err *ErrMakeLink) Error() string {
+ return fmt.Sprintf("cannot create filesystem link to %q: %s",
+ err.Target.Path, err.UnderlyingError)
+}
+
+// ErrMountOwnedByAnotherUser indicates that the mountpoint root directory is
+// owned by a user that isn't trusted in the current context, so we don't
+// consider fscrypt to be properly setup on the filesystem.
+type ErrMountOwnedByAnotherUser struct {
+ Mount *Mount
+}
+
+func (err *ErrMountOwnedByAnotherUser) Error() string {
+ return fmt.Sprintf("another non-root user owns the root directory of %s", err.Mount.Path)
+}
+
+// ErrNoCreatePermission indicates that the current user lacks permission to
+// create fscrypt metadata on the given filesystem.
+type ErrNoCreatePermission struct {
+ Mount *Mount
+}
+
+func (err *ErrNoCreatePermission) Error() string {
+ return fmt.Sprintf("user lacks permission to create fscrypt metadata on %s", err.Mount.Path)
+}
+
+// ErrNotAMountpoint indicates that a path is not a mountpoint.
+type ErrNotAMountpoint struct {
+ Path string
+}
+
+func (err *ErrNotAMountpoint) Error() string {
+ return fmt.Sprintf("%q is not a mountpoint", err.Path)
+}
+
+// ErrNotSetup indicates that a filesystem is not setup for fscrypt.
+type ErrNotSetup struct {
+ Mount *Mount
+}
+
+func (err *ErrNotSetup) Error() string {
+ return fmt.Sprintf("filesystem %s is not setup for use with fscrypt", err.Mount.Path)
+}
+
+// ErrSetupByAnotherUser indicates that one or more of the fscrypt metadata
+// directories is owned by a user that isn't trusted in the current context, so
+// we don't consider fscrypt to be properly setup on the filesystem.
+type ErrSetupByAnotherUser struct {
+ Mount *Mount
+}
+
+func (err *ErrSetupByAnotherUser) Error() string {
+ return fmt.Sprintf("another non-root user owns fscrypt metadata directories on %s", err.Mount.Path)
+}
+
+// ErrSetupNotSupported indicates that the given filesystem type is not
+// supported for fscrypt setup.
+type ErrSetupNotSupported struct {
+ Mount *Mount
+}
+
+func (err *ErrSetupNotSupported) Error() string {
+ return fmt.Sprintf("filesystem type %s is not supported for fscrypt setup",
+ err.Mount.FilesystemType)
+}
+
+// ErrPolicyNotFound indicates that the policy metadata was not found.
+type ErrPolicyNotFound struct {
+ Descriptor string
+ Mount *Mount
+}
+
+func (err *ErrPolicyNotFound) Error() string {
+ return fmt.Sprintf("policy metadata for %s not found on filesystem %s",
+ err.Descriptor, err.Mount.Path)
+}
+
+// ErrProtectorNotFound indicates that the protector metadata was not found.
+type ErrProtectorNotFound struct {
+ Descriptor string
+ Mount *Mount
+}
+
+func (err *ErrProtectorNotFound) Error() string {
+ return fmt.Sprintf("protector metadata for %s not found on filesystem %s",
+ err.Descriptor, err.Mount.Path)
+}
+
+// SortDescriptorsByLastMtime indicates whether descriptors are sorted by last
+// modification time when being listed. This can be set to true to get
+// consistent output for testing.
+var SortDescriptorsByLastMtime = false
+
+// Mount contains information for a specific mounted filesystem.
+//
+// Path - Absolute path where the directory is mounted
+// FilesystemType - Type of the mounted filesystem, e.g. "ext4"
+// Device - Device for filesystem (empty string if we cannot find one)
+// DeviceNumber - Device number of the filesystem. This is set even if
+// Device isn't, since all filesystems have a device
+// number assigned by the kernel, even pseudo-filesystems.
+// Subtree - The mounted subtree of the filesystem. This is usually
+// "/", meaning that the entire filesystem is mounted, but
+// it can differ for bind mounts.
+// ReadOnly - True if this is a read-only mount
+//
+// In order to use a Mount to store fscrypt metadata, some directories must be
+// setup first. Specifically, the directories created look like:
+// <mountpoint>
+// └── .fscrypt
+//
+// ├── policies
+// └── protectors
+//
+// These "policies" and "protectors" directories will contain files that are
+// the corresponding metadata structures for policies and protectors. The public
+// interface includes functions for setting up these directories and Adding,
+// Getting, and Removing these files.
+//
+// There is also the ability to reference another filesystem's metadata. This is
+// used when a Policy on filesystem A is protected with Protector on filesystem
+// B. In this scenario, we store a "link file" in the protectors directory.
+//
+// We also allow ".fscrypt" to be a symlink which was previously created. This
+// allows login protectors to be created when the root filesystem is read-only,
+// provided that "/.fscrypt" is a symlink pointing to a writable location.
+type Mount struct {
+ Path string
+ FilesystemType string
+ Device string
+ DeviceNumber DeviceNumber
+ Subtree string
+ ReadOnly bool
+}
+
+// PathSorter allows mounts to be sorted by Path.
+type PathSorter []*Mount
+
+func (p PathSorter) Len() int { return len(p) }
+func (p PathSorter) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
+func (p PathSorter) Less(i, j int) bool { return p[i].Path < p[j].Path }
+
+const (
+ // Names of the various directories used in fscrypt
+ baseDirName = ".fscrypt"
+ policyDirName = "policies"
+ protectorDirName = "protectors"
+ tempPrefix = ".tmp"
+ linkFileExtension = ".link"
+
+ // The base directory should be read-only (except for the creator)
+ basePermissions = 0755
+
+ // The metadata files shouldn't be readable or writable by other users.
+ // Having them be world-readable wouldn't necessarily be a huge issue,
+ // but given that some of these files contain (strong) password hashes,
+ // we error on the side of caution -- similar to /etc/shadow.
+ // Note: existing files on-disk might have mode 0644, as that was the
+ // mode used by fscrypt v0.3.2 and earlier.
+ filePermissions = os.FileMode(0600)
+
+ // Maximum size of a metadata file. This value is arbitrary, and it can
+ // be changed. We just set a reasonable limit that shouldn't be reached
+ // in practice, except by users trying to cause havoc by creating
+ // extremely large files in the metadata directories.
+ maxMetadataFileSize = 16384
+)
+
+// SetupMode is a mode for creating the fscrypt metadata directories.
+type SetupMode int
+
+const (
+ // SingleUserWritable specifies to make the fscrypt metadata directories
+ // writable by a single user (usually root) only.
+ SingleUserWritable SetupMode = iota
+ // WorldWritable specifies to make the fscrypt metadata directories
+ // world-writable (with the sticky bit set).
+ WorldWritable
+)
+
+func (m *Mount) String() string {
+ return fmt.Sprintf(`%s
+ FilesystemType: %s
+ Device: %s`, m.Path, m.FilesystemType, m.Device)
+}
+
+// BaseDir returns the path to the base fscrypt directory for this filesystem.
+func (m *Mount) BaseDir() string {
+ rawBaseDir := filepath.Join(m.Path, baseDirName)
+ // We allow the base directory to be a symlink, but some callers need
+ // the real path, so dereference the symlink here if needed. Since the
+ // directory the symlink points to may not exist yet, we have to read
+ // the symlink manually rather than use filepath.EvalSymlinks.
+ target, err := os.Readlink(rawBaseDir)
+ if err != nil {
+ return rawBaseDir // not a symlink
+ }
+ if filepath.IsAbs(target) {
+ return target
+ }
+ return filepath.Join(m.Path, target)
+}
+
+// ProtectorDir returns the directory containing the protector metadata.
+func (m *Mount) ProtectorDir() string {
+ return filepath.Join(m.BaseDir(), protectorDirName)
+}
+
+// protectorPath returns the full path to a regular protector file with the
+// specified descriptor.
+func (m *Mount) protectorPath(descriptor string) string {
+ return filepath.Join(m.ProtectorDir(), descriptor)
+}
+
+// linkedProtectorPath returns the full path to a linked protector file with the
+// specified descriptor.
+func (m *Mount) linkedProtectorPath(descriptor string) string {
+ return m.protectorPath(descriptor) + linkFileExtension
+}
+
+// PolicyDir returns the directory containing the policy metadata.
+func (m *Mount) PolicyDir() string {
+ return filepath.Join(m.BaseDir(), policyDirName)
+}
+
+// PolicyPath returns the full path to a regular policy file with the
+// specified descriptor.
+func (m *Mount) PolicyPath(descriptor string) string {
+ return filepath.Join(m.PolicyDir(), descriptor)
+}
+
+// tempMount creates a temporary directory alongside this Mount's base fscrypt
+// directory and returns a temporary Mount which represents this temporary
+// directory. The caller is responsible for removing this temporary directory.
+func (m *Mount) tempMount() (*Mount, error) {
+ tempDir, err := os.MkdirTemp(filepath.Dir(m.BaseDir()), tempPrefix)
+ return &Mount{Path: tempDir}, err
+}
+
+// ErrEncryptionNotEnabled indicates that encryption is not enabled on the given
+// filesystem.
+type ErrEncryptionNotEnabled struct {
+ Mount *Mount
+}
+
+func (err *ErrEncryptionNotEnabled) Error() string {
+ return fmt.Sprintf("encryption not enabled on filesystem %s (%s).",
+ err.Mount.Path, err.Mount.Device)
+}
+
+// ErrEncryptionNotSupported indicates that encryption is not supported on the
+// given filesystem.
+type ErrEncryptionNotSupported struct {
+ Mount *Mount
+}
+
+func (err *ErrEncryptionNotSupported) Error() string {
+ return fmt.Sprintf("This kernel doesn't support encryption on %s filesystems.",
+ err.Mount.FilesystemType)
+}
+
+// EncryptionSupportError adds filesystem-specific context to the
+// ErrEncryptionNotEnabled and ErrEncryptionNotSupported errors from the
+// metadata package.
+func (m *Mount) EncryptionSupportError(err error) error {
+ switch err {
+ case metadata.ErrEncryptionNotEnabled:
+ return &ErrEncryptionNotEnabled{m}
+ case metadata.ErrEncryptionNotSupported:
+ return &ErrEncryptionNotSupported{m}
+ }
+ return err
+}
+
+// isFscryptSetupAllowed decides whether the given filesystem is allowed to be
+// set up for fscrypt, without actually accessing it. This basically checks
+// whether the filesystem type is one of the types that supports encryption, or
+// at least is in some stage of planning for encrption support in the future.
+//
+// We need this list so that we can skip filesystems that are irrelevant for
+// fscrypt without having to look for the fscrypt metadata directories on them,
+// which can trigger errors, long delays, or side effects on some filesystems.
+//
+// Unfortunately, this means that if a completely new filesystem adds encryption
+// support, then it will need to be manually added to this list. But it seems
+// to be a worthwhile tradeoff to avoid the above issues.
+func (m *Mount) isFscryptSetupAllowed() bool {
+ if m.Path == "/" {
+ // The root filesystem is always allowed, since it's where login
+ // protectors are stored.
+ return true
+ }
+ switch m.FilesystemType {
+ case "ext4", "f2fs", "ubifs", "btrfs", "ceph", "xfs", "lustre":
+ return true
+ default:
+ return false
+ }
+}
+
+// CheckSupport returns an error if this filesystem does not support encryption.
+func (m *Mount) CheckSupport() error {
+ if !m.isFscryptSetupAllowed() {
+ return &ErrEncryptionNotSupported{m}
+ }
+ return m.EncryptionSupportError(metadata.CheckSupport(m.Path))
+}
+
+func checkOwnership(path string, info os.FileInfo, trustedUser *user.User) bool {
+ if trustedUser == nil {
+ return true
+ }
+ trustedUID := uint32(util.AtoiOrPanic(trustedUser.Uid))
+ actualUID := info.Sys().(*syscall.Stat_t).Uid
+ if actualUID != 0 && actualUID != trustedUID {
+ log.Printf("WARNING: %q is owned by uid %d, but expected %d or 0",
+ path, actualUID, trustedUID)
+ return false
+ }
+ return true
+}
+
+// CheckSetup returns an error if any of the fscrypt metadata directories do not
+// exist. Will log any unexpected errors or incorrect permissions.
+func (m *Mount) CheckSetup(trustedUser *user.User) error {
+ if !m.isFscryptSetupAllowed() {
+ return &ErrNotSetup{m}
+ }
+ // Check that the mountpoint directory itself is not a symlink and has
+ // proper ownership, as otherwise we can't trust anything beneath it.
+ info, err := loggedLstat(m.Path)
+ if err != nil {
+ return &ErrNotSetup{m}
+ }
+ if (info.Mode() & os.ModeSymlink) != 0 {
+ log.Printf("mountpoint directory %q cannot be a symlink", m.Path)
+ return &ErrNotSetup{m}
+ }
+ if !info.IsDir() {
+ log.Printf("mountpoint %q is not a directory", m.Path)
+ return &ErrNotSetup{m}
+ }
+ if !checkOwnership(m.Path, info, trustedUser) {
+ return &ErrMountOwnedByAnotherUser{m}
+ }
+
+ // Check BaseDir similarly. However, unlike the other directories, we
+ // allow BaseDir to be a symlink, to support the use case of metadata
+ // for a read-only filesystem being redirected to a writable location.
+ info, err = loggedStat(m.BaseDir())
+ if err != nil {
+ return &ErrNotSetup{m}
+ }
+ if !info.IsDir() {
+ log.Printf("%q is not a directory", m.BaseDir())
+ return &ErrNotSetup{m}
+ }
+ if !checkOwnership(m.Path, info, trustedUser) {
+ return &ErrMountOwnedByAnotherUser{m}
+ }
+
+ // Check that the policies and protectors directories aren't symlinks and
+ // have proper ownership.
+ subdirs := []string{m.PolicyDir(), m.ProtectorDir()}
+ for _, path := range subdirs {
+ info, err := loggedLstat(path)
+ if err != nil {
+ return &ErrNotSetup{m}
+ }
+ if (info.Mode() & os.ModeSymlink) != 0 {
+ log.Printf("directory %q cannot be a symlink", path)
+ return &ErrNotSetup{m}
+ }
+ if !info.IsDir() {
+ log.Printf("%q is not a directory", path)
+ return &ErrNotSetup{m}
+ }
+ // We are no longer too picky about the mode, given that
+ // 'fscrypt setup' now offers a choice of two different modes,
+ // and system administrators could customize it further.
+ // However, we can at least verify that if the directory is
+ // world-writable, then the sticky bit is also set.
+ if info.Mode()&(os.ModeSticky|0002) == 0002 {
+ log.Printf("%q is world-writable but doesn't have sticky bit set", path)
+ return &ErrInsecurePermissions{path}
+ }
+ if !checkOwnership(path, info, trustedUser) {
+ return &ErrSetupByAnotherUser{m}
+ }
+ }
+ return nil
+}
+
+// makeDirectories creates the three metadata directories with the correct
+// permissions. Note that this function overrides the umask.
+func (m *Mount) makeDirectories(setupMode SetupMode) error {
+ // Zero the umask so we get the permissions we want
+ oldMask := unix.Umask(0)
+ defer func() {
+ unix.Umask(oldMask)
+ }()
+
+ if err := os.Mkdir(m.BaseDir(), basePermissions); err != nil {
+ return err
+ }
+
+ var dirMode os.FileMode
+ switch setupMode {
+ case SingleUserWritable:
+ dirMode = 0755
+ case WorldWritable:
+ dirMode = os.ModeSticky | 0777
+ }
+ if err := os.Mkdir(m.PolicyDir(), dirMode); err != nil {
+ return err
+ }
+ return os.Mkdir(m.ProtectorDir(), dirMode)
+}
+
+// GetSetupMode returns the current mode for fscrypt metadata creation on this
+// filesystem.
+func (m *Mount) GetSetupMode() (SetupMode, *user.User, error) {
+ info1, err1 := os.Stat(m.PolicyDir())
+ info2, err2 := os.Stat(m.ProtectorDir())
+
+ if err1 == nil && err2 == nil {
+ mask := os.ModeSticky | 0777
+ mode1 := info1.Mode() & mask
+ mode2 := info2.Mode() & mask
+ uid1 := info1.Sys().(*syscall.Stat_t).Uid
+ uid2 := info2.Sys().(*syscall.Stat_t).Uid
+ user, err := util.UserFromUID(int64(uid1))
+ if err == nil && mode1 == mode2 && uid1 == uid2 {
+ switch mode1 {
+ case mask:
+ return WorldWritable, nil, nil
+ case 0755:
+ return SingleUserWritable, user, nil
+ }
+ }
+ log.Printf("filesystem %s uses custom permissions on metadata directories", m.Path)
+ }
+ return -1, nil, errors.New("unable to determine setup mode")
+}
+
+// Setup sets up the filesystem for use with fscrypt. Note that this merely
+// creates the appropriate files on the filesystem. It does not actually modify
+// the filesystem's feature flags. This operation is atomic; it either succeeds
+// or no files in the baseDir are created.
+func (m *Mount) Setup(mode SetupMode) error {
+ if m.CheckSetup(nil) == nil {
+ return &ErrAlreadySetup{m}
+ }
+ if !m.isFscryptSetupAllowed() {
+ return &ErrSetupNotSupported{m}
+ }
+ // We build the directories under a temp Mount and then move into place.
+ temp, err := m.tempMount()
+ if err != nil {
+ return err
+ }
+ defer os.RemoveAll(temp.Path)
+
+ if err = temp.makeDirectories(mode); err != nil {
+ return err
+ }
+
+ // Atomically move directory into place.
+ return os.Rename(temp.BaseDir(), m.BaseDir())
+}
+
+// RemoveAllMetadata removes all the policy and protector metadata from the
+// filesystem. This operation is atomic; it either succeeds or no files in the
+// baseDir are removed.
+// WARNING: Will cause data loss if the metadata is used to encrypt
+// directories (this could include directories on other filesystems).
+func (m *Mount) RemoveAllMetadata() error {
+ if err := m.CheckSetup(nil); err != nil {
+ return err
+ }
+ // temp will hold the old metadata temporarily
+ temp, err := m.tempMount()
+ if err != nil {
+ return err
+ }
+ defer os.RemoveAll(temp.Path)
+
+ // Move directory into temp (to be destroyed on defer)
+ return os.Rename(m.BaseDir(), temp.BaseDir())
+}
+
+func syncDirectory(dirPath string) error {
+ dirFile, err := os.Open(dirPath)
+ if err != nil {
+ return err
+ }
+ if err = dirFile.Sync(); err != nil {
+ dirFile.Close()
+ return err
+ }
+ return dirFile.Close()
+}
+
+func (m *Mount) overwriteDataNonAtomic(path string, data []byte) error {
+ file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|unix.O_NOFOLLOW, 0)
+ if err != nil {
+ return err
+ }
+ if _, err = file.Write(data); err != nil {
+ log.Printf("WARNING: overwrite of %q failed; file will be corrupted!", path)
+ file.Close()
+ return err
+ }
+ if err = file.Sync(); err != nil {
+ file.Close()
+ return err
+ }
+ if err = file.Close(); err != nil {
+ return err
+ }
+ log.Printf("successfully overwrote %q non-atomically", path)
+ return nil
+}
+
+// writeData writes the given data to the given path such that, if possible, the
+// data is either written to stable storage or an error is returned. If a file
+// already exists at the path, it will be replaced.
+//
+// However, if the process doesn't have write permission to the directory but
+// does have write permission to the file itself, then as a fallback the file is
+// overwritten in-place rather than replaced. Note that this may be non-atomic.
+func (m *Mount) writeData(path string, data []byte, owner *user.User, mode os.FileMode) error {
+ // Write the data to a temporary file, sync it, then rename into place
+ // so that the operation will be atomic.
+ dirPath := filepath.Dir(path)
+ tempFile, err := os.CreateTemp(dirPath, tempPrefix)
+ if err != nil {
+ log.Print(err)
+ if os.IsPermission(err) {
+ if _, err = os.Lstat(path); err == nil {
+ log.Printf("trying non-atomic overwrite of %q", path)
+ return m.overwriteDataNonAtomic(path, data)
+ }
+ return &ErrNoCreatePermission{m}
+ }
+ return err
+ }
+ defer os.Remove(tempFile.Name())
+
+ // Ensure the new file has the right permissions mask.
+ if err = tempFile.Chmod(mode); err != nil {
+ tempFile.Close()
+ return err
+ }
+ // Override the file owner if one was specified. This happens when root
+ // needs to create files owned by a particular user.
+ if owner != nil {
+ if err = util.Chown(tempFile, owner); err != nil {
+ log.Printf("could not set owner of %q to %v: %v",
+ path, owner.Username, err)
+ tempFile.Close()
+ return err
+ }
+ }
+ if _, err = tempFile.Write(data); err != nil {
+ tempFile.Close()
+ return err
+ }
+ if err = tempFile.Sync(); err != nil {
+ tempFile.Close()
+ return err
+ }
+ if err = tempFile.Close(); err != nil {
+ return err
+ }
+
+ if err = os.Rename(tempFile.Name(), path); err != nil {
+ return err
+ }
+ // Ensure the rename has been persisted before returning success.
+ return syncDirectory(dirPath)
+}
+
+// addMetadata writes the metadata structure to the file with the specified
+// path. This will overwrite any existing data. The operation is atomic.
+func (m *Mount) addMetadata(path string, md metadata.Metadata, owner *user.User) error {
+ if err := md.CheckValidity(); err != nil {
+ return errors.Wrap(err, "provided metadata is invalid")
+ }
+
+ data, err := proto.Marshal(md)
+ if err != nil {
+ return err
+ }
+
+ mode := filePermissions
+ // If the file already exists, then preserve its owner and mode if
+ // possible. This is necessary because by default, for atomicity
+ // reasons we'll replace the file rather than overwrite it.
+ info, err := os.Lstat(path)
+ if err == nil {
+ if owner == nil && util.IsUserRoot() {
+ uid := info.Sys().(*syscall.Stat_t).Uid
+ if owner, err = util.UserFromUID(int64(uid)); err != nil {
+ log.Print(err)
+ }
+ }
+ mode = info.Mode() & 0777
+ } else if !os.IsNotExist(err) {
+ log.Print(err)
+ }
+
+ if owner != nil {
+ log.Printf("writing metadata to %q and setting owner to %s", path, owner.Username)
+ } else {
+ log.Printf("writing metadata to %q", path)
+ }
+ return m.writeData(path, data, owner, mode)
+}
+
+// readMetadataFileSafe gets the contents of a metadata file extra-carefully,
+// considering that it could be a malicious file created to cause a
+// denial-of-service. Specifically, the following checks are done:
+//
+// - It must be a regular file, not another type of file like a symlink or FIFO.
+// (Symlinks aren't bad by themselves, but given that a malicious user could
+// point one to absolutely anywhere, and there is no known use case for the
+// metadata files themselves being symlinks, it seems best to disallow them.)
+// - It must have a reasonable size (<= maxMetadataFileSize).
+// - If trustedUser is non-nil, then the file must be owned by the given user
+// or by root.
+//
+// Take care to avoid TOCTOU (time-of-check-time-of-use) bugs when doing these
+// tests. Notably, we must open the file before checking the file type, as the
+// file type could change between any previous checks and the open. When doing
+// this, O_NOFOLLOW is needed to avoid following a symlink (this applies to the
+// last path component only), and O_NONBLOCK is needed to avoid blocking if the
+// file is a FIFO.
+//
+// This function returns the data read as well as the UID of the user who owns
+// the file. The returned UID is needed for login protectors, where the UID
+// needs to be cross-checked with the UID stored in the file itself.
+func readMetadataFileSafe(path string, trustedUser *user.User) ([]byte, int64, error) {
+ file, err := os.OpenFile(path, os.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
+ if err != nil {
+ return nil, -1, err
+ }
+ defer file.Close()
+
+ info, err := file.Stat()
+ if err != nil {
+ return nil, -1, err
+ }
+ if !info.Mode().IsRegular() {
+ return nil, -1, &ErrCorruptMetadata{path, errors.New("not a regular file")}
+ }
+ if !checkOwnership(path, info, trustedUser) {
+ return nil, -1, &ErrCorruptMetadata{path, errors.New("metadata file belongs to another user")}
+ }
+ // Clear O_NONBLOCK, since it has served its purpose when opening the
+ // file, and the behavior of reading from a regular file with O_NONBLOCK
+ // is technically unspecified.
+ if _, err = unix.FcntlInt(file.Fd(), unix.F_SETFL, 0); err != nil {
+ return nil, -1, &os.PathError{Op: "clearing O_NONBLOCK", Path: path, Err: err}
+ }
+ // Read the file contents, allowing at most maxMetadataFileSize bytes.
+ reader := &io.LimitedReader{R: file, N: maxMetadataFileSize + 1}
+ data, err := io.ReadAll(reader)
+ if err != nil {
+ return nil, -1, err
+ }
+ if reader.N == 0 {
+ return nil, -1, &ErrCorruptMetadata{path, errors.New("metadata file size limit exceeded")}
+ }
+ return data, int64(info.Sys().(*syscall.Stat_t).Uid), nil
+}
+
+// getMetadata reads the metadata structure from the file with the specified
+// path. Only reads normal metadata files, not linked metadata.
+func (m *Mount) getMetadata(path string, trustedUser *user.User, md metadata.Metadata) (int64, error) {
+ data, owner, err := readMetadataFileSafe(path, trustedUser)
+ if err != nil {
+ log.Printf("could not read metadata from %q: %v", path, err)
+ return -1, err
+ }
+
+ if err := proto.Unmarshal(data, md); err != nil {
+ return -1, &ErrCorruptMetadata{path, err}
+ }
+
+ if err := md.CheckValidity(); err != nil {
+ return -1, &ErrCorruptMetadata{path, err}
+ }
+
+ log.Printf("successfully read metadata from %q", path)
+ return owner, nil
+}
+
+// removeMetadata deletes the metadata struct from the file with the specified
+// path. Works with regular or linked metadata.
+func (m *Mount) removeMetadata(path string) error {
+ if err := os.Remove(path); err != nil {
+ log.Printf("could not remove metadata file at %q: %v", path, err)
+ return err
+ }
+
+ log.Printf("successfully removed metadata file at %q", path)
+ return nil
+}
+
+// AddProtector adds the protector metadata to this filesystem's storage. This
+// will overwrite the value of an existing protector with this descriptor. This
+// will fail with ErrLinkedProtector if a linked protector with this descriptor
+// already exists on the filesystem.
+func (m *Mount) AddProtector(data *metadata.ProtectorData, owner *user.User) error {
+ var err error
+ if err = m.CheckSetup(nil); err != nil {
+ return err
+ }
+ if isRegularFile(m.linkedProtectorPath(data.ProtectorDescriptor)) {
+ return errors.Errorf("cannot modify linked protector %s on filesystem %s",
+ data.ProtectorDescriptor, m.Path)
+ }
+ path := m.protectorPath(data.ProtectorDescriptor)
+ return m.addMetadata(path, data, owner)
+}
+
+// AddLinkedProtector adds a link in this filesystem to the protector metadata
+// in the dest filesystem, if one doesn't already exist. On success, the return
+// value is a nil error and a bool that is true iff the link is newly created.
+func (m *Mount) AddLinkedProtector(descriptor string, dest *Mount, trustedUser *user.User,
+ ownerIfCreating *user.User) (bool, error) {
+ if err := m.CheckSetup(trustedUser); err != nil {
+ return false, err
+ }
+ // Check that the link is good (descriptor exists, filesystem has UUID).
+ if _, err := dest.GetRegularProtector(descriptor, trustedUser); err != nil {
+ return false, err
+ }
+
+ linkPath := m.linkedProtectorPath(descriptor)
+
+ // Check whether the link already exists.
+ existingLink, _, err := readMetadataFileSafe(linkPath, trustedUser)
+ if err == nil {
+ existingLinkedMnt, err := getMountFromLink(string(existingLink))
+ if err != nil {
+ return false, errors.Wrap(err, linkPath)
+ }
+ if existingLinkedMnt != dest {
+ return false, errors.Errorf("link %q points to %q, but expected %q",
+ linkPath, existingLinkedMnt.Path, dest.Path)
+ }
+ return false, nil
+ }
+ if !os.IsNotExist(err) {
+ return false, err
+ }
+
+ var newLink string
+ newLink, err = makeLink(dest)
+ if err != nil {
+ return false, err
+ }
+ return true, m.writeData(linkPath, []byte(newLink), ownerIfCreating, filePermissions)
+}
+
+// GetRegularProtector looks up the protector metadata by descriptor. This will
+// fail with ErrProtectorNotFound if the descriptor is a linked protector.
+func (m *Mount) GetRegularProtector(descriptor string, trustedUser *user.User) (*metadata.ProtectorData, error) {
+ if err := m.CheckSetup(trustedUser); err != nil {
+ return nil, err
+ }
+ data := new(metadata.ProtectorData)
+ path := m.protectorPath(descriptor)
+ owner, err := m.getMetadata(path, trustedUser, data)
+ if os.IsNotExist(err) {
+ err = &ErrProtectorNotFound{descriptor, m}
+ }
+ if err != nil {
+ return nil, err
+ }
+ // Login protectors have their UID stored in the file. Since normally
+ // any user can create files in the fscrypt metadata directories, for a
+ // login protector to be considered valid it *must* be owned by the
+ // claimed user or by root. Note: fscrypt v0.3.2 and later always makes
+ // login protectors owned by the user, but previous versions could
+ // create them owned by root -- that is the main reason we allow root.
+ if data.Source == metadata.SourceType_pam_passphrase && owner != 0 && owner != data.Uid {
+ log.Printf("WARNING: %q claims to be the login protector for uid %d, but it is owned by uid %d. Needs to be %d or 0.",
+ path, data.Uid, owner, data.Uid)
+ return nil, &ErrCorruptMetadata{path, errors.New("login protector belongs to wrong user")}
+ }
+ return data, nil
+}
+
+// GetProtector returns the Mount of the filesystem containing the information
+// and that protector's data. If the descriptor is a regular (not linked)
+// protector, the mount will return itself.
+func (m *Mount) GetProtector(descriptor string, trustedUser *user.User) (*Mount, *metadata.ProtectorData, error) {
+ if err := m.CheckSetup(trustedUser); err != nil {
+ return nil, nil, err
+ }
+ // Get the link data from the link file
+ path := m.linkedProtectorPath(descriptor)
+ link, _, err := readMetadataFileSafe(path, trustedUser)
+ if err != nil {
+ // If the link doesn't exist, try for a regular protector.
+ if os.IsNotExist(err) {
+ data, err := m.GetRegularProtector(descriptor, trustedUser)
+ return m, data, err
+ }
+ return nil, nil, err
+ }
+ log.Printf("following protector link %s", path)
+ linkedMnt, err := getMountFromLink(string(link))
+ if err != nil {
+ return nil, nil, errors.Wrap(err, path)
+ }
+ data, err := linkedMnt.GetRegularProtector(descriptor, trustedUser)
+ if err != nil {
+ return nil, nil, &ErrFollowLink{string(link), err}
+ }
+ return linkedMnt, data, nil
+}
+
+// RemoveProtector deletes the protector metadata (or a link to another
+// filesystem's metadata) from the filesystem storage.
+func (m *Mount) RemoveProtector(descriptor string) error {
+ if err := m.CheckSetup(nil); err != nil {
+ return err
+ }
+ // We first try to remove the linkedProtector. If that metadata does not
+ // exist, we try to remove the normal protector.
+ err := m.removeMetadata(m.linkedProtectorPath(descriptor))
+ if os.IsNotExist(err) {
+ err = m.removeMetadata(m.protectorPath(descriptor))
+ if os.IsNotExist(err) {
+ err = &ErrProtectorNotFound{descriptor, m}
+ }
+ }
+ return err
+}
+
+// ListProtectors lists the descriptors of all protectors on this filesystem.
+// This does not include linked protectors. If trustedUser is non-nil, then
+// the protectors are restricted to those owned by the given user or by root.
+func (m *Mount) ListProtectors(trustedUser *user.User) ([]string, error) {
+ return m.listMetadata(m.ProtectorDir(), "protectors", trustedUser)
+}
+
+// AddPolicy adds the policy metadata to the filesystem storage.
+func (m *Mount) AddPolicy(data *metadata.PolicyData, owner *user.User) error {
+ if err := m.CheckSetup(nil); err != nil {
+ return err
+ }
+
+ return m.addMetadata(m.PolicyPath(data.KeyDescriptor), data, owner)
+}
+
+// GetPolicy looks up the policy metadata by descriptor.
+func (m *Mount) GetPolicy(descriptor string, trustedUser *user.User) (*metadata.PolicyData, error) {
+ if err := m.CheckSetup(trustedUser); err != nil {
+ return nil, err
+ }
+ data := new(metadata.PolicyData)
+ _, err := m.getMetadata(m.PolicyPath(descriptor), trustedUser, data)
+ if os.IsNotExist(err) {
+ err = &ErrPolicyNotFound{descriptor, m}
+ }
+ return data, err
+}
+
+// RemovePolicy deletes the policy metadata from the filesystem storage.
+func (m *Mount) RemovePolicy(descriptor string) error {
+ if err := m.CheckSetup(nil); err != nil {
+ return err
+ }
+ err := m.removeMetadata(m.PolicyPath(descriptor))
+ if os.IsNotExist(err) {
+ err = &ErrPolicyNotFound{descriptor, m}
+ }
+ return err
+}
+
+// ListPolicies lists the descriptors of all policies on this filesystem. If
+// trustedUser is non-nil, then the policies are restricted to those owned by
+// the given user or by root.
+func (m *Mount) ListPolicies(trustedUser *user.User) ([]string, error) {
+ return m.listMetadata(m.PolicyDir(), "policies", trustedUser)
+}
+
+type namesAndTimes struct {
+ names []string
+ times []time.Time
+}
+
+func (c namesAndTimes) Len() int {
+ return len(c.names)
+}
+
+func (c namesAndTimes) Less(i, j int) bool {
+ return c.times[i].Before(c.times[j])
+}
+
+func (c namesAndTimes) Swap(i, j int) {
+ c.names[i], c.names[j] = c.names[j], c.names[i]
+ c.times[i], c.times[j] = c.times[j], c.times[i]
+}
+
+func sortFileListByLastMtime(directoryPath string, names []string) error {
+ c := namesAndTimes{names: names, times: make([]time.Time, len(names))}
+ for i, name := range names {
+ fi, err := os.Lstat(filepath.Join(directoryPath, name))
+ if err != nil {
+ return err
+ }
+ c.times[i] = fi.ModTime()
+ }
+ sort.Sort(c)
+ return nil
+}
+
+// listDirectory returns a list of descriptors for a metadata directory,
+// including files which are links to other filesystem's metadata.
+func (m *Mount) listDirectory(directoryPath string) ([]string, error) {
+ dir, err := os.Open(directoryPath)
+ if err != nil {
+ return nil, err
+ }
+ defer dir.Close()
+
+ names, err := dir.Readdirnames(-1)
+ if err != nil {
+ return nil, err
+ }
+
+ if SortDescriptorsByLastMtime {
+ if err := sortFileListByLastMtime(directoryPath, names); err != nil {
+ return nil, err
+ }
+ }
+
+ descriptors := make([]string, 0, len(names))
+ for _, name := range names {
+ // Be sure to include links as well
+ descriptors = append(descriptors, strings.TrimSuffix(name, linkFileExtension))
+ }
+ return descriptors, nil
+}
+
+func (m *Mount) listMetadata(dirPath string, metadataType string, owner *user.User) ([]string, error) {
+ log.Printf("listing %s in %q", metadataType, dirPath)
+ if err := m.CheckSetup(owner); err != nil {
+ return nil, err
+ }
+ names, err := m.listDirectory(dirPath)
+ if err != nil {
+ return nil, err
+ }
+ filesIgnoredDescription := ""
+ if owner != nil {
+ filteredNames := make([]string, 0, len(names))
+ uid := uint32(util.AtoiOrPanic(owner.Uid))
+ for _, name := range names {
+ info, err := os.Lstat(filepath.Join(dirPath, name))
+ if err != nil {
+ continue
+ }
+ fileUID := info.Sys().(*syscall.Stat_t).Uid
+ if fileUID != uid && fileUID != 0 {
+ continue
+ }
+ filteredNames = append(filteredNames, name)
+ }
+ numIgnored := len(names) - len(filteredNames)
+ if numIgnored != 0 {
+ filesIgnoredDescription =
+ fmt.Sprintf(" (ignored %d %s not owned by %s or root)",
+ numIgnored, metadataType, owner.Username)
+ }
+ names = filteredNames
+ }
+ log.Printf("found %d %s%s", len(names), metadataType, filesIgnoredDescription)
+ return names, nil
+}
--- /dev/null
+/*
+ * 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")
+ }
+}
--- /dev/null
+/*
+ * mountpoint.go - Contains all the functionality for finding mountpoints and
+ * using UUIDs to refer to them. Specifically, we can find the mountpoint of a
+ * path, get info about a mountpoint, and find mountpoints with a specific UUID.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package filesystem
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/pkg/errors"
+)
+
+var (
+ // These maps hold data about the state of the system's filesystems.
+ //
+ // They only contain one Mount per filesystem, even if there are
+ // additional bind mounts, since we want to store fscrypt metadata in
+ // only one place per filesystem. When it is ambiguous which Mount
+ // should be used for a filesystem, mountsByDevice will contain an
+ // explicit nil entry, and mountsByPath won't contain an entry.
+ mountsByDevice map[DeviceNumber]*Mount
+ mountsByPath map[string]*Mount
+ // Used to make the mount functions thread safe
+ mountMutex sync.Mutex
+ // True if the maps have been successfully initialized.
+ mountsInitialized bool
+ // Supported tokens for filesystem links
+ uuidToken = "UUID"
+ pathToken = "PATH"
+ // Location to perform UUID lookup
+ uuidDirectory = "/dev/disk/by-uuid"
+)
+
+// Unescape octal-encoded escape sequences in a string from the mountinfo file.
+// The kernel encodes the ' ', '\t', '\n', and '\\' bytes this way. This
+// function exactly inverts what the kernel does, including by preserving
+// invalid UTF-8.
+func unescapeString(str string) string {
+ var sb strings.Builder
+ for i := 0; i < len(str); i++ {
+ b := str[i]
+ if b == '\\' && i+3 < len(str) {
+ if parsed, err := strconv.ParseInt(str[i+1:i+4], 8, 8); err == nil {
+ b = uint8(parsed)
+ i += 3
+ }
+ }
+ sb.WriteByte(b)
+ }
+ return sb.String()
+}
+
+// EscapeString is the reverse of unescapeString. Use this to avoid injecting
+// spaces or newlines into output that uses these characters as separators.
+func EscapeString(str string) string {
+ var sb strings.Builder
+ for _, b := range []byte(str) {
+ switch b {
+ case ' ', '\t', '\n', '\\':
+ sb.WriteString(fmt.Sprintf("\\%03o", b))
+ default:
+ sb.WriteByte(b)
+ }
+ }
+ return sb.String()
+}
+
+// We get the device name via the device number rather than use the mount source
+// field directly. This is necessary to handle a rootfs that was mounted via
+// the kernel command line, since mountinfo always shows /dev/root for that.
+// This assumes that the device nodes are in the standard location.
+func getDeviceName(num DeviceNumber) string {
+ linkPath := fmt.Sprintf("/sys/dev/block/%v", num)
+ if target, err := os.Readlink(linkPath); err == nil {
+ return fmt.Sprintf("/dev/%s", filepath.Base(target))
+ }
+ return ""
+}
+
+// Parse one line of /proc/self/mountinfo.
+//
+// The line contains the following space-separated fields:
+//
+// [0] mount ID
+// [1] parent ID
+// [2] major:minor
+// [3] root
+// [4] mount point
+// [5] mount options
+// [6...n-1] optional field(s)
+// [n] separator
+// [n+1] filesystem type
+// [n+2] mount source
+// [n+3] super options
+//
+// For more details, see https://www.kernel.org/doc/Documentation/filesystems/proc.txt
+func parseMountInfoLine(line string) *Mount {
+ fields := strings.Split(line, " ")
+ if len(fields) < 10 {
+ return nil
+ }
+
+ // Count the optional fields. In case new fields are appended later,
+ // don't simply assume that n == len(fields) - 4.
+ n := 6
+ for fields[n] != "-" {
+ n++
+ if n >= len(fields) {
+ return nil
+ }
+ }
+ if n+3 >= len(fields) {
+ return nil
+ }
+
+ var mnt *Mount = &Mount{}
+ var err error
+ mnt.DeviceNumber, err = newDeviceNumberFromString(fields[2])
+ if err != nil {
+ return nil
+ }
+ mnt.Subtree = unescapeString(fields[3])
+ mnt.Path = unescapeString(fields[4])
+ for _, opt := range strings.Split(fields[5], ",") {
+ if opt == "ro" {
+ mnt.ReadOnly = true
+ }
+ }
+ mnt.FilesystemType = unescapeString(fields[n+1])
+ mnt.Device = getDeviceName(mnt.DeviceNumber)
+ return mnt
+}
+
+type mountpointTreeNode struct {
+ mount *Mount
+ parent *mountpointTreeNode
+ children []*mountpointTreeNode
+}
+
+func addUncontainedSubtreesRecursive(dst map[string]bool,
+ node *mountpointTreeNode, allUncontainedSubtrees map[string]bool) {
+ if allUncontainedSubtrees[node.mount.Subtree] {
+ dst[node.mount.Subtree] = true
+ }
+ for _, child := range node.children {
+ addUncontainedSubtreesRecursive(dst, child, allUncontainedSubtrees)
+ }
+}
+
+// findMainMount finds the "main" Mount of a filesystem. The "main" Mount is
+// where the filesystem's fscrypt metadata is stored.
+//
+// Normally, there is just one Mount and it's of the entire filesystem
+// (mnt.Subtree == "/"). But in general, the filesystem might be mounted in
+// multiple places, including "bind mounts" where mnt.Subtree != "/". Also, the
+// filesystem might have a combination of read-write and read-only mounts.
+//
+// To handle most cases, we could just choose a mount with mnt.Subtree == "/",
+// preferably a read-write mount. However, that doesn't work in containers
+// where the "/" subtree might not be mounted. Here's a real-world example:
+//
+// mnt.Subtree mnt.Path
+// ----------- --------
+// /var/lib/lxc/base/rootfs /
+// /var/cache/pacman/pkg /var/cache/pacman/pkg
+// /srv/repo/x86_64 /srv/http/x86_64
+//
+// In this case, all mnt.Subtree are independent. To handle this case, we must
+// choose the Mount whose mnt.Path contains the others, i.e. the first one.
+// Note: the fscrypt metadata won't be usable from outside the container since
+// it won't be at the real root of the filesystem, but that may be acceptable.
+//
+// However, we can't look *only* at mnt.Path, since in some cases mnt.Subtree is
+// needed to correctly handle bind mounts. For example, in the following case,
+// the first Mount should be chosen:
+//
+// mnt.Subtree mnt.Path
+// ----------- --------
+// /foo /foo
+// /foo/dir /dir
+//
+// To solve this, we divide the mounts into non-overlapping trees of mnt.Path.
+// Then, we choose one of these trees which contains (exactly or via path
+// prefix) *all* mnt.Subtree. We then return the root of this tree. In both
+// the above examples, this algorithm returns the first Mount.
+func findMainMount(filesystemMounts []*Mount) *Mount {
+ // Index this filesystem's mounts by path. Note: paths are unique here,
+ // since non-last mounts were already excluded earlier.
+ //
+ // Also build the set of all mounted subtrees.
+ filesystemMountsByPath := make(map[string]*mountpointTreeNode)
+ allSubtrees := make(map[string]bool)
+ for _, mnt := range filesystemMounts {
+ filesystemMountsByPath[mnt.Path] = &mountpointTreeNode{mount: mnt}
+ allSubtrees[mnt.Subtree] = true
+ }
+
+ // Divide the mounts into non-overlapping trees of mountpoints.
+ for path, mntNode := range filesystemMountsByPath {
+ for path != "/" && mntNode.parent == nil {
+ path = filepath.Dir(path)
+ if parent := filesystemMountsByPath[path]; parent != nil {
+ mntNode.parent = parent
+ parent.children = append(parent.children, mntNode)
+ }
+ }
+ }
+
+ // Build the set of mounted subtrees that aren't contained in any other
+ // mounted subtree.
+ allUncontainedSubtrees := make(map[string]bool)
+ for subtree := range allSubtrees {
+ contained := false
+ for t := subtree; t != "/" && !contained; {
+ t = filepath.Dir(t)
+ contained = allSubtrees[t]
+ }
+ if !contained {
+ allUncontainedSubtrees[subtree] = true
+ }
+ }
+
+ // Select the root of a mountpoint tree whose mounted subtrees contain
+ // *all* mounted subtrees. Equivalently, select a mountpoint tree in
+ // which every uncontained subtree is mounted.
+ var mainMount *Mount
+ for _, mntNode := range filesystemMountsByPath {
+ mnt := mntNode.mount
+ if mntNode.parent != nil {
+ continue
+ }
+ uncontainedSubtrees := make(map[string]bool)
+ addUncontainedSubtreesRecursive(uncontainedSubtrees, mntNode, allUncontainedSubtrees)
+ if len(uncontainedSubtrees) != len(allUncontainedSubtrees) {
+ continue
+ }
+ // If there's more than one eligible mount, they should have the
+ // same Subtree. Otherwise it's ambiguous which one to use.
+ if mainMount != nil && mainMount.Subtree != mnt.Subtree {
+ log.Printf("Unsupported case: %q (%v) has multiple non-overlapping mounts. This filesystem will be ignored!",
+ mnt.Device, mnt.DeviceNumber)
+ return nil
+ }
+ // Prefer a read-write mount to a read-only one.
+ if mainMount == nil || mainMount.ReadOnly {
+ mainMount = mnt
+ }
+ }
+ return mainMount
+}
+
+// This is separate from loadMountInfo() only for unit testing.
+func readMountInfo(r io.Reader) error {
+ mountsByDevice = make(map[DeviceNumber]*Mount)
+ mountsByPath = make(map[string]*Mount)
+ allMountsByDevice := make(map[DeviceNumber][]*Mount)
+ allMountsByPath := make(map[string]*Mount)
+
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ line := scanner.Text()
+ mnt := parseMountInfoLine(line)
+ if mnt == nil {
+ log.Printf("ignoring invalid mountinfo line %q", line)
+ continue
+ }
+
+ // We can only use mountpoints that are directories for fscrypt.
+ if !isDir(mnt.Path) {
+ log.Printf("ignoring mountpoint %q because it is not a directory", mnt.Path)
+ continue
+ }
+
+ // Note this overrides the info if we have seen the mountpoint
+ // earlier in the file. This is correct behavior because the
+ // mountpoints are listed in mount order.
+ allMountsByPath[mnt.Path] = mnt
+ }
+ // For each filesystem, choose a "main" Mount and discard any additional
+ // bind mounts. fscrypt only cares about the main Mount, since it's
+ // where the fscrypt metadata is stored. Store all the main Mounts in
+ // mountsByDevice and mountsByPath so that they can be found later.
+ for _, mnt := range allMountsByPath {
+ allMountsByDevice[mnt.DeviceNumber] =
+ append(allMountsByDevice[mnt.DeviceNumber], mnt)
+ }
+ for deviceNumber, filesystemMounts := range allMountsByDevice {
+ mnt := findMainMount(filesystemMounts)
+ mountsByDevice[deviceNumber] = mnt // may store an explicit nil entry
+ if mnt != nil {
+ mountsByPath[mnt.Path] = mnt
+ }
+ }
+ return nil
+}
+
+// loadMountInfo populates the Mount mappings by parsing /proc/self/mountinfo.
+// It returns an error if the Mount mappings cannot be populated.
+func loadMountInfo() error {
+ if !mountsInitialized {
+ file, err := os.Open("/proc/self/mountinfo")
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ if err := readMountInfo(file); err != nil {
+ return err
+ }
+ mountsInitialized = true
+ }
+ return nil
+}
+
+func filesystemLacksMainMountError(deviceNumber DeviceNumber) error {
+ return errors.Errorf("Device %q (%v) lacks a \"main\" mountpoint in the current mount namespace, so it's ambiguous where to store the fscrypt metadata.",
+ getDeviceName(deviceNumber), deviceNumber)
+}
+
+// AllFilesystems lists all mounted filesystems ordered by path to their "main"
+// Mount. Use CheckSetup() to see if they are set up for use with fscrypt.
+func AllFilesystems() ([]*Mount, error) {
+ mountMutex.Lock()
+ defer mountMutex.Unlock()
+ if err := loadMountInfo(); err != nil {
+ return nil, err
+ }
+
+ mounts := make([]*Mount, 0, len(mountsByPath))
+ for _, mount := range mountsByPath {
+ mounts = append(mounts, mount)
+ }
+
+ sort.Sort(PathSorter(mounts))
+ return mounts, nil
+}
+
+// UpdateMountInfo updates the filesystem mountpoint maps with the current state
+// of the filesystem mountpoints. Returns error if the initialization fails.
+func UpdateMountInfo() error {
+ mountMutex.Lock()
+ defer mountMutex.Unlock()
+ mountsInitialized = false
+ return loadMountInfo()
+}
+
+// FindMount returns the main Mount object for the filesystem which contains the
+// file at the specified path. An error is returned if the path is invalid or if
+// we cannot load the required mount data. If a mount has been updated since the
+// last call to one of the mount functions, run UpdateMountInfo to see changes.
+func FindMount(path string) (*Mount, error) {
+ mountMutex.Lock()
+ defer mountMutex.Unlock()
+ if err := loadMountInfo(); err != nil {
+ return nil, err
+ }
+ // First try to find the mount by the number of the containing device.
+ deviceNumber, err := getNumberOfContainingDevice(path)
+ if err != nil {
+ return nil, err
+ }
+ mnt, ok := mountsByDevice[deviceNumber]
+ if ok {
+ if mnt == nil {
+ return nil, filesystemLacksMainMountError(deviceNumber)
+ }
+ return mnt, nil
+ }
+ // The mount couldn't be found by the number of the containing device.
+ // Fall back to walking up the directory hierarchy and checking for a
+ // mount at each directory path. This is necessary for btrfs, where
+ // files report a different st_dev from the /proc/self/mountinfo entry.
+ curPath, err := canonicalizePath(path)
+ if err != nil {
+ return nil, err
+ }
+ for {
+ mnt := mountsByPath[curPath]
+ if mnt != nil {
+ return mnt, nil
+ }
+ // Move to the parent directory unless we have reached the root.
+ parent := filepath.Dir(curPath)
+ if parent == curPath {
+ return nil, errors.Errorf("couldn't find mountpoint containing %q", path)
+ }
+ curPath = parent
+ }
+}
+
+// GetMount is like FindMount, except GetMount also returns an error if the path
+// doesn't name the same file as the filesystem's "main" Mount. For example, if
+// a filesystem is fully mounted at "/mnt" and if "/mnt/a" exists, then
+// FindMount("/mnt/a") will succeed whereas GetMount("/mnt/a") will fail. This
+// is true even if "/mnt/a" is a bind mount of part of the same filesystem.
+func GetMount(mountpoint string) (*Mount, error) {
+ mnt, err := FindMount(mountpoint)
+ if err != nil {
+ return nil, &ErrNotAMountpoint{mountpoint}
+ }
+ // Check whether 'mountpoint' names the same directory as 'mnt.Path'.
+ // Use os.SameFile() (i.e., compare inode numbers) rather than compare
+ // canonical paths, since filesystems may be mounted in multiple places.
+ fi1, err := os.Stat(mountpoint)
+ if err != nil {
+ return nil, err
+ }
+ fi2, err := os.Stat(mnt.Path)
+ if err != nil {
+ return nil, err
+ }
+ if !os.SameFile(fi1, fi2) {
+ return nil, &ErrNotAMountpoint{mountpoint}
+ }
+ return mnt, nil
+}
+
+func uuidToDeviceNumber(uuid string) (DeviceNumber, error) {
+ uuidSymlinkPath := filepath.Join(uuidDirectory, uuid)
+ return getDeviceNumber(uuidSymlinkPath)
+}
+
+func deviceNumberToMount(deviceNumber DeviceNumber) (*Mount, bool) {
+ mountMutex.Lock()
+ defer mountMutex.Unlock()
+ if err := loadMountInfo(); err != nil {
+ log.Print(err)
+ return nil, false
+ }
+ mnt, ok := mountsByDevice[deviceNumber]
+ return mnt, ok
+}
+
+// getMountFromLink returns the main Mount, if any, for the filesystem which the
+// given link points to. The link should contain a series of token-value pairs
+// (<token>=<value>), one per line. The supported tokens are "UUID" and "PATH".
+// If the UUID is present and it works, then it is used; otherwise, PATH is used
+// if it is present. (The fallback from UUID to PATH will keep the link working
+// if the UUID of the target filesystem changes but its mountpoint doesn't.)
+//
+// If a mount has been updated since the last call to one of the mount
+// functions, make sure to run UpdateMountInfo first.
+func getMountFromLink(link string) (*Mount, error) {
+ // Parse the link.
+ uuid := ""
+ path := ""
+ lines := strings.Split(link, "\n")
+ for _, line := range lines {
+ line := strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ pair := strings.Split(line, "=")
+ if len(pair) != 2 {
+ log.Printf("ignoring invalid line in filesystem link file: %q", line)
+ continue
+ }
+ token := pair[0]
+ value := pair[1]
+ switch token {
+ case uuidToken:
+ uuid = value
+ case pathToken:
+ path = value
+ default:
+ log.Printf("ignoring unknown link token %q", token)
+ }
+ }
+ // At least one of UUID and PATH must be present.
+ if uuid == "" && path == "" {
+ return nil, &ErrFollowLink{link, errors.Errorf("invalid filesystem link file")}
+ }
+
+ // Try following the UUID.
+ errMsg := ""
+ if uuid != "" {
+ deviceNumber, err := uuidToDeviceNumber(uuid)
+ if err == nil {
+ mnt, ok := deviceNumberToMount(deviceNumber)
+ if mnt != nil {
+ log.Printf("resolved filesystem link using UUID %q", uuid)
+ return mnt, nil
+ }
+ if ok {
+ return nil, &ErrFollowLink{link, filesystemLacksMainMountError(deviceNumber)}
+ }
+ log.Printf("cannot find filesystem with UUID %q", uuid)
+ } else {
+ log.Printf("cannot find filesystem with UUID %q: %v", uuid, err)
+ }
+ errMsg += fmt.Sprintf("cannot find filesystem with UUID %q", uuid)
+ if path != "" {
+ log.Printf("falling back to using mountpoint path instead of UUID")
+ }
+ }
+ // UUID didn't work. As a fallback, try the mountpoint path.
+ if path != "" {
+ mnt, err := GetMount(path)
+ if mnt != nil {
+ log.Printf("resolved filesystem link using mountpoint path %q", path)
+ return mnt, nil
+ }
+ log.Print(err)
+ if errMsg == "" {
+ errMsg = fmt.Sprintf("cannot find filesystem with main mountpoint %q", path)
+ } else {
+ errMsg += fmt.Sprintf(" or main mountpoint %q", path)
+ }
+ }
+ // No method worked; return an error.
+ return nil, &ErrFollowLink{link, errors.New(errMsg)}
+}
+
+func (mnt *Mount) getFilesystemUUID() (string, error) {
+ dirEntries, err := os.ReadDir(uuidDirectory)
+ if err != nil {
+ return "", err
+ }
+ for _, dirEntry := range dirEntries {
+ fileInfo, err := dirEntry.Info()
+ if err != nil {
+ continue
+ }
+ if fileInfo.Mode()&os.ModeSymlink == 0 {
+ continue // Only interested in UUID symlinks
+ }
+ uuid := fileInfo.Name()
+ deviceNumber, err := uuidToDeviceNumber(uuid)
+ if err != nil {
+ log.Print(err)
+ continue
+ }
+ if mnt.DeviceNumber == deviceNumber {
+ return uuid, nil
+ }
+ }
+ return "", errors.Errorf("cannot determine UUID of device %q (%v)",
+ mnt.Device, mnt.DeviceNumber)
+}
+
+// makeLink creates the contents of a link file which will point to the given
+// filesystem. This will normally be a string of the form
+// "UUID=<uuid>\nPATH=<path>\n". If the UUID cannot be determined, the UUID
+// portion will be omitted.
+func makeLink(mnt *Mount) (string, error) {
+ uuid, err := mnt.getFilesystemUUID()
+ if err != nil {
+ // The UUID could not be determined. This happens for btrfs
+ // filesystems, as the device number found via
+ // /dev/disk/by-uuid/* for btrfs filesystems differs from the
+ // actual device number of the mounted filesystem. Just rely
+ // entirely on the fallback to mountpoint path.
+ log.Print(err)
+ return fmt.Sprintf("%s=%s\n", pathToken, mnt.Path), nil
+ }
+ return fmt.Sprintf("%s=%s\n%s=%s\n", uuidToken, uuid, pathToken, mnt.Path), nil
+}
--- /dev/null
+/*
+ * 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)
+ }
+ }
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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)
+ }
+ }
+}
--- /dev/null
+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
+)
--- /dev/null
+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=
--- /dev/null
+/*
+ * fs_keyring.go - Add/remove encryption policy keys to/from filesystem
+ *
+ * Copyright 2019 Google LLC
+ * Author: Eric Biggers (ebiggers@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package keyring
+
+/*
+#include <string.h>
+*/
+import "C"
+
+import (
+ "encoding/hex"
+ "log"
+ "os"
+ "os/user"
+ "sync"
+ "unsafe"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sys/unix"
+
+ "github.com/google/fscrypt/crypto"
+ "github.com/google/fscrypt/filesystem"
+ "github.com/google/fscrypt/security"
+ "github.com/google/fscrypt/util"
+)
+
+var (
+ fsKeyringSupported bool
+ fsKeyringSupportedKnown bool
+ fsKeyringSupportedLock sync.Mutex
+)
+
+func checkForFsKeyringSupport(mount *filesystem.Mount) bool {
+ dir, err := os.Open(mount.Path)
+ if err != nil {
+ log.Printf("Unexpected error opening %q. Assuming filesystem keyring is unsupported.",
+ mount.Path)
+ return false
+ }
+ defer dir.Close()
+
+ // FS_IOC_ADD_ENCRYPTION_KEY with a NULL argument will fail with ENOTTY
+ // if the ioctl isn't supported. Otherwise it should fail with EFAULT.
+ //
+ // Note that there's no need to check for FS_IOC_REMOVE_ENCRYPTION_KEY
+ // support separately, since it's guaranteed to be available if
+ // FS_IOC_ADD_ENCRYPTION_KEY is. There's also no need to check for
+ // support on every filesystem separately, since either the kernel
+ // supports the ioctls on all fscrypt-capable filesystems or it doesn't.
+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, dir.Fd(), unix.FS_IOC_ADD_ENCRYPTION_KEY, 0)
+ if errno == unix.ENOTTY {
+ log.Printf("Kernel doesn't support filesystem keyring. Falling back to user keyring.")
+ return false
+ }
+ if errno == unix.EFAULT {
+ log.Printf("Detected support for filesystem keyring")
+ } else {
+ // EFAULT is expected, but as long as we didn't get ENOTTY the
+ // ioctl should be available.
+ log.Printf("Unexpected error from FS_IOC_ADD_ENCRYPTION_KEY(%q, NULL): %v", mount.Path, errno)
+ }
+ return true
+}
+
+// IsFsKeyringSupported returns true if the kernel supports the ioctls to
+// add/remove fscrypt keys directly to/from the filesystem. For support to be
+// detected, the given Mount must be for a filesystem that supports fscrypt.
+func IsFsKeyringSupported(mount *filesystem.Mount) bool {
+ fsKeyringSupportedLock.Lock()
+ defer fsKeyringSupportedLock.Unlock()
+ if !fsKeyringSupportedKnown {
+ fsKeyringSupported = checkForFsKeyringSupport(mount)
+ fsKeyringSupportedKnown = true
+ }
+ return fsKeyringSupported
+}
+
+// buildKeySpecifier converts the key descriptor string to an FscryptKeySpecifier.
+func buildKeySpecifier(spec *unix.FscryptKeySpecifier, descriptor string) error {
+ descriptorBytes, err := hex.DecodeString(descriptor)
+ if err != nil {
+ return errors.Errorf("key descriptor %q is invalid", descriptor)
+ }
+ switch len(descriptorBytes) {
+ case unix.FSCRYPT_KEY_DESCRIPTOR_SIZE:
+ spec.Type = unix.FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR
+ case unix.FSCRYPT_KEY_IDENTIFIER_SIZE:
+ spec.Type = unix.FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER
+ default:
+ return errors.Errorf("key descriptor %q has unknown length", descriptor)
+ }
+ copy(spec.U[:], descriptorBytes)
+ return nil
+}
+
+type savedPrivs struct {
+ ruid, euid, suid int
+}
+
+// dropPrivsIfNeeded drops privileges (UIDs only) to the given user if we're
+// working with a v2 policy key, and if the user is different from the user the
+// process is currently running as.
+//
+// This is needed to change the effective UID so that FS_IOC_ADD_ENCRYPTION_KEY
+// and FS_IOC_REMOVE_ENCRYPTION_KEY will add/remove a claim to the key for the
+// intended user, and so that FS_IOC_GET_ENCRYPTION_KEY_STATUS will return the
+// correct status flags for the user.
+func dropPrivsIfNeeded(user *user.User, spec *unix.FscryptKeySpecifier) (*savedPrivs, error) {
+ if spec.Type == unix.FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR {
+ // v1 policy keys don't have any concept of user claims.
+ return nil, nil
+ }
+ targetUID := util.AtoiOrPanic(user.Uid)
+ ruid, euid, suid := security.GetUids()
+ if euid == targetUID {
+ return nil, nil
+ }
+ if err := security.SetUids(targetUID, targetUID, euid); err != nil {
+ return nil, err
+ }
+ return &savedPrivs{ruid, euid, suid}, nil
+}
+
+// restorePrivs restores root privileges if needed.
+func restorePrivs(privs *savedPrivs) error {
+ if privs != nil {
+ return security.SetUids(privs.ruid, privs.euid, privs.suid)
+ }
+ return nil
+}
+
+// validateKeyDescriptor validates that the correct key descriptor was provided.
+// This isn't really necessary; this is just an extra sanity check.
+func validateKeyDescriptor(spec *unix.FscryptKeySpecifier, descriptor string) (string, error) {
+ if spec.Type != unix.FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER {
+ // v1 policy key: the descriptor is chosen arbitrarily by
+ // userspace, so there's nothing to validate.
+ return descriptor, nil
+ }
+ // v2 policy key. The descriptor ("identifier" in the kernel UAPI) is
+ // calculated as a cryptographic hash of the key itself. The kernel
+ // ignores the provided value, and calculates and returns it itself. So
+ // verify that the returned value is as expected. If it's not, the key
+ // doesn't actually match the encryption policy we thought it was for.
+ actual := hex.EncodeToString(spec.U[:unix.FSCRYPT_KEY_IDENTIFIER_SIZE])
+ if descriptor == actual {
+ return descriptor, nil
+ }
+ return actual,
+ errors.Errorf("provided and actual key descriptors differ (%q != %q)",
+ descriptor, actual)
+}
+
+// fsAddEncryptionKey adds the specified encryption key to the specified filesystem.
+func fsAddEncryptionKey(key *crypto.Key, descriptor string,
+ mount *filesystem.Mount, user *user.User) error {
+
+ dir, err := os.Open(mount.Path)
+ if err != nil {
+ return err
+ }
+ defer dir.Close()
+
+ argKey, err := crypto.NewBlankKey(int(unsafe.Sizeof(unix.FscryptAddKeyArg{})) + key.Len())
+ if err != nil {
+ return err
+ }
+ defer argKey.Wipe()
+ arg := (*unix.FscryptAddKeyArg)(argKey.UnsafePtr())
+
+ if err = buildKeySpecifier(&arg.Key_spec, descriptor); err != nil {
+ return err
+ }
+
+ raw := unsafe.Pointer(uintptr(argKey.UnsafePtr()) + unsafe.Sizeof(*arg))
+ arg.Raw_size = uint32(key.Len())
+ C.memcpy(raw, key.UnsafePtr(), C.size_t(key.Len()))
+
+ savedPrivs, err := dropPrivsIfNeeded(user, &arg.Key_spec)
+ if err != nil {
+ return err
+ }
+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, dir.Fd(),
+ unix.FS_IOC_ADD_ENCRYPTION_KEY, uintptr(argKey.UnsafePtr()))
+ restorePrivs(savedPrivs)
+
+ log.Printf("FS_IOC_ADD_ENCRYPTION_KEY(%q, %s, <raw>) = %v", mount.Path, descriptor, errno)
+ if errno != 0 {
+ return errors.Wrapf(errno,
+ "error adding key with descriptor %s to filesystem %s",
+ descriptor, mount.Path)
+ }
+ if descriptor, err = validateKeyDescriptor(&arg.Key_spec, descriptor); err != nil {
+ fsRemoveEncryptionKey(descriptor, mount, user)
+ return err
+ }
+ return nil
+}
+
+// fsRemoveEncryptionKey removes the specified encryption key from the specified
+// filesystem.
+func fsRemoveEncryptionKey(descriptor string, mount *filesystem.Mount,
+ user *user.User) error {
+
+ dir, err := os.Open(mount.Path)
+ if err != nil {
+ return err
+ }
+ defer dir.Close()
+
+ var arg unix.FscryptRemoveKeyArg
+ if err = buildKeySpecifier(&arg.Key_spec, descriptor); err != nil {
+ return err
+ }
+
+ ioc := uintptr(unix.FS_IOC_REMOVE_ENCRYPTION_KEY)
+ iocName := "FS_IOC_REMOVE_ENCRYPTION_KEY"
+ var savedPrivs *savedPrivs
+ if user == nil {
+ ioc = unix.FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS
+ iocName = "FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS"
+ } else {
+ savedPrivs, err = dropPrivsIfNeeded(user, &arg.Key_spec)
+ if err != nil {
+ return err
+ }
+ }
+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, dir.Fd(), ioc, uintptr(unsafe.Pointer(&arg)))
+ restorePrivs(savedPrivs)
+
+ log.Printf("%s(%q, %s) = %v, removal_status_flags=0x%x",
+ iocName, mount.Path, descriptor, errno, arg.Removal_status_flags)
+ switch errno {
+ case 0:
+ switch {
+ case arg.Removal_status_flags&unix.FSCRYPT_KEY_REMOVAL_STATUS_FLAG_OTHER_USERS != 0:
+ return ErrKeyAddedByOtherUsers
+ case arg.Removal_status_flags&unix.FSCRYPT_KEY_REMOVAL_STATUS_FLAG_FILES_BUSY != 0:
+ return ErrKeyFilesOpen
+ }
+ return nil
+ case unix.ENOKEY:
+ // ENOKEY means either the key is completely missing or that the
+ // current user doesn't have a claim to it. Distinguish between
+ // these two cases by getting the key status.
+ if user != nil {
+ status, _ := fsGetEncryptionKeyStatus(descriptor, mount, user)
+ if status == KeyPresentButOnlyOtherUsers {
+ return ErrKeyAddedByOtherUsers
+ }
+ }
+ return ErrKeyNotPresent
+ default:
+ return errors.Wrapf(errno,
+ "error removing key with descriptor %s from filesystem %s",
+ descriptor, mount.Path)
+ }
+}
+
+// fsGetEncryptionKeyStatus gets the status of the specified encryption key on
+// the specified filesystem.
+func fsGetEncryptionKeyStatus(descriptor string, mount *filesystem.Mount,
+ user *user.User) (KeyStatus, error) {
+
+ dir, err := os.Open(mount.Path)
+ if err != nil {
+ return KeyStatusUnknown, err
+ }
+ defer dir.Close()
+
+ var arg unix.FscryptGetKeyStatusArg
+ err = buildKeySpecifier(&arg.Key_spec, descriptor)
+ if err != nil {
+ return KeyStatusUnknown, err
+ }
+
+ savedPrivs, err := dropPrivsIfNeeded(user, &arg.Key_spec)
+ if err != nil {
+ return KeyStatusUnknown, err
+ }
+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, dir.Fd(),
+ unix.FS_IOC_GET_ENCRYPTION_KEY_STATUS, uintptr(unsafe.Pointer(&arg)))
+ restorePrivs(savedPrivs)
+
+ log.Printf("FS_IOC_GET_ENCRYPTION_KEY_STATUS(%q, %s) = %v, status=%d, status_flags=0x%x",
+ mount.Path, descriptor, errno, arg.Status, arg.Status_flags)
+ if errno != 0 {
+ return KeyStatusUnknown,
+ errors.Wrapf(errno,
+ "error getting status of key with descriptor %s on filesystem %s",
+ descriptor, mount.Path)
+ }
+ switch arg.Status {
+ case unix.FSCRYPT_KEY_STATUS_ABSENT:
+ return KeyAbsent, nil
+ case unix.FSCRYPT_KEY_STATUS_PRESENT:
+ if arg.Key_spec.Type != unix.FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR &&
+ (arg.Status_flags&unix.FSCRYPT_KEY_STATUS_FLAG_ADDED_BY_SELF) == 0 {
+ return KeyPresentButOnlyOtherUsers, nil
+ }
+ return KeyPresent, nil
+ case unix.FSCRYPT_KEY_STATUS_INCOMPLETELY_REMOVED:
+ return KeyAbsentButFilesBusy, nil
+ default:
+ return KeyStatusUnknown,
+ errors.Errorf("unknown key status (%d) for key with descriptor %s on filesystem %s",
+ arg.Status, descriptor, mount.Path)
+ }
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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)
+}
--- /dev/null
+/*
+ * user_keyring.go - Add/remove encryption policy keys to/from user keyrings.
+ * This is the deprecated mechanism; see fs_keyring.go for the new mechanism.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package keyring
+
+import (
+ "os/user"
+ "runtime"
+ "unsafe"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sys/unix"
+
+ "fmt"
+ "log"
+
+ "github.com/google/fscrypt/crypto"
+ "github.com/google/fscrypt/security"
+ "github.com/google/fscrypt/util"
+)
+
+// ErrAccessUserKeyring indicates that a user's keyring cannot be
+// accessed.
+type ErrAccessUserKeyring struct {
+ TargetUser *user.User
+ UnderlyingError error
+}
+
+func (err *ErrAccessUserKeyring) Error() string {
+ return fmt.Sprintf("could not access user keyring for %q: %s",
+ err.TargetUser.Username, err.UnderlyingError)
+}
+
+// ErrSessionUserKeyring indicates that a user's keyring is not linked
+// into the session keyring.
+type ErrSessionUserKeyring struct {
+ TargetUser *user.User
+}
+
+func (err *ErrSessionUserKeyring) Error() string {
+ return fmt.Sprintf("user keyring for %q is not linked into the session keyring",
+ err.TargetUser.Username)
+}
+
+// KeyType is always logon as required by filesystem encryption.
+const KeyType = "logon"
+
+// userAddKey puts the provided policy key into the user keyring for the
+// specified user with the provided description, and type logon.
+func userAddKey(key *crypto.Key, description string, targetUser *user.User) error {
+ runtime.LockOSThread() // ensure target user keyring remains possessed in thread keyring
+ defer runtime.UnlockOSThread()
+
+ // Create our payload (containing an FscryptKey)
+ payload, err := crypto.NewBlankKey(int(unsafe.Sizeof(unix.FscryptKey{})))
+ if err != nil {
+ return err
+ }
+ defer payload.Wipe()
+
+ // Cast the payload to an FscryptKey so we can initialize the fields.
+ fscryptKey := (*unix.FscryptKey)(payload.UnsafePtr())
+ // Mode is ignored by the kernel
+ fscryptKey.Mode = 0
+ fscryptKey.Size = uint32(key.Len())
+ copy(fscryptKey.Raw[:], key.Data())
+
+ keyringID, err := UserKeyringID(targetUser, true)
+ if err != nil {
+ return err
+ }
+ keyID, err := unix.AddKey(KeyType, description, payload.Data(), keyringID)
+ log.Printf("KeyctlAddKey(%s, %s, <data>, %d) = %d, %v",
+ KeyType, description, keyringID, keyID, err)
+ if err != nil {
+ return errors.Wrapf(err,
+ "error adding key with description %s to user keyring for %q",
+ description, targetUser.Username)
+ }
+ return nil
+}
+
+// userRemoveKey tries to remove a policy key from the user keyring with the
+// provided description. An error is returned if the key does not exist.
+func userRemoveKey(description string, targetUser *user.User) error {
+ runtime.LockOSThread() // ensure target user keyring remains possessed in thread keyring
+ defer runtime.UnlockOSThread()
+
+ keyID, keyringID, err := userFindKey(description, targetUser)
+ if err != nil {
+ return ErrKeyNotPresent
+ }
+
+ _, err = unix.KeyctlInt(unix.KEYCTL_UNLINK, keyID, keyringID, 0, 0)
+ log.Printf("KeyctlUnlink(%d, %d) = %v", keyID, keyringID, err)
+ if err != nil {
+ return errors.Wrapf(err,
+ "error removing key with description %s from user keyring for %q",
+ description, targetUser.Username)
+ }
+ return nil
+}
+
+// userFindKey tries to locate a key with the provided description in the user
+// keyring for the target user. The key ID and keyring ID are returned if we can
+// find the key. An error is returned if the key does not exist.
+func userFindKey(description string, targetUser *user.User) (int, int, error) {
+ runtime.LockOSThread() // ensure target user keyring remains possessed in thread keyring
+ defer runtime.UnlockOSThread()
+
+ keyringID, err := UserKeyringID(targetUser, false)
+ if err != nil {
+ return 0, 0, err
+ }
+
+ keyID, err := unix.KeyctlSearch(keyringID, KeyType, description, 0)
+ log.Printf("KeyctlSearch(%d, %s, %s) = %d, %v", keyringID, KeyType, description, keyID, err)
+ if err != nil {
+ return 0, 0, errors.Wrapf(err,
+ "error searching for key %s in user keyring for %q",
+ description, targetUser.Username)
+ }
+ return keyID, keyringID, err
+}
+
+// UserKeyringID returns the key id of the target user's user keyring. We also
+// ensure that the keyring will be accessible by linking it into the thread
+// keyring and linking it into the root user keyring (permissions allowing). If
+// checkSession is true, an error is returned if a normal user requests their
+// user keyring, but it is not in the current session keyring.
+func UserKeyringID(targetUser *user.User, checkSession bool) (int, error) {
+ runtime.LockOSThread() // ensure target user keyring remains possessed in thread keyring
+ defer runtime.UnlockOSThread()
+
+ uid := util.AtoiOrPanic(targetUser.Uid)
+ targetKeyring, err := userKeyringIDLookup(uid)
+ if err != nil {
+ return 0, &ErrAccessUserKeyring{targetUser, err}
+ }
+
+ if !util.IsUserRoot() {
+ // Make sure the returned keyring will be accessible by checking
+ // that it is in the session keyring.
+ if checkSession && !isUserKeyringInSession(uid) {
+ return 0, &ErrSessionUserKeyring{targetUser}
+ }
+ return targetKeyring, nil
+ }
+
+ // Make sure the returned keyring will be accessible by linking it into
+ // the root user's user keyring (which will not be garbage collected).
+ rootKeyring, err := userKeyringIDLookup(0)
+ if err != nil {
+ return 0, errors.Wrapf(err, "error looking up root's user keyring")
+ }
+
+ if rootKeyring != targetKeyring {
+ if err = keyringLink(targetKeyring, rootKeyring); err != nil {
+ return 0, errors.Wrapf(err,
+ "error linking user keyring for %q into root's user keyring",
+ targetUser.Username)
+ }
+ }
+ return targetKeyring, nil
+}
+
+func userKeyringIDLookup(uid int) (keyringID int, err error) {
+
+ // Our goals here are to:
+ // - Find the user keyring (for the provided uid)
+ // - Link it into the current thread keyring (so we can use it)
+ // - Make no permanent changes to the process privileges
+ // Complicating this are the facts that:
+ // - The value of KEY_SPEC_USER_KEYRING is determined by the ruid
+ // - Keyring linking permissions use the euid
+ // So we have to change both the ruid and euid to make this work,
+ // setting the suid to 0 so that we can later switch back.
+ ruid, euid, suid := security.GetUids()
+ if ruid != uid || euid != uid {
+ if err = security.SetUids(uid, uid, 0); err != nil {
+ return
+ }
+ defer func() {
+ resetErr := security.SetUids(ruid, euid, suid)
+ if resetErr != nil {
+ err = resetErr
+ }
+ }()
+ }
+
+ // We get the value of KEY_SPEC_USER_KEYRING. Note that this will also
+ // trigger the creation of the uid keyring if it does not yet exist.
+ keyringID, err = unix.KeyctlGetKeyringID(unix.KEY_SPEC_USER_KEYRING, true)
+ log.Printf("keyringID(_uid.%d) = %d, %v", uid, keyringID, err)
+ if err != nil {
+ return 0, err
+ }
+
+ // We still want to use this keyring after our privileges are reset. So
+ // we link it into the thread keyring, preventing a loss of access.
+ //
+ // We must be under LockOSThread() for this to work reliably. Note that
+ // we can't just use the process keyring, since it doesn't work reliably
+ // in Go programs, due to the Go runtime creating threads before the
+ // program starts and has a chance to create the process keyring.
+ if err = keyringLink(keyringID, unix.KEY_SPEC_THREAD_KEYRING); err != nil {
+ return 0, err
+ }
+
+ return keyringID, nil
+}
+
+// isUserKeyringInSession tells us if the user's uid keyring is in the current
+// session keyring.
+func isUserKeyringInSession(uid int) bool {
+ // We cannot use unix.KEY_SPEC_SESSION_KEYRING directly as that might
+ // create a session keyring if one does not exist.
+ sessionKeyring, err := unix.KeyctlGetKeyringID(unix.KEY_SPEC_SESSION_KEYRING, false)
+ log.Printf("keyringID(session) = %d, %v", sessionKeyring, err)
+ if err != nil {
+ return false
+ }
+
+ description := fmt.Sprintf("_uid.%d", uid)
+ id, err := unix.KeyctlSearch(sessionKeyring, "keyring", description, 0)
+ log.Printf("KeyctlSearch(%d, keyring, %s) = %d, %v", sessionKeyring, description, id, err)
+ return err == nil
+}
+
+func keyringLink(keyID int, keyringID int) error {
+ _, err := unix.KeyctlInt(unix.KEYCTL_LINK, keyID, keyringID, 0, 0)
+ log.Printf("KeyctlLink(%d, %d) = %v", keyID, keyringID, err)
+ return err
+}
--- /dev/null
+/*
+ * 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")
+}
--- /dev/null
+/*
+ * 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)
+}
--- /dev/null
+/*
+ * 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)
+ }
+}
--- /dev/null
+/*
+ * 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
+)
--- /dev/null
+//
+// 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
+}
--- /dev/null
+/*
+ * 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";
+}
--- /dev/null
+/*
+ * 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)
+}
--- /dev/null
+/*
+ * 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")
+ }
+}
--- /dev/null
+/*
+ * constants.go - PAM flags and item types from github.com/msteinert/pam
+ *
+ * Modifications Copyright 2017 Google Inc.
+ * Modifications Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/*
+ * Copyright 2011, krockot
+ * Copyright 2015, Michael Steinert <mike.steinert@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package pam
+
+/*
+#cgo LDFLAGS: -lpam
+
+#include <security/pam_modules.h>
+*/
+import "C"
+
+// Item is a PAM information type.
+type Item int
+
+// PAM Item types.
+const (
+ // Service is the name which identifies the PAM stack.
+ Service Item = C.PAM_SERVICE
+ // User identifies the username identity used by a service.
+ User = C.PAM_USER
+ // Tty is the terminal name.
+ Tty = C.PAM_TTY
+ // Rhost is the requesting host name.
+ Rhost = C.PAM_RHOST
+ // Authtok is the currently active authentication token.
+ Authtok = C.PAM_AUTHTOK
+ // Oldauthtok is the old authentication token.
+ Oldauthtok = C.PAM_OLDAUTHTOK
+ // Ruser is the requesting user name.
+ Ruser = C.PAM_RUSER
+ // UserPrompt is the string use to prompt for a username.
+ UserPrompt = C.PAM_USER_PROMPT
+)
+
+// Flag is used as input to various PAM functions. Flags can be combined with a
+// bitwise or. Refer to the official PAM documentation for which flags are
+// accepted by which functions.
+type Flag int
+
+// PAM Flag types.
+const (
+ // Silent indicates that no messages should be emitted.
+ Silent Flag = C.PAM_SILENT
+ // DisallowNullAuthtok indicates that authorization should fail
+ // if the user does not have a registered authentication token.
+ DisallowNullAuthtok = C.PAM_DISALLOW_NULL_AUTHTOK
+ // EstablishCred indicates that credentials should be established
+ // for the user.
+ EstablishCred = C.PAM_ESTABLISH_CRED
+ // DeleteCred inidicates that credentials should be deleted.
+ DeleteCred = C.PAM_DELETE_CRED
+ // ReinitializeCred indicates that credentials should be fully
+ // reinitialized.
+ ReinitializeCred = C.PAM_REINITIALIZE_CRED
+ // RefreshCred indicates that the lifetime of existing credentials
+ // should be extended.
+ RefreshCred = C.PAM_REFRESH_CRED
+ // ChangeExpiredAuthtok indicates that the authentication token
+ // should be changed if it has expired.
+ ChangeExpiredAuthtok = C.PAM_CHANGE_EXPIRED_AUTHTOK
+ // PrelimCheck indicates that the modules are being probed as to their
+ // ready status for altering the user's authentication token.
+ PrelimCheck = C.PAM_PRELIM_CHECK
+ // UpdateAuthtok informs the module that this is the call it should
+ // change the authorization tokens.
+ UpdateAuthtok = C.PAM_UPDATE_AUTHTOK
+)
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * pam.c - Functions to let us call into libpam from Go.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+#include "pam.h"
+
+#include <security/pam_appl.h>
+#include <security/pam_ext.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h> // mlock/munlock
+
+#include "_cgo_export.h" // for input callbacks
+
+static int conversation(int num_msg, const struct pam_message** msg,
+ struct pam_response** resp, void* appdata_ptr) {
+ if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) {
+ return PAM_CONV_ERR;
+ }
+
+ // Allocate the response table with num_msg entries.
+ *resp = calloc(num_msg, sizeof **resp);
+ if (!*resp) {
+ return PAM_BUF_ERR;
+ }
+
+ // Check each message to see if we need to run a callback.
+ char* callback_msg = NULL;
+ char* callback_resp = NULL;
+ int i;
+ for (i = 0; i < num_msg; ++i) {
+ callback_msg = (char*)msg[i]->msg;
+
+ // We run our input callback if the style tells us we need data. Otherwise,
+ // we just print the error messages or text info to standard output.
+ switch (msg[i]->msg_style) {
+ case PAM_PROMPT_ECHO_OFF:
+ callback_resp = passphraseInput(callback_msg);
+ break;
+ case PAM_PROMPT_ECHO_ON:
+ callback_resp = userInput(callback_msg);
+ break;
+ case PAM_ERROR_MSG:
+ case PAM_TEXT_INFO:
+ fprintf(stderr, "%s\n", callback_msg);
+ continue;
+ }
+
+ if (!callback_resp) {
+ // If the callback failed, free each nonempty response in the response
+ // table and the response table itself.
+ while (--i >= 0) {
+ free((*resp)[i].resp);
+ }
+ free(*resp);
+ *resp = NULL;
+ return PAM_CONV_ERR;
+ }
+
+ (*resp)[i].resp = callback_resp;
+ }
+
+ return PAM_SUCCESS;
+}
+
+static const struct pam_conv conv = {conversation, NULL};
+const struct pam_conv* goConv = &conv;
+
+void freeData(pam_handle_t* pamh, void* data, int error_status) { free(data); }
+
+void freeArray(pam_handle_t* pamh, void** array, int error_status) {
+ int i;
+ for (i = 0; array[i]; ++i) {
+ free(array[i]);
+ }
+ free(array);
+}
+
+void* copyIntoSecret(void* data) {
+ size_t size = strlen(data) + 1; // include null terminator
+ void* copy = calloc(1, size); // initialize to avoid a compiler warning
+ mlock(copy, size);
+ memcpy(copy, data, size);
+ return copy;
+}
+
+void freeSecret(pam_handle_t* pamh, char* data, int error_status) {
+ size_t size = strlen(data) + 1; // Include null terminator
+ // Use volatile function pointer to actually clear the memory.
+ static void* (*const volatile memset_sec)(void*, int, size_t) = &memset;
+ memset_sec(data, 0, size);
+ munlock(data, size);
+ free(data);
+}
+
+void infoMessage(pam_handle_t* pamh, const char* message) {
+ pam_info(pamh, "%s", message);
+}
--- /dev/null
+/*
+ * pam.go - Utility functions for interfacing with the PAM libraries.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package pam
+
+/*
+#cgo LDFLAGS: -lpam
+#include "pam.h"
+
+#include <pwd.h>
+#include <stdlib.h>
+#include <security/pam_modules.h>
+*/
+import "C"
+import (
+ "errors"
+ "log"
+ "os/user"
+ "unsafe"
+
+ "github.com/google/fscrypt/security"
+)
+
+// Handle wraps the C pam_handle_t type. This is used from within modules.
+type Handle struct {
+ handle *C.pam_handle_t
+ status C.int
+ origPrivs *security.Privileges
+ // PamUser is the user for whom the PAM module is running.
+ PamUser *user.User
+}
+
+// NewHandle creates a Handle from a raw pointer.
+func NewHandle(pamh unsafe.Pointer) (*Handle, error) {
+ var err error
+ h := &Handle{
+ handle: (*C.pam_handle_t)(pamh),
+ status: C.PAM_SUCCESS,
+ }
+
+ var pamUsername *C.char
+ h.status = C.pam_get_user(h.handle, &pamUsername, nil)
+ if err = h.err(); err != nil {
+ return nil, err
+ }
+
+ h.PamUser, err = user.Lookup(C.GoString(pamUsername))
+ return h, err
+}
+
+func (h *Handle) setData(name string, data unsafe.Pointer, cleanup C.CleanupFunc) error {
+ cName := C.CString(name)
+ defer C.free(unsafe.Pointer(cName))
+ h.status = C.pam_set_data(h.handle, cName, data, cleanup)
+ return h.err()
+}
+
+func (h *Handle) getData(name string) (unsafe.Pointer, error) {
+ var data unsafe.Pointer
+ cName := C.CString(name)
+ defer C.free(unsafe.Pointer(cName))
+ h.status = C.pam_get_data(h.handle, cName, &data)
+ return data, h.err()
+}
+
+// ClearData removes the PAM data with the specified name.
+func (h *Handle) ClearData(name string) error {
+ return h.setData(name, unsafe.Pointer(C.CString("")), C.CleanupFunc(C.freeData))
+}
+
+// SetSecret sets a copy of the C string secret into the PAM data with the
+// specified name. This copy will be held in locked memory until this PAM data
+// is cleared.
+func (h *Handle) SetSecret(name string, secret unsafe.Pointer) error {
+ return h.setData(name, C.copyIntoSecret(secret), C.CleanupFunc(C.freeSecret))
+}
+
+// GetSecret returns a pointer to the C string PAM data with the specified name.
+// This is a pointer directly to the data, so it shouldn't be modified. It
+// should have been previously set with SetSecret().
+func (h *Handle) GetSecret(name string) (unsafe.Pointer, error) {
+ return h.getData(name)
+}
+
+// SetString sets a string value for the PAM data with the specified name.
+func (h *Handle) SetString(name string, s string) error {
+ return h.setData(name, unsafe.Pointer(C.CString(s)), C.CleanupFunc(C.freeData))
+}
+
+// GetString gets a string value for the PAM data with the specified name. It
+// should have been previously set with SetString().
+func (h *Handle) GetString(name string) (string, error) {
+ data, err := h.getData(name)
+ if err != nil {
+ return "", err
+ }
+ return C.GoString((*C.char)(data)), nil
+}
+
+// GetItem retrieves a PAM information item. This is a pointer directly to the
+// data, so it shouldn't be modified.
+func (h *Handle) GetItem(i Item) (unsafe.Pointer, error) {
+ var data unsafe.Pointer
+ h.status = C.pam_get_item(h.handle, C.int(i), &data)
+ if err := h.err(); err != nil {
+ return nil, err
+ }
+ if data == nil {
+ return nil, errors.New("item not found")
+ }
+ return data, nil
+}
+
+// GetServiceName retrieves the name of the application running the PAM transaction.
+func (h *Handle) GetServiceName() string {
+ data, err := h.GetItem(Service)
+ if err != nil {
+ return "[unknown service]"
+ }
+ return C.GoString((*C.char)(data))
+}
+
+// StartAsPamUser sets the effective privileges to that of the PAM user.
+func (h *Handle) StartAsPamUser() error {
+ userPrivs, err := security.UserPrivileges(h.PamUser)
+ if err != nil {
+ return err
+ }
+ origPrivs, err := security.ProcessPrivileges()
+ if err != nil {
+ return err
+ }
+ if err = security.SetProcessPrivileges(userPrivs); err != nil {
+ return err
+ }
+ h.origPrivs = origPrivs
+ return nil
+}
+
+// StopAsPamUser restores the original privileges that were running the
+// PAM module (this is usually root).
+func (h *Handle) StopAsPamUser() error {
+ if h.origPrivs == nil {
+ return nil
+ }
+ err := security.SetProcessPrivileges(h.origPrivs)
+ h.origPrivs = nil
+ if err != nil {
+ log.Print(err)
+ }
+ return err
+}
+
+func (h *Handle) err() error {
+ if h.status == C.PAM_SUCCESS {
+ return nil
+ }
+ s := C.GoString(C.pam_strerror(h.handle, C.int(h.status)))
+ return errors.New(s)
+}
+
+// InfoMessage sends a message to the application using pam_info().
+func (h *Handle) InfoMessage(message string) {
+ cMessage := C.CString(message)
+ defer C.free(unsafe.Pointer(cMessage))
+ C.infoMessage(h.handle, cMessage)
+}
+
+// Transaction represents a wrapped pam_handle_t type created with pam_start
+// from an application.
+type Transaction Handle
+
+// Start initializes a pam Transaction. End() should be called after the
+// Transaction is no longer needed.
+func Start(service, username string) (*Transaction, error) {
+ cService := C.CString(service)
+ defer C.free(unsafe.Pointer(cService))
+ cUsername := C.CString(username)
+ defer C.free(unsafe.Pointer(cUsername))
+
+ t := &Transaction{
+ handle: nil,
+ status: C.PAM_SUCCESS,
+ }
+ t.status = C.pam_start(
+ cService,
+ cUsername,
+ C.goConv,
+ &t.handle)
+ return t, (*Handle)(t).err()
+}
+
+// End finalizes a pam Transaction with pam_end().
+func (t *Transaction) End() {
+ C.pam_end(t.handle, t.status)
+}
+
+// Authenticate returns a boolean indicating if the user authenticated correctly
+// or not. If the authentication check did not complete, an error is returned.
+func (t *Transaction) Authenticate(quiet bool) (bool, error) {
+ var flags C.int = C.PAM_DISALLOW_NULL_AUTHTOK
+ if quiet {
+ flags |= C.PAM_SILENT
+ }
+ t.status = C.pam_authenticate(t.handle, flags)
+ if t.status == C.PAM_AUTH_ERR {
+ return false, nil
+ }
+ return true, (*Handle)(t).err()
+}
--- /dev/null
+/*
+ * pam.h - Functions to let us call into libpam from Go.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+#ifndef FSCRYPT_PAM_H
+#define FSCRYPT_PAM_H
+
+#include <security/pam_appl.h>
+
+// Conversation that will call back into Go code when appropriate.
+extern const struct pam_conv *goConv;
+
+// CleaupFuncs are used to cleanup specific PAM data.
+typedef void (*CleanupFunc)(pam_handle_t *pamh, void *data, int error_status);
+
+// CleaupFunc that calls free() on data.
+void freeData(pam_handle_t *pamh, void *data, int error_status);
+
+// CleaupFunc that frees each item in a null terminated array of pointers and
+// then frees the array itself.
+void freeArray(pam_handle_t *pamh, void **array, int error_status);
+
+// Creates a copy of a C string, which resides in a locked buffer.
+void *copyIntoSecret(void *data);
+
+// CleaupFunc that Zeros wipes a C string and unlocks and frees its memory.
+void freeSecret(pam_handle_t *pamh, char *data, int error_status);
+
+// Sends a message to the application using pam_info().
+void infoMessage(pam_handle_t *pamh, const char *message);
+
+#endif // FSCRYPT_PAM_H
--- /dev/null
+/*
+ * 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) {}
--- /dev/null
+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
--- /dev/null
+/*
+ * pam_fscrypt.go - Checks the validity of a login token key against PAM.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package main
+
+/*
+#cgo LDFLAGS: -lpam -fPIC
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <security/pam_appl.h>
+*/
+import "C"
+import (
+ "fmt"
+ "log"
+ "log/syslog"
+ "os"
+ "strconv"
+ "unsafe"
+
+ "github.com/pkg/errors"
+
+ "github.com/google/fscrypt/actions"
+ "github.com/google/fscrypt/crypto"
+ "github.com/google/fscrypt/keyring"
+ "github.com/google/fscrypt/pam"
+ "github.com/google/fscrypt/security"
+)
+
+const (
+ moduleName = "pam_fscrypt"
+ // authtokLabel tags the AUTHTOK in the PAM data.
+ authtokLabel = "fscrypt_authtok"
+ // pidLabel tags the pid in the PAM data.
+ pidLabel = "fscrypt_pid"
+ // These flags are used to toggle behavior of the PAM module.
+ debugFlag = "debug"
+
+ // This option is accepted for compatibility with existing config files,
+ // but now we lock policies by default and this option is a no-op.
+ lockPoliciesFlag = "lock_policies"
+
+ // Only unlock directories, don't lock them.
+ unlockOnlyFlag = "unlock_only"
+
+ // This option is accepted for compatibility with existing config files,
+ // but it no longer does anything. pam_fscrypt now drops caches if and
+ // only if it is needed. (Usually it is not needed anymore, as the
+ // FS_IOC_REMOVE_ENCRYPTION_KEY ioctl handles this automatically.)
+ dropCachesFlag = "drop_caches"
+)
+
+var (
+ // PamFuncs for our 4 provided methods
+ authenticateFunc = PamFunc{"Authenticate", Authenticate}
+ openSessionFunc = PamFunc{"OpenSession", OpenSession}
+ closeSessionFunc = PamFunc{"CloseSession", CloseSession}
+ chauthtokFunc = PamFunc{"Chauthtok", Chauthtok}
+)
+
+// Authenticate copies the AUTHTOK (if necessary) into the PAM data so it can be
+// used in pam_sm_open_session.
+func Authenticate(handle *pam.Handle, _ map[string]bool) error {
+ if err := handle.StartAsPamUser(); err != nil {
+ return err
+ }
+ defer handle.StopAsPamUser()
+
+ // Save the PID in the PAM data so that the Session hook can try to
+ // detect the unsupported situation where the process was forked.
+ if err := handle.SetString(pidLabel, strconv.Itoa(os.Getpid())); err != nil {
+ return errors.Wrap(err, "could not save pid in PAM data")
+ }
+
+ // If this user doesn't have a login protector, no unlocking is needed.
+ if _, err := loginProtector(handle); err != nil {
+ log.Printf("no protector, no need for AUTHTOK: %s", err)
+ return nil
+ }
+
+ log.Print("copying AUTHTOK for use in the session open")
+ authtok, err := handle.GetItem(pam.Authtok)
+ if err != nil {
+ return errors.Wrap(err, "could not get AUTHTOK")
+ }
+ err = handle.SetSecret(authtokLabel, authtok)
+ return errors.Wrap(err, "could not set AUTHTOK data")
+}
+
+func beginProvisioningOp(handle *pam.Handle, policy *actions.Policy) error {
+ if policy.NeedsRootToProvision() {
+ return handle.StopAsPamUser()
+ }
+ return nil
+}
+
+func endProvisioningOp(handle *pam.Handle, policy *actions.Policy) error {
+ if policy.NeedsRootToProvision() {
+ return handle.StartAsPamUser()
+ }
+ return nil
+}
+
+// Set up the PAM user's keyring if needed by any encryption policies.
+func setupUserKeyringIfNeeded(handle *pam.Handle, policies []*actions.Policy) error {
+ needed := false
+ for _, policy := range policies {
+ if policy.NeedsUserKeyring() {
+ needed = true
+ break
+ }
+ }
+ if !needed {
+ return nil
+ }
+ err := handle.StopAsPamUser()
+ if err != nil {
+ return err
+ }
+ _, err = keyring.UserKeyringID(handle.PamUser, true)
+ if err != nil {
+ log.Printf("Setting up keyrings in PAM: %v", err)
+ }
+ return handle.StartAsPamUser()
+}
+
+// The Go runtime doesn't support being forked, as it is multithreaded but
+// fork() deletes all threads except one. Some programs, such as xrdp, misuse
+// libpam by fork()-ing the process between pam_authenticate() and
+// pam_open_session(). Try to detect such unsupported cases and bail out early
+// rather than deadlocking the Go runtime, which would prevent the user from
+// logging in entirely. This isn't guaranteed to work, as we are already
+// running Go code here, so we may have already deadlocked. But in practice the
+// deadlock doesn't occur until hashing the login passphrase is attempted.
+func isUnsupportedFork(handle *pam.Handle) bool {
+ pidString, err := handle.GetString(pidLabel)
+ if err != nil {
+ return false
+ }
+ expectedPid, err := strconv.Atoi(pidString)
+ if err != nil {
+ log.Printf("%s parse error: %v", pidLabel, err)
+ return false
+ }
+ if os.Getpid() == expectedPid {
+ return false
+ }
+ handle.InfoMessage(fmt.Sprintf("%s couldn't automatically unlock directories, see syslog", moduleName))
+ if logger, err := syslog.New(syslog.LOG_WARNING, moduleName); err == nil {
+ fmt.Fprintf(logger,
+ "not unlocking directories because %s forked the process between authenticating the user and opening the session, which is incompatible with %s. See https://github.com/google/fscrypt/issues/350",
+ handle.GetServiceName(), moduleName)
+ logger.Close()
+ }
+ return true
+}
+
+// OpenSession provisions any policies protected with the login protector.
+func OpenSession(handle *pam.Handle, _ map[string]bool) error {
+ // We will always clear the AUTHTOK data
+ defer handle.ClearData(authtokLabel)
+ // Increment the count as we add a session
+ if _, err := AdjustCount(handle, +1); err != nil {
+ return err
+ }
+
+ if err := handle.StartAsPamUser(); err != nil {
+ return err
+ }
+ defer handle.StopAsPamUser()
+
+ // If there are no polices for the login protector, no unlocking needed.
+ protector, err := loginProtector(handle)
+ if err != nil {
+ log.Printf("no protector to unlock: %s", err)
+ return nil
+ }
+ policies := policiesUsingProtector(protector, false)
+ if len(policies) == 0 {
+ log.Print("no policies to unlock")
+ return nil
+ }
+
+ if isUnsupportedFork(handle) {
+ return nil
+ }
+
+ if err = setupUserKeyringIfNeeded(handle, policies); err != nil {
+ return errors.Wrapf(err, "setting up user keyring")
+ }
+
+ log.Printf("unlocking %d policies protected with AUTHTOK", len(policies))
+ keyFn := func(_ actions.ProtectorInfo, retry bool) (*crypto.Key, error) {
+ if retry {
+ // Login passphrase and login protector have diverged.
+ // We could prompt the user for the old passphrase and
+ // rewrap, but we currently don't.
+ return nil, pam.ErrPassphrase
+ }
+
+ authtok, err := handle.GetSecret(authtokLabel)
+ if err != nil {
+ // pam_sm_authenticate was not run before the session is
+ // opened. This can happen when a user does something
+ // like "sudo su <user>". We could prompt for the
+ // login passphrase here, but we currently don't.
+ return nil, errors.Wrap(err, "AUTHTOK data missing")
+ }
+
+ return crypto.NewKeyFromCString(authtok)
+ }
+ if err := protector.Unlock(keyFn); err != nil {
+ return errors.Wrapf(err, "unlocking protector %s", protector.Descriptor())
+ }
+ defer protector.Lock()
+
+ // We don't stop provisioning polices on error, we try all of them.
+ for _, policy := range policies {
+ if err := policy.UnlockWithProtector(protector); err != nil {
+ log.Printf("unlocking policy %s: %s", policy.Descriptor(), err)
+ continue
+ }
+ defer policy.Lock()
+
+ if err := beginProvisioningOp(handle, policy); err != nil {
+ return err
+ }
+ provisionErr := policy.Provision()
+ if err := endProvisioningOp(handle, policy); err != nil {
+ return err
+ }
+ if provisionErr != nil {
+ log.Printf("provisioning policy %s: %s", policy.Descriptor(), provisionErr)
+ continue
+ }
+ log.Printf("policy %s provisioned by %v", policy.Descriptor(),
+ handle.PamUser.Username)
+ }
+ return nil
+}
+
+// CloseSession can deprovision all keys provisioned at the start of the
+// session. It can also clear the cache so these changes take effect.
+func CloseSession(handle *pam.Handle, args map[string]bool) error {
+ // Only do stuff on session close when we are the last session
+ if count, err := AdjustCount(handle, -1); err != nil || count != 0 {
+ log.Printf("count is %d and we are not locking", count)
+ return err
+ }
+
+ if args[lockPoliciesFlag] {
+ log.Print("ignoring deprecated 'lock_policies' option (now the default)")
+ }
+
+ if args[dropCachesFlag] {
+ log.Print("ignoring deprecated 'drop_caches' option (now auto-detected)")
+ }
+
+ // Don't automatically drop privileges, since we may need them to
+ // deprovision policies or to drop caches.
+
+ if !args[unlockOnlyFlag] {
+ log.Print("locking policies protected with login protector")
+ needDropCaches, errLock := lockLoginPolicies(handle)
+
+ var errCache error
+ if needDropCaches {
+ log.Print("dropping appropriate filesystem caches at session close")
+ errCache = security.DropFilesystemCache()
+ }
+ if errLock != nil {
+ return errLock
+ }
+ return errCache
+ }
+ return nil
+}
+
+// lockLoginPolicies deprovisions all policy keys that are protected by the
+// user's login protector. It returns true if dropping filesystem caches will
+// be needed afterwards to completely lock the relevant directories.
+func lockLoginPolicies(handle *pam.Handle) (bool, error) {
+ needDropCaches := false
+
+ if err := handle.StartAsPamUser(); err != nil {
+ return needDropCaches, err
+ }
+ defer handle.StopAsPamUser()
+
+ // If there are no polices for the login protector, no locking needed.
+ protector, err := loginProtector(handle)
+ if err != nil {
+ log.Printf("nothing to lock: %s", err)
+ return needDropCaches, nil
+ }
+ policies := policiesUsingProtector(protector, true)
+ if len(policies) == 0 {
+ log.Print("no policies to lock")
+ return needDropCaches, nil
+ }
+
+ if err = setupUserKeyringIfNeeded(handle, policies); err != nil {
+ return needDropCaches, errors.Wrapf(err, "setting up user keyring")
+ }
+
+ // We will try to deprovision all of the policies.
+ for _, policy := range policies {
+ if policy.NeedsUserKeyring() {
+ needDropCaches = true
+ }
+ if err := beginProvisioningOp(handle, policy); err != nil {
+ return needDropCaches, err
+ }
+ deprovisionErr := policy.Deprovision(false)
+ if err := endProvisioningOp(handle, policy); err != nil {
+ return needDropCaches, err
+ }
+ if deprovisionErr != nil {
+ log.Printf("deprovisioning policy %s: %s", policy.Descriptor(), deprovisionErr)
+ continue
+ }
+ log.Printf("policy %s deprovisioned by %v", policy.Descriptor(), handle.PamUser.Username)
+ }
+ return needDropCaches, nil
+}
+
+var noOldAuthTokMessage string = `
+pam_fscrypt: cannot update login protector for '%s' because old passphrase
+was not given. This is expected when changing a user's passphrase as root.
+You'll need to manually update the protector's passphrase using:
+
+ fscrypt metadata change-passphrase --protector=%s:%s
+`
+
+// Chauthtok rewraps the login protector when the passphrase changes.
+func Chauthtok(handle *pam.Handle, _ map[string]bool) error {
+ if err := handle.StartAsPamUser(); err != nil {
+ return err
+ }
+ defer handle.StopAsPamUser()
+
+ protector, err := loginProtector(handle)
+ if err != nil {
+ log.Printf("no login protector to rewrap: %s", err)
+ return nil
+ }
+
+ oldKeyFn := func(_ actions.ProtectorInfo, retry bool) (*crypto.Key, error) {
+ if retry {
+ // If the OLDAUTHTOK disagrees with the login protector,
+ // we do nothing, as the protector will (probably) still
+ // disagree after the login passphrase changes.
+ return nil, pam.ErrPassphrase
+ }
+ authtok, err := handle.GetItem(pam.Oldauthtok)
+ if err != nil {
+ handle.InfoMessage(fmt.Sprintf(noOldAuthTokMessage,
+ handle.PamUser.Username,
+ protector.Context.Mount.Path, protector.Descriptor()))
+ return nil, errors.Wrap(err, "could not get OLDAUTHTOK")
+ }
+ return crypto.NewKeyFromCString(authtok)
+ }
+
+ newKeyFn := func(_ actions.ProtectorInfo, _ bool) (*crypto.Key, error) {
+ authtok, err := handle.GetItem(pam.Authtok)
+ if err != nil {
+ return nil, errors.Wrap(err, "could not get AUTHTOK")
+ }
+ return crypto.NewKeyFromCString(authtok)
+ }
+
+ log.Print("rewrapping login protector")
+ if err = protector.Unlock(oldKeyFn); err != nil {
+ return err
+ }
+ defer protector.Lock()
+
+ return protector.Rewrap(newKeyFn)
+}
+
+//export pam_sm_authenticate
+func pam_sm_authenticate(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+ return authenticateFunc.Run(pamh, argc, argv)
+}
+
+// pam_sm_setcred needed because we use pam_sm_authenticate.
+//
+//export pam_sm_setcred
+func pam_sm_setcred(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+ return C.PAM_SUCCESS
+}
+
+//export pam_sm_open_session
+func pam_sm_open_session(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+ return openSessionFunc.Run(pamh, argc, argv)
+}
+
+//export pam_sm_close_session
+func pam_sm_close_session(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+ return closeSessionFunc.Run(pamh, argc, argv)
+}
+
+//export pam_sm_chauthtok
+func pam_sm_chauthtok(pamh unsafe.Pointer, flags, argc C.int, argv **C.char) C.int {
+ // Only do rewrapping if we have both AUTHTOKs and a login protector.
+ if pam.Flag(flags)&pam.PrelimCheck != 0 {
+ return C.PAM_SUCCESS
+ }
+ return chauthtokFunc.Run(pamh, argc, argv)
+}
+
+// main() is needed to make a shared library compile
+func main() {}
--- /dev/null
+/*
+ * run_fscrypt.go - Helpers for running functions in the PAM module.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package main
+
+/*
+#cgo LDFLAGS: -lpam -fPIC
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <security/pam_appl.h>
+*/
+import "C"
+import (
+ "fmt"
+ "io"
+ "log"
+ "log/syslog"
+ "os"
+ "os/user"
+ "path/filepath"
+ "runtime/debug"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+
+ "github.com/pkg/errors"
+
+ "github.com/google/fscrypt/actions"
+ "github.com/google/fscrypt/filesystem"
+ "github.com/google/fscrypt/metadata"
+ "github.com/google/fscrypt/pam"
+ "github.com/google/fscrypt/util"
+)
+
+const (
+ // countDirectory is in a tmpfs filesystem so it will reset on reboot.
+ countDirectory = "/run/fscrypt"
+ // count files should only be readable and writable by root
+ countDirectoryPermissions = 0700
+ countFilePermissions = 0600
+ countFileFormat = "%d\n"
+ // uidMin is the first UID that can be used for a regular user (as
+ // opposed to a system user or root). This value is fairly standard
+ // across Linux distros, but it can be adjusted if needed.
+ uidMin = 1000
+)
+
+// PamFunc is used to define the various actions in the PAM module.
+type PamFunc struct {
+ // Name of the function being executed
+ name string
+ // Go implementation of this function
+ impl func(handle *pam.Handle, args map[string]bool) error
+}
+
+// isSystemUser checks if a user is a system user. pam_fscrypt should never
+// need to do anything for system users since they should never have login
+// protectors. Therefore, we detect them early to avoid wasting resources.
+func isSystemUser(user *user.User) bool {
+ uid := util.AtoiOrPanic(user.Uid)
+ return uid < uidMin && uid != 0
+}
+
+// Run is used to convert between the Go functions and exported C funcs.
+func (f *PamFunc) Run(pamh unsafe.Pointer, argc C.int, argv **C.char) (ret C.int) {
+ args := parseArgs(argc, argv)
+ errorWriter := setupLogging(args)
+
+ // Log any panics to the errorWriter
+ defer func() {
+ if r := recover(); r != nil {
+ ret = C.PAM_SERVICE_ERR
+ fmt.Fprintf(errorWriter,
+ "%s(%v) panicked: %s\nPlease open a bug.\n%s",
+ f.name, args, r, debug.Stack())
+ }
+ }()
+
+ log.Printf("%s(%v) starting", f.name, args)
+ handle, err := pam.NewHandle(pamh)
+ if err == nil {
+ if isSystemUser(handle.PamUser) {
+ log.Printf("invoked for system user %q (%s), doing nothing",
+ handle.PamUser.Username, handle.PamUser.Uid)
+ err = nil
+ } else {
+ err = f.impl(handle, args)
+ }
+ }
+ if err != nil {
+ fmt.Fprintf(errorWriter, "%s(%v) failed: %s", f.name, args, err)
+ return C.PAM_SERVICE_ERR
+ }
+ log.Printf("%s(%v) succeeded", f.name, args)
+ return C.PAM_SUCCESS
+}
+
+// parseArgs takes a list of C arguments into a PAM function and returns a map
+// where a key has a value of true if it appears in the argument list.
+func parseArgs(argc C.int, argv **C.char) map[string]bool {
+ args := make(map[string]bool)
+ if argc == 0 || argv == nil {
+ return args
+ }
+ for _, cString := range util.PointerSlice(unsafe.Pointer(argv))[:argc] {
+ args[C.GoString((*C.char)(cString))] = true
+ }
+ return args
+}
+
+// setupLogging directs turns off standard logging (or redirects it to debug
+// syslog if the "debug" argument is passed) and returns a writer to the error
+// syslog.
+func setupLogging(args map[string]bool) io.Writer {
+ log.SetFlags(0) // Syslog already includes time data itself
+ log.SetOutput(io.Discard)
+ if args[debugFlag] {
+ debugWriter, err := syslog.New(syslog.LOG_DEBUG, moduleName)
+ if err == nil {
+ log.SetOutput(debugWriter)
+ }
+ }
+
+ errorWriter, err := syslog.New(syslog.LOG_ERR, moduleName)
+ if err != nil {
+ return io.Discard
+ }
+ return errorWriter
+}
+
+// loginProtector returns the login protector corresponding to the PAM_USER if
+// one exists. This protector descriptor (if found) will be cached in the pam
+// data, under descriptorLabel.
+func loginProtector(handle *pam.Handle) (*actions.Protector, error) {
+ ctx, err := actions.NewContextFromMountpoint(actions.LoginProtectorMountpoint,
+ handle.PamUser)
+ if err != nil {
+ return nil, err
+ }
+ // Ensure that pam_fscrypt only processes metadata files owned by the
+ // user or root, even if the user is root themselves. (Normally, when
+ // fscrypt is run as root it is allowed to process all metadata files.
+ // This implements stricter behavior for pam_fscrypt.)
+ if !ctx.Config.GetAllowCrossUserMetadata() {
+ ctx.TrustedUser = handle.PamUser
+ }
+
+ // Find the user's PAM protector.
+ options, err := ctx.ProtectorOptions()
+ if err != nil {
+ return nil, err
+ }
+ uid := int64(util.AtoiOrPanic(handle.PamUser.Uid))
+ for _, option := range options {
+ if option.Source() == metadata.SourceType_pam_passphrase && option.UID() == uid {
+ return actions.GetProtectorFromOption(ctx, option)
+ }
+ }
+ return nil, errors.Errorf("no PAM protector for UID=%d on %q", uid, ctx.Mount.Path)
+}
+
+// policiesUsingProtector searches all the mountpoints for any policies
+// protected with the specified protector. If provisioned is true, then only
+// policies provisioned by the target user are returned; otherwise only policies
+// *not* provisioned by the target user are returned.
+func policiesUsingProtector(protector *actions.Protector, provisioned bool) []*actions.Policy {
+ mounts, err := filesystem.AllFilesystems()
+ if err != nil {
+ log.Print(err)
+ return nil
+ }
+
+ var policies []*actions.Policy
+ for _, mount := range mounts {
+ // Skip mountpoints that do not use the protector.
+ if _, _, err := mount.GetProtector(protector.Descriptor(),
+ protector.Context.TrustedUser); err != nil {
+ if _, ok := err.(*filesystem.ErrNotSetup); !ok {
+ log.Print(err)
+ }
+ continue
+ }
+ policyDescriptors, err := mount.ListPolicies(protector.Context.TrustedUser)
+ if err != nil {
+ log.Printf("listing policies: %s", err)
+ continue
+ }
+
+ // Clone context but modify the mountpoint
+ ctx := *protector.Context
+ ctx.Mount = mount
+ for _, policyDescriptor := range policyDescriptors {
+ policy, err := actions.GetPolicy(&ctx, policyDescriptor)
+ if err != nil {
+ log.Printf("reading policy: %s", err)
+ continue
+ }
+
+ if !policy.UsesProtector(protector) {
+ continue
+ }
+ if provisioned {
+ if !policy.IsProvisionedByTargetUser() {
+ log.Printf("policy %s not provisioned by %v",
+ policy.Descriptor(), ctx.TargetUser.Username)
+ continue
+ }
+ } else {
+ if policy.IsProvisionedByTargetUser() {
+ log.Printf("policy %s already provisioned by %v",
+ policy.Descriptor(), ctx.TargetUser.Username)
+ continue
+ }
+ }
+ policies = append(policies, policy)
+ }
+ }
+ return policies
+}
+
+// AdjustCount changes the session count for the pam user by the specified
+// amount. If the count file does not exist, create it as if it had a count of
+// zero. If the adjustment would bring the count below zero, the count is set to
+// zero. The value of the new count is returned. Requires root privileges.
+func AdjustCount(handle *pam.Handle, delta int) (int, error) {
+ // Make sure the directory exists
+ if err := os.MkdirAll(countDirectory, countDirectoryPermissions); err != nil {
+ return 0, err
+ }
+
+ path := filepath.Join(countDirectory, handle.PamUser.Uid+".count")
+ file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, countFilePermissions)
+ if err != nil {
+ return 0, err
+ }
+ if err = unix.Flock(int(file.Fd()), unix.LOCK_EX); err != nil {
+ return 0, err
+ }
+ defer file.Close()
+
+ newCount := util.MaxInt(getCount(file)+delta, 0)
+ if _, err = file.Seek(0, io.SeekStart); err != nil {
+ return 0, err
+ }
+ if _, err = fmt.Fprintf(file, countFileFormat, newCount); err != nil {
+ return 0, err
+ }
+
+ log.Printf("Session count for UID=%s updated to %d", handle.PamUser.Uid, newCount)
+ return newCount, nil
+}
+
+// Returns the count in the file (or zero if the count cannot be read).
+func getCount(file *os.File) int {
+ var count int
+ if _, err := fmt.Fscanf(file, countFileFormat, &count); err != nil {
+ return 0
+ }
+ return count
+}
--- /dev/null
+/*
+ * 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")
+ }
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * privileges.go - Functions for managing users and privileges.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+// Package security manages:
+// - Cache clearing (cache.go)
+// - Privilege manipulation (privileges.go)
+package security
+
+// Use the libc versions of setreuid, setregid, and setgroups instead of the
+// "sys/unix" versions. The "sys/unix" versions use the raw syscalls which
+// operate on the calling thread only, whereas the libc versions operate on the
+// whole process. And we need to operate on the whole process, firstly for
+// pam_fscrypt to prevent the privileges of Go worker threads from diverging
+// from the PAM stack's "main" thread, violating libc's assumption and causing
+// an abort() later in the PAM stack; and secondly because Go code may migrate
+// between OS-level threads while it's running.
+//
+// See also: https://github.com/golang/go/issues/1435
+
+/*
+#define _GNU_SOURCE // for getresuid and setresuid
+#include <sys/types.h>
+#include <unistd.h> // getting and setting uids and gids
+#include <grp.h> // setgroups
+*/
+import "C"
+
+import (
+ "log"
+ "os/user"
+ "syscall"
+
+ "github.com/pkg/errors"
+
+ "github.com/google/fscrypt/util"
+)
+
+// Privileges encapsulate the effective uid/gid and groups of a process.
+type Privileges struct {
+ euid C.uid_t
+ egid C.gid_t
+ groups []C.gid_t
+}
+
+// ProcessPrivileges returns the process's current effective privileges.
+func ProcessPrivileges() (*Privileges, error) {
+ ruid := C.getuid()
+ euid := C.geteuid()
+ rgid := C.getgid()
+ egid := C.getegid()
+
+ var groups []C.gid_t
+ n, err := C.getgroups(0, nil)
+ if n < 0 {
+ return nil, err
+ }
+ // If n == 0, the user isn't in any groups, so groups == nil is fine.
+ if n > 0 {
+ groups = make([]C.gid_t, n)
+ n, err = C.getgroups(n, &groups[0])
+ if n < 0 {
+ return nil, err
+ }
+ groups = groups[:n]
+ }
+ log.Printf("Current privs (real, effective): uid=(%d,%d) gid=(%d,%d) groups=%v",
+ ruid, euid, rgid, egid, groups)
+ return &Privileges{euid, egid, groups}, nil
+}
+
+// UserPrivileges returns the default privileges for the specified user.
+func UserPrivileges(user *user.User) (*Privileges, error) {
+ privs := &Privileges{
+ euid: C.uid_t(util.AtoiOrPanic(user.Uid)),
+ egid: C.gid_t(util.AtoiOrPanic(user.Gid)),
+ }
+ userGroups, err := user.GroupIds()
+ if err != nil {
+ return nil, util.SystemError(err.Error())
+ }
+ privs.groups = make([]C.gid_t, len(userGroups))
+ for i, group := range userGroups {
+ privs.groups[i] = C.gid_t(util.AtoiOrPanic(group))
+ }
+ return privs, nil
+}
+
+// SetProcessPrivileges sets the privileges of the current process to have those
+// specified by privs. The original privileges can be obtained by first saving
+// the output of ProcessPrivileges, calling SetProcessPrivileges with the
+// desired privs, then calling SetProcessPrivileges with the saved privs.
+func SetProcessPrivileges(privs *Privileges) error {
+ log.Printf("Setting euid=%d egid=%d groups=%v", privs.euid, privs.egid, privs.groups)
+
+ // If setting privs as root, we need to set the euid to 0 first, so that
+ // we will have the necessary permissions to make the other changes to
+ // the groups/egid/euid, regardless of our original euid.
+ C.seteuid(0)
+
+ // Separately handle the case where the user is in no groups.
+ numGroups := C.size_t(len(privs.groups))
+ groupsPtr := (*C.gid_t)(nil)
+ if numGroups > 0 {
+ groupsPtr = &privs.groups[0]
+ }
+
+ if res, err := C.setgroups(numGroups, groupsPtr); res < 0 {
+ return errors.Wrapf(err.(syscall.Errno), "setting groups")
+ }
+ if res, err := C.setegid(privs.egid); res < 0 {
+ return errors.Wrapf(err.(syscall.Errno), "setting egid")
+ }
+ if res, err := C.seteuid(privs.euid); res < 0 {
+ return errors.Wrapf(err.(syscall.Errno), "setting euid")
+ }
+ ProcessPrivileges()
+ return nil
+}
+
+// SetUids sets the process's real, effective, and saved UIDs.
+func SetUids(ruid, euid, suid int) error {
+ log.Printf("Setting ruid=%d euid=%d suid=%d", ruid, euid, suid)
+ // We elevate all the privs before setting them. This prevents issues
+ // with (ruid=1000,euid=1000,suid=0), where just a single call to
+ // setresuid might fail with permission denied.
+ if res, err := C.setresuid(0, 0, 0); res < 0 {
+ return errors.Wrapf(err.(syscall.Errno), "setting uids")
+ }
+ if res, err := C.setresuid(C.uid_t(ruid), C.uid_t(euid), C.uid_t(suid)); res < 0 {
+ return errors.Wrapf(err.(syscall.Errno), "setting uids")
+ }
+ return nil
+}
+
+// GetUids gets the process's real, effective, and saved UIDs.
+func GetUids() (int, int, int) {
+ var ruid, euid, suid C.uid_t
+ C.getresuid(&ruid, &euid, &suid)
+ return int(ruid), int(euid), int(suid)
+}
--- /dev/null
+/*
+ * 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) {}
--- /dev/null
+//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"
+)
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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)
+}
--- /dev/null
+/*
+ * 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")
+ }
+}