From 8cd1b3ba2e7a12cd68e2dfd0cbb5ec09ff92783b Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 27 Nov 2019 12:04:13 -0800 Subject: Automatically generate recovery passphrase when useful If a user re-installs their system (or otherwise loses the /.fscrypt directory on the root filesystem) they also lose access to any login passphrase-protected directories on other filesystems, unless additional protectors were manually added. This can be unexpected, as it may be expected that the old login passphrase would still work. We can't really fix this by storing a login protector on every filesystem because: - If a user were to have N login protectors, it would take them N times longer to log in, as every login protector would need to be unlocked. - If a user were to change their login passphrase while any external volumes were unmounted, login protectors would get out of sync. - It's preferable that an external volume isn't unlockable by itself using only a login passphrase, as login passphrases are often weak. Instead, generate a recovery passphrase when creating a login passphrase-protected directory on a non-root filesystem. The recovery passphrase is added as a custom_passphrase protector, thus giving the policy two protectors: one pam_passphrase and one custom_passphrase. Then this passphrase is written to a file in the new encrypted directory. Writing the passphrase to a file here is okay since it's encrypted, but it's obviously useless by itself; it's up to the user to store this passphrase somewhere else if they need it. Use a recovery passphrase instead of a "recovery code" that encodes the policy key directly because a passphrase is more user-friendly: it can safely be made much shorter than a key, and it works just like any other fscrypt protector. Also, it's not as critical to allow recovery when the .fscrypt directory on the *same* filesystem is deleted. Resolves https://github.com/google/fscrypt/issues/164 --- actions/recovery_test.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 actions/recovery_test.go (limited to 'actions/recovery_test.go') diff --git a/actions/recovery_test.go b/actions/recovery_test.go new file mode 100644 index 0000000..4332972 --- /dev/null +++ b/actions/recovery_test.go @@ -0,0 +1,90 @@ +/* + * recovery_test.go - tests for recovery passphrases + * + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package actions + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/fscrypt/crypto" +) + +func TestRecoveryPassphrase(t *testing.T) { + tempDir, err := ioutil.TempDir("", "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, recoveryFile); err != nil { + t.Fatal(err) + } + contentsBytes, err := ioutil.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)") + } +} -- cgit v1.2.3