blob: 6afda5a0c9f01f7833c16f35b4168e4097eaa7db [file] [log] [blame]
// 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 "chrome/browser/webauthn/gpm_enclave_transaction.h"
#include <algorithm>
#include <tuple>
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/task/thread_pool.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "chrome/browser/ui/webauthn/user_actions.h"
#include "chrome/browser/webauthn/proto/enclave_local_state.pb.h"
#include "components/device_event_log/device_event_log.h"
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
#include "components/webauthn/core/browser/passkey_model.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/enclave/types.h"
#include "device/fido/features.h"
#include "device/fido/fido_constants.h"
namespace {
using UserPresentAndVerifiedBits = device::enclave::UserPresentAndVerifiedBits;
void MaybeRecordUserActionForWinUv(device::FidoRequestType request_type,
EnclaveUserVerificationMethod uv_method) {
#if BUILDFLAG(IS_WIN)
if (uv_method == EnclaveUserVerificationMethod::kUVKeyWithSystemUI ||
uv_method == EnclaveUserVerificationMethod::kDeferredUVKeyWithSystemUI) {
webauthn::user_actions::RecordGpmWinUvShown(request_type);
}
#endif // BUILDFLAG(IS_WIN)
}
base::OnceCallback<void(bool)> UvKeyDeferralCallback(
base::OnceClosure success_closure,
base::OnceClosure failure_closure) {
return base::BindOnce(
[](base::OnceClosure success_closure, base::OnceClosure failure_closure,
bool success) {
if (success) {
std::move(success_closure).Run();
return;
}
std::move(failure_closure).Run();
},
std::move(success_closure), std::move(failure_closure));
}
} // namespace
GPMEnclaveTransaction::GPMEnclaveTransaction(
Delegate* delegate,
webauthn::PasskeyModel* passkey_model,
device::FidoRequestType request_type,
std::string rp_id,
EnclaveManager* enclave_manager,
std::optional<std::string> pin,
std::optional<std::vector<uint8_t>> selected_credential_id,
EnclaveRequestCallback enclave_request_callback)
: delegate_(delegate),
passkey_model_(passkey_model),
request_type_(request_type),
rp_id_(std::move(rp_id)),
enclave_manager_(enclave_manager),
pin_(std::move(pin)),
selected_credential_id_(std::move(selected_credential_id)),
enclave_request_callback_(std::move(enclave_request_callback)) {
CHECK(delegate_);
CHECK(passkey_model_);
CHECK(enclave_manager_);
CHECK((request_type_ == device::FidoRequestType::kMakeCredential) ^
selected_credential_id_.has_value());
CHECK(enclave_request_callback_);
}
GPMEnclaveTransaction::~GPMEnclaveTransaction() = default;
void GPMEnclaveTransaction::Start() {
access_token_fetcher_ = enclave_manager_->GetAccessToken(base::BindOnce(
&GPMEnclaveTransaction::MaybeHashPinAndStartEnclaveTransaction,
weak_ptr_factory_.GetWeakPtr()));
}
void GPMEnclaveTransaction::MaybeHashPinAndStartEnclaveTransaction(
std::optional<std::string> token) {
if (!pin_) {
StartEnclaveTransaction(std::move(token), nullptr);
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
base::BindOnce(
[](std::string pin,
std::unique_ptr<webauthn_pb::EnclaveLocalState_WrappedPIN>
wrapped_pin) -> std::unique_ptr<device::enclave::ClaimedPIN> {
return EnclaveManager::MakeClaimedPINSlowly(std::move(pin),
std::move(wrapped_pin));
},
*pin_, enclave_manager_->GetWrappedPIN()),
base::BindOnce(&GPMEnclaveTransaction::StartEnclaveTransaction,
weak_ptr_factory_.GetWeakPtr(), std::move(token)));
}
void GPMEnclaveTransaction::StartEnclaveTransaction(
std::optional<std::string> token,
std::unique_ptr<device::enclave::ClaimedPIN> claimed_pin) {
// The UI has advanced to the point where it wants to perform an enclave
// transaction. This code collects the needed values and triggers
// `enclave_request_callback_` which surfaces in
// `EnclaveDiscovery::OnUIRequest`.
if (!token || !enclave_manager_->is_ready()) {
delegate_->HandleEnclaveTransactionError();
return;
}
EnclaveUserVerificationMethod uv_method = delegate_->GetUvMethod();
CHECK(uv_method != EnclaveUserVerificationMethod::kPIN || pin_.has_value());
if (uv_method == EnclaveUserVerificationMethod::kDeferredUVKeyWithSystemUI &&
enclave_manager_->deferred_uv_key_creation_locked()) {
// Another transaction is trying to create a UV key. Provide a callback that
// will try again.
enclave_manager_->AddPendingUvRequest(UvKeyDeferralCallback(
base::BindOnce(&GPMEnclaveTransaction::StartEnclaveTransaction,
weak_ptr_factory_.GetWeakPtr(), std::move(token),
std::move(claimed_pin)),
base::BindOnce(
[](base::WeakPtr<GPMEnclaveTransaction> transaction) {
if (!transaction) {
return;
}
transaction->delegate_->HandleEnclaveTransactionError();
},
weak_ptr_factory_.GetWeakPtr())));
return;
}
auto request = std::make_unique<device::enclave::CredentialRequest>();
request->access_token = std::move(*token);
// A request to the enclave can either provide a wrapped secret, which only
// the enclave can decrypt, or can provide the security domain secret
// directly. The latter is only possible immediately after registering a
// device because that's the only time that the actual security domain secret
// is in memory.
bool use_unwrapped_secret = false;
switch (uv_method) {
case EnclaveUserVerificationMethod::kUserPresenceOnly:
request->signing_callback =
enclave_manager_->IdentityKeySigningCallback();
request->up_and_uv_bits = UserPresentAndVerifiedBits::kPresentOnly;
break;
case EnclaveUserVerificationMethod::kImplicit:
request->signing_callback =
enclave_manager_->IdentityKeySigningCallback();
use_unwrapped_secret = true;
request->up_and_uv_bits = UserPresentAndVerifiedBits::kPresentAndVerified;
break;
case EnclaveUserVerificationMethod::kPIN:
request->signing_callback =
enclave_manager_->IdentityKeySigningCallback();
CHECK(claimed_pin);
request->claimed_pin = std::move(claimed_pin);
request->pin_result_callback =
base::BindOnce(&GPMEnclaveTransaction::HandlePINValidationResult,
weak_ptr_factory_.GetWeakPtr());
request->up_and_uv_bits = UserPresentAndVerifiedBits::kPresentAndVerified;
break;
case EnclaveUserVerificationMethod::kUVKeyWithChromeUI:
case EnclaveUserVerificationMethod::kUVKeyWithSystemUI: {
EnclaveManager::UVKeyOptions uv_options;
delegate_->BuildUVKeyOptions(uv_options);
request->signing_callback =
enclave_manager_->UserVerifyingKeySigningCallback(
std::move(uv_options));
request->up_and_uv_bits = UserPresentAndVerifiedBits::kPresentAndVerified;
MaybeRecordUserActionForWinUv(request_type_, uv_method);
break;
}
case EnclaveUserVerificationMethod::kDeferredUVKeyWithSystemUI:
// This submits a UV key, but is signed with the HW key. We still count
// it as being user verified because this will trigger UV creation and
// the system will verify the user for that operation.
request->signing_callback =
enclave_manager_->IdentityKeySigningCallback();
request->up_and_uv_bits = UserPresentAndVerifiedBits::kPresentAndVerified;
std::tie(uv_key_lock_, request->uv_key_creation_callback) =
enclave_manager_->UserVerifyingKeyCreationCallback();
MaybeRecordUserActionForWinUv(request_type_, uv_method);
break;
case EnclaveUserVerificationMethod::kNoUserVerificationAndNoUserPresence:
request->signing_callback =
enclave_manager_->IdentityKeySigningCallback();
request->up_and_uv_bits = UserPresentAndVerifiedBits::kNeither;
break;
case EnclaveUserVerificationMethod::kUnsatisfiable:
NOTREACHED();
}
request->unregister_callback =
base::BindOnce(&EnclaveManager::Unenroll, enclave_manager_->GetWeakPtr(),
base::DoNothing());
switch (request_type_) {
case device::FidoRequestType::kMakeCredential: {
if (use_unwrapped_secret) {
std::tie(request->key_version, request->secret) =
enclave_manager_->TakeSecret().value();
} else {
std::tie(request->key_version, request->wrapped_secret) =
enclave_manager_->GetCurrentWrappedSecret();
}
request->save_passkey_callback =
base::BindOnce(&GPMEnclaveTransaction::OnPasskeyCreated,
weak_ptr_factory_.GetWeakPtr());
std::vector<std::vector<uint8_t>> existing_credential_ids;
std::ranges::transform(
passkey_model_->GetPasskeysForRelyingPartyId(rp_id_),
std::back_inserter(existing_credential_ids),
[](const sync_pb::WebauthnCredentialSpecifics& cred) {
const std::string& cred_id = cred.credential_id();
return std::vector<uint8_t>(cred_id.begin(), cred_id.end());
});
request->existing_cred_ids = std::move(existing_credential_ids);
break;
}
case device::FidoRequestType::kGetAssertion: {
CHECK(selected_credential_id_);
std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> selected_credential;
std::vector<sync_pb::WebauthnCredentialSpecifics> credentials =
passkey_model_->GetPasskeysForRelyingPartyId(rp_id_);
for (auto& cred : credentials) {
if (std::ranges::equal(base::as_byte_span(cred.credential_id()),
base::span(*selected_credential_id_))) {
selected_credential =
std::make_unique<sync_pb::WebauthnCredentialSpecifics>(
std::move(cred));
request->save_passkey_callback = base::BindOnce(
[](base::WeakPtr<GPMEnclaveTransaction> txn,
sync_pb::WebauthnCredentialSpecifics passkey) {
if (txn) {
txn->OnPasskeyEncryptedBlobUpdated(passkey.credential_id(),
passkey.encrypted());
}
},
weak_ptr_factory_.GetWeakPtr());
break;
}
}
if (!selected_credential) {
// The credential may not be available if it was deleted during the
// WebAuthn request. This can be done by the user or the website. This
// should not be normal, jump immediately to an error state.
FIDO_LOG(ERROR) << "Could not find credential";
delegate_->HandleEnclaveTransactionError();
return;
}
passkey_model_->UpdatePasskeyTimestamp(
selected_credential->credential_id(), base::Time::Now());
if (use_unwrapped_secret) {
std::tie(std::ignore, request->secret) =
enclave_manager_->TakeSecret().value();
} else {
if (selected_credential->key_version()) {
std::optional<std::vector<uint8_t>> wrapped_secret =
enclave_manager_->GetWrappedSecret(
selected_credential->key_version());
if (wrapped_secret) {
request->wrapped_secret = std::move(*wrapped_secret);
} else {
FIDO_LOG(ERROR)
<< "Unexpectedly did not have a wrapped key for epoch "
<< selected_credential->key_version();
}
}
if (!request->wrapped_secret.has_value()) {
request->wrapped_secret =
enclave_manager_->GetCurrentWrappedSecret().second;
}
}
request->entity = std::move(selected_credential);
break;
}
}
CHECK(request->wrapped_secret.has_value() ^ request->secret.has_value());
enclave_request_callback_.Run(std::move(request));
}
void GPMEnclaveTransaction::HandlePINValidationResult(
device::enclave::PINValidationResult result) {
delegate_->HandlePINValidationResult(result);
}
void GPMEnclaveTransaction::OnPasskeyCreated(
sync_pb::WebauthnCredentialSpecifics passkey) {
passkey_model_->CreatePasskey(passkey);
delegate_->OnPasskeyCreated(passkey);
}
void GPMEnclaveTransaction::OnPasskeyEncryptedBlobUpdated(
const std::string& credential_id,
const std::string& encrypted_data) {
passkey_model_->UpdatePasskeyEncryptedBlob(credential_id, encrypted_data);
}