// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/gcm_driver/crypto/gcm_key_store.h"

#include <stddef.h>

#include <utility>

#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/strings/string_view_util.h"
#include "base/task/sequenced_task_runner.h"
#include "components/gcm_driver/crypto/p256_key_util.h"
#include "components/leveldb_proto/public/proto_database_provider.h"
#include "components/leveldb_proto/public/shared_proto_database_client_list.h"
#include "crypto/evp.h"
#include "crypto/keypair.h"
#include "crypto/openssl_util.h"
#include "crypto/random.h"
#include "third_party/boringssl/src/include/openssl/bytestring.h"
#include "third_party/boringssl/src/include/openssl/evp.h"
#include "third_party/boringssl/src/include/openssl/pkcs8.h"

namespace gcm {

namespace {

using EntryVectorType =
    leveldb_proto::ProtoDatabase<EncryptionData>::KeyEntryVector;

// Number of cryptographically secure random bytes to generate as a key pair's
// authentication secret. Must be at least 16 bytes.
const size_t kAuthSecretBytes = 16;

std::string DatabaseKey(const std::string& app_id,
                        const std::string& authorized_entity) {
  DCHECK_EQ(std::string::npos, app_id.find(','));
  DCHECK_EQ(std::string::npos, authorized_entity.find(','));
  DCHECK_NE("*", authorized_entity) << "Wildcards require special handling";
  return authorized_entity.empty()
             ? app_id  // No comma, for compatibility with existing keys.
             : app_id + ',' + authorized_entity;
}

}  // namespace

enum class GCMKeyStore::State {
   UNINITIALIZED,
   INITIALIZING,
   INITIALIZED,
   FAILED
};

GCMKeyStore::GCMKeyStore(
    const base::FilePath& key_store_path,
    const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner)
    : key_store_path_(key_store_path),
      blocking_task_runner_(blocking_task_runner),
      state_(State::UNINITIALIZED) {
  DCHECK(blocking_task_runner);
}

GCMKeyStore::~GCMKeyStore() = default;

void GCMKeyStore::GetKeys(const std::string& app_id,
                          const std::string& authorized_entity,
                          bool fallback_to_empty_authorized_entity,
                          KeysCallback callback) {
  LazyInitialize(
      base::BindOnce(&GCMKeyStore::GetKeysAfterInitialize,
                     weak_factory_.GetWeakPtr(), app_id, authorized_entity,
                     fallback_to_empty_authorized_entity, std::move(callback)));
}

void GCMKeyStore::GetKeysAfterInitialize(
    const std::string& app_id,
    const std::string& authorized_entity,
    bool fallback_to_empty_authorized_entity,
    KeysCallback callback) {
  DCHECK(state_ == State::INITIALIZED || state_ == State::FAILED);
  bool success = false;

  if (state_ == State::INITIALIZED) {
    auto outer_iter = key_data_.find(app_id);
    if (outer_iter != key_data_.end()) {
      const auto& inner_map = outer_iter->second;
      auto inner_iter = inner_map.find(authorized_entity);
      if (fallback_to_empty_authorized_entity && inner_iter == inner_map.end())
        inner_iter = inner_map.find(std::string());
      if (inner_iter != inner_map.end()) {
        const auto& map_entry = inner_iter->second;
        std::move(callback).Run(map_entry.first, map_entry.second);
        success = true;
      }
    }
  }

  if (!success)
    std::move(callback).Run(std::nullopt /* key */,
                            std::string() /* auth_secret */);
}

void GCMKeyStore::CreateKeys(const std::string& app_id,
                             const std::string& authorized_entity,
                             KeysCallback callback) {
  LazyInitialize(base::BindOnce(&GCMKeyStore::CreateKeysAfterInitialize,
                                weak_factory_.GetWeakPtr(), app_id,
                                authorized_entity, std::move(callback)));
}

void GCMKeyStore::CreateKeysAfterInitialize(
    const std::string& app_id,
    const std::string& authorized_entity,
    KeysCallback callback) {
  DCHECK(state_ == State::INITIALIZED || state_ == State::FAILED);
  if (state_ != State::INITIALIZED) {
    std::move(callback).Run(std::nullopt /* key */,
                            std::string() /* auth_secret */);
    return;
  }

  // Only allow creating new keys if no keys currently exist. Multiple Instance
  // ID tokens can share an app_id (with different authorized entities), but
  // InstanceID tokens can't share an app_id with a non-InstanceID registration.
  // This invariant is necessary for the fallback_to_empty_authorized_entity
  // mode of GetKey (needed by GCMEncryptionProvider::DecryptMessage, which
  // can't distinguish Instance ID tokens from non-InstanceID registrations).
  DCHECK(!key_data_.count(app_id) ||
         (!authorized_entity.empty() &&
          !key_data_[app_id].count(authorized_entity) &&
          !key_data_[app_id].count(std::string())))
      << "Instance ID tokens cannot share an app_id with a non-InstanceID GCM "
         "registration";

  auto key = crypto::keypair::PrivateKey::GenerateEcP256();

  // Create the authentication secret, which has to be a cryptographically
  // secure random number of at least 128 bits (16 bytes).
  std::string auth_secret(kAuthSecretBytes, '\0');
  crypto::RandBytes(base::as_writable_byte_span(auth_secret));

  // Store the keys in a new EncryptionData object.
  EncryptionData encryption_data;
  encryption_data.set_app_id(app_id);
  if (!authorized_entity.empty())
    encryption_data.set_authorized_entity(authorized_entity);
  encryption_data.set_auth_secret(auth_secret);

  encryption_data.set_private_key(base::as_string_view(key.ToPrivateKeyInfo()));

  // Write them immediately to our cache, so subsequent calls to
  // {Get/Create/Remove}Keys can see them.
  key_data_[app_id].emplace(authorized_entity,
                            std::make_pair(key, auth_secret));

  std::unique_ptr<EntryVectorType> entries_to_save(new EntryVectorType());
  std::unique_ptr<std::vector<std::string>> keys_to_remove(
      new std::vector<std::string>());

  entries_to_save->push_back(
      std::make_pair(DatabaseKey(app_id, authorized_entity), encryption_data));

  database_->UpdateEntries(
      std::move(entries_to_save), std::move(keys_to_remove),
      base::BindOnce(&GCMKeyStore::DidStoreKeys, weak_factory_.GetWeakPtr(),
                     key, auth_secret, std::move(callback)));
}

void GCMKeyStore::DidStoreKeys(crypto::keypair::PrivateKey key,
                               const std::string& auth_secret,
                               KeysCallback callback,
                               bool success) {
  if (!success) {
    DVLOG(1) << "Unable to store the created key in the GCM Key Store.";

    // Our cache is now inconsistent. Reject requests until restarted.
    state_ = State::FAILED;

    std::move(callback).Run(std::nullopt /* key */,
                            std::string() /* auth_secret */);
    return;
  }

  std::move(callback).Run(key, auth_secret);
}

void GCMKeyStore::RemoveKeys(const std::string& app_id,
                             const std::string& authorized_entity,
                             base::OnceClosure callback) {
  LazyInitialize(base::BindOnce(&GCMKeyStore::RemoveKeysAfterInitialize,
                                weak_factory_.GetWeakPtr(), app_id,
                                authorized_entity, std::move(callback)));
}

void GCMKeyStore::RemoveKeysAfterInitialize(
    const std::string& app_id,
    const std::string& authorized_entity,
    base::OnceClosure callback) {
  DCHECK(state_ == State::INITIALIZED || state_ == State::FAILED);

  const auto& outer_iter = key_data_.find(app_id);
  if (outer_iter == key_data_.end() || state_ != State::INITIALIZED) {
    std::move(callback).Run();
    return;
  }

  std::unique_ptr<EntryVectorType> entries_to_save(new EntryVectorType());
  std::unique_ptr<std::vector<std::string>> keys_to_remove(
      new std::vector<std::string>());

  bool had_keys = false;
  auto& inner_map = outer_iter->second;
  for (auto it = inner_map.begin(); it != inner_map.end();) {
    // Wildcard "*" matches all non-empty authorized entities (InstanceID only).
    if (authorized_entity == "*" ? !it->first.empty()
                                 : it->first == authorized_entity) {
      had_keys = true;

      keys_to_remove->push_back(DatabaseKey(app_id, it->first));

      // Clear keys immediately from our cache, so subsequent calls to
      // {Get/Create/Remove}Keys don't see them.
      it = inner_map.erase(it);
    } else {
      ++it;
    }
  }
  if (!had_keys) {
    std::move(callback).Run();
    return;
  }
  if (inner_map.empty())
    key_data_.erase(app_id);

  database_->UpdateEntries(
      std::move(entries_to_save), std::move(keys_to_remove),
      base::BindOnce(&GCMKeyStore::DidRemoveKeys, weak_factory_.GetWeakPtr(),
                     std::move(callback)));
}

void GCMKeyStore::DidRemoveKeys(base::OnceClosure callback, bool success) {
  if (!success) {
    DVLOG(1) << "Unable to delete a key from the GCM Key Store.";

    // Our cache is now inconsistent. Reject requests until restarted.
    state_ = State::FAILED;
  }

  std::move(callback).Run();
}

void GCMKeyStore::DidUpgradeDatabase(bool success) {
  if (!success) {
    DVLOG(1) << "Unable to upgrade the GCM Key Store database.";
    // Our cache is now inconsistent. Reject requests until restarted.
    state_ = State::FAILED;
    delayed_task_controller_.SetReady();
    return;
  }

  database_->LoadEntries(
      base::BindOnce(&GCMKeyStore::DidLoadKeys, weak_factory_.GetWeakPtr()));
}

void GCMKeyStore::LazyInitialize(base::OnceClosure done_closure) {
  if (delayed_task_controller_.CanRunTaskWithoutDelay()) {
    std::move(done_closure).Run();
    return;
  }

  delayed_task_controller_.AddTask(std::move(done_closure));
  if (state_ == State::INITIALIZING)
    return;

  state_ = State::INITIALIZING;

  database_ = leveldb_proto::ProtoDatabaseProvider::GetUniqueDB<EncryptionData>(
      leveldb_proto::ProtoDbType::GCM_KEY_STORE, key_store_path_,
      blocking_task_runner_);

  database_->Init(
      leveldb_proto::CreateSimpleOptions(),
      base::BindOnce(&GCMKeyStore::DidInitialize, weak_factory_.GetWeakPtr()));
}

void GCMKeyStore::DidInitialize(leveldb_proto::Enums::InitStatus status) {
  bool success = status == leveldb_proto::Enums::kOK;
  if (!success) {
    DVLOG(1) << "Unable to initialize the GCM Key Store.";
    state_ = State::FAILED;

    delayed_task_controller_.SetReady();
    return;
  }

  database_->LoadEntries(
      base::BindOnce(&GCMKeyStore::DidLoadKeys, weak_factory_.GetWeakPtr()));
}

void GCMKeyStore::UpgradeDatabase(
    std::unique_ptr<std::vector<EncryptionData>> entries) {
  std::unique_ptr<EntryVectorType> entries_to_save =
      std::make_unique<EntryVectorType>();
  std::unique_ptr<std::vector<std::string>> keys_to_remove =
      std::make_unique<std::vector<std::string>>();

  // Loop over entries, create list of database entries to overwrite.
  for (EncryptionData& entry : *entries) {
    if (!entry.keys_size())
      continue;
    std::string decrypted_private_key;
    if (!DecryptPrivateKey(entry.keys(0).private_key(),
                           &decrypted_private_key)) {
      DVLOG(1) << "Unable to decrypt private key: "
               << entry.keys(0).private_key();
      state_ = State::FAILED;
      delayed_task_controller_.SetReady();
      return;
    }

    entry.set_private_key(decrypted_private_key);
    entry.clear_keys();
    entries_to_save->push_back(std::make_pair(
        DatabaseKey(entry.app_id(), entry.authorized_entity()), entry));
  }

  database_->UpdateEntries(std::move(entries_to_save),
                           std::move(keys_to_remove),
                           base::BindOnce(&GCMKeyStore::DidUpgradeDatabase,
                                          weak_factory_.GetWeakPtr()));
}

void GCMKeyStore::DidLoadKeys(
    bool success,
    std::unique_ptr<std::vector<EncryptionData>> entries) {
  if (!success) {
    DVLOG(1) << "Unable to load entries into the GCM Key Store.";
    state_ = State::FAILED;

    delayed_task_controller_.SetReady();
    return;
  }

  for (const EncryptionData& entry : *entries) {
    std::string authorized_entity;
    if (entry.has_authorized_entity())
      authorized_entity = entry.authorized_entity();

    // The old format of EncryptionData has a KeyPair in it. Previously
    // we used to cache the key pair and auth secret in key_data_.
    // The new code adds the pair {PrivateKey, auth_secret} to
    // key_data_ instead.
    if (entry.keys_size()) {
      if (state_ == State::FAILED)
        return;

      // Old format of EncryptionData. Upgrade database so there are no such
      // entries. We'll reload keys from the database once this is done.
      UpgradeDatabase(std::move(entries));
      return;
    }

    if (entry.private_key().empty()) {
      continue;
    }
    auto key = crypto::keypair::PrivateKey::FromPrivateKeyInfo(
        base::as_byte_span(entry.private_key()));
    if (!key || !key->IsEc()) {
      continue;
    }
    key_data_[entry.app_id()].emplace(
        authorized_entity, std::make_pair(*key, entry.auth_secret()));
  }

  state_ = State::INITIALIZED;

  delayed_task_controller_.SetReady();
}

bool GCMKeyStore::DecryptPrivateKey(const std::string& to_decrypt_str,
                                    std::string* decrypted) {
  auto to_decrypt = base::as_byte_span(to_decrypt_str);

  crypto::OpenSSLErrStackTracer err_tracer(FROM_HERE);
  CBS cbs;
  CBS_init(&cbs, to_decrypt.data(), to_decrypt.size());
  bssl::UniquePtr<EVP_PKEY> pkey(
      PKCS8_parse_encrypted_private_key(&cbs, "", 0));

  // Hack for reading keys generated by an older version of the OpenSSL code.
  // Some implementations encode the empty password as "\0\0" (passwords are
  // normally encoded in big-endian UCS-2 with a NUL terminator) and some
  // encode as the empty string. PKCS8_parse_encrypted_private_key
  // distinguishes the two by whether the password is nullptr.
  if (!pkey) {
    CBS_init(&cbs, to_decrypt.data(), to_decrypt.size());
    pkey.reset(PKCS8_parse_encrypted_private_key(&cbs, nullptr, 0));
  }

  if (!pkey || CBS_len(&cbs) != 0 || EVP_PKEY_id(pkey.get()) != EVP_PKEY_EC) {
    return false;
  }

  auto decrypted_vec = crypto::evp::PrivateKeyToBytes(pkey.get());
  decrypted->assign(decrypted_vec.begin(), decrypted_vec.end());
  return true;
}

}  // namespace gcm
