blob: 9264fb871ec27d3ece454a1791be60834b52ab7b [file] [log] [blame]
// Copyright 2017 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cryptohome/dircrypto_util.h"
#include <string>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
extern "C" {
#include <ext2fs/ext2_fs.h>
#include <keyutils.h>
}
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_file.h>
#include <base/posix/eintr_wrapper.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include <brillo/secure_blob.h>
// Add missing chromeos specific partition wide drop cache.
#define FS_IOC_DROP_CACHE _IO('f', 129)
namespace dircrypto {
namespace {
constexpr char kKeyType[] = "logon";
constexpr char kKeyNamePrefix[] = "ext4:";
constexpr char kKeyringName[] = "dircrypt";
constexpr char kStatefulPartitionPath[] = "/mnt/stateful_partition";
key_serial_t GetSessionKeyring() {
key_serial_t keyring =
keyctl_search(KEY_SPEC_SESSION_KEYRING, "keyring", kKeyringName, 0);
if (keyring == kInvalidKeySerial) {
PLOG(ERROR) << "keyctl_search failed";
return kInvalidKeySerial;
}
return keyring;
}
key_serial_t KeyReferenceToKeySerial(const brillo::SecureBlob& key_reference) {
std::string key_name =
kKeyNamePrefix + base::ToLowerASCII(base::HexEncode(
key_reference.data(), key_reference.size()));
key_serial_t key =
keyctl_search(GetSessionKeyring(), "logon", key_name.c_str(), 0);
return key;
}
base::ScopedFD GetStatefulPartitionScopedFd() {
base::ScopedFD fd = base::ScopedFD(
HANDLE_EINTR(open(kStatefulPartitionPath, O_RDONLY | O_DIRECTORY)));
if (!fd.is_valid())
PLOG(ERROR) << "Failed to open file descriptor " << kStatefulPartitionPath;
return fd;
}
bool DropMountCaches(const base::FilePath& dir) {
base::ScopedFD fd(
HANDLE_EINTR(open(dir.value().c_str(), O_RDONLY | O_DIRECTORY)));
if (!fd.is_valid()) {
PLOG(ERROR) << "Invalid directory: " << dir.value();
return false;
}
if (ioctl(fd.get(), FS_IOC_DROP_CACHE, nullptr) < 0) {
PLOG(ERROR) << "Failed: drop cache for mount point. Dir:" << dir.value();
return false;
}
return true;
}
void BuildFscryptKeySpec(const KeyReference& key_reference,
struct fscrypt_key_specifier* key_spec) {
switch (key_reference.policy_version) {
case FSCRYPT_POLICY_V1:
key_spec->type = FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR;
DCHECK_EQ(FSCRYPT_KEY_DESCRIPTOR_SIZE, key_reference.reference.size());
memcpy(key_spec->u.descriptor, key_reference.reference.char_data(),
key_reference.reference.size());
break;
case FSCRYPT_POLICY_V2:
key_spec->type = FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER;
break;
default:
NOTREACHED() << "Invalid policy version";
}
}
} // namespace
// Kernel versions before 5.4 do not support the fscrypt key management ioctls.
// In absence of these ioctls, we fall back to the legacy interface of adding
// removing keys.
namespace legacy {
static bool AddKeyToSessionKeyring(const brillo::SecureBlob& key,
KeyReference* key_reference) {
if (key.size() > FS_MAX_KEY_SIZE ||
key_reference->reference.size() != FS_KEY_DESCRIPTOR_SIZE) {
LOG(ERROR) << "Invalid arguments: key.size() = " << key.size()
<< "key_descriptor.size() = " << key_reference->reference.size();
return false;
}
key_serial_t keyring = GetSessionKeyring();
if (keyring == kInvalidKeySerial) {
PLOG(ERROR) << "keyctl_search failed";
return false;
}
struct fscrypt_key fs_key = {};
fs_key.mode = FS_ENCRYPTION_MODE_AES_256_XTS;
memcpy(fs_key.raw, key.char_data(), key.size());
fs_key.size = key.size();
std::string key_name = kKeyNamePrefix + base::ToLowerASCII(base::HexEncode(
key_reference->reference.data(),
key_reference->reference.size()));
key_serial_t key_serial = add_key(kKeyType, key_name.c_str(), &fs_key,
sizeof(fscrypt_key), keyring);
if (key_serial == kInvalidKeySerial) {
PLOG(ERROR) << "Failed to insert key into keyring";
return false;
}
/* Set the permission on the key.
* Possessor: (everyone given the key is in a session keyring belonging to
* init):
* -- View, Search
* User: (root)
* -- View, Search, Write, Setattr
* Group, Other:
* -- None
*/
const key_perm_t kPermissions = KEY_POS_VIEW | KEY_POS_SEARCH | KEY_USR_VIEW |
KEY_USR_WRITE | KEY_USR_SEARCH |
KEY_USR_SETATTR;
if (keyctl_setperm(key_serial, kPermissions) != 0) {
PLOG(ERROR) << "Could not change permission on key " << key_serial;
return false;
}
return true;
}
static bool UnlinkSessionKey(const KeyReference& key_reference) {
key_serial_t keyring = GetSessionKeyring();
key_serial_t key = KeyReferenceToKeySerial(key_reference.reference);
if (key == kInvalidKeySerial) {
PLOG(ERROR) << "keyctl_search failed";
return false;
}
if (keyring == kInvalidKeySerial)
return false;
if (keyctl_unlink(key, keyring) == -1) {
PLOG(ERROR) << "Failed to unlink the key";
return false;
}
return true;
}
static bool InvalidateSessionKey(const KeyReference& key_reference,
const base::FilePath& mount_path) {
// First, attempt to selectively drop caches for mount point.
// This can fail if the directory does not support the operation or if
// the process does not have the correct capabilities (CAP_SYS_ADMIN).
if (!DropMountCaches(mount_path)) {
LOG(ERROR) << "Failed to drop cache for user mount.";
// Use drop_caches to drop all clear cache. Otherwise, cached decrypted data
// will stay visible. This should invalidate the key provided no one touches
// the encrypted directories while this function is running.
constexpr char kData = '3';
if (base::WriteFile(base::FilePath("/proc/sys/vm/drop_caches"), &kData,
sizeof(kData)) != sizeof(kData)) {
LOG(ERROR) << "Failed to drop all caches.";
return false;
}
}
// At this point, the key should be invalidated, but try to invalidate it just
// in case.
// If the key was already invalidated, this should fail with ENOKEY.
key_serial_t key = KeyReferenceToKeySerial(key_reference.reference);
if (key == kInvalidKeySerial) {
if (errno != ENOKEY) {
PLOG(ERROR) << "Failed to find key to invalidate";
}
return true;
}
if (keyctl_invalidate(key) == 0) {
LOG(ERROR) << "We ended up invalidating key " << key;
} else if (errno != ENOKEY) {
PLOG(ERROR) << "Failed to invalidate key" << key;
}
return true;
}
static bool RemoveSessionKey(const KeyReference& key_reference,
const base::FilePath& dir) {
// Unlink the key.
// NOTE: Even after this, the key will still stay valid as long as the
// encrypted contents are on the page cache.
if (!UnlinkSessionKey(key_reference)) {
LOG(ERROR) << "Failed to unlink the key.";
}
// Run Sync() to make all dirty cache clear.
sync();
return InvalidateSessionKey(key_reference, dir);
}
} // namespace legacy
// CheckFscryptKeyIoctlSupport is used to decide whether:
// (1) The filesystem-level keyring is supported.
// (2) Whether fscrypt v2 encryption policies can be used.
bool CheckFscryptKeyIoctlSupport() {
base::ScopedFD fd = GetStatefulPartitionScopedFd();
if (!fd.is_valid()) {
PLOG(ERROR) << "Failed to open stateful partition";
return false;
}
errno = 0;
bool ret = false;
ioctl(fd.get(), FS_IOC_ADD_ENCRYPTION_KEY, nullptr);
if (errno != EOPNOTSUPP && errno != ENOTTY)
ret = true;
if (!ret)
VLOG(3) << "fscrypt v2 encryption policies not supported; "
<< "falling back to v1 encryption policies.";
return ret;
}
bool SetDirectoryKey(const base::FilePath& dir,
const KeyReference& key_reference) {
base::ScopedFD fd(
HANDLE_EINTR(open(dir.value().c_str(), O_RDONLY | O_DIRECTORY)));
if (!fd.is_valid()) {
PLOG(ERROR) << "Fscrypt: Invalid directory " << dir.value();
return false;
}
union {
struct fscrypt_policy_v1 v1;
struct fscrypt_policy_v2 v2;
} policy;
memset(&policy, 0, sizeof(policy));
switch (key_reference.policy_version) {
case FSCRYPT_POLICY_V1:
DCHECK_EQ(static_cast<size_t>(FSCRYPT_KEY_DESCRIPTOR_SIZE),
key_reference.reference.size());
policy.v1.version = FSCRYPT_POLICY_V1;
policy.v1.contents_encryption_mode = FS_ENCRYPTION_MODE_AES_256_XTS;
policy.v1.filenames_encryption_mode = FS_ENCRYPTION_MODE_AES_256_CTS;
policy.v1.flags = 0;
memcpy(policy.v1.master_key_descriptor, key_reference.reference.data(),
key_reference.reference.size());
break;
case FSCRYPT_POLICY_V2:
DCHECK_EQ(static_cast<size_t>(FSCRYPT_KEY_IDENTIFIER_SIZE),
key_reference.reference.size());
policy.v2.version = FSCRYPT_POLICY_V2;
policy.v2.contents_encryption_mode = FS_ENCRYPTION_MODE_AES_256_XTS;
policy.v2.filenames_encryption_mode = FS_ENCRYPTION_MODE_AES_256_CTS;
policy.v2.flags = FSCRYPT_POLICY_FLAGS_PAD_16;
memcpy(policy.v2.master_key_identifier, key_reference.reference.data(),
key_reference.reference.size());
break;
default:
NOTREACHED() << "Invalid encryption policy version";
}
if (ioctl(fd.get(), FS_IOC_SET_ENCRYPTION_POLICY, &policy) < 0) {
PLOG(ERROR) << "Failed to set the encryption policy of " << dir.value();
return false;
}
return true;
}
static int GetDirectoryPolicy(const base::FilePath& dir,
struct fscrypt_get_policy_ex_arg* arg) {
base::ScopedFD fd(
HANDLE_EINTR(open(dir.value().c_str(), O_RDONLY | O_DIRECTORY)));
if (!fd.is_valid()) {
PLOG(ERROR) << "Fscrypt: Invalid directory " << dir.value();
errno = EINVAL;
return -1;
}
int err = 0;
// FS_IOC_GET_ENCRYPTION_POLICY only supports v1 policies.
if (CheckFscryptKeyIoctlSupport())
err = ioctl(fd.get(), FS_IOC_GET_ENCRYPTION_POLICY_EX, arg);
else
err = ioctl(fd.get(), FS_IOC_GET_ENCRYPTION_POLICY, &(arg->policy.v1));
return err;
}
int GetDirectoryPolicyVersion(const base::FilePath& dir) {
struct fscrypt_get_policy_ex_arg arg = {};
memset(&arg, 0, sizeof(arg));
arg.policy_size = sizeof(arg.policy);
if (GetDirectoryPolicy(dir, &arg) < 0)
return -1;
return arg.policy.version;
}
KeyState GetDirectoryKeyState(const base::FilePath& dir) {
struct fscrypt_get_policy_ex_arg arg = {};
memset(&arg, 0, sizeof(arg));
arg.policy_size = sizeof(arg.policy);
if (GetDirectoryPolicy(dir, &arg) < 0) {
switch (errno) {
case ENODATA:
case ENOENT:
return KeyState::NO_KEY;
case ENOTTY:
case EOPNOTSUPP:
return KeyState::NOT_SUPPORTED;
default:
PLOG(ERROR) << "Failed to get the encryption policy of " << dir.value();
return KeyState::UNKNOWN;
}
}
return KeyState::ENCRYPTED;
}
static bool AddFscryptKey(const brillo::SecureBlob& key,
KeyReference* key_reference) {
brillo::SecureBlob add_key_arg(sizeof(struct fscrypt_add_key_arg) +
key.size());
struct fscrypt_add_key_arg* arg =
(struct fscrypt_add_key_arg*)add_key_arg.data();
BuildFscryptKeySpec(*key_reference, &(arg->key_spec));
arg->raw_size = key.size();
memcpy(arg->raw, key.char_data(), key.size());
base::ScopedFD fd = GetStatefulPartitionScopedFd();
if (!fd.is_valid())
return false;
if (ioctl(fd.get(), FS_IOC_ADD_ENCRYPTION_KEY, arg) < 0) {
PLOG(ERROR) << "Failed to add encryption key";
return false;
}
// For v2 policies, store the returned key identifier in the key reference.
if (key_reference->policy_version == FSCRYPT_POLICY_V2) {
key_reference->reference.resize(FSCRYPT_KEY_IDENTIFIER_SIZE);
memcpy(key_reference->reference.char_data(), arg->key_spec.u.identifier,
FSCRYPT_KEY_IDENTIFIER_SIZE);
}
return true;
}
static bool RemoveFscryptKey(const KeyReference& key_reference) {
struct fscrypt_remove_key_arg arg;
memset(&arg, 0, sizeof(fscrypt_remove_key_arg));
BuildFscryptKeySpec(key_reference, &arg.key_spec);
// Set the identifier for v2 policies.
if (key_reference.policy_version == FSCRYPT_POLICY_V2) {
memcpy(arg.key_spec.u.identifier, key_reference.reference.char_data(),
FSCRYPT_KEY_IDENTIFIER_SIZE);
}
auto fd = GetStatefulPartitionScopedFd();
if (!fd.is_valid())
return false;
if (ioctl(fd.get(), FS_IOC_REMOVE_ENCRYPTION_KEY, &arg) < 0) {
PLOG(ERROR) << "Failed to remove encryption key";
return false;
}
// Check removal status flags if there are files still open after removing the
// encryption key.
if (arg.removal_status_flags & FSCRYPT_KEY_REMOVAL_STATUS_FLAG_OTHER_USERS) {
LOG(ERROR) << "Failed to remove fscrypt key: still used by other users.";
} else if (arg.removal_status_flags &
FSCRYPT_KEY_REMOVAL_STATUS_FLAG_FILES_BUSY) {
LOG(ERROR)
<< "Some files are still in use after removing encryption key; these "
"files were not locked.";
}
return true;
}
bool AddDirectoryKey(const brillo::SecureBlob& key,
KeyReference* key_reference) {
return CheckFscryptKeyIoctlSupport()
? AddFscryptKey(key, key_reference)
: legacy::AddKeyToSessionKeyring(key, key_reference);
}
bool RemoveDirectoryKey(const KeyReference& key_reference,
const base::FilePath& dir) {
// If the key reference is empty (eg. after a crash), create
// the key reference from the policy set on the directory.
KeyReference ref;
if (key_reference.reference.size() == 0) {
struct fscrypt_get_policy_ex_arg arg;
memset(&arg, 0, sizeof(fscrypt_get_policy_ex_arg));
arg.policy_size = sizeof(arg.policy);
LOG(INFO)
<< "Empty key reference; attempting to get policy from directory.";
if (GetDirectoryPolicy(dir, &arg) < 0) {
LOG(ERROR) << "Failed to get fscrypt policy from directory " << dir;
return false;
}
switch (arg.policy.version) {
case FSCRYPT_POLICY_V1:
ref.reference.resize(FSCRYPT_KEY_DESCRIPTOR_SIZE);
memcpy(ref.reference.char_data(), arg.policy.v1.master_key_descriptor,
FSCRYPT_KEY_DESCRIPTOR_SIZE);
ref.policy_version = FSCRYPT_POLICY_V1;
break;
case FSCRYPT_POLICY_V2:
ref.reference.resize(FSCRYPT_KEY_IDENTIFIER_SIZE);
memcpy(ref.reference.char_data(), arg.policy.v2.master_key_identifier,
FSCRYPT_KEY_IDENTIFIER_SIZE);
ref.policy_version = FSCRYPT_POLICY_V2;
break;
default:
NOTREACHED() << "Invalid encryption policy version: "
<< arg.policy.version;
return false;
}
} else {
ref = key_reference;
}
return CheckFscryptKeyIoctlSupport() ? RemoveFscryptKey(ref)
: legacy::RemoveSessionKey(ref, dir);
}
} // namespace dircrypto