/* * Copyright (C) 2016 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. */ #include "EncryptInplace.h" #include #include #include #include #include #include #include #include enum EncryptInPlaceError { kSuccess, kFailed, kFilesystemNotFound, }; static uint64_t round_up(uint64_t val, size_t amount) { if (val % amount) val += amount - (val % amount); return val; } class InPlaceEncrypter { public: bool EncryptInPlace(const std::string& crypto_blkdev, const std::string& real_blkdev, uint64_t nr_sec); bool ProcessUsedBlock(uint64_t block_num); private: // aligned 32K writes tends to make flash happy. // SD card association recommends it. static const size_t kIOBufferSize = 32768; // Avoid spamming the logs. Print the "Encrypting blocks" log message once // every 10000 blocks (which is usually every 40 MB or so), and once at the end. static const int kLogInterval = 10000; std::string DescribeFilesystem(); void InitFs(const std::string& fs_type, uint64_t blocks_to_encrypt, uint64_t total_blocks, unsigned int block_size); void UpdateProgress(size_t blocks, bool done); bool EncryptPendingData(); bool DoEncryptInPlace(); // ext4 methods bool ReadExt4BlockBitmap(uint32_t group, uint8_t* buf); uint64_t FirstBlockInGroup(uint32_t group); uint32_t NumBlocksInGroup(uint32_t group); uint32_t NumBaseMetaBlocksInGroup(uint64_t group); EncryptInPlaceError EncryptInPlaceExt4(); // f2fs methods EncryptInPlaceError EncryptInPlaceF2fs(); std::string real_blkdev_; std::string crypto_blkdev_; uint64_t nr_sec_; android::base::unique_fd realfd_; android::base::unique_fd cryptofd_; std::string fs_type_; uint64_t blocks_done_; uint64_t blocks_to_encrypt_; unsigned int block_size_; std::vector io_buffer_; uint64_t first_pending_block_; size_t blocks_pending_; }; std::string InPlaceEncrypter::DescribeFilesystem() { if (fs_type_.empty()) return "full block device " + real_blkdev_; else return fs_type_ + " filesystem on " + real_blkdev_; } // Finishes initializing the encrypter, now that the filesystem details are known. void InPlaceEncrypter::InitFs(const std::string& fs_type, uint64_t blocks_to_encrypt, uint64_t total_blocks, unsigned int block_size) { fs_type_ = fs_type; blocks_done_ = 0; blocks_to_encrypt_ = blocks_to_encrypt; block_size_ = block_size; // Allocate the I/O buffer. kIOBufferSize should always be a multiple of // the filesystem block size, but round it up just in case. io_buffer_.resize(round_up(kIOBufferSize, block_size)); first_pending_block_ = 0; blocks_pending_ = 0; LOG(INFO) << "Encrypting " << DescribeFilesystem() << " in-place via " << crypto_blkdev_; LOG(INFO) << blocks_to_encrypt << " blocks (" << (blocks_to_encrypt * block_size) / 1000000 << " MB) of " << total_blocks << " blocks are in-use"; } void InPlaceEncrypter::UpdateProgress(size_t blocks, bool done) { // A log message already got printed for blocks_done_ if one was due, so the // next message will be due at the *next* block rounded up to kLogInterval. uint64_t blocks_next_msg = round_up(blocks_done_ + 1, kLogInterval); blocks_done_ += blocks; // Ensure that a log message gets printed at the end, but not if one was // already printed due to the block count being a multiple of kLogInterval. // E.g. we want to show "50000 of 50327" and then "50327 of "50327", but not // "50000 of 50000" and then redundantly "50000 of 50000" again. if (done && blocks_done_ % kLogInterval != 0) blocks_next_msg = blocks_done_; if (blocks_done_ >= blocks_next_msg) LOG(DEBUG) << "Encrypted " << blocks_next_msg << " of " << blocks_to_encrypt_ << " blocks"; } bool InPlaceEncrypter::EncryptPendingData() { if (blocks_pending_ == 0) return true; ssize_t bytes = blocks_pending_ * block_size_; uint64_t offset = first_pending_block_ * block_size_; if (pread64(realfd_, &io_buffer_[0], bytes, offset) != bytes) { PLOG(ERROR) << "Error reading real_blkdev " << real_blkdev_ << " for inplace encrypt"; return false; } if (pwrite64(cryptofd_, &io_buffer_[0], bytes, offset) != bytes) { PLOG(ERROR) << "Error writing crypto_blkdev " << crypto_blkdev_ << " for inplace encrypt"; return false; } UpdateProgress(blocks_pending_, false); blocks_pending_ = 0; return true; } bool InPlaceEncrypter::ProcessUsedBlock(uint64_t block_num) { // Flush if the amount of pending data has reached the I/O buffer size, if // there's a gap between the pending blocks and the next block (due to // block(s) not being used by the filesystem and thus not needing // encryption), or if the next block will be aligned to the I/O buffer size. if (blocks_pending_ * block_size_ == io_buffer_.size() || block_num != first_pending_block_ + blocks_pending_ || (block_num * block_size_) % io_buffer_.size() == 0) { if (!EncryptPendingData()) return false; first_pending_block_ = block_num; } blocks_pending_++; return true; } // Reads the block bitmap for block group |group| into |buf|. bool InPlaceEncrypter::ReadExt4BlockBitmap(uint32_t group, uint8_t* buf) { uint64_t offset = (uint64_t)aux_info.bg_desc[group].bg_block_bitmap * info.block_size; if (pread64(realfd_, buf, info.block_size, offset) != (ssize_t)info.block_size) { PLOG(ERROR) << "Failed to read block bitmap for block group " << group; return false; } return true; } uint64_t InPlaceEncrypter::FirstBlockInGroup(uint32_t group) { return aux_info.first_data_block + (group * (uint64_t)info.blocks_per_group); } uint32_t InPlaceEncrypter::NumBlocksInGroup(uint32_t group) { uint64_t remaining = aux_info.len_blocks - FirstBlockInGroup(group); return std::min(info.blocks_per_group, remaining); } // In block groups with an uninitialized block bitmap, we only need to encrypt // the backup superblock and the block group descriptors (if they are present). uint32_t InPlaceEncrypter::NumBaseMetaBlocksInGroup(uint64_t group) { if (!ext4_bg_has_super_block(group)) return 0; return 1 + aux_info.bg_desc_blocks; } EncryptInPlaceError InPlaceEncrypter::EncryptInPlaceExt4() { if (setjmp(setjmp_env)) // NOLINT return kFilesystemNotFound; if (read_ext(realfd_, 0) != 0) return kFilesystemNotFound; LOG(DEBUG) << "ext4 filesystem has " << aux_info.groups << " block groups"; uint64_t blocks_to_encrypt = 0; for (uint32_t group = 0; group < aux_info.groups; group++) { if (aux_info.bg_desc[group].bg_flags & EXT4_BG_BLOCK_UNINIT) blocks_to_encrypt += NumBaseMetaBlocksInGroup(group); else blocks_to_encrypt += (NumBlocksInGroup(group) - aux_info.bg_desc[group].bg_free_blocks_count); } InitFs("ext4", blocks_to_encrypt, aux_info.len_blocks, info.block_size); // Encrypt each block group. std::vector block_bitmap(info.block_size); for (uint32_t group = 0; group < aux_info.groups; group++) { if (!ReadExt4BlockBitmap(group, &block_bitmap[0])) return kFailed; uint64_t first_block_num = FirstBlockInGroup(group); bool uninit = (aux_info.bg_desc[group].bg_flags & EXT4_BG_BLOCK_UNINIT); uint32_t block_count = uninit ? NumBaseMetaBlocksInGroup(group) : NumBlocksInGroup(group); // Encrypt each used block in the block group. for (uint32_t i = 0; i < block_count; i++) { if (uninit || bitmap_get_bit(&block_bitmap[0], i)) ProcessUsedBlock(first_block_num + i); } } return kSuccess; } static int encrypt_f2fs_block(uint64_t block_num, void* _encrypter) { InPlaceEncrypter* encrypter = reinterpret_cast(_encrypter); if (!encrypter->ProcessUsedBlock(block_num)) return -1; return 0; } EncryptInPlaceError InPlaceEncrypter::EncryptInPlaceF2fs() { std::unique_ptr fs_info( generate_f2fs_info(realfd_), free_f2fs_info); if (!fs_info) return kFilesystemNotFound; InitFs("f2fs", get_num_blocks_used(fs_info.get()), fs_info->total_blocks, fs_info->block_size); if (run_on_used_blocks(0, fs_info.get(), encrypt_f2fs_block, this) != 0) return kFailed; return kSuccess; } bool InPlaceEncrypter::DoEncryptInPlace() { EncryptInPlaceError rc; rc = EncryptInPlaceExt4(); if (rc != kFilesystemNotFound) return rc == kSuccess; rc = EncryptInPlaceF2fs(); if (rc != kFilesystemNotFound) return rc == kSuccess; LOG(WARNING) << "No recognized filesystem found on " << real_blkdev_ << ". Falling back to encrypting the full block device."; InitFs("", nr_sec_, nr_sec_, 512); for (uint64_t i = 0; i < nr_sec_; i++) { if (!ProcessUsedBlock(i)) return false; } return true; } bool InPlaceEncrypter::EncryptInPlace(const std::string& crypto_blkdev, const std::string& real_blkdev, uint64_t nr_sec) { real_blkdev_ = real_blkdev; crypto_blkdev_ = crypto_blkdev; nr_sec_ = nr_sec; realfd_.reset(open64(real_blkdev.c_str(), O_RDONLY | O_CLOEXEC)); if (realfd_ < 0) { PLOG(ERROR) << "Error opening real_blkdev " << real_blkdev << " for inplace encrypt"; return false; } cryptofd_.reset(open64(crypto_blkdev.c_str(), O_WRONLY | O_CLOEXEC)); if (cryptofd_ < 0) { PLOG(ERROR) << "Error opening crypto_blkdev " << crypto_blkdev << " for inplace encrypt"; return false; } bool success = DoEncryptInPlace(); if (success) success &= EncryptPendingData(); if (success && fsync(cryptofd_) != 0) { PLOG(ERROR) << "Error syncing " << crypto_blkdev_; success = false; } if (!success) { LOG(ERROR) << "In-place encryption of " << DescribeFilesystem() << " failed"; return false; } if (blocks_done_ != blocks_to_encrypt_) { LOG(WARNING) << "blocks_to_encrypt (" << blocks_to_encrypt_ << ") was incorrect; we actually encrypted " << blocks_done_ << " blocks. Encryption progress was inaccurate"; } // Ensure that the final progress message is printed, so the series of log // messages ends with e.g. "Encrypted 50327 of 50327 blocks" rather than // "Encrypted 50000 of 50327 blocks". UpdateProgress(0, true); LOG(INFO) << "Successfully encrypted " << DescribeFilesystem(); return true; } // Encrypts |real_blkdev| in-place by reading the data from |real_blkdev| and // writing it to |crypto_blkdev|, which should be a dm-crypt or dm-default-key // device backed by |real_blkdev|. The size to encrypt is |nr_sec| 512-byte // sectors; however, if a filesystem is detected, then its size will be used // instead, and only the in-use blocks of the filesystem will be encrypted. bool encrypt_inplace(const std::string& crypto_blkdev, const std::string& real_blkdev, uint64_t nr_sec) { LOG(DEBUG) << "encrypt_inplace(" << crypto_blkdev << ", " << real_blkdev << ", " << nr_sec << ")"; InPlaceEncrypter encrypter; return encrypter.EncryptInPlace(crypto_blkdev, real_blkdev, nr_sec); }