| // Copyright 2017 The Fuchsia 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 "src/security/zxcrypt/volume.h" |
| |
| #include <arpa/inet.h> |
| #include <errno.h> |
| #include <inttypes.h> |
| #include <lib/zircon-internal/debug.h> |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <zircon/compiler.h> |
| #include <zircon/device/block.h> |
| #include <zircon/errors.h> |
| #include <zircon/status.h> |
| #include <zircon/types.h> |
| |
| #include <algorithm> |
| |
| #include <fbl/algorithm.h> |
| |
| #include "src/security/fcrypto/bytes.h" |
| #include "src/security/fcrypto/cipher.h" |
| #include "src/security/fcrypto/hkdf.h" |
| #include "src/security/fcrypto/secret.h" |
| |
| #define ZXDEBUG 0 |
| |
| namespace zxcrypt { |
| |
| // Several copies of the metadata for a zxcrypt volume is saved at the beginning and end of the |
| // devices. The number of copies is given by |kMetadataBlocks * kReservedSlices|, and the locations |
| // of each block can be iterated through using |Begin| and |Next|. The metadata block, or |
| // superblock, consists of a fixed type GUID, an instance GUID, a 32-bit version, a set of "key |
| // slots" The key slots are data cipher key material encrypted with a wrapping crypto::AEAD key |
| // derived from the caller-provided root key and specific slot. |
| |
| // Determines what algorithms are in use when creating new zxcrypt devices. |
| const Volume::Version Volume::kDefaultVersion = Volume::kAES256_XTS_SHA256; |
| |
| // The amount of data that can "in-flight" to the underlying block device before the zxcrypt |
| // driver begins queuing transactions |
| // |
| // TODO(aarongreen): See fxbug.dev/31498. Tune this value. Possibly break into several smaller |
| // VMOs if we want to allow some to be recycled; support for this doesn't currently exist. Up to 64 |
| // MB may be in flight at once. The device's max_transfer_size will be capped at 1/4 of this value. |
| __EXPORT |
| const uint32_t Volume::kBufferSize = 1U << 24; |
| static_assert(Volume::kBufferSize % ZX_MAX_PAGE_SIZE == 0, "kBufferSize must be page aligned"); |
| |
| namespace { |
| |
| // The number of metadata blocks in a reserved metadata slice, each holding a copy of the |
| // superblock. |
| const size_t kMetadataBlocks = 2; |
| |
| // HKDF labels |
| const size_t kMaxLabelLen = 16; |
| const char* kWrapKeyLabel = "wrap key %" PRIu64; |
| const char* kWrapIvLabel = "wrap iv %" PRIu64; |
| |
| // Header is type GUID | instance GUID | version. |
| const size_t kHeaderLen = sizeof(zxcrypt_magic) + BLOCK_GUID_LEN + sizeof(uint32_t); |
| |
| } // namespace |
| |
| Volume::Volume() { Reset(); } |
| |
| Volume::~Volume() {} |
| |
| void Volume::Reset() { |
| reserved_blocks_ = 0; |
| reserved_slices_ = 0; |
| block_.Resize(0); |
| offset_ = UINT64_MAX; |
| guid_.Resize(0); |
| header_.Resize(0); |
| aead_ = crypto::AEAD::kUninitialized; |
| wrap_key_.Clear(); |
| wrap_iv_.Resize(0); |
| cipher_ = crypto::Cipher::kUninitialized; |
| data_key_.Clear(); |
| data_iv_.Resize(0); |
| slot_len_ = 0; |
| num_key_slots_ = 0; |
| digest_ = crypto::digest::kUninitialized; |
| } |
| |
| zx_status_t Volume::Format(const crypto::Secret& key, key_slot_t slot) { |
| zx_status_t rc; |
| |
| if ((rc = CreateBlock()) != ZX_OK) { |
| xprintf("CreateBlock failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = SealBlock(key, slot)) != ZX_OK) { |
| xprintf("SealBlock failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = CommitBlock()) != ZX_OK) { |
| xprintf("CommitBlock failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| |
| if ((rc = Flush()) != ZX_OK) { |
| return rc; |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t Volume::Unlock(const crypto::Secret& key, key_slot_t slot) { |
| zx_status_t rc; |
| |
| for (rc = Begin(); rc == ZX_ERR_NEXT; rc = Next()) { |
| if ((rc = Read()) != ZX_OK) { |
| xprintf("failed to read block at %" PRIu64 ": %d\n", offset_, rc); |
| } else if ((rc = UnsealBlock(key, slot)) != ZX_OK) { |
| xprintf("failed to open block at %" PRIu64 ": %d\n", offset_, rc); |
| } else { |
| return ZX_OK; |
| } |
| } |
| |
| return ZX_ERR_ACCESS_DENIED; |
| } |
| |
| zx_status_t Volume::Shred() { |
| zx_status_t rc; |
| |
| if (!block_.get()) { |
| xprintf("not initialized\n"); |
| return ZX_ERR_BAD_STATE; |
| } |
| if ((rc = block_.Randomize()) != ZX_OK) { |
| return rc; |
| } |
| for (rc = Begin(); rc == ZX_ERR_NEXT; rc = Next()) { |
| if ((rc = Write()) != ZX_OK) { |
| return rc; |
| } |
| } |
| |
| // We must ensure that writes intending to destroy data actually make it out |
| // to the underlying storage before we return, or write deferral elsewhere in |
| // the storage stack could mean that this data is still retrievable. |
| if ((rc = Flush()) != ZX_OK) { |
| return rc; |
| } |
| |
| Reset(); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t Volume::GetSlotOffset(key_slot_t slot, zx_off_t* out) const { |
| if (!block_.get()) { |
| xprintf("not initialized\n"); |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| zx_off_t off; |
| if (mul_overflow(slot, slot_len_, &off) || add_overflow(kHeaderLen, off, &off) || |
| off > block_.len() - slot_len_) { |
| xprintf("bad key slot: %" PRIu64 "\n", slot); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| if (out) { |
| *out = off; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t Volume::Init() { |
| zx_status_t rc; |
| |
| // Get block info; align our blocks to pages |
| BlockInfo blk; |
| if ((rc = GetBlockInfo(&blk)) != ZX_OK) { |
| xprintf("failed to get block info: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| // Check that we meet the minimum size. |
| if (blk.block_count < kMetadataBlocks) { |
| xprintf("device is too small; have %" PRIu64 " blocks, need %" PRIu64 "\n", blk.block_count, |
| kMetadataBlocks); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| reserved_blocks_ = kMetadataBlocks; |
| // Allocate block buffer |
| if ((rc = block_.Resize(blk.block_size)) != ZX_OK) { |
| return rc; |
| } |
| // Get FVM info |
| uint64_t fvm_slice_size; |
| switch ((rc = GetFvmSliceSize(&fvm_slice_size))) { |
| case ZX_OK: { |
| // This *IS* an FVM partition. |
| // Ensure first kReservedSlices + 1 slices are allocated |
| size_t blocks_per_slice = fvm_slice_size / blk.block_size; |
| reserved_blocks_ = fbl::round_up(reserved_blocks_, blocks_per_slice); |
| reserved_slices_ = reserved_blocks_ / blocks_per_slice; |
| size_t required_slices = reserved_slices_ + 1; |
| size_t contiguous_chunk_size = 0; |
| |
| // We're going to go through the vslice address space, ensuring that all |
| // of the first `required_slices` slices are allocated. |
| for (size_t slice_off = 0; slice_off < required_slices; slice_off += contiguous_chunk_size) { |
| // Ask about the next contiguous range starting at `slice_off`. |
| SliceRegion ranges[MAX_SLICE_REGIONS]; |
| uint64_t range_count; |
| if ((rc = DoBlockFvmVsliceQuery(slice_off, ranges, &range_count)) != ZX_OK || |
| range_count == 0 || ((contiguous_chunk_size = ranges[0].count) == 0)) { |
| xprintf("FVM Vslice Query failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| |
| // If it's already allocated, continue to the next range. The for loop |
| // advances by `contiguous_chunk_size` |
| if (ranges[0].allocated) { |
| continue; |
| }; |
| |
| // Otherwise, allocate it -- either up to the end of the contiguous |
| // unallocated chunk (if that still doesn't cover the number of slices |
| // we need, in which case we'll keep looping) or just as many slices as |
| // we require. |
| uint64_t extend_start_slice = slice_off; |
| uint64_t extend_length = std::min(required_slices - slice_off, contiguous_chunk_size); |
| |
| if ((rc = DoBlockFvmExtend(extend_start_slice, extend_length)) != ZX_OK) { |
| xprintf("failed to extend FVM partition: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| } |
| break; |
| } |
| case ZX_ERR_NOT_SUPPORTED: |
| // This is *NOT* an FVM partition. |
| xprintf("Not an FVM partition\n"); |
| break; |
| default: |
| xprintf("init failed: %s\n", zx_status_get_string(rc)); |
| // An error occurred |
| return rc; |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t Volume::Configure(Volume::Version version) { |
| zx_status_t rc; |
| |
| switch (version) { |
| case Volume::kAES256_XTS_SHA256: |
| aead_ = crypto::AEAD::kAES128_GCM_SIV; |
| cipher_ = crypto::Cipher::kAES256_XTS; |
| digest_ = crypto::digest::kSHA256; |
| break; |
| |
| default: |
| xprintf("unknown version: %u\n", version); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| size_t key_len, iv_len, tag_len; |
| if ((rc = crypto::Cipher::GetKeyLen(cipher_, &key_len)) != ZX_OK || |
| (rc = crypto::Cipher::GetIVLen(cipher_, &iv_len)) != ZX_OK || |
| (rc = crypto::AEAD::GetTagLen(aead_, &tag_len)) != ZX_OK) { |
| return rc; |
| } |
| |
| slot_len_ = key_len + iv_len + tag_len; |
| num_key_slots_ = (block_.len() - kHeaderLen) / slot_len_; |
| if (num_key_slots_ == 0) { |
| xprintf("block size is too small; have %zu, need %zu\n", block_.len(), kHeaderLen + slot_len_); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t Volume::DeriveSlotKeys(const crypto::Secret& key, key_slot_t slot) { |
| zx_status_t rc; |
| |
| crypto::HKDF hkdf; |
| char label[kMaxLabelLen]; |
| |
| // We tolerate 128-bit keys here because some hardware we wish to operate on |
| // only has 128-bits of random keys in hardware. We believe that this |
| // entropy is sufficient for our purposes. |
| size_t key_len = key.len(); |
| if (key_len == 16) { |
| rc = hkdf.Init(digest_, key, guid_, crypto::HKDF::ALLOW_WEAK_KEY); |
| } else if (key_len == 32) { |
| rc = hkdf.Init(digest_, key, guid_); |
| } else { |
| xprintf("invalid key length %lu (acceptable values are 16, 32)\n", key_len); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (rc != ZX_OK) { |
| xprintf("hkdf.Init failed %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| snprintf(label, kMaxLabelLen, kWrapKeyLabel, slot); |
| size_t len; |
| if ((rc = crypto::AEAD::GetKeyLen(aead_, &len)) != ZX_OK || |
| (rc = hkdf.Derive(label, len, &wrap_key_)) != ZX_OK) { |
| xprintf("failed to derive wrap key: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| snprintf(label, kMaxLabelLen, kWrapIvLabel, slot); |
| crypto::Secret wrap_iv; |
| if ((rc = crypto::AEAD::GetIVLen(aead_, &len)) != ZX_OK || |
| (rc = hkdf.Derive(label, len, &wrap_iv_)) != ZX_OK) { |
| xprintf("failed to derive wrap IV: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| |
| return ZX_OK; |
| } |
| |
| // Block methods |
| |
| zx_status_t Volume::Begin() { |
| offset_ = 0; |
| return ZX_ERR_NEXT; |
| } |
| |
| zx_status_t Volume::Next() { |
| offset_ += block_.len(); |
| return (offset_ / block_.len()) < kMetadataBlocks ? ZX_ERR_NEXT : ZX_ERR_STOP; |
| } |
| |
| zx_status_t Volume::CreateBlock() { |
| zx_status_t rc; |
| |
| // Create a "backdrop" of random data |
| if ((rc = block_.Randomize()) != ZX_OK) { |
| return rc; |
| } |
| |
| // Write the variant 1/version 1 type GUID according to RFC 4122. |
| uint8_t* out = block_.get(); |
| memcpy(out, zxcrypt_magic, sizeof(zxcrypt_magic)); |
| out += sizeof(zxcrypt_magic); |
| |
| // Create a variant 1/version 4 instance GUID according to RFC 4122. |
| if ((rc = guid_.Randomize(BLOCK_GUID_LEN)) != ZX_OK) { |
| return rc; |
| } |
| guid_[6] = (guid_[6] & 0x0F) | 0x40; |
| guid_[8] = (guid_[8] & 0x3F) | 0x80; |
| memcpy(out, guid_.get(), BLOCK_GUID_LEN); |
| out += BLOCK_GUID_LEN; |
| |
| // Write the 32-bit version. |
| if ((rc = Configure(kDefaultVersion)) != ZX_OK) { |
| return rc; |
| } |
| uint32_t version = htonl(kDefaultVersion); |
| memcpy(out, &version, sizeof(version)); |
| |
| // Generate the data key and IV, and save the AAD. |
| size_t key_len, iv_len; |
| if ((rc = crypto::Cipher::GetKeyLen(cipher_, &key_len)) != ZX_OK || |
| (rc = crypto::Cipher::GetIVLen(cipher_, &iv_len)) != ZX_OK || |
| (rc = data_key_.Generate(key_len)) != ZX_OK || (rc = data_iv_.Resize(iv_len)) != ZX_OK || |
| (rc = data_iv_.Randomize()) != ZX_OK || |
| (rc = header_.Copy(block_.get(), kHeaderLen)) != ZX_OK) { |
| return rc; |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t Volume::CommitBlock() { |
| zx_status_t rc; |
| |
| // Make a copy to compare the read result to; this reduces the number of |
| // writes we must do. |
| crypto::Bytes block; |
| if ((rc = block.Copy(block_)) != ZX_OK) { |
| xprintf("zxcrypt: Cannot copy block: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| for (rc = Begin(); rc == ZX_ERR_NEXT; rc = Next()) { |
| if ((rc = Read()) != ZX_OK) { |
| xprintf("zxcrypt: CommitBlock Read failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| // Only write back blocks that don't match. |
| if (block_ == block) { |
| continue; |
| } |
| if ((rc = block_.Copy(block)) != ZX_OK || (rc = Write()) != ZX_OK) { |
| xprintf("zxcrypt: CommitBlock Write failed for offset %" PRIu64 ": %s\n", offset_, |
| zx_status_get_string(rc)); |
| } |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t Volume::SealBlock(const crypto::Secret& key, key_slot_t slot) { |
| zx_status_t rc; |
| |
| // Encrypt the data key |
| zx_off_t nonce; |
| crypto::AEAD aead; |
| crypto::Bytes ptext, ctext; |
| zx_off_t off; |
| zx_off_t data_key_off = 0; |
| zx_off_t data_iv_off = data_key_.len(); |
| if ((rc = GetSlotOffset(slot, &off)) != ZX_OK) { |
| xprintf("GetSlotOffset for slot %" PRIu64 " failed: %s\n", slot, zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = ptext.Copy(data_key_.get(), data_key_.len(), data_key_off)) != ZX_OK) { |
| xprintf("ptext.Copy (key) failed: %s", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = ptext.Copy(data_iv_.get(), data_iv_.len(), data_iv_off)) != ZX_OK) { |
| xprintf("ptext.Copy (iv) failed: %s", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = DeriveSlotKeys(key, slot)) != ZX_OK) { |
| xprintf("DeriveSlotKeys failed: %s", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = aead.InitSeal(aead_, wrap_key_, wrap_iv_)) != ZX_OK) { |
| xprintf("aead.InitSeal failed: %s", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = aead.Seal(ptext, header_, &nonce, &ctext)) != ZX_OK) { |
| xprintf("aead.Seal failed: %s", zx_status_get_string(rc)); |
| return rc; |
| } |
| // Check that we'll be able to unseal. |
| if (memcmp(&nonce, wrap_iv_.get(), sizeof(nonce)) != 0) { |
| xprintf("unexpected nonce: %" PRIu64 "\n", nonce); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| memcpy(block_.get() + off, ctext.get(), ctext.len()); |
| return ZX_OK; |
| } |
| |
| zx_status_t Volume::UnsealBlock(const crypto::Secret& key, key_slot_t slot) { |
| zx_status_t rc; |
| |
| // Check the type GUID matches |kTypeGuid|. |
| const uint8_t* in = block_.get(); |
| if (memcmp(in, zxcrypt_magic, sizeof(zxcrypt_magic)) != 0) { |
| xprintf("not a zxcrypt device\n"); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| in += sizeof(zxcrypt_magic); |
| |
| // Save the instance GUID |
| if ((rc = guid_.Copy(in, BLOCK_GUID_LEN)) != ZX_OK) { |
| return rc; |
| } |
| in += BLOCK_GUID_LEN; |
| |
| // Read the version |
| uint32_t version; |
| memcpy(&version, in, sizeof(version)); |
| in += sizeof(version); |
| |
| // Read in the data |
| zx_off_t off; |
| size_t key_len, iv_len; |
| uint8_t* key_buf; |
| crypto::AEAD aead; |
| crypto::Bytes ctext, ptext; |
| if ((rc = Configure(Version(ntohl(version)))) != ZX_OK) { |
| xprintf("Configure failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = GetSlotOffset(slot, &off)) != ZX_OK) { |
| xprintf("GetSlotOffset failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = DeriveSlotKeys(key, slot)) != ZX_OK) { |
| xprintf("DeriveSlotKeys failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = crypto::Cipher::GetKeyLen(cipher_, &key_len)) != ZX_OK) { |
| xprintf("Cipher::GetKeyLen failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = crypto::Cipher::GetIVLen(cipher_, &iv_len)) != ZX_OK) { |
| xprintf("Cipher::GetIVLen failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = data_key_.Allocate(key_len, &key_buf)) != ZX_OK) { |
| xprintf("Secret::Allocate failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = ctext.Copy(block_.get() + off, slot_len_)) != ZX_OK) { |
| xprintf("ctext.Copy failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = aead.InitOpen(aead_, wrap_key_, wrap_iv_)) != ZX_OK) { |
| xprintf("aead.InitOpen failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = header_.Copy(block_.get(), kHeaderLen)) != ZX_OK) { |
| xprintf("header_.Copy failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| |
| // Extract nonce from IV. |
| zx_off_t nonce; |
| memcpy(&nonce, wrap_iv_.get(), sizeof(nonce)); |
| if ((rc = aead.Open(nonce, ctext, header_, &ptext)) != ZX_OK) { |
| xprintf("aead.Open failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| if ((rc = data_iv_.Copy(ptext.get() + key_len, iv_len)) != ZX_OK) { |
| xprintf("data_iv_.Copy failed: %s\n", zx_status_get_string(rc)); |
| return rc; |
| } |
| memcpy(key_buf, ptext.get(), key_len); |
| |
| return ZX_OK; |
| } |
| |
| } // namespace zxcrypt |