/* * Copyright (C) 2020 The Android Open Source Project * * 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. */ // Utility functions for VtsKernelEncryptionTest. #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Keymaster.h" #include "vts_kernel_encryption.h" using namespace android::dm; namespace android { namespace kernel { // Offset in bytes to the filesystem superblock, relative to the beginning of // the block device constexpr int kExt4SuperBlockOffset = 1024; constexpr int kF2fsSuperBlockOffset = 1024; // For F2FS: the offsets in bytes to the filesystem magic number and filesystem // UUID, relative to the beginning of the block device constexpr int kF2fsMagicOffset = kF2fsSuperBlockOffset; constexpr int kF2fsUuidOffset = kF2fsSuperBlockOffset + 108; // hw-wrapped key size in bytes constexpr int kHwWrappedKeySize = 32; std::string Errno() { return std::string(": ") + strerror(errno); } // Recursively deletes the file or directory at |path|, if it exists. void DeleteRecursively(const std::string &path) { if (unlink(path.c_str()) == 0 || errno == ENOENT) return; ASSERT_EQ(EISDIR, errno) << "Failed to unlink " << path << Errno(); std::unique_ptr dirp(opendir(path.c_str()), closedir); // If the directory was assigned an encryption policy that the kernel lacks // crypto API support for, then opening it will fail, and it will be empty. // So, we have to allow opening the directory to fail. if (dirp != nullptr) { struct dirent *entry; while ((entry = readdir(dirp.get())) != nullptr) { std::string filename(entry->d_name); if (filename != "." && filename != "..") DeleteRecursively(path + "/" + filename); } } ASSERT_EQ(0, rmdir(path.c_str())) << "Failed to remove directory " << path << Errno(); } // Generates some "random" bytes. Not secure; this is for testing only. void RandomBytesForTesting(std::vector &bytes) { for (size_t i = 0; i < bytes.size(); i++) { bytes[i] = rand(); } } // Generates a "random" key. Not secure; this is for testing only. std::vector GenerateTestKey(size_t size) { std::vector key(size); RandomBytesForTesting(key); return key; } std::string BytesToHex(const std::vector &bytes) { std::ostringstream o; for (uint8_t b : bytes) { o << std::hex << std::setw(2) << std::setfill('0') << (int)b; } return o.str(); } bool GetFirstApiLevel(int *first_api_level) { *first_api_level = android::base::GetIntProperty("ro.product.first_api_level", 0); if (*first_api_level == 0) { ADD_FAILURE() << "ro.product.first_api_level is unset"; return false; } GTEST_LOG_(INFO) << "ro.product.first_api_level = " << *first_api_level; return true; } // Gets the block device and type of the filesystem mounted on |mountpoint|. // This block device is the one on which the filesystem is directly located. In // the case of device-mapper that means something like /dev/mapper/dm-5, not the // underlying device like /dev/block/by-name/userdata. static bool GetFsBlockDeviceAndType(const std::string &mountpoint, std::string *fs_blk_device, std::string *fs_type) { std::unique_ptr mnts(setmntent("/proc/mounts", "re"), endmntent); if (!mnts) { ADD_FAILURE() << "Failed to open /proc/mounts" << Errno(); return false; } struct mntent *mnt; while ((mnt = getmntent(mnts.get())) != nullptr) { if (mnt->mnt_dir == mountpoint) { *fs_blk_device = mnt->mnt_fsname; *fs_type = mnt->mnt_type; return true; } } ADD_FAILURE() << "No /proc/mounts entry found for " << mountpoint; return false; } // Gets the UUID of the filesystem of type |fs_type| that's located on // |fs_blk_device|. // // Unfortunately there's no kernel API to get the UUID; instead we have to read // it from the filesystem superblock. static bool GetFilesystemUuid(const std::string &fs_blk_device, const std::string &fs_type, FilesystemUuid *fs_uuid) { android::base::unique_fd fd( open(fs_blk_device.c_str(), O_RDONLY | O_CLOEXEC)); if (fd < 0) { ADD_FAILURE() << "Failed to open fs block device " << fs_blk_device << Errno(); return false; } if (fs_type == "ext4") { struct ext4_super_block sb; if (pread(fd, &sb, sizeof(sb), kExt4SuperBlockOffset) != sizeof(sb)) { ADD_FAILURE() << "Error reading ext4 superblock from " << fs_blk_device << Errno(); return false; } if (sb.s_magic != cpu_to_le16(EXT4_SUPER_MAGIC)) { ADD_FAILURE() << "Failed to find ext4 superblock on " << fs_blk_device; return false; } static_assert(sizeof(sb.s_uuid) == kFilesystemUuidSize); memcpy(fs_uuid->bytes, sb.s_uuid, kFilesystemUuidSize); } else if (fs_type == "f2fs") { // Android doesn't have an f2fs equivalent of libext4_utils, so we have to // hard-code the offset to the magic number and UUID. __le32 magic; if (pread(fd, &magic, sizeof(magic), kF2fsMagicOffset) != sizeof(magic)) { ADD_FAILURE() << "Error reading f2fs superblock from " << fs_blk_device << Errno(); return false; } if (magic != cpu_to_le32(F2FS_SUPER_MAGIC)) { ADD_FAILURE() << "Failed to find f2fs superblock on " << fs_blk_device; return false; } if (pread(fd, fs_uuid->bytes, kFilesystemUuidSize, kF2fsUuidOffset) != kFilesystemUuidSize) { ADD_FAILURE() << "Failed to read f2fs filesystem UUID from " << fs_blk_device << Errno(); return false; } } else { ADD_FAILURE() << "Unknown filesystem type " << fs_type; return false; } return true; } // Gets the raw block device of the filesystem that is mounted from // |fs_blk_device|. By "raw block device" we mean a block device from which we // can read the encrypted file contents and filesystem metadata. When metadata // encryption is disabled, this is simply |fs_blk_device|. When metadata // encryption is enabled, then |fs_blk_device| is a dm-default-key device and // the "raw block device" is the parent of this dm-default-key device. // // We don't just use the block device listed in the fstab, because (a) it can be // a logical partition name which needs extra code to map to a block device, and // (b) due to block-level checkpointing, there can be a dm-bow device between // the fstab partition and dm-default-key. dm-bow can remap sectors, but for // encryption testing we don't want any sector remapping. So the correct block // device to read ciphertext from is the one directly underneath dm-default-key. static bool GetRawBlockDevice(const std::string &fs_blk_device, std::string *raw_blk_device) { DeviceMapper &dm = DeviceMapper::Instance(); if (!dm.IsDmBlockDevice(fs_blk_device)) { GTEST_LOG_(INFO) << fs_blk_device << " is not a device-mapper device; metadata encryption is disabled"; *raw_blk_device = fs_blk_device; return true; } const std::optional name = dm.GetDmDeviceNameByPath(fs_blk_device); if (!name) { ADD_FAILURE() << "Failed to get name of device-mapper device " << fs_blk_device; return false; } std::vector table; if (!dm.GetTableInfo(*name, &table)) { ADD_FAILURE() << "Failed to get table of device-mapper device " << *name; return false; } if (table.size() != 1) { GTEST_LOG_(INFO) << fs_blk_device << " has multiple device-mapper targets; assuming " "metadata encryption is disabled"; *raw_blk_device = fs_blk_device; return true; } const std::string target_type = dm.GetTargetType(table[0].spec); if (target_type != "default-key") { GTEST_LOG_(INFO) << fs_blk_device << " is a dm-" << target_type << " device, not dm-default-key; assuming metadata " "encryption is disabled"; *raw_blk_device = fs_blk_device; return true; } std::optional parent = dm.GetParentBlockDeviceByPath(fs_blk_device); if (!parent) { ADD_FAILURE() << "Failed to get parent of dm-default-key device " << *name; return false; } *raw_blk_device = *parent; return true; } // Gets information about the filesystem mounted on |mountpoint|. bool GetFilesystemInfo(const std::string &mountpoint, FilesystemInfo *info) { if (!GetFsBlockDeviceAndType(mountpoint, &info->fs_blk_device, &info->type)) return false; if (!GetFilesystemUuid(info->fs_blk_device, info->type, &info->uuid)) return false; if (!GetRawBlockDevice(info->fs_blk_device, &info->raw_blk_device)) return false; GTEST_LOG_(INFO) << info->fs_blk_device << " is mounted on " << mountpoint << " with type " << info->type << "; UUID is " << BytesToHex(info->uuid.bytes) << ", raw block device is " << info->raw_blk_device; return true; } // Returns true if the given data seems to be random. // // Check compressibility rather than byte frequencies. Compressibility is a // stronger test since it also detects repetitions. // // To check compressibility, use LZMA rather than DEFLATE/zlib/gzip because LZMA // compression is stronger and supports a much larger dictionary. DEFLATE is // limited to a 32 KiB dictionary. So, data repeating after 32 KiB (or more) // would not be detected with DEFLATE. But LZMA can detect it. bool VerifyDataRandomness(const std::vector &bytes) { // To avoid flakiness, allow the data to be compressed a tiny bit by chance. // There is at most a 2^-32 chance that random data can be compressed to be 4 // bytes shorter. In practice it's even lower due to compression overhead. size_t destLen = bytes.size() - std::min(4, bytes.size()); std::vector dest(destLen); uint8_t outProps[LZMA_PROPS_SIZE]; size_t outPropsSize = LZMA_PROPS_SIZE; int ret; ret = LzmaCompress(dest.data(), &destLen, bytes.data(), bytes.size(), outProps, &outPropsSize, 6, // compression level (0 <= level <= 9) bytes.size(), // dictionary size -1, -1, -1, -1, // lc, lp, bp, fb (-1 selects the default) 1); // number of threads if (ret == SZ_ERROR_OUTPUT_EOF) return true; // incompressible if (ret == SZ_OK) { ADD_FAILURE() << "Data is not random! Compressed " << bytes.size() << " to " << destLen << " bytes"; } else { ADD_FAILURE() << "LZMA compression error: ret=" << ret; } return false; } static bool TryPrepareHwWrappedKey(Keymaster &keymaster, const std::string &master_key_string, std::string *exported_key_string, bool rollback_resistance) { // This key is used to drive a CMAC-based KDF auto paramBuilder = km::AuthorizationSetBuilder().AesEncryptionKey(kHwWrappedKeySize * 8); if (rollback_resistance) { paramBuilder.Authorization(km::TAG_ROLLBACK_RESISTANCE); } paramBuilder.Authorization(km::TAG_STORAGE_KEY); std::string wrapped_key_blob; if (keymaster.importKey(paramBuilder, master_key_string, &wrapped_key_blob) && keymaster.exportKey(wrapped_key_blob, exported_key_string)) { return true; } // It's fine for Keymaster not to support hardware-wrapped keys, but // if generateKey works, importKey must too. if (keymaster.generateKey(paramBuilder, &wrapped_key_blob) && keymaster.exportKey(wrapped_key_blob, exported_key_string)) { ADD_FAILURE() << "generateKey succeeded but importKey failed"; } return false; } bool CreateHwWrappedKey(std::vector *master_key, std::vector *exported_key) { *master_key = GenerateTestKey(kHwWrappedKeySize); Keymaster keymaster; if (!keymaster) { ADD_FAILURE() << "Unable to find keymaster"; return false; } std::string master_key_string(master_key->begin(), master_key->end()); std::string exported_key_string; // Make two attempts to create a key, first with and then without // rollback resistance. if (TryPrepareHwWrappedKey(keymaster, master_key_string, &exported_key_string, true) || TryPrepareHwWrappedKey(keymaster, master_key_string, &exported_key_string, false)) { exported_key->assign(exported_key_string.begin(), exported_key_string.end()); return true; } GTEST_LOG_(INFO) << "Skipping test because device doesn't support " "hardware-wrapped keys"; return false; } static void PushBigEndian32(uint32_t val, std::vector *vec) { for (int i = 24; i >= 0; i -= 8) { vec->push_back((val >> i) & 0xFF); } } static void GetFixedInputString(uint32_t counter, const std::vector &label, const std::vector &context, uint32_t derived_key_len, std::vector *fixed_input_string) { PushBigEndian32(counter, fixed_input_string); fixed_input_string->insert(fixed_input_string->end(), label.begin(), label.end()); fixed_input_string->push_back(0); fixed_input_string->insert(fixed_input_string->end(), context.begin(), context.end()); PushBigEndian32(derived_key_len, fixed_input_string); } static bool AesCmacKdfHelper(const std::vector &key, const std::vector &label, const std::vector &context, uint32_t output_key_size, std::vector *output_data) { output_data->resize(output_key_size); for (size_t count = 0; count < (output_key_size / kAesBlockSize); count++) { std::vector fixed_input_string; GetFixedInputString(count + 1, label, context, (output_key_size * 8), &fixed_input_string); if (!AES_CMAC(output_data->data() + (kAesBlockSize * count), key.data(), key.size(), fixed_input_string.data(), fixed_input_string.size())) { ADD_FAILURE() << "AES_CMAC failed while deriving subkey from HW wrapped key"; return false; } } return true; } bool DeriveHwWrappedEncryptionKey(const std::vector &master_key, std::vector *enc_key) { std::vector label{0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20}; // Context in fixed input string comprises of software provided context, // padding to eight bytes (if required) and the key policy. std::vector context = { 'i', 'n', 'l', 'i', 'n', 'e', ' ', 'e', 'n', 'c', 'r', 'y', 'p', 't', 'i', 'o', 'n', ' ', 'k', 'e', 'y', 0x0, 0x0, 0x0, 0x00, 0x00, 0x00, 0x02, 0x43, 0x00, 0x82, 0x50, 0x0, 0x0, 0x0, 0x0}; return AesCmacKdfHelper(master_key, label, context, kAes256XtsKeySize, enc_key); } bool DeriveHwWrappedRawSecret(const std::vector &master_key, std::vector *secret) { std::vector label{0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20}; // Context in fixed input string comprises of software provided context, // padding to eight bytes (if required) and the key policy. std::vector context = {'r', 'a', 'w', ' ', 's', 'e', 'c', 'r', 'e', 't', 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00, 0x00, 0x00, 0x02, 0x17, 0x00, 0x80, 0x50, 0x0, 0x0, 0x0, 0x0}; return AesCmacKdfHelper(master_key, label, context, kAes256KeySize, secret); } } // namespace kernel } // namespace android