/*
 * 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 <ext4_utils/ext4.h>
#include <ext4_utils/ext4_utils.h>
#include <f2fs_sparseblock.h>
#include <fcntl.h>

#include <algorithm>
#include <vector>

#include <android-base/logging.h>
#include <android-base/unique_fd.h>

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<uint8_t> 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<uint64_t>(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<uint8_t> 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<InPlaceEncrypter*>(_encrypter);
    if (!encrypter->ProcessUsedBlock(block_num)) return -1;
    return 0;
}

EncryptInPlaceError InPlaceEncrypter::EncryptInPlaceF2fs() {
    std::unique_ptr<struct f2fs_info, void (*)(struct f2fs_info*)> 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);
}
