chromium/chromeos/components/kcer/chaps/session_chaps_client.cc

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

#include "chromeos/components/kcer/chaps/session_chaps_client.h"

#include <stdint.h>

#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/logging.h"
#include "chromeos/components/kcer/attributes.pb.h"
#include "chromeos/constants/pkcs11_definitions.h"
#include "third_party/cros_system_api/dbus/chaps/dbus-constants.h"

namespace kcer {
namespace {

// Arbitrary limit for the FindObjects method, equals to 8Mb of object handles
// (each handle is 8 bytes), should be more than enough. The main
// consideration is the amount of data sent over D-Bus at once and the time
// required to process it in Chrome. The current implementation doesn't expect
// Chaps to hold too many objects (usually <100 of a given type, <10000 99.9%
// of the time), the processing of the objects is handled on the UI thread. If
// we find a need to process millions of objects, FindObjects can be changed
// to do multiple smaller requests and the objects could be processed on a
// worker thread.
inline constexpr uint64_t kFindObjectsMaxCount = 1 << 20;

// Pass CKF_RW_SESSION so Chrome can modify data, e.g. generate new key pairs.
// CKF_SERIAL_SESSION should always be set according to
// http://docs.oasis-open.org/pkcs11/pkcs11-base/v2.40/os/pkcs11-base-v2.40-os.html#_Toc416959688
// and chaps verifies that.
inline constexpr uint64_t kSessionFlags =
    chromeos::PKCS11_CKF_RW_SESSION | chromeos::PKCS11_CKF_SERIAL_SESSION;

}  // namespace

SessionChapsClient::SessionChapsClient() = default;
SessionChapsClient::~SessionChapsClient() = default;

SessionChapsClientImpl::SessionChapsClientImpl(ChapsServiceGetter getter)
    : chaps_service_getter_(std::move(getter)) {}
SessionChapsClientImpl::~SessionChapsClientImpl() = default;

// static
std::vector<uint8_t> SessionChapsClient::SerializeToBytes(
    const chaps::AttributeList& attr_list) {
  std::vector<uint8_t> result;
  result.resize(attr_list.ByteSizeLong());
  attr_list.SerializeToArray(result.data(), result.size());
  return result;
}

// static
bool SessionChapsClient::IsSessionError(uint32_t result_code) {
  return result_code == chromeos::PKCS11_CKR_SESSION_HANDLE_INVALID ||
         result_code == chromeos::PKCS11_CKR_SESSION_CLOSED;
}

//==============================================================================

void SessionChapsClientImpl::GetMechanismList(
    SlotId slot_id,
    GetMechanismListCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  crosapi::mojom::ChapsService* chaps_service = chaps_service_getter_.Run();
  if (!chaps_service) {
    return std::move(callback).Run({}, chaps::CKR_DBUS_CLIENT_IS_NULL);
  }

  return chaps_service->GetMechanismList(slot_id.value(), std::move(callback));
}

//==============================================================================

void SessionChapsClientImpl::CreateObject(
    SlotId slot_id,
    const std::vector<uint8_t>& attributes,
    int attempts_left,
    CreateObjectCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  crosapi::mojom::ChapsService* chaps_service = chaps_service_getter_.Run();
  if (!chaps_service) {
    return std::move(callback).Run(ObjectHandle(0),
                                   chaps::CKR_DBUS_CLIENT_IS_NULL);
  }

  SessionId session_id = GetSessionForSlot(slot_id);
  if (session_id.value() == chromeos::PKCS11_INVALID_SESSION_ID) {
    if (attempts_left <= 0) {
      return std::move(callback).Run(ObjectHandle(0),
                                     chaps::CKR_FAILED_TO_OPEN_SESSION);
    }
    attempts_left--;

    auto chaps_callback = base::BindOnce(
        &SessionChapsClientImpl::CreateObject, weak_factory_.GetWeakPtr(),
        slot_id, attributes, attempts_left, std::move(callback));
    return chaps_service->OpenSession(
        slot_id.value(), kSessionFlags,
        base::BindOnce(&SessionChapsClientImpl::SaveSessionId,
                       weak_factory_.GetWeakPtr(), slot_id,
                       std::move(chaps_callback)));
  }

  return chaps_service->CreateObject(
      session_id.value(), attributes,
      base::BindOnce(&SessionChapsClientImpl::DidCreateObject,
                     weak_factory_.GetWeakPtr(), slot_id, std::move(callback)));
}

void SessionChapsClientImpl::DidCreateObject(SlotId slot_id,
                                             CreateObjectCallback callback,
                                             uint64_t object_handle,
                                             uint32_t result_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (IsSessionError(result_code)) {
    sessions_map_.erase(slot_id);
  }
  return std::move(callback).Run(ObjectHandle(object_handle), result_code);
}

//==============================================================================

void SessionChapsClientImpl::DestroyObject(SlotId slot_id,
                                           ObjectHandle object_handle,
                                           int attempts_left,
                                           DestroyObjectCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  crosapi::mojom::ChapsService* chaps_service = chaps_service_getter_.Run();
  if (!chaps_service) {
    return std::move(callback).Run(chaps::CKR_DBUS_CLIENT_IS_NULL);
  }

  SessionId session_id = GetSessionForSlot(slot_id);
  if (session_id.value() == chromeos::PKCS11_INVALID_SESSION_ID) {
    if (attempts_left <= 0) {
      return std::move(callback).Run(chaps::CKR_FAILED_TO_OPEN_SESSION);
    }
    attempts_left--;

    auto chaps_callback = base::BindOnce(
        &SessionChapsClientImpl::DestroyObject, weak_factory_.GetWeakPtr(),
        slot_id, object_handle, attempts_left, std::move(callback));
    return chaps_service->OpenSession(
        slot_id.value(), kSessionFlags,
        base::BindOnce(&SessionChapsClientImpl::SaveSessionId,
                       weak_factory_.GetWeakPtr(), slot_id,
                       std::move(chaps_callback)));
  }

  return chaps_service->DestroyObject(
      session_id.value(), object_handle.value(),
      base::BindOnce(&SessionChapsClientImpl::DidDestroyObject,
                     weak_factory_.GetWeakPtr(), slot_id, std::move(callback)));
}

void SessionChapsClientImpl::DidDestroyObject(SlotId slot_id,
                                              DestroyObjectCallback callback,
                                              uint32_t result_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (IsSessionError(result_code)) {
    sessions_map_.erase(slot_id);
  }
  return std::move(callback).Run(result_code);
}

//==============================================================================

void SessionChapsClientImpl::GetAttributeValue(
    SlotId slot_id,
    ObjectHandle object_handle,
    std::vector<uint8_t> attributes_query,
    int attempts_left,
    GetAttributeValueCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  crosapi::mojom::ChapsService* chaps_service = chaps_service_getter_.Run();
  if (!chaps_service) {
    return std::move(callback).Run({}, chaps::CKR_DBUS_CLIENT_IS_NULL);
  }

  SessionId session_id = GetSessionForSlot(slot_id);
  if (session_id.value() == chromeos::PKCS11_INVALID_SESSION_ID) {
    if (attempts_left <= 0) {
      return std::move(callback).Run({}, chaps::CKR_FAILED_TO_OPEN_SESSION);
    }
    attempts_left--;

    auto chaps_callback = base::BindOnce(
        &SessionChapsClientImpl::GetAttributeValue, weak_factory_.GetWeakPtr(),
        slot_id, object_handle, std::move(attributes_query), attempts_left,
        std::move(callback));
    return chaps_service->OpenSession(
        slot_id.value(), kSessionFlags,
        base::BindOnce(&SessionChapsClientImpl::SaveSessionId,
                       weak_factory_.GetWeakPtr(), slot_id,
                       std::move(chaps_callback)));
  }

  return chaps_service->GetAttributeValue(
      session_id.value(), object_handle.value(), attributes_query,
      base::BindOnce(&SessionChapsClientImpl::DidGetAttributeValue,
                     weak_factory_.GetWeakPtr(), slot_id, std::move(callback)));
}

void SessionChapsClientImpl::DidGetAttributeValue(
    SlotId slot_id,
    GetAttributeValueCallback callback,
    const std::vector<uint8_t>& attributes,
    uint32_t result_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (IsSessionError(result_code)) {
    sessions_map_.erase(slot_id);
  }
  return std::move(callback).Run(attributes, result_code);
}

//==============================================================================

void SessionChapsClientImpl::SetAttributeValue(
    SlotId slot_id,
    ObjectHandle object_handle,
    std::vector<uint8_t> attributes,
    int attempts_left,
    SetAttributeValueCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  crosapi::mojom::ChapsService* chaps_service = chaps_service_getter_.Run();
  if (!chaps_service) {
    return std::move(callback).Run(chaps::CKR_DBUS_CLIENT_IS_NULL);
  }

  SessionId session_id = GetSessionForSlot(slot_id);
  if (session_id.value() == chromeos::PKCS11_INVALID_SESSION_ID) {
    if (attempts_left <= 0) {
      return std::move(callback).Run(chaps::CKR_FAILED_TO_OPEN_SESSION);
    }
    attempts_left--;

    auto chaps_callback = base::BindOnce(
        &SessionChapsClientImpl::SetAttributeValue, weak_factory_.GetWeakPtr(),
        slot_id, object_handle, std::move(attributes), attempts_left,
        std::move(callback));
    return chaps_service->OpenSession(
        slot_id.value(), kSessionFlags,
        base::BindOnce(&SessionChapsClientImpl::SaveSessionId,
                       weak_factory_.GetWeakPtr(), slot_id,
                       std::move(chaps_callback)));
  }

  return chaps_service->SetAttributeValue(
      session_id.value(), object_handle.value(), attributes,
      base::BindOnce(&SessionChapsClientImpl::DidSetAttributeValue,
                     weak_factory_.GetWeakPtr(), slot_id, std::move(callback)));
}

void SessionChapsClientImpl::DidSetAttributeValue(
    SlotId slot_id,
    SetAttributeValueCallback callback,
    uint32_t result_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (IsSessionError(result_code)) {
    sessions_map_.erase(slot_id);
  }
  return std::move(callback).Run(result_code);
}

//==============================================================================

void SessionChapsClientImpl::FindObjects(SlotId slot_id,
                                         std::vector<uint8_t> attributes,
                                         int attempts_left,
                                         FindObjectsCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  crosapi::mojom::ChapsService* chaps_service = chaps_service_getter_.Run();
  if (!chaps_service) {
    return std::move(callback).Run({}, chaps::CKR_DBUS_CLIENT_IS_NULL);
  }

  SessionId session_id = GetSessionForSlot(slot_id);

  if (session_id.value() == chromeos::PKCS11_INVALID_SESSION_ID) {
    if (attempts_left <= 0) {
      return std::move(callback).Run({}, chaps::CKR_FAILED_TO_OPEN_SESSION);
    }
    attempts_left--;

    auto chaps_callback = base::BindOnce(
        &SessionChapsClientImpl::FindObjects, weak_factory_.GetWeakPtr(),
        slot_id, std::move(attributes), attempts_left, std::move(callback));
    return chaps_service->OpenSession(
        slot_id.value(), kSessionFlags,
        base::BindOnce(&SessionChapsClientImpl::SaveSessionId,
                       weak_factory_.GetWeakPtr(), slot_id,
                       std::move(chaps_callback)));
  }

  return chaps_service->FindObjectsInit(
      session_id.value(), attributes,
      base::BindOnce(&SessionChapsClientImpl::DidFindObjectsInit,
                     weak_factory_.GetWeakPtr(), slot_id, std::move(callback)));
}

void SessionChapsClientImpl::DidFindObjectsInit(SlotId slot_id,
                                                FindObjectsCallback callback,
                                                uint32_t result_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (IsSessionError(result_code)) {
    sessions_map_.erase(slot_id);
    return std::move(callback).Run({}, result_code);
  }
  if (result_code != chromeos::PKCS11_CKR_OK) {
    return std::move(callback).Run({}, result_code);
  }

  SessionId session_id = GetSessionForSlot(slot_id);
  CHECK_NE(session_id.value(), chromeos::PKCS11_INVALID_SESSION_ID);

  crosapi::mojom::ChapsService* chaps_service = chaps_service_getter_.Run();
  if (!chaps_service) {
    return std::move(callback).Run({}, chaps::CKR_DBUS_CLIENT_IS_NULL);
  }
  return chaps_service->FindObjects(
      session_id.value(), kFindObjectsMaxCount,
      base::BindOnce(&SessionChapsClientImpl::DidFindObjects,
                     weak_factory_.GetWeakPtr(), slot_id, std::move(callback)));
}

void SessionChapsClientImpl::DidFindObjects(
    SlotId slot_id,
    FindObjectsCallback callback,
    const std::vector<uint64_t>& object_list,
    uint32_t result_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (IsSessionError(result_code)) {
    sessions_map_.erase(slot_id);
    return std::move(callback).Run({}, result_code);
  }
  if (result_code != chromeos::PKCS11_CKR_OK) {
    return std::move(callback).Run({}, result_code);
  }

  SessionId session_id = GetSessionForSlot(slot_id);
  CHECK_NE(session_id.value(), chromeos::PKCS11_INVALID_SESSION_ID);

  std::vector<ObjectHandle> typed_list(object_list.begin(), object_list.end());

  crosapi::mojom::ChapsService* chaps_service = chaps_service_getter_.Run();
  if (!chaps_service) {
    return std::move(callback).Run({}, chaps::CKR_DBUS_CLIENT_IS_NULL);
  }
  return chaps_service->FindObjectsFinal(
      session_id.value(),
      base::BindOnce(&SessionChapsClientImpl::DidFindObjectsFinal,
                     weak_factory_.GetWeakPtr(), slot_id, std::move(callback),
                     std::move(typed_list)));
}

void SessionChapsClientImpl::DidFindObjectsFinal(
    SlotId slot_id,
    FindObjectsCallback callback,
    std::vector<ObjectHandle> object_list,
    uint32_t result_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (IsSessionError(result_code)) {
    sessions_map_.erase(slot_id);
  }
  if (result_code != chromeos::PKCS11_CKR_OK) {
    // It's not clear if finishing a search can actually fail. The caller
    // shouldn't need to care since the objects were already successfully
    // retrieved, but future FindObjects calls within the same PKCS#11 session
    // will fail if the current search is not finished properly.
    LOG(ERROR) << "Failed to call FindObjectsFinal";
  }
  return std::move(callback).Run(std::move(object_list),
                                 chromeos::PKCS11_CKR_OK);
}

//==============================================================================

void SessionChapsClientImpl::Sign(SlotId slot_id,
                                  uint64_t mechanism_type,
                                  std::vector<uint8_t> mechanism_parameter,
                                  ObjectHandle key_handle,
                                  std::vector<uint8_t> data,
                                  int attempts_left,
                                  SignCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  crosapi::mojom::ChapsService* chaps_service = chaps_service_getter_.Run();
  if (!chaps_service) {
    return std::move(callback).Run({}, chaps::CKR_DBUS_CLIENT_IS_NULL);
  }

  SessionId session_id = GetSessionForSlot(slot_id);
  if (session_id.value() == chromeos::PKCS11_INVALID_SESSION_ID) {
    if (attempts_left <= 0) {
      return std::move(callback).Run({}, chaps::CKR_FAILED_TO_OPEN_SESSION);
    }
    attempts_left--;

    auto chaps_callback = base::BindOnce(
        &SessionChapsClientImpl::Sign, weak_factory_.GetWeakPtr(), slot_id,
        mechanism_type, std::move(mechanism_parameter), key_handle,
        std::move(data), attempts_left, std::move(callback));
    return chaps_service->OpenSession(
        slot_id.value(), kSessionFlags,
        base::BindOnce(&SessionChapsClientImpl::SaveSessionId,
                       weak_factory_.GetWeakPtr(), slot_id,
                       std::move(chaps_callback)));
  }

  return chaps_service->SignInit(
      session_id.value(), mechanism_type, mechanism_parameter,
      key_handle.value(),
      base::BindOnce(&SessionChapsClientImpl::DidSignInit,
                     weak_factory_.GetWeakPtr(), slot_id, std::move(data),
                     std::move(callback)));
}

void SessionChapsClientImpl::DidSignInit(SlotId slot_id,
                                         std::vector<uint8_t> data,
                                         SignCallback callback,
                                         uint32_t result_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (IsSessionError(result_code)) {
    sessions_map_.erase(slot_id);
    return std::move(callback).Run({}, result_code);
  }
  if (result_code != chromeos::PKCS11_CKR_OK) {
    return std::move(callback).Run({}, result_code);
  }

  SessionId session_id = GetSessionForSlot(slot_id);
  CHECK_NE(session_id.value(), chromeos::PKCS11_INVALID_SESSION_ID);

  // Maximum supported RSA key is 4096 bits, its signature is 4096 bits, 512
  // bytes. The only supported EC key is P-256, its signature is 512 bits, 64
  // bytes.
  constexpr uint64_t kMaxOutLength = 512;

  crosapi::mojom::ChapsService* chaps_service = chaps_service_getter_.Run();
  if (!chaps_service) {
    return std::move(callback).Run({}, chaps::CKR_DBUS_CLIENT_IS_NULL);
  }

  return chaps_service->Sign(
      session_id.value(), data, kMaxOutLength,
      base::BindOnce(&SessionChapsClientImpl::DidSign,
                     weak_factory_.GetWeakPtr(), slot_id, std::move(callback)));
}

void SessionChapsClientImpl::DidSign(SlotId slot_id,
                                     SignCallback callback,
                                     uint64_t actual_out_length,
                                     const std::vector<uint8_t>& signature,
                                     uint32_t result_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (IsSessionError(result_code)) {
    sessions_map_.erase(slot_id);
  }
  return std::move(callback).Run(std::move(signature), result_code);
}

//==============================================================================

void SessionChapsClientImpl::GenerateKeyPair(
    SlotId slot_id,
    uint64_t mechanism_type,
    std::vector<uint8_t> mechanism_parameter,
    std::vector<uint8_t> public_attributes,
    std::vector<uint8_t> private_attributes,
    int attempts_left,
    GenerateKeyPairCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  crosapi::mojom::ChapsService* chaps_service = chaps_service_getter_.Run();
  if (!chaps_service) {
    return std::move(callback).Run(ObjectHandle(0), ObjectHandle(0),
                                   chaps::CKR_DBUS_CLIENT_IS_NULL);
  }

  SessionId session_id = GetSessionForSlot(slot_id);
  if (session_id.value() == chromeos::PKCS11_INVALID_SESSION_ID) {
    if (attempts_left <= 0) {
      return std::move(callback).Run(ObjectHandle(0), ObjectHandle(0),
                                     chaps::CKR_FAILED_TO_OPEN_SESSION);
    }
    attempts_left--;

    auto chaps_callback = base::BindOnce(
        &SessionChapsClientImpl::GenerateKeyPair, weak_factory_.GetWeakPtr(),
        slot_id, mechanism_type, std::move(mechanism_parameter),
        std::move(public_attributes), std::move(private_attributes),
        attempts_left, std::move(callback));
    return chaps_service->OpenSession(
        slot_id.value(), kSessionFlags,
        base::BindOnce(&SessionChapsClientImpl::SaveSessionId,
                       weak_factory_.GetWeakPtr(), slot_id,
                       std::move(chaps_callback)));
  }

  return chaps_service->GenerateKeyPair(
      session_id.value(), mechanism_type, mechanism_parameter,
      public_attributes, private_attributes,
      base::BindOnce(&SessionChapsClientImpl::DidGenerateKeyPair,
                     weak_factory_.GetWeakPtr(), slot_id, std::move(callback)));
}

void SessionChapsClientImpl::DidGenerateKeyPair(
    SlotId slot_id,
    GenerateKeyPairCallback callback,
    uint64_t public_key_handle,
    uint64_t private_key_handle,
    uint32_t result_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (IsSessionError(result_code)) {
    sessions_map_.erase(slot_id);
  }
  return std::move(callback).Run(ObjectHandle(public_key_handle),
                                 ObjectHandle(private_key_handle), result_code);
}
//==============================================================================

void SessionChapsClientImpl::SaveSessionId(SlotId slot_id,
                                           base::OnceClosure callback,
                                           uint64_t session_id,
                                           uint32_t result_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (result_code == chromeos::PKCS11_CKR_OK) {
    sessions_map_[slot_id] = SessionId(session_id);
  } else {
    LOG(ERROR) << "Failed to open "
                  "session, error: "
               << result_code;
  }
  return std::move(callback).Run();
}

SessionChapsClient::SessionId SessionChapsClientImpl::GetSessionForSlot(
    SlotId slot_id) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  auto iter = sessions_map_.find(slot_id);
  if (iter == sessions_map_.end()) {
    return SessionId(chromeos::PKCS11_INVALID_SESSION_ID);
  }

  return iter->second;
}

}  // namespace kcer