blob: 7696fd2979c8dc923aa3edb435cc0f6ec78a0f52 [file]
// 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/enclave_manager.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <limits>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <variant>
#include <vector>
#include "base/barrier_callback.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/numerics/checked_math.h"
#include "base/numerics/safe_conversions.h"
#include "base/sequence_checker.h"
#include "base/stl_util.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_view_util.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/types/expected.h"
#include "base/types/strong_alias.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/webauthn/enclave_manager_interface.h"
#include "chrome/browser/webauthn/proto/enclave_local_state.pb.h"
#include "chrome/browser/webauthn/unexportable_key_utils.h"
#include "chrome/browser/webauthn/webauthn_metrics_util.h"
#include "components/cbor/diagnostic_writer.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "components/device_event_log/device_event_log.h"
#include "components/os_crypt/async/browser/os_crypt_async.h"
#include "components/os_crypt/async/common/encryptor.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
#include "components/signin/public/identity_manager/primary_account_change_event.h"
#include "components/trusted_vault/frontend_trusted_vault_connection.h"
#include "components/trusted_vault/proto/recovery_key_store.pb.h"
#include "components/trusted_vault/recovery_key_store_connection.h"
#include "components/trusted_vault/recovery_key_store_connection_impl.h"
#include "components/trusted_vault/securebox.h"
#include "components/trusted_vault/trusted_vault_access_token_fetcher_frontend.h"
#include "components/trusted_vault/trusted_vault_access_token_fetcher_impl.h"
#include "components/trusted_vault/trusted_vault_connection.h"
#include "components/trusted_vault/trusted_vault_server_constants.h"
#include "components/unexportable_keys/ref_counted_unexportable_key.h"
#include "components/unexportable_keys/unexportable_key_id.h"
#include "content/public/browser/render_frame_host.h"
#include "crypto/aead.h"
#include "crypto/hash.h"
#include "crypto/kdf.h"
#include "crypto/keypair.h"
#include "crypto/openssl_util.h"
#include "crypto/random.h"
#include "crypto/sha2.h"
#include "crypto/subtle_passkey.h"
#include "crypto/unexportable_key.h"
#include "crypto/user_verifying_key.h"
#include "device/fido/enclave/constants.h"
#include "device/fido/enclave/enclave_authenticator.h"
#include "device/fido/enclave/transact.h"
#include "device/fido/enclave/types.h"
#include "device/fido/network_context_factory.h"
#include "device/fido/public/features.h"
#include "google_apis/gaia/core_account_id.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_id.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "net/base/url_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "third_party/boringssl/src/include/openssl/base.h"
#include "third_party/boringssl/src/include/openssl/bytestring.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "ash/shell.h"
#endif
#if BUILDFLAG(IS_MAC)
#include "components/trusted_vault/icloud_recovery_key_mac.h"
#endif // BUILDFLAG(IS_MAC)
namespace enclave = device::enclave;
using trusted_vault::TrustedVaultKeyAndVersion;
using webauthn_pb::EnclaveLocalState;
namespace {
std::string ToString(EnclaveManager::ActionForUMA action) {
std::string_view action_string;
switch (action) {
case EnclaveManager::ActionForUMA::kRegisterIfNeeded:
action_string = "RegisterIfNeeded";
break;
case EnclaveManager::ActionForUMA::kSetupWithPIN:
action_string = "SetupWithPIN";
break;
case EnclaveManager::ActionForUMA::kAddDeviceToAccount:
action_string = "AddDeviceToAccount";
break;
case EnclaveManager::ActionForUMA::kAddDeviceAndPINToAccount:
action_string = "AddDeviceAndPINToAccount";
break;
case EnclaveManager::ActionForUMA::kSetPIN:
action_string = "SetPIN";
break;
case EnclaveManager::ActionForUMA::kChangePIN:
action_string = "ChangePIN";
break;
#if BUILDFLAG(IS_MAC)
case EnclaveManager::ActionForUMA::kAddICloudRecoveryKey:
action_string = "AddICloudRecoveryKey";
break;
#endif // BUILDFLAG(IS_MAC)
case EnclaveManager::ActionForUMA::kUnenroll:
action_string = "Unenroll";
break;
case EnclaveManager::ActionForUMA::kConsiderSecurityDomainState:
action_string = "ConsiderSecurityDomainState";
break;
}
return base::StrCat(
{"WebAuthentication.Enclave.ActionOutcome.", action_string});
}
} // namespace
// Holds the arguments to `StoreKeys` so that they can be processed when the
// state machine is ready for them.
struct EnclaveManager::StoreKeysArgs {
GaiaId gaia_id;
std::vector<TrustedVaultKeyAndVersion> keys;
};
struct EnclaveManager::PendingAction {
base::OnceCallback<void(ActionOutcome)> callback;
base::flat_set<GaiaId> gaia_ids_to_remove;
bool want_registration = false;
bool renew_pin = false;
std::unique_ptr<StoreKeysArgs> store_keys_args;
std::unique_ptr<StoreKeysArgs> opportunistic_store_keys_args;
bool setup_account = false;
std::string pin; // the PIN to add to set up an account with.
std::string set_pin; // the PIN to set on an existing account.
std::string updated_pin; // a new PIN, to replace the current PIN.
std::string rapt; // ReAuthentication Proof Token.
bool update_wrapped_pin; // copy `wrapped_pin` to the state.
std::unique_ptr<EnclaveLocalState::WrappedPIN> wrapped_pin;
std::optional<std::string> pin_public_key; // the current PIN PK in the SDS.
#if BUILDFLAG(IS_MAC)
std::unique_ptr<trusted_vault::ICloudRecoveryKey> icloud_recovery_key;
#endif // BUILDFLAG(IS_MAC)
bool unregister = false; // whether to unregister from the enclave.
};
base::OnceCallback<void(EnclaveManager::ActionOutcome)>
EnclaveManager::ToActionOutcomeCallback(EnclaveManager::Callback callback,
ActionForUMA action) {
return base::BindOnce(
[](ActionForUMA action, EnclaveManager::Callback callback,
ActionOutcome outcome) {
base::UmaHistogramEnumeration(ToString(action), outcome);
std::move(callback).Run(outcome == ActionOutcome::kSuccess);
},
action, std::move(callback));
}
EnclaveManager::StoreKeysLock::StoreKeysLock(
base::WeakPtr<EnclaveManager> manager)
: manager_(std::move(manager)) {}
EnclaveManager::StoreKeysLock::~StoreKeysLock() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!manager_) {
return;
}
CHECK_GT(manager_->store_keys_lock_depth_, 0u);
manager_->store_keys_lock_depth_--;
}
namespace webauthn {
crypto::SubtlePassKey MakeCryptoPassKey() {
return crypto::SubtlePassKey();
}
} // namespace webauthn
namespace {
// Used so the EnclaveManager can be forced into invalid states for testing.
static bool g_invariant_override_ = false;
// The maximum number of bytes that will be downloaded from the above two URLs.
constexpr size_t kMaxFetchBodyBytes = 128 * 1024;
// The number of days between GPM PIN Vault refreshes.
constexpr int kRefreshDays = 30;
const net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("recovery_key_store_fetch", R"(
semantics {
sender: "Google Password Manager"
description:
"If a user enrolls a Google Password Manager PIN, it is hashed and "
"sent to the Recovery Key Store so that they can recover their "
"credentials with it in the future. This key store involves "
"dedicated hardware to limit the number of guesses permitted. The "
"PIN hash is encrypted directly to this hardware and these network "
"fetches cover downloading the neccessary public key and uploading "
"the encrypted package to the key store."
trigger:
"A user enrolls a PIN in Google Password Manager."
user_data {
type: ACCESS_TOKEN
}
data: "An encrypted PIN."
internal {
contacts {
email: "chrome-webauthn@google.com"
}
}
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2024-02-08"
}
policy {
cookies_allowed: NO
setting: "Users can disable this feature by opening settings "
"and signing out of the Google account in their profile, or by "
"disabling password sync on the profile. Password sync can be "
"disabled from the Sync and Google Services screen."
chrome_policy {
SyncDisabled {
SyncDisabled: true
}
SyncTypesListDisabled {
SyncTypesListDisabled: {
entries: "passwords"
}
}
}
})");
// This prefix is the protobuf encoding for a 32-byte value with tag 1024.
// This means that, with the hash appended, the serialised state file is still a
// valid protobuf, which is handy for debugging.
static const uint8_t kHashPrefix[] = {0x82, 0x40, 32};
// The parsed response to an enclave "recovery_key_store/wrap" command.
struct EnclaveRecoveryKeyStoreWrapResponse {
EnclaveRecoveryKeyStoreWrapResponse() = default;
~EnclaveRecoveryKeyStoreWrapResponse() = default;
EnclaveRecoveryKeyStoreWrapResponse(
const EnclaveRecoveryKeyStoreWrapResponse& other) = delete;
EnclaveRecoveryKeyStoreWrapResponse& operator=(
const EnclaveRecoveryKeyStoreWrapResponse& other) = delete;
EnclaveRecoveryKeyStoreWrapResponse(
EnclaveRecoveryKeyStoreWrapResponse&& other) = default;
EnclaveRecoveryKeyStoreWrapResponse& operator=(
EnclaveRecoveryKeyStoreWrapResponse&& other) = default;
// The protobuf that can be sent to the recovery key store.
std::unique_ptr<trusted_vault_pb::Vault> vault;
// The chosen cohort public key.
std::vector<uint8_t> cohort_public_key;
// The cert.xml serial number used to select a cohort.
int cert_xml_serial_number;
};
// Since protobuf maps `bytes` to `std::string` (rather than
// `std::vector<uint8_t>`), functions for jumping between these representations
// are needed.
template <size_t N>
base::span<const uint8_t, N> ToSizedSpan(const std::string& s) {
CHECK_EQ(s.size(), N);
return base::span<const uint8_t, N>(base::as_byte_span(s));
}
template <size_t N>
std::array<uint8_t, N> ToArray(base::span<const uint8_t, N> in) {
std::array<uint8_t, N> ret;
std::ranges::copy(in, ret.begin());
return ret;
}
std::vector<uint8_t> ToVector(const std::string& s) {
const auto span = base::as_byte_span(s);
return std::vector<uint8_t>(span.begin(), span.end());
}
std::string VecToString(base::span<const uint8_t> v) {
return std::string(base::as_string_view(v));
}
bool IsValidSubjectPublicKeyInfo(base::span<const uint8_t> spki) {
return crypto::keypair::PublicKey::FromSubjectPublicKeyInfo(spki).has_value();
}
bool IsValidUncompressedP256X962(base::span<const uint8_t> x962) {
return crypto::keypair::PublicKey::FromEcP256Point(x962).has_value();
}
std::optional<int> CheckPINInvariants(
const EnclaveLocalState::WrappedPIN& wrapped_pin) {
// The nonce is 12 bytes, and the tag is 16 bytes, so this establishes
// a lower-bound of one byte of plaintext.
if (wrapped_pin.wrapped_pin().size() < 12 + 1 + 16) {
return __LINE__;
}
if (wrapped_pin.claim_key().size() != 32) {
return __LINE__;
}
if (wrapped_pin.form() == wrapped_pin.FORM_UNSPECIFIED) {
return __LINE__;
}
if (wrapped_pin.hash() == wrapped_pin.HASH_UNSPECIFIED) {
return __LINE__;
}
if (wrapped_pin.hash_difficulty() <= 0) {
return __LINE__;
}
if (wrapped_pin.hash_salt().empty()) {
return __LINE__;
}
return std::nullopt;
}
// CheckInvariants checks all the invariants of `user`, returning either a
// line-number for the failing check, or else `nullopt` to indicate success.
std::optional<int> CheckInvariants(const EnclaveLocalState::User& user) {
if (g_invariant_override_) {
return std::nullopt;
}
if (user.wrapped_identity_private_key().empty() !=
user.identity_public_key().empty()) {
return __LINE__;
}
if (!user.identity_public_key().empty() &&
!IsValidSubjectPublicKeyInfo(
base::as_byte_span(user.identity_public_key()))) {
return __LINE__;
}
if (user.wrapped_identity_private_key().empty() != user.device_id().empty()) {
return __LINE__;
}
if (user.wrapped_uv_private_key().empty() != user.uv_public_key().empty()) {
return __LINE__;
}
if (!user.uv_public_key().empty() &&
!IsValidSubjectPublicKeyInfo(base::as_byte_span(user.uv_public_key()))) {
return __LINE__;
}
if (user.registered() && user.wrapped_identity_private_key().empty()) {
return __LINE__;
}
if (user.registered() != !user.wrapped_member_private_key().empty()) {
return __LINE__;
}
if (user.wrapped_member_private_key().empty() !=
user.member_public_key().empty()) {
return __LINE__;
}
if (!user.member_public_key().empty() &&
!IsValidUncompressedP256X962(
base::as_byte_span(user.member_public_key()))) {
return __LINE__;
}
if (user.joined() && !user.registered()) {
return __LINE__;
}
if (!user.wrapped_security_domain_secrets().empty() != user.joined()) {
return __LINE__;
}
if (user.has_wrapped_pin()) {
return CheckPINInvariants(user.wrapped_pin());
}
if (user.deferred_uv_key_creation() &&
!user.wrapped_uv_private_key().empty()) {
return __LINE__;
}
return std::nullopt;
}
// Parses the wrapped_pin value from an enclave CBOR response.
std::optional<std::string> ParseWrappedPinFromCbor(
const cbor::Value& response) {
const cbor::Value::MapValue& response_map = response.GetArray()[0].GetMap();
const cbor::Value& ok_response =
response_map.find(cbor::Value(enclave::kResponseSuccessKey))->second;
if (!ok_response.is_map()) {
FIDO_LOG(ERROR) << "PIN change response is not a map: "
<< cbor::DiagnosticWriter::Write(response);
return std::nullopt;
}
const cbor::Value::MapValue& ok_response_map = ok_response.GetMap();
const auto wrapped_pin_value =
ok_response_map.find(cbor::Value(enclave::kWrappedPinKey));
if (wrapped_pin_value == ok_response_map.end() ||
!wrapped_pin_value->second.is_bytestring()) {
FIDO_LOG(ERROR) << "Wrapped PIN was not a bytestring";
return std::nullopt;
}
return VecToString(wrapped_pin_value->second.GetBytestring());
}
// Build an enclave request that registers a new device and requests a new
// wrapped asymmetric key which will be used to join the security domain.
cbor::Value BuildRegistrationMessage(
const std::string& device_id,
const crypto::UnexportableSigningKey& identity_key,
scoped_refptr<crypto::RefCountedUserVerifyingSigningKey> uv_key,
bool defer_uv_key) {
cbor::Value::MapValue pub_keys;
const char* key_type = identity_key.IsHardwareBacked()
? enclave::kHardwareKey
: enclave::kSoftwareKey;
pub_keys.emplace(key_type, identity_key.GetSubjectPublicKeyInfo());
if (uv_key) {
const char* uv_key_type = uv_key->key().IsHardwareBacked()
? enclave::kUserVerificationKey
: enclave::kSoftwareUserVerificationKey;
pub_keys.emplace(uv_key_type, uv_key->key().GetPublicKey());
}
cbor::Value::MapValue request1;
request1.emplace(enclave::kRequestCommandKey, enclave::kRegisterCommandName);
request1.emplace(enclave::kRegisterDeviceIdKey,
std::vector<uint8_t>(device_id.begin(), device_id.end()));
request1.emplace(enclave::kRegisterPubKeysKey, std::move(pub_keys));
if (defer_uv_key) {
CHECK(!uv_key);
// The enclave ignores the value. The presence of the entry signals that the
// UV key is pending.
request1.emplace(enclave::kRegisterUVKeyPending, true);
}
cbor::Value::MapValue request2;
request2.emplace(enclave::kRequestCommandKey,
enclave::kGenKeyPairCommandName);
request2.emplace(enclave::kWrappingPurpose,
enclave::kKeyPurposeSecurityDomainMemberKey);
cbor::Value::ArrayValue requests;
requests.emplace_back(std::move(request1));
requests.emplace_back(std::move(request2));
return cbor::Value(std::move(requests));
}
cbor::Value BuildUnregisterMessage(const std::string& device_id) {
cbor::Value::MapValue request;
request.emplace(enclave::kRequestCommandKey, enclave::kForgetCommandName);
request.emplace(enclave::kRegisterDeviceIdKey,
std::vector<uint8_t>(device_id.begin(), device_id.end()));
cbor::Value::ArrayValue requests;
requests.emplace_back(std::move(request));
return cbor::Value(std::move(requests));
}
EnclaveLocalState::User* StateForUser(EnclaveLocalState* local_state,
const CoreAccountInfo& account) {
auto it = local_state->mutable_users()->find(account.gaia.ToString());
if (it == local_state->mutable_users()->end()) {
return nullptr;
}
return &(it->second);
}
EnclaveLocalState::User* CreateStateForUser(EnclaveLocalState* local_state,
const CoreAccountInfo& account) {
auto pair = local_state->mutable_users()->insert(
{account.gaia.ToString(), EnclaveLocalState::User()});
CHECK(pair.second);
return &(pair.first->second);
}
// Returns true if `response` contains exactly `num_responses` results, and none
// of them is an error. This is used for checking whether an enclave response is
// successful or not.
bool IsAllOk(const cbor::Value& response, const size_t num_responses) {
if (!response.is_array()) {
return false;
}
const cbor::Value::ArrayValue& responses = response.GetArray();
if (responses.size() != num_responses) {
return false;
}
for (size_t i = 0; i < num_responses; i++) {
const cbor::Value& inner_response = responses[i];
if (!inner_response.is_map()) {
return false;
}
const cbor::Value::MapValue& inner_response_map = inner_response.GetMap();
if (inner_response_map.find(cbor::Value(enclave::kResponseSuccessKey)) ==
inner_response_map.end()) {
return false;
}
}
return true;
}
// Returns the request error, if present, for the |error_index|th response
// returned by the enclave. Returns nullopt for debug errors.
std::optional<device::enclave::RequestError> GetRequestError(
const cbor::Value& response,
const size_t error_index) {
if (!response.is_array()) {
return std::nullopt;
}
const cbor::Value::ArrayValue& responses = response.GetArray();
if (responses.size() <= error_index) {
return std::nullopt;
}
const cbor::Value& inner_response = responses.at(error_index);
if (!inner_response.is_map()) {
return std::nullopt;
}
const cbor::Value::MapValue& inner_response_map = inner_response.GetMap();
const auto error_it =
inner_response_map.find(cbor::Value(enclave::kResponseErrorKey));
if (error_it == inner_response_map.end() || !error_it->second.is_integer()) {
return std::nullopt;
}
return device::enclave::GetRequestError(error_it->second.GetInteger());
}
// Update `user` with the wrapped security domain member key in `response`.
// This is used when registering with the enclave, which provides a wrapped
// asymmetric key that becomes the security domain member key for this device.
bool SetSecurityDomainMemberKey(EnclaveLocalState::User* user,
const cbor::Value& wrap_response) {
if (!wrap_response.is_map()) {
return false;
}
const cbor::Value::MapValue& map = wrap_response.GetMap();
const auto pub_it =
map.find(cbor::Value(enclave::kWrappingResponsePublicKey));
const auto priv_it =
map.find(cbor::Value(enclave::kWrappingResponseWrappedPrivateKey));
if (pub_it == map.end() || priv_it == map.end() ||
!pub_it->second.is_bytestring() || !priv_it->second.is_bytestring()) {
return false;
}
user->set_wrapped_member_private_key(
VecToString(priv_it->second.GetBytestring()));
user->set_member_public_key(VecToString(pub_it->second.GetBytestring()));
return true;
}
// Build an enclave request to wrap the given security domain secrets.
cbor::Value::ArrayValue BuildSecretWrappingEnclaveRequest(
const base::flat_map<int32_t, std::vector<uint8_t>>
new_security_domain_secrets) {
cbor::Value::ArrayValue requests;
for (const auto& it : new_security_domain_secrets) {
cbor::Value::MapValue request;
request.emplace(enclave::kRequestCommandKey, enclave::kWrapKeyCommandName);
request.emplace(enclave::kWrappingPurpose,
enclave::kKeyPurposeSecurityDomainSecret);
request.emplace(enclave::kWrappingKeyToWrap, it.second);
requests.emplace_back(std::move(request));
}
return requests;
}
// Build an enclave request to encrypt a PIN to the recovery key store.
cbor::Value::ArrayValue BuildRecoveryKeyStorePINWrappingEnclaveRequest(
base::span<const uint8_t> hashed_pin,
std::string cert_xml,
std::string sig_xml) {
cbor::Value::MapValue request;
request.emplace(enclave::kRequestCommandKey,
enclave::kRecoveryKeyStoreWrapCommandName);
request.emplace(enclave::kRecoveryKeyStorePinHash, hashed_pin);
request.emplace(enclave::kRecoveryKeyStoreCertXml, ToVector(cert_xml));
request.emplace(enclave::kRecoveryKeyStoreSigXml, ToVector(sig_xml));
cbor::Value::ArrayValue requests;
requests.emplace_back(std::move(request));
return requests;
}
// Build an enclave request for recovery_key_store/wrap_pin_and_secret, which
// wraps a PIN with the security domain secret, and creates Vault parameters for
// the PIN, wrapping the security domain secret.
cbor::Value BuildPINAndSecurityDomainSecretWrappingEnclaveRequest(
base::span<const uint8_t> hashed_pin,
base::span<const uint8_t, 32> claim_key,
std::string cert_xml,
std::string sig_xml,
base::span<const uint8_t> wrapped_secret) {
cbor::Value::MapValue request;
request.emplace(enclave::kRequestCommandKey,
enclave::kRecoveryKeyStoreWrapPinAndSecretCommandName);
request.emplace(enclave::kRecoveryKeyStorePinHash, hashed_pin);
request.emplace(enclave::kClaimKey, claim_key);
request.emplace(enclave::kRecoveryKeyStoreCertXml, ToVector(cert_xml));
request.emplace(enclave::kRecoveryKeyStoreSigXml, ToVector(sig_xml));
request.emplace(enclave::kRequestWrappedSecretKey, wrapped_secret);
return cbor::Value(request);
}
// Build an enclave request to renew a PIN.
cbor::Value BuildPINRenewalRequest(std::string cert_xml,
std::string sig_xml,
base::span<const uint8_t> wrapped_secret,
base::span<const uint8_t> wrapped_pin) {
cbor::Value::MapValue request;
request.emplace(enclave::kRequestCommandKey,
enclave::kRecoveryKeyStoreRewrapCommandName);
request.emplace(enclave::kRecoveryKeyStoreCertXml, ToVector(cert_xml));
request.emplace(enclave::kRecoveryKeyStoreSigXml, ToVector(sig_xml));
request.emplace(enclave::kRequestWrappedSecretKey, wrapped_secret);
request.emplace(enclave::kRequestWrappedPINDataKey, wrapped_pin);
request.emplace(enclave::kRecoveryKeyStoreCreateNewVault, true);
return cbor::Value(std::move(request));
}
cbor::Value ConcatEnclaveRequests(cbor::Value::ArrayValue head,
cbor::Value::ArrayValue tail) {
for (auto& request : tail) {
head.emplace_back(std::move(request));
}
return cbor::Value(std::move(head));
}
// Update `user` with the wrapped secrets in `response`. The
// `new_security_domain_secrets` argument is used to determine the version
// numbers of the wrapped secrets and this value must be the same as was passed
// to `BuildSecretWrappingEnclaveRequest` to generate the enclave request.
bool StoreWrappedSecrets(EnclaveLocalState::User* user,
const base::flat_map<int32_t, std::vector<uint8_t>>
new_security_domain_secrets,
base::span<const cbor::Value> responses) {
CHECK_EQ(new_security_domain_secrets.size(), responses.size());
size_t i = 0;
for (const auto& it : new_security_domain_secrets) {
const cbor::Value& wrapped_value =
responses[i++]
.GetMap()
.find(cbor::Value(enclave::kResponseSuccessKey))
->second;
if (!wrapped_value.is_bytestring()) {
return false;
}
const std::vector<uint8_t>& wrapped = wrapped_value.GetBytestring();
if (wrapped.empty()) {
return false;
}
user->mutable_wrapped_security_domain_secrets()->insert(
{it.first, VecToString(wrapped)});
}
return true;
}
const char* TrustedVaultRegistrationStatusToString(
trusted_vault::TrustedVaultRegistrationStatus status) {
switch (status) {
case trusted_vault::TrustedVaultRegistrationStatus::
kRegistrationNotAttempted:
return "RegistrationNotAttempted";
case trusted_vault::TrustedVaultRegistrationStatus::kRegistrationCancelled:
return "RegistrationCancelled";
case trusted_vault::TrustedVaultRegistrationStatus::kSuccess:
return "Success";
case trusted_vault::TrustedVaultRegistrationStatus::kAlreadyRegistered:
return "AlreadyRegistered";
case trusted_vault::TrustedVaultRegistrationStatus::kLocalDataObsolete:
return "LocalDataObsolete";
case trusted_vault::TrustedVaultRegistrationStatus::
kTransientAccessTokenFetchError:
return "TransientAccessTokenFetchError";
case trusted_vault::TrustedVaultRegistrationStatus::
kPersistentAccessTokenFetchError:
return "PersistentAccessTokenFetchError";
case trusted_vault::TrustedVaultRegistrationStatus::
kPrimaryAccountChangeAccessTokenFetchError:
return "PrimaryAccountChangeAccessTokenFetchError";
case trusted_vault::TrustedVaultRegistrationStatus::kNetworkError:
return "NetworkError";
case trusted_vault::TrustedVaultRegistrationStatus::kOtherError:
return "OtherError";
}
}
// Parse the contents of the decrypted state file. In the event of an error, an
// empty state is returned. This causes a corrupt state file to reset the
// enclave state for the current profile. Users will have to re-register with
// the enclave.
std::unique_ptr<EnclaveLocalState> ParseStateFile(
const std::string& contents_str) {
auto ret = std::make_unique<EnclaveLocalState>();
const base::span<const uint8_t> contents = base::as_byte_span(contents_str);
if (contents.size() < crypto::kSHA256Length + sizeof(kHashPrefix)) {
FIDO_LOG(ERROR) << "Enclave state too small to be valid";
return ret;
}
const base::span<const uint8_t> digest = contents.last(crypto::kSHA256Length);
const base::span<const uint8_t> payload = contents.first(
contents.size() - crypto::kSHA256Length - sizeof(kHashPrefix));
const std::array<uint8_t, crypto::kSHA256Length> calculated =
crypto::SHA256Hash(payload);
if (calculated != digest) {
FIDO_LOG(ERROR) << "Checksum mismatch. Discarding state.";
return ret;
}
if (!ret->ParseFromArray(payload.data(), payload.size())) {
FIDO_LOG(ERROR) << "Parse failure loading enclave state";
// Just in case the failed parse left partial state, reset it.
ret = std::make_unique<EnclaveLocalState>();
}
return ret;
}
base::flat_set<GaiaId> GetGaiaIDs(
const std::vector<gaia::ListedAccount>& listed_accounts) {
return base::MakeFlatSet<GaiaId>(
listed_accounts, /*comp=*/{},
[](const gaia::ListedAccount& listed_account) {
return listed_account.gaia_id;
});
}
base::flat_set<GaiaId> GetGaiaIDs(
const google::protobuf::Map<std::string, EnclaveLocalState::User>& users) {
return base::MakeFlatSet<GaiaId>(
users, /*comp=*/{}, [](const auto& it) { return GaiaId(it.first); });
}
std::string UserVerifyingLabelToString(crypto::UserVerifyingKeyLabel label) {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \
BUILDFLAG(IS_CHROMEOS)
return label;
#else
return std::string("placeholder");
#endif
}
std::optional<crypto::UserVerifyingKeyLabel> UserVerifyingKeyLabelFromString(
std::string saved_label) {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \
BUILDFLAG(IS_CHROMEOS)
return saved_label;
#else
return std::nullopt;
#endif
}
// Returns a GURL from a feature param. If the feature does not result in a
// valid URL, the default is returned instead.
GURL GetUrl(const base::FeatureParam<std::string>& feature_param) {
GURL url(feature_param.Get());
if (url.is_valid()) {
return url;
}
FIDO_LOG(ERROR) << "Finch provided " << feature_param.name
<< " URL not valid: " << feature_param.Get();
GURL default_url(feature_param.default_value);
CHECK(default_url.is_valid());
return default_url;
}
// Fetch the contents of the given URL.
std::unique_ptr<network::SimpleURLLoader> FetchURL(
network::mojom::URLLoaderFactory* url_loader_factory,
const GURL& url,
base::OnceCallback<void(std::optional<std::string>)> callback) {
auto network_request = std::make_unique<network::ResourceRequest>();
CHECK(url.is_valid());
network_request->url = std::move(url);
auto loader = network::SimpleURLLoader::Create(std::move(network_request),
kTrafficAnnotation);
loader->SetTimeoutDuration(base::Seconds(10));
loader->SetURLLoaderFactoryOptions(
network::mojom::kURLLoadOptionBlockAllCookies);
loader->DownloadToString(url_loader_factory, std::move(callback),
kMaxFetchBodyBytes);
return loader;
}
// Takes a CBOR array of bytestrings and returns those bytestrings assembled
// into an ASN.1 SEQUENCE.
std::optional<std::string> CBORListOfBytestringToASN1Sequence(
const cbor::Value& array) {
if (!array.is_array()) {
return std::nullopt;
}
const std::vector<cbor::Value>& bytestrings = array.GetArray();
base::CheckedNumeric<size_t> total_bytes_checked = 0;
for (const auto& bytestring : bytestrings) {
if (!bytestring.is_bytestring()) {
return std::nullopt;
}
total_bytes_checked += bytestring.GetBytestring().size();
}
// 16 bytes is more than sufficient for the ASN.1 header that needs to be
// prepended. (If it were not then `CBB_finish` would fail, below, so this is
// not a memory-safety-load-bearing assumption.)
total_bytes_checked += 16;
if (!total_bytes_checked.IsValid()) {
return std::nullopt;
}
const size_t total_bytes = total_bytes_checked.ValueOrDie();
std::string cert_path;
cert_path.resize(total_bytes);
bssl::ScopedCBB cbb;
CBB_init_fixed(cbb.get(), reinterpret_cast<uint8_t*>(cert_path.data()),
cert_path.size());
CBB inner;
if (!CBB_add_asn1(cbb.get(), &inner, CBS_ASN1_SEQUENCE)) {
return std::nullopt;
}
for (const auto& bytestring : bytestrings) {
const std::vector<uint8_t>& bytes = bytestring.GetBytestring();
if (!CBB_add_bytes(&inner, bytes.data(), bytes.size())) {
return std::nullopt;
}
}
if (!CBB_flush(cbb.get())) {
return std::nullopt;
}
cert_path.resize(CBB_len(cbb.get()));
return cert_path;
}
// Stores public metadata about a PIN. This is recorded in, for example, the
// Vault metadata so that MagicArch can show the correct UI and accept GPM PIN
// entries.
struct PinMetadata {
static PinMetadata FromProto(const EnclaveLocalState::WrappedPIN& pin) {
return PinMetadata{
.n = pin.hash_difficulty(),
.is_six_digits =
pin.form() == EnclaveLocalState::WrappedPIN::FORM_SIX_DIGITS,
.salt = ToArray<16>(ToSizedSpan<16>(pin.hash_salt()))};
}
int n = 0; // The scrypt `N` parameter.
bool is_six_digits = false;
std::array<uint8_t, 16> salt;
};
// Convert the response to an enclave "recovery_key_store/wrap" command, into a
// protobuf that can be sent to the recovery key store service and extracts the
// data required to build a wrapped PIN.
std::optional<EnclaveRecoveryKeyStoreWrapResponse>
ParseRecoveryKeyStoreWrapResponse(
const PinMetadata& pin_metadata,
const cbor::Value& recovery_key_store_wrap_response) {
if (!recovery_key_store_wrap_response.is_map()) {
return std::nullopt;
}
const cbor::Value::MapValue& response =
recovery_key_store_wrap_response.GetMap();
cbor::Value::MapValue::const_iterator it;
#define GET_BYTESTRING(name) \
it = response.find(cbor::Value(#name)); \
if (it == response.end() || !it->second.is_bytestring()) { \
return std::nullopt; \
} \
const std::vector<uint8_t>& name = it->second.GetBytestring();
GET_BYTESTRING(cohort_public_key);
GET_BYTESTRING(encrypted_recovery_key);
GET_BYTESTRING(vault_handle);
GET_BYTESTRING(counter_id);
GET_BYTESTRING(app_public_key);
GET_BYTESTRING(wrapped_app_private_key);
GET_BYTESTRING(wrapped_wrapping_key);
#undef GET_BYTESTRING
it = response.find(cbor::Value("max_attempts"));
if (it == response.end() || !it->second.is_unsigned()) {
return std::nullopt;
}
const int64_t max_attempts = it->second.GetUnsigned();
if (max_attempts > std::numeric_limits<int32_t>::max()) {
return std::nullopt;
}
// "certs_in_path" contains an array of bytestrings. Each is an X.509
// certificate in the verified path from leaf to root, omitting the root
// itself. The protobuf wants this in an ASN.1 SEQUENCE.
it = response.find(cbor::Value("certs_in_path"));
if (it == response.end()) {
return std::nullopt;
}
std::optional<std::string> cert_path =
CBORListOfBytestringToASN1Sequence(it->second);
if (!cert_path) {
return std::nullopt;
}
int cert_xml_serial_number = 0;
it = response.find(cbor::Value("serial"));
if (it == response.end() || !it->second.is_integer()) {
return std::nullopt;
}
cert_xml_serial_number = it->second.GetInteger();
auto vault = std::make_unique<trusted_vault_pb::Vault>();
auto* params = vault->mutable_vault_parameters();
params->set_backend_public_key(VecToString(cohort_public_key));
params->set_counter_id(VecToString(counter_id));
params->set_max_attempts(base::checked_cast<int32_t>(max_attempts));
params->set_vault_handle(VecToString(vault_handle));
vault->set_recovery_key(VecToString(encrypted_recovery_key));
auto* app_key = vault->add_application_keys();
// This key name mirrors what Android sets.
app_key->set_key_name("security_domain_member_key_encrypted_locally");
auto* asymmetric_key_pair = app_key->mutable_asymmetric_key_pair();
asymmetric_key_pair->set_public_key(VecToString(app_public_key));
asymmetric_key_pair->set_wrapped_private_key(
VecToString(wrapped_app_private_key));
asymmetric_key_pair->set_wrapping_key(VecToString(wrapped_wrapping_key));
trusted_vault_pb::VaultMetadata metadata;
metadata.set_lskf_type(pin_metadata.is_six_digits
? trusted_vault_pb::VaultMetadata::PIN
: trusted_vault_pb::VaultMetadata::PASSWORD);
metadata.set_hash_type(trusted_vault_pb::VaultMetadata::SCRYPT);
metadata.set_hash_salt(VecToString(pin_metadata.salt));
metadata.set_hash_difficulty(pin_metadata.n);
metadata.set_cert_path(std::move(*cert_path));
std::string metadata_bytes;
if (!metadata.SerializeToString(&metadata_bytes)) {
return std::nullopt;
}
vault->set_vault_metadata(std::move(metadata_bytes));
EnclaveRecoveryKeyStoreWrapResponse result;
result.vault = std::move(vault);
result.cohort_public_key = std::move(cohort_public_key);
result.cert_xml_serial_number = cert_xml_serial_number;
return result;
}
base::flat_map<int32_t, std::vector<uint8_t>> GetNewSecretsToStore(
const EnclaveLocalState::User& user,
const EnclaveManager::StoreKeysArgs& args) {
const auto& existing = user.wrapped_security_domain_secrets();
base::flat_map<int32_t, std::vector<uint8_t>> new_secrets;
for (const TrustedVaultKeyAndVersion& key_with_version : args.keys) {
if (existing.find(key_with_version.version) == existing.end()) {
new_secrets.emplace(key_with_version.version, key_with_version.key);
}
}
return new_secrets;
}
#if BUILDFLAG(IS_CHROMEOS)
UserVerifyingKeyProviderConfigChromeos MakeUserVerifyingKeyConfig(
EnclaveManager::UVKeyOptions options) {
UserVerifyingKeyProviderConfigChromeos config{options.dialog_controller,
/*window=*/nullptr,
options.rp_id};
if (options.render_frame_host_id) {
auto* rfh = content::RenderFrameHost::FromID(options.render_frame_host_id);
// This is ultimately invoked from GpmEnclaveController, which can't outlive
// the RFH where the request originated.
CHECK(rfh);
config.window = rfh->GetNativeView()->GetToplevelWindow();
}
return config;
}
#else
crypto::UserVerifyingKeyProvider::Config MakeUserVerifyingKeyConfig(
EnclaveManager::UVKeyOptions options) {
crypto::UserVerifyingKeyProvider::Config config;
#if BUILDFLAG(IS_MAC)
config.keychain_access_group =
EnclaveManager::kEnclaveKeysKeychainAccessGroup;
config.lacontext = std::move(options.local_auth_token);
#endif // BUILDFLAG(IS_MAC)
return config;
}
#endif
std::unique_ptr<crypto::UserVerifyingKeyProvider>
GetUserVerifyingKeyProviderForSigning(EnclaveManager::UVKeyOptions options) {
return GetWebAuthnUserVerifyingKeyProvider(
MakeUserVerifyingKeyConfig(std::move(options)));
}
std::unique_ptr<crypto::UserVerifyingKeyProvider>
GetUserVerifyingKeyProviderForCreateAndDeleteOnly() {
// Passing an empty UVKeyOptions suffices to call
// `GenerateUserVerifyingSigningKey()` and `DeleteUserVerifyingSigningKey()`,
// but you must not attempt to generate a signature.
return GetWebAuthnUserVerifyingKeyProvider(
MakeUserVerifyingKeyConfig(EnclaveManager::UVKeyOptions{}));
}
struct HashedPIN {
~HashedPIN() { std::ranges::fill(hashed, 0); }
// Copies the values of this structure into a `WrappedPIN` protobuf with a
// random claim key. The inner `wrapped_pin` member is not set and needs to be
// filled in by the caller once that value is available.
std::unique_ptr<EnclaveLocalState::WrappedPIN> ToWrappedPIN() const {
uint8_t claim_key[32];
crypto::RandBytes(claim_key);
auto ret = std::make_unique<EnclaveLocalState::WrappedPIN>();
ret->set_claim_key(VecToString(claim_key));
ret->set_form(this->metadata.is_six_digits
? EnclaveLocalState::WrappedPIN::FORM_SIX_DIGITS
: EnclaveLocalState::WrappedPIN::FORM_ARBITRARY);
ret->set_hash(EnclaveLocalState::WrappedPIN::HASH_SCRYPT);
ret->set_hash_difficulty(this->metadata.n);
ret->set_hash_salt(VecToString(this->metadata.salt));
return ret;
}
PinMetadata metadata;
uint8_t hashed[32];
};
std::unique_ptr<HashedPIN> HashPINSlowly(std::string_view pin) {
auto hashed = std::make_unique<HashedPIN>();
crypto::RandBytes(hashed->metadata.salt);
// This is the primary work factor in scrypt. This value matches
// the original recommended parameters. Those are a little out
// of date in 2024, but Android is using 4096. Since this work
// factor falls on the server when MagicArch is used, I've stuck
// with this norm.
hashed->metadata.n = 16384;
hashed->metadata.is_six_digits =
pin.size() == 6 && std::ranges::all_of(pin, [](char c) -> bool {
return c >= '0' && c <= '9';
});
crypto::kdf::Scrypt({.cost = static_cast<uint64_t>(hashed->metadata.n),
.block_size = 8,
.parallelization = 1},
base::as_byte_span(pin), hashed->metadata.salt,
hashed->hashed, webauthn::MakeCryptoPassKey());
return hashed;
}
std::pair<int32_t, std::vector<uint8_t>> GetCurrentWrappedSecretForUser(
const EnclaveLocalState::User* user) {
CHECK(!user->wrapped_security_domain_secrets().empty());
std::optional<int32_t> max_version;
const std::string* max_wrapped_secret = nullptr;
for (const auto& it : user->wrapped_security_domain_secrets()) {
if (!max_version.has_value() || *max_version < it.first) {
max_version = it.first;
max_wrapped_secret = &it.second;
}
}
return std::make_pair(*max_version, ToVector(*max_wrapped_secret));
}
// Parse a Vault and security domain member keys from a CBOR map. These maps
// result from enclave operations that return a Vault for insertion into the
// security domain.
static std::optional<std::pair<EnclaveRecoveryKeyStoreWrapResponse,
trusted_vault::MemberKeysSource>>
ParseVaultAndMemberResponse(const int32_t key_version,
const PinMetadata& pin_metadata,
const cbor::Value::MapValue& response) {
auto it = response.find(cbor::Value("wrapped"));
if (it == response.end()) {
FIDO_LOG(ERROR) << "response missing 'wrapped'";
return std::nullopt;
}
std::optional<EnclaveRecoveryKeyStoreWrapResponse> wrap_response =
ParseRecoveryKeyStoreWrapResponse(pin_metadata, it->second);
if (!wrap_response) {
FIDO_LOG(ERROR) << "Failed to translate response into an UpdateVaultProto";
return std::nullopt;
}
it = response.find(cbor::Value("wrapped_sds"));
if (it == response.end() || !it->second.is_bytestring()) {
FIDO_LOG(ERROR) << "response has invalid 'wrapped_sds'";
return std::nullopt;
}
const std::vector<uint8_t>& wrapped_sds = it->second.GetBytestring();
it = response.find(cbor::Value("member_proof"));
if (it == response.end() || !it->second.is_bytestring()) {
FIDO_LOG(ERROR) << "response has invalid 'member_proof'";
return std::nullopt;
}
const std::vector<uint8_t>& member_proof = it->second.GetBytestring();
auto member_keys_source =
trusted_vault::MemberKeys(key_version, wrapped_sds, member_proof);
return std::make_pair(std::move(*wrap_response),
std::move(member_keys_source));
}
class UvKeyCreationLockImpl : public EnclaveManager::UvKeyCreationLock {
public:
explicit UvKeyCreationLockImpl(base::OnceClosure release_callback) {
on_release_ = std::move(release_callback);
}
~UvKeyCreationLockImpl() override { std::move(on_release_).Run(); }
private:
base::OnceClosure on_release_;
};
webauthn::metrics::WebAuthenticationGPMRecoveryEvent
ToWebAuthenticationGPMRecoveryEvent(
EnclaveManager::OutOfContextRecoveryOutcome outcome) {
switch (outcome) {
case EnclaveManager::OutOfContextRecoveryOutcome::
kStoreKeysFromOpportunisticFlowSucceeded:
return webauthn::metrics::WebAuthenticationGPMRecoveryEvent::
kStoreKeysFromOpportunisticFlowSucceeded;
case EnclaveManager::OutOfContextRecoveryOutcome::
kStoreKeysFromOpportunisticFlowIgnoredRedundant:
return webauthn::metrics::WebAuthenticationGPMRecoveryEvent::
kStoreKeysFromOpportunisticFlowIgnoredRedundant;
case EnclaveManager::OutOfContextRecoveryOutcome::
kStoreKeysFromOpportunisticFlowFailed:
return webauthn::metrics::WebAuthenticationGPMRecoveryEvent::
kStoreKeysFromOpportunisticFlowFailed;
}
}
} // namespace
// StateMachine performs a sequence of actions, as specified by the public
// `set_` functions, when `Start` is called. It always operates within the
// context of a specific Google account and will be destroyed by the
// EnclaveManager if the currently signed-in user changes. It works on a copy of
// the EnclaveLocalState and writes updated versions to the EnclaveManager
// once they are ready. A StateMachine is owned by the EnclaveManager and at
// most one exists at any given time.
class EnclaveManager::StateMachine {
public:
explicit StateMachine(EnclaveManager* manager,
webauthn_pb::EnclaveLocalState local_state,
std::unique_ptr<CoreAccountInfo> primary_account_info,
std::unique_ptr<PendingAction> action)
: manager_(manager),
local_state_(std::move(local_state)),
user_(StateForUser(&local_state_, *primary_account_info)),
primary_account_info_(std::move(primary_account_info)),
action_(std::move(action)) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&StateMachine::Process,
weak_ptr_factory_.GetWeakPtr(), None()));
}
~StateMachine() {
if (action_->callback) {
std::move(action_->callback)
.Run(ActionOutcome::kStateMachineHasBeenDestroyed);
FIDO_LOG(EVENT) << "Action outcome: "
<< ToString(ActionOutcome::kStateMachineHasBeenDestroyed);
}
}
static std::string ToString(ActionOutcome outcome) {
switch (outcome) {
case ActionOutcome::kSuccess:
return "Success";
case ActionOutcome::kGenericError:
return "GenericError";
case ActionOutcome::kStateMachineHasBeenDestroyed:
return "StateMachineHasBeenDestroyed";
case ActionOutcome::kActionCancelled:
return "ActionCancelled";
case ActionOutcome::
kDoDownloadingRecoveryKeyStoreKeysFailedFetchingCertXmlOrSigXml:
return "DoDownloadingRecoveryKeyStoreKeysFailedFetchingCertXmlOrSigXml";
case ActionOutcome::kDoGeneratingKeysFailedEventFailure:
return "DoGeneratingKeysFailedEventFailure";
case ActionOutcome::kDoJoiningDomainFailedTrustedVaultRegistrationError:
return "DoJoiningDomainFailedTrustedVaultRegistrationError";
case ActionOutcome::
kDoJoiningPINToDomainFailedSecretWrappingMalformedResponse:
return "DoJoiningPINToDomainFailedSecretWrappingMalformedResponse";
case ActionOutcome::
kDoJoiningPINToDomainFailedTrustedVaultRegistrationStatusFailure:
return "DoJoiningPINToDomainFailedTrustedVaultRegistrationStatusFailur"
"e";
case ActionOutcome::
kDoJoiningUpdatedPINToDomainFailedTrustedVaultRegistrationStatusError:
return "DoJoiningUpdatedPINToDomainFailedTrustedVaultRegistrationStatus"
"Error";
case ActionOutcome::kDoNextActionFailedRenewPinWhileUserNotRegistered:
return "DoNextActionFailedRenewPinWhileUserNotRegistered";
case ActionOutcome::
kDoNextActionFailedSetOrUpdatePinWhileUserNotRegistered:
return "DoNextActionFailedSetOrUpdatePinWhileUserNotRegistered";
case ActionOutcome::
kDoRegisteringWithEnclaveFailedEnclaveRegistrationError:
return "DoRegisteringWithEnclaveFailedEnclaveRegistrationError";
case ActionOutcome::kDoRegisteringWithEnclaveFailedEventFailure:
return "DoRegisteringWithEnclaveFailedEventFailure";
case ActionOutcome::kDoRegisteringWithEnclaveFailedWrappedKeyWasInvalid:
return "DoRegisteringWithEnclaveFailedWrappedKeyWasInvalid";
case ActionOutcome::kDoRenewingPINFailedCohortNotYetDeprecated:
return "DoRenewingPINFailedCohortNotYetDeprecated";
case ActionOutcome::kDoRenewingPINFailedErrorResponse:
return "DoRenewingPINFailedErrorResponse";
case ActionOutcome::kDoRenewingPINFailedEventFailure:
return "DoRenewingPINFailedEventFailure";
case ActionOutcome::kDoRenewingPINFailedParseWrappedPinFromCborFailure:
return "DoRenewingPINFailedParseWrappedPinFromCborFailure";
case ActionOutcome::kDoRenewingPINFailedRecoveryStoreDowngrade:
return "DoRenewingPINFailedRecoveryStoreDowngrade";
case ActionOutcome::kDoSettingPINFailedCanNotParseWrappedPinFromCbor:
return "DoSettingPINFailedCanNotParseWrappedPinFromCbor";
case ActionOutcome::kDoSettingPINFailedEventFailure:
return "DoSettingPINFailedEventFailure";
case ActionOutcome::kDoSettingPINFailedPinChangeResultedInErrorResponse:
return "DoSettingPINFailedPinChangeResultedInErrorResponse";
case ActionOutcome::
kDoStoringOpportunisticallyRetrievedKeyFailedNoSystemUvNoGpmPin:
return "DoStoringOpportunisticallyRetrievedKeyFailedNoSystemUvNoGpmPin";
case ActionOutcome::
kDoStoringOpportunisticallyRetrievedKeyFailedWrappedPinParsingProblem:
return "DoStoringOpportunisticallyRetrievedKeyFailedWrappedPinParsingPr"
"oblem";
case ActionOutcome::kDoSyncingWithSecurityDomainFailedAlreadyHasPin:
return "DoSyncingWithSecurityDomainFailedAlreadyHasPin";
case ActionOutcome::
kDoSyncingWithSecurityDomainFailedSecurityDomainHasBeenReset:
return "DoSyncingWithSecurityDomainFailedSecurityDomainHasBeenReset";
case ActionOutcome::
kDoSyncingWithSecurityDomainFailedTriedToChangePinButSdsReportsNoPin:
return "DoSyncingWithSecurityDomainFailedTriedToChangePinButSdsReportsN"
"oPin";
case ActionOutcome::
kDoSyncingWithSecurityDomainFailedTrustedVaultErrorResponse:
return "DoSyncingWithSecurityDomainFailedTrustedVaultErrorResponse";
case ActionOutcome::kDoUnregisteringFailedEnclaveResponseError:
return "DoUnregisteringFailedEnclaveResponseError";
case ActionOutcome::kDoUnregisteringFailedEventFailure:
return "DoUnregisteringFailedEventFailure";
case ActionOutcome::
kDoWaitingForEnclaveTokenForPINWrappingFailedEventFailure:
return "DoWaitingForEnclaveTokenForPINWrappingFailedEventFailure";
case ActionOutcome::
kDoWaitingForEnclaveTokenForRegistrationFailedEventFailure:
return "DoWaitingForEnclaveTokenForRegistrationFailedEventFailure";
case ActionOutcome::
kDoWaitingForEnclaveTokenForUnregisterFailedEventFailure:
return "DoWaitingForEnclaveTokenForUnregisterFailedEventFailure";
case ActionOutcome::
kDoWaitingForEnclaveTokenForWrappingFailedToGetAccessToken:
return "DoWaitingForEnclaveTokenForWrappingFailedToGetAccessToken";
case ActionOutcome::
kDoWaitingForRecoveryKeyStoreFailedToUploadToRecoveryKeyStore:
return "DoWaitingForRecoveryKeyStoreFailedToUploadToRecoveryKeyStore";
case ActionOutcome::kDoWrappingPINAndSecretFailedErrorResponse:
return "DoWrappingPINAndSecretFailedErrorResponse";
case ActionOutcome::kDoWrappingPINAndSecretFailedEventFailure:
return "DoWrappingPINAndSecretFailedEventFailure";
case ActionOutcome::
kDoWrappingPINAndSecretFailedToTranslateResponseToProto:
return "DoWrappingPINAndSecretFailedToTranslateResponseToProto";
case ActionOutcome::kDoWrappingSecretsFailedToStoreWrappedSecrets:
return "DoWrappingSecretsFailedToStoreWrappedSecrets";
case ActionOutcome::kDoWrappingSecretsFailedToWrapSecurityDomainSecrets:
return "DoWrappingSecretsFailedToWrapSecurityDomainSecrets";
case ActionOutcome::kDoWrappingSecretsFailedWrappingResultedInError:
return "DoWrappingSecretsFailedWrappingResultedInError";
case ActionOutcome::
kUploadVaultAndMemberFromResponseFailedResponseWasNotMap:
return "UploadVaultAndMemberFromResponseFailedResponseWasNotMap";
case ActionOutcome::
kUploadVaultAndMemberFromResponseFailedToParseResponse:
return "UploadVaultAndMemberFromResponseFailedToParseResponse";
case ActionOutcome::kDoNextActionFailedAccountMismatch:
return "DoNextActionFailedAccountMismatch";
case ActionOutcome::kAddDeviceToAccountNotStartedWrappedPinParsingError:
return "AddDeviceToAccountNotStartedWrappedPinParsingError";
case ActionOutcome::
kConsiderSecurityDomainStateNotStartedWrappedPinParsingError:
return "ConsiderSecurityDomainStateNotStartedWrappedPinParsingError";
}
}
private:
// This class is a state machine that uses the following states. It moves from
// state to state in response to `Event` values.
enum class State {
kStop,
kNextAction,
kGeneratingKeys,
kWaitingForEnclaveTokenForRegistration,
kRegisteringWithEnclave,
kWaitingForEnclaveTokenForWrapping,
kWrappingSecrets,
kJoiningDomain,
kHashingPIN,
kDownloadingRecoveryKeyStoreKeys,
kWaitingForEnclaveTokenForPINWrapping,
kWrappingPINAndSecret,
kWaitingForRecoveryKeyStore,
kJoiningPINToDomain,
kJoiningUpdatedPINToDomain,
#if BUILDFLAG(IS_MAC)
kJoiningICloudKeychainToDomain,
#endif // BUILDFLAG(IS_MAC)
// Setting the PIN using `recovery_key_store/wrap_pin_and_secret`.
kSettingPIN,
kRenewingPIN,
kWaitingForEnclaveTokenForUnregister,
kUnregistering,
kSyncingWithSecurityDomain,
kStoringOpportunisticallyRetrievedKey,
};
enum class FetchedFile {
kCertFile,
kSigFile,
};
struct OpportunisticRetrievalCheckResult {
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
account_state;
SystemUv system_uv;
};
using DeferredUVKeyCreation =
base::StrongAlias<class DeferredUVKeyCreation, std::monostate>;
using MaybeUVKey =
std::variant<DeferredUVKeyCreation,
std::unique_ptr<crypto::UserVerifyingSigningKey>>;
using None = base::StrongAlias<class None, std::monostate>;
using Failure = base::StrongAlias<class KeyGenerationFailure, std::monostate>;
using FileContents = base::StrongAlias<class FileContents, std::string>;
using KeyReady = base::StrongAlias<
class KeyGenerated,
std::pair<MaybeUVKey, std::unique_ptr<crypto::UnexportableSigningKey>>>;
using EnclaveResponse = base::StrongAlias<class EnclaveResponse, cbor::Value>;
using JoinStatus =
base::StrongAlias<class JoinStatus,
std::pair<trusted_vault::TrustedVaultRegistrationStatus,
/*key_version=*/int>>;
using AccessToken = base::StrongAlias<class AccessToken, std::string>;
using FileFetched =
base::StrongAlias<class FileFetched,
std::pair<FetchedFile, std::optional<std::string>>>;
using PINHashed =
base::StrongAlias<class PINHashed, std::unique_ptr<HashedPIN>>;
using Response = base::StrongAlias<class Response, std::string>;
using Event = std::variant<
None,
Failure,
FileContents,
KeyReady,
EnclaveResponse,
AccessToken,
JoinStatus,
FileFetched,
PINHashed,
Response,
trusted_vault::RecoveryKeyStoreStatus,
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult,
OpportunisticRetrievalCheckResult>;
void Process(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!processing_) << ToString(state_);
processing_ = true;
const State initial_state = state_;
const std::string event_str = ToString(event);
switch (state_) {
case State::kStop:
// This should never be observed here as this special case is handled
// below.
NOTREACHED();
case State::kNextAction:
CHECK(std::holds_alternative<None>(event)) << ToString(event);
DoNextAction();
break;
case State::kGeneratingKeys:
DoGeneratingKeys(std::move(event));
break;
case State::kWaitingForEnclaveTokenForRegistration:
DoWaitingForEnclaveTokenForRegistration(std::move(event));
break;
case State::kRegisteringWithEnclave:
DoRegisteringWithEnclave(std::move(event));
break;
case State::kWaitingForEnclaveTokenForWrapping:
DoWaitingForEnclaveTokenForWrapping(std::move(event));
break;
case State::kWrappingSecrets:
DoWrappingSecrets(std::move(event));
break;
case State::kJoiningDomain:
DoJoiningDomain(std::move(event));
break;
case State::kHashingPIN:
DoHashingPIN(std::move(event));
break;
case State::kDownloadingRecoveryKeyStoreKeys:
DoDownloadingRecoveryKeyStoreKeys(std::move(event));
break;
case State::kWaitingForEnclaveTokenForPINWrapping:
DoWaitingForEnclaveTokenForPINWrapping(std::move(event));
break;
case State::kWrappingPINAndSecret:
DoWrappingPINAndSecret(std::move(event));
break;
case State::kWaitingForRecoveryKeyStore:
DoWaitingForRecoveryKeyStore(std::move(event));
break;
case State::kJoiningPINToDomain:
DoJoiningPINToDomain(std::move(event));
break;
case State::kSettingPIN:
DoSettingPIN(std::move(event));
break;
case State::kJoiningUpdatedPINToDomain:
DoJoiningUpdatedPINToDomain(std::move(event));
break;
case State::kRenewingPIN:
DoRenewingPIN(std::move(event));
break;
#if BUILDFLAG(IS_MAC)
case State::kJoiningICloudKeychainToDomain:
DoJoiningICloudKeychainToDomain(std::move(event));
break;
#endif // BUILDFLAG(IS_MAC)
case State::kWaitingForEnclaveTokenForUnregister:
DoWaitingForEnclaveTokenForUnregister(std::move(event));
break;
case State::kUnregistering:
DoUnregistering(std::move(event));
break;
case State::kSyncingWithSecurityDomain:
DoSyncingWithSecurityDomain(std::move(event));
break;
case State::kStoringOpportunisticallyRetrievedKey:
DoStoringOpportunisticallyRetrievedKey(std::move(event));
break;
}
FIDO_LOG(EVENT) << ToString(initial_state) << " -" << event_str << "-> "
<< ToString(state_);
if (state_ == State::kStop) {
HandleStoppedState();
// `this` has been deleted now.
return;
}
// The only internal state transition (i.e. where one state moves to another
// without waiting for an external event) allowed is to `kNextAction`.
if (state_ != State::kNextAction) {
processing_ = false;
return;
}
const State prior_state = state_;
DoNextAction();
FIDO_LOG(EVENT) << ToString(prior_state) << " --> " << ToString(state_);
if (state_ == State::kStop) {
HandleStoppedState();
// `this` has been deleted now.
return;
}
processing_ = false;
}
void Stop(ActionOutcome outcome) {
state_ = State::kStop;
outcome_ = outcome;
}
void HandleStoppedState() {
std::move(action_->callback).Run(outcome_);
FIDO_LOG(EVENT) << "Action outcome: " << ToString(outcome_);
manager_->Stopped();
// `this` has been deleted now.
}
static std::string ToString(State state) {
switch (state) {
case State::kStop:
return "Stop";
case State::kNextAction:
return "NextAction";
case State::kGeneratingKeys:
return "GeneratingKeys";
case State::kWaitingForEnclaveTokenForRegistration:
return "WaitingForEnclaveTokenForRegistration";
case State::kRegisteringWithEnclave:
return "RegisteringWithEnclave";
case State::kWaitingForEnclaveTokenForWrapping:
return "WaitingForEnclaveTokenForWrapping";
case State::kWrappingSecrets:
return "WrappingSecrets";
case State::kJoiningDomain:
return "JoiningDomain";
case State::kHashingPIN:
return "HashingPIN";
case State::kDownloadingRecoveryKeyStoreKeys:
return "DownloadingRecoveryKeyStoreKeys";
case State::kWaitingForEnclaveTokenForPINWrapping:
return "WaitingForEnclaveTokenForPINWrapping";
case State::kWrappingPINAndSecret:
return "WrappingPINAndSecret";
case State::kWaitingForRecoveryKeyStore:
return "WaitingForRecoveryKeyStore";
case State::kJoiningPINToDomain:
return "JoiningPINToDomain";
case State::kSettingPIN:
return "SettingPIN";
case State::kJoiningUpdatedPINToDomain:
return "JoiningUpdatedPINToDomain";
case State::kRenewingPIN:
return "RenewingPIN";
#if BUILDFLAG(IS_MAC)
case State::kJoiningICloudKeychainToDomain:
return "JoiningICloudKeychainToDomain";
#endif // BUILDFLAG(IS_MAC)
case State::kWaitingForEnclaveTokenForUnregister:
return "WaitingForEnclaveTokenForUnregister";
case State::kUnregistering:
return "Unregistering";
case State::kSyncingWithSecurityDomain:
return "kSyncingWithSecurityDomain";
case State::kStoringOpportunisticallyRetrievedKey:
return "kStoringOpportunisticallyRetrievedKey";
}
}
static const char* ToString(trusted_vault::RecoveryKeyStoreStatus status) {
switch (status) {
case trusted_vault::RecoveryKeyStoreStatus::kSuccess:
return "Success";
case trusted_vault::RecoveryKeyStoreStatus::
kTransientAccessTokenFetchError:
return "TransientError";
case trusted_vault::RecoveryKeyStoreStatus::
kPersistentAccessTokenFetchError:
return "AccessTokenError";
case trusted_vault::RecoveryKeyStoreStatus::
kPrimaryAccountChangeAccessTokenFetchError:
return "AccountChangedError";
case trusted_vault::RecoveryKeyStoreStatus::kNetworkError:
return "NetworkError";
case trusted_vault::RecoveryKeyStoreStatus::kOtherError:
return "OtherError";
}
}
static const char* ToString(
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::State
state) {
switch (state) {
case trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::
State::kError:
return "Error";
case trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::
State::kEmpty:
return "kEmpty";
case trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::
State::kRecoverable:
return "kRecoverable";
case trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::
State::kIrrecoverable:
return "Irrecoverable";
}
}
static std::string ToString(const Event& event) {
return std::visit(
absl::Overload{
[](const None&) { return std::string(); },
[](const Failure&) { return std::string("Failure"); },
[](const FileContents&) { return std::string("FileContents"); },
[](const KeyReady&) { return std::string("KeyReady"); },
[](const EnclaveResponse&) {
return std::string("EnclaveResponse");
},
[](const AccessToken&) { return std::string("AccessToken"); },
[](const JoinStatus& status) {
return base::StrCat(
{"JoinStatus(",
TrustedVaultRegistrationStatusToString(status.value().first),
", ", base::NumberToString(status.value().second), ")"});
},
[](const FileFetched& fetched) {
const FetchedFile fetched_file = fetched.value().first;
const std::optional<std::string>& contents =
fetched.value().second;
return base::StrCat(
{"FileFetched(", ToString(fetched_file), ", ",
(contents ? base::StringPrintf("%zu bytes", contents->size())
: "error"),
")"});
},
[](const PINHashed&) { return std::string("PINHashed"); },
[](const Response& response) {
const std::string& response_str = response.value();
return base::StringPrintf("Response(%zu bytes)",
response_str.size());
},
[](const trusted_vault::RecoveryKeyStoreStatus& status) {
return base::StrCat(
{"UpdateRecoveryKeyStoreStatus(", ToString(status), ")"});
},
[](const trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult&
result) {
return base::StrCat(
{"DownloadAuthenticationFactorsRegistrationStateResult(",
ToString(result.state), " ", "has_gpm_pin: ",
result.gpm_pin_metadata.has_value() ? "yes" : "no", ")"});
},
[](const OpportunisticRetrievalCheckResult& result) {
const bool has_pin =
result.account_state.gpm_pin_metadata &&
result.account_state.gpm_pin_metadata->usable_pin_metadata;
bool has_system_uv;
switch (result.system_uv) {
case SystemUv::kSupported:
has_system_uv = true;
break;
case SystemUv::kNotSupported:
has_system_uv = false;
break;
}
return base::StrCat(
{"InformationAboutGpmPinAndSystemUv(",
"has_gpm_pin: ", has_pin ? "yes" : "no",
"has_system_uv: ", has_system_uv ? "yes" : "no", ")"});
},
},
event);
}
static std::string ToString(FetchedFile fetched_file) {
switch (fetched_file) {
case FetchedFile::kCertFile:
return "cert.xml";
case FetchedFile::kSigFile:
return "cert.sig.xml";
}
}
void DoNextAction() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!action_->gaia_ids_to_remove.empty()) {
for (const auto& gaia_id : action_->gaia_ids_to_remove) {
local_state_.mutable_users()->erase(gaia_id.ToString());
}
manager_->WriteState(&local_state_);
Stop(ActionOutcome::kSuccess);
return;
}
if (action_->opportunistic_store_keys_args) {
state_ = State::kStoringOpportunisticallyRetrievedKey;
VerifySystemUvAndGpmPinBeforeStoringOpportunisticallyRetrievedKey();
return;
}
if ((action_->want_registration || action_->store_keys_args ||
!action_->pin.empty()) &&
!user_->registered()) {
action_->want_registration = false;
StartEnclaveRegistration();
return;
}
if (user_->registered() && !action_->pin.empty()) {
if (action_->setup_account) {
CHECK(!action_->store_keys_args);
action_->setup_account = false;
// Create `store_keys_args_for_joining_` as if we had received the keys
// for the security domain from an external source.
store_keys_args_for_joining_ = std::make_unique<StoreKeysArgs>();
store_keys_args_for_joining_->gaia_id = primary_account_info_->gaia;
uint8_t security_domain_secret[32];
crypto::RandBytes(security_domain_secret);
// Zero is a special value that indicates that the epoch is unknown.
store_keys_args_for_joining_->keys.emplace_back(
std::vector<uint8_t>(std::begin(security_domain_secret),
std::end(security_domain_secret)),
/*key_version=*/0);
} else {
CHECK(action_->store_keys_args);
store_keys_args_for_joining_ = std::move(action_->store_keys_args);
}
state_ = State::kHashingPIN;
HashPIN(std::move(action_->pin));
return;
}
if (user_->registered() && action_->store_keys_args) {
if (primary_account_info_->gaia != action_->store_keys_args->gaia_id) {
// This happens when we stored keys for a different account, e.g.
// because a new account became the primary account between storing keys
// and enrollment.
Stop(ActionOutcome::kDoNextActionFailedAccountMismatch);
return;
}
auto store_keys_args = std::move(action_->store_keys_args);
action_->store_keys_args.reset();
new_security_domain_secrets_ =
GetNewSecretsToStore(*user_, *store_keys_args);
store_keys_args_for_joining_ = std::move(store_keys_args);
if (!new_security_domain_secrets_.empty()) {
state_ = State::kWaitingForEnclaveTokenForWrapping;
GetAccessTokenInternal();
} else if (!user_->joined() && !user_->member_public_key().empty()) {
JoinSecurityDomain();
}
return;
}
#if BUILDFLAG(IS_MAC)
if (action_->icloud_recovery_key) {
state_ = State::kJoiningICloudKeychainToDomain;
JoinICloudKeychainToDomain(std::move(action_->icloud_recovery_key));
return;
}
#endif // BUILDFLAG(IS_MAC)
if (!action_->set_pin.empty() || !action_->updated_pin.empty()) {
if (!user_->registered()) {
Stop(ActionOutcome::
kDoNextActionFailedSetOrUpdatePinWhileUserNotRegistered);
return;
}
is_set_pin_ = !action_->set_pin.empty();
is_pin_update_ = !action_->updated_pin.empty();
CHECK(is_set_pin_ ^ is_pin_update_);
rapt_ = std::move(action_->rapt);
SyncWithSecurityDomain();
return;
}
if (action_->renew_pin) {
if (!user_->registered()) {
Stop(ActionOutcome::kDoNextActionFailedRenewPinWhileUserNotRegistered);
return;
}
is_pin_renewal_ = true;
SyncWithSecurityDomain();
return;
}
if (action_->unregister) {
if (!user_->registered()) {
Stop(ActionOutcome::kSuccess);
return;
}
state_ = State::kWaitingForEnclaveTokenForUnregister;
GetAccessTokenInternal();
return;
}
if (action_->update_wrapped_pin) {
*user_->mutable_wrapped_pin() = std::move(*action_->wrapped_pin);
manager_->WriteState(&local_state_);
}
Stop(ActionOutcome::kSuccess);
}
void FetchComplete(FetchedFile file, std::optional<std::string> contents) {
Process(FileFetched(std::make_pair(file, std::move(contents))));
}
void StartEnclaveRegistration() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
state_ = State::kGeneratingKeys;
manager_->user_verifying_key_.reset();
AreUserVerifyingKeysSupported(base::BindOnce(
[](base::WeakPtr<StateMachine> state_machine,
bool is_uv_key_supported) {
if (!state_machine) {
return;
}
// The key provider is only used to create a new key, but not sign
// with it, so passing empty options here is ok.
auto key_provider =
GetUserVerifyingKeyProviderForCreateAndDeleteOnly();
if (!is_uv_key_supported || !key_provider) {
// UV keys are not available, so skip to generating an identity
// key.
state_machine->GenerateIdentityKey(nullptr);
return;
}
if (state_machine->user_->wrapped_uv_private_key().empty()) {
#if BUILDFLAG(IS_WIN)
// On Windows we don't want to create a UV key at registration
// time. Instead we defer creation until one is going to be
// used in a UV request.
state_machine->GenerateIdentityKey(DeferredUVKeyCreation());
#else
// Create a new UV key.
key_provider->GenerateUserVerifyingSigningKey(
device::enclave::kSigningAlgorithms,
base::BindOnce(
[](base::WeakPtr<StateMachine> state_machine,
base::expected<
std::unique_ptr<crypto::UserVerifyingSigningKey>,
crypto::UserVerifyingKeyCreationError>
maybe_uv_key) {
if (!state_machine) {
return;
}
std::unique_ptr<crypto::UserVerifyingSigningKey> uv_key;
if (maybe_uv_key.has_value()) {
uv_key = std::move(maybe_uv_key.value());
} else {
FIDO_LOG(ERROR)
<< "UV key creation failed with error "
<< static_cast<int>(maybe_uv_key.error());
}
state_machine->GenerateIdentityKey(std::move(uv_key));
},
state_machine));
#endif
return;
}
// Use the existing UV key.
key_provider->GetUserVerifyingSigningKey(
state_machine->user_->wrapped_uv_private_key(),
base::BindOnce(
[](base::WeakPtr<StateMachine> state_machine,
base::expected<
std::unique_ptr<crypto::UserVerifyingSigningKey>,
crypto::UserVerifyingKeyCreationError> maybe_uv_key) {
if (!state_machine) {
return;
}
std::unique_ptr<crypto::UserVerifyingSigningKey> uv_key;
if (maybe_uv_key.has_value()) {
uv_key = std::move(maybe_uv_key.value());
} else {
FIDO_LOG(ERROR) << "UV key retrieval failed with error "
<< static_cast<int>(maybe_uv_key.error());
}
state_machine->GenerateIdentityKey(std::move(uv_key));
},
state_machine));
},
weak_ptr_factory_.GetWeakPtr()));
}
void GenerateIdentityKey(MaybeUVKey uv_key) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(state_ == State::kGeneratingKeys);
std::optional<std::vector<uint8_t>> existing_key_id;
if (!user_->wrapped_identity_private_key().empty()) {
existing_key_id = ToVector(user_->wrapped_identity_private_key());
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
base::BindOnce(
[](std::optional<std::vector<uint8_t>> key_id,
MaybeUVKey uv_key) -> Event {
std::unique_ptr<crypto::UnexportableKeyProvider> provider =
GetWebAuthnUnexportableKeyProvider();
if (!provider) {
return Failure();
}
if (key_id) {
std::unique_ptr<crypto::UnexportableSigningKey> key =
provider->FromWrappedSigningKeySlowly(*key_id);
if (key) {
return KeyReady(
std::make_pair(std::move(uv_key), std::move(key)));
}
}
std::unique_ptr<crypto::UnexportableSigningKey> key =
provider->GenerateSigningKeySlowly(
device::enclave::kSigningAlgorithms);
if (!key) {
return Failure();
}
return KeyReady(
std::make_pair(std::move(uv_key), std::move(key)));
},
std::move(existing_key_id), std::move(uv_key)),
base::BindOnce(&StateMachine::Process, weak_ptr_factory_.GetWeakPtr()));
}
void DoGeneratingKeys(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (std::holds_alternative<Failure>(event)) {
Stop(ActionOutcome::kDoGeneratingKeysFailedEventFailure);
return;
}
CHECK(std::holds_alternative<KeyReady>(event)) << ToString(event);
bool state_dirty = false;
MaybeUVKey maybe_uv_key =
std::move(std::get_if<KeyReady>(&event)->value().first);
// TODO(crbug.com/40253837): There is a presubmit bug that makes the script
// complain about the unique_ptr within the holds_alternative if they are
// on different lines. The type alias is just to work around that.
using UVSigningKey = std::unique_ptr<crypto::UserVerifyingSigningKey>;
if (std::holds_alternative<UVSigningKey>(maybe_uv_key)) {
auto uv_key = std::move(std::get<UVSigningKey>(maybe_uv_key));
if (uv_key) {
manager_->user_verifying_key_ =
base::MakeRefCounted<crypto::RefCountedUserVerifyingSigningKey>(
std::move(uv_key));
user_->set_deferred_uv_key_creation(false);
}
} else {
CHECK(std::holds_alternative<DeferredUVKeyCreation>(maybe_uv_key));
user_->set_deferred_uv_key_creation(true);
}
manager_->identity_key_ =
unexportable_keys::MakeRefCountedUnexportableSigningKey(
std::move(std::get_if<KeyReady>(&event)->value().second));
if (manager_->user_verifying_key_) {
const std::vector<uint8_t> uv_public_key =
manager_->user_verifying_key_->key().GetPublicKey();
const std::string uv_public_key_str = VecToString(uv_public_key);
if (user_->uv_public_key() != uv_public_key_str) {
user_->set_uv_public_key(uv_public_key_str);
user_->set_wrapped_uv_private_key(UserVerifyingLabelToString(
manager_->user_verifying_key_->key().GetKeyLabel()));
state_dirty = true;
}
}
const std::vector<uint8_t> spki =
manager_->identity_key_->key().GetSubjectPublicKeyInfo();
const std::string spki_str = VecToString(spki);
if (user_->identity_public_key() != spki_str) {
std::array<uint8_t, crypto::kSHA256Length> device_id =
crypto::SHA256Hash(spki);
user_->set_identity_public_key(spki_str);
user_->set_wrapped_identity_private_key(
VecToString(manager_->identity_key_->key().GetWrappedKey()));
user_->set_identity_key_is_software_backed(
!manager_->identity_key_->key().IsHardwareBacked());
user_->set_device_id(VecToString(device_id));
state_dirty = true;
}
if (state_dirty) {
manager_->WriteState(&local_state_);
}
state_ = State::kWaitingForEnclaveTokenForRegistration;
GetAccessTokenInternal();
}
void DoWaitingForEnclaveTokenForRegistration(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
access_token_fetcher_.reset();
if (std::holds_alternative<Failure>(event)) {
FIDO_LOG(ERROR) << "Failed to get access token for enclave";
Stop(ActionOutcome::
kDoWaitingForEnclaveTokenForRegistrationFailedEventFailure);
return;
}
CHECK(std::holds_alternative<AccessToken>(event)) << ToString(event);
state_ = State::kRegisteringWithEnclave;
std::string token = std::move(std::get_if<AccessToken>(&event)->value());
pending_transaction_ = enclave::Transact(
manager_->network_context_factory_, enclave::GetEnclaveIdentity(),
std::move(token),
/*reauthentication_token=*/std::nullopt,
BuildRegistrationMessage(
user_->device_id(), manager_->identity_key_->key(),
manager_->user_verifying_key_, user_->deferred_uv_key_creation()),
enclave::EnclaveTransactionTypeForUMA::kDeviceRegister,
enclave::SigningCallback(),
base::BindOnce(&StateMachine::OnEnclaveResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void DoRegisteringWithEnclave(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (std::holds_alternative<Failure>(event)) {
Stop(ActionOutcome::kDoRegisteringWithEnclaveFailedEventFailure);
return;
}
cbor::Value response =
std::move(std::get_if<EnclaveResponse>(&event)->value());
if (!IsAllOk(response, 2)) {
FIDO_LOG(ERROR) << "Registration resulted in error response: "
<< cbor::DiagnosticWriter::Write(response);
Stop(ActionOutcome::
kDoRegisteringWithEnclaveFailedEnclaveRegistrationError);
return;
}
if (!SetSecurityDomainMemberKey(
user_, response.GetArray()[1]
.GetMap()
.find(cbor::Value(enclave::kResponseSuccessKey))
->second)) {
FIDO_LOG(ERROR) << "Wrapped member key was invalid: "
<< cbor::DiagnosticWriter::Write(response);
Stop(ActionOutcome::kDoRegisteringWithEnclaveFailedWrappedKeyWasInvalid);
return;
}
user_->set_registered(true);
manager_->WriteState(&local_state_);
state_ = State::kNextAction;
}
void DoWaitingForEnclaveTokenForWrapping(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
access_token_fetcher_.reset();
if (std::holds_alternative<Failure>(event)) {
FIDO_LOG(ERROR) << "Failed to get access token for enclave";
Stop(ActionOutcome::
kDoWaitingForEnclaveTokenForWrappingFailedToGetAccessToken);
return;
}
state_ = State::kWrappingSecrets;
std::string token = std::move(std::get_if<AccessToken>(&event)->value());
pending_transaction_ = enclave::Transact(
manager_->network_context_factory_, enclave::GetEnclaveIdentity(),
std::move(token),
/*reauthentication_token=*/std::nullopt,
cbor::Value(
BuildSecretWrappingEnclaveRequest(new_security_domain_secrets_)),
enclave::EnclaveTransactionTypeForUMA::kKeysWrapSecrets,
manager_->IdentityKeySigningCallback(),
base::BindOnce(
[](base::WeakPtr<StateMachine> machine,
base::expected<cbor::Value, enclave::TransactError> response) {
if (!machine) {
return;
}
if (!response.has_value()) {
machine->Process(Failure());
} else {
machine->Process(EnclaveResponse(std::move(response.value())));
}
},
weak_ptr_factory_.GetWeakPtr()));
}
void DoWrappingSecrets(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const auto new_security_domain_secrets =
std::move(new_security_domain_secrets_);
new_security_domain_secrets_.clear();
if (std::holds_alternative<Failure>(event)) {
FIDO_LOG(ERROR) << "Failed to wrap security domain secrets";
Stop(ActionOutcome::kDoWrappingSecretsFailedToWrapSecurityDomainSecrets);
return;
}
cbor::Value response =
std::move(std::get_if<EnclaveResponse>(&event)->value());
if (!IsAllOk(response, new_security_domain_secrets.size())) {
FIDO_LOG(ERROR) << "Wrapping resulted in error response: "
<< cbor::DiagnosticWriter::Write(response);
Stop(ActionOutcome::kDoWrappingSecretsFailedWrappingResultedInError);
return;
}
if (!StoreWrappedSecrets(user_, new_security_domain_secrets,
response.GetArray())) {
FIDO_LOG(ERROR) << "Failed to store wrapped secrets";
Stop(ActionOutcome::kDoWrappingSecretsFailedToStoreWrappedSecrets);
return;
}
if (action_->wrapped_pin) {
*user_->mutable_wrapped_pin() = std::move(*action_->wrapped_pin);
action_->wrapped_pin.reset();
}
if (!user_->joined()) {
JoinSecurityDomain();
} else {
manager_->WriteState(&local_state_);
state_ = State::kNextAction;
}
}
void DoJoiningDomain(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
join_request_.reset();
manager_->SetSecret(store_keys_args_for_joining_->keys.back().version,
store_keys_args_for_joining_->keys.back().key);
store_keys_args_for_joining_.reset();
CHECK(std::holds_alternative<JoinStatus>(event));
const trusted_vault::TrustedVaultRegistrationStatus status =
std::get_if<JoinStatus>(&event)->value().first;
switch (status) {
case trusted_vault::TrustedVaultRegistrationStatus::kSuccess:
case trusted_vault::TrustedVaultRegistrationStatus::kAlreadyRegistered:
user_->set_joined(true);
manager_->WriteState(&local_state_);
state_ = State::kNextAction;
break;
default:
manager_->ClearRegistration();
Stop(
ActionOutcome::kDoJoiningDomainFailedTrustedVaultRegistrationError);
break;
}
}
void SyncWithSecurityDomain() {
state_ = State::kSyncingWithSecurityDomain;
download_account_state_request_ =
manager_->trusted_vault_conn_
->DownloadAuthenticationFactorsRegistrationState(
*primary_account_info_,
base::BindOnce(
[](base::WeakPtr<EnclaveManager::StateMachine> machine,
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult
result) {
if (!machine) {
return;
}
machine->Process(std::move(result));
},
weak_ptr_factory_.GetWeakPtr()),
/*keep_alive_callback=*/base::DoNothing());
}
void DoSyncingWithSecurityDomain(Event event) {
CHECK(std::holds_alternative<
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult>(
event));
const auto& result = std::get<
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult>(
event);
if (result.state ==
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::
State::kError) {
Stop(ActionOutcome::
kDoSyncingWithSecurityDomainFailedTrustedVaultErrorResponse);
return;
}
if (manager_->IsSecurityDomainReset(result)) {
// The security domain has been reset. Clear the registration and bail
// out.
manager_->ClearRegistration();
FIDO_LOG(ERROR) << "The security domain has been reset.";
Stop(ActionOutcome::
kDoSyncingWithSecurityDomainFailedSecurityDomainHasBeenReset);
return;
}
if (!result.gpm_pin_metadata && (is_pin_renewal_ || is_pin_update_)) {
// Chrome is trying to renew or update a PIN but the security domain
// reports there is no PIN. Don't delete the local PIN state in case
// there's a bug in the server, but also don't try renewing or updating it
// as this risks joining to an out of date PIN.
FIDO_LOG(ERROR) << "Tried to change the PIN, but SDS repots no PIN";
Stop(
ActionOutcome::
kDoSyncingWithSecurityDomainFailedTriedToChangePinButSdsReportsNoPin);
return;
}
if (result.gpm_pin_metadata) {
// This code saves the PIN public key even if the security domain reports
// it is not usable or if it is invalid. This is necessary because the
// security domain requires the current PIN public key to be set when
// joining a PIN, which Chrome will do later during processing.
if (result.gpm_pin_metadata->public_key) {
FIDO_LOG(EVENT) << "GPM PIN public key updated";
action_->pin_public_key =
std::move(*result.gpm_pin_metadata->public_key);
}
if (result.gpm_pin_metadata->usable_pin_metadata) {
const auto& metadata = *result.gpm_pin_metadata->usable_pin_metadata;
auto wrapped_pin = std::make_unique<EnclaveLocalState::WrappedPIN>();
if (wrapped_pin->ParseFromString(metadata.wrapped_pin) &&
!CheckPINInvariants(*wrapped_pin).has_value()) {
FIDO_LOG(EVENT) << "Updating wrapped GPM PIN";
*user_->mutable_wrapped_pin() = std::move(*wrapped_pin);
} else {
FIDO_LOG(ERROR)
<< "Wrapped PIN from security domain update is invalid: "
<< base::HexEncode(base::as_byte_span(metadata.wrapped_pin));
}
}
}
if (is_set_pin_ && result.gpm_pin_metadata) {
// There is already a PIN.
Stop(ActionOutcome::kDoSyncingWithSecurityDomainFailedAlreadyHasPin);
return;
}
if (is_pin_renewal_) {
// The PIN isn't being changed, so no need to hash.
DownloadRecoveryKeyStoreKeys();
return;
}
state_ = State::kHashingPIN;
HashPIN(action_->set_pin.empty() ? std::move(action_->updated_pin)
: std::move(action_->set_pin));
}
void DoHashingPIN(Event event) {
// The new PIN has been hashed. Next we fetch the public keys of the
// recovery key store.
CHECK(std::holds_alternative<PINHashed>(event));
hashed_pin_ = std::move(std::get_if<PINHashed>(&event)->value());
wrapped_pin_proto_ = hashed_pin_->ToWrappedPIN();
DownloadRecoveryKeyStoreKeys();
}
void DoDownloadingRecoveryKeyStoreKeys(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(std::holds_alternative<FileFetched>(event)) << ToString(event);
auto& file_fetched = std::get_if<FileFetched>(&event)->value();
const FetchedFile fetched_file = file_fetched.first;
std::optional<std::string>& contents = file_fetched.second;
switch (fetched_file) {
case FetchedFile::kCertFile:
cert_xml_loader_.reset();
cert_xml_ = std::move(contents);
break;
case FetchedFile::kSigFile:
sig_xml_loader_.reset();
sig_xml_ = std::move(contents);
break;
}
if (cert_xml_loader_ || sig_xml_loader_) {
// One of the fetches is still running.
return;
}
if (!cert_xml_ || !sig_xml_) {
// One (or both) fetches failed.
Stop(ActionOutcome::
kDoDownloadingRecoveryKeyStoreKeysFailedFetchingCertXmlOrSigXml);
return;
}
state_ = State::kWaitingForEnclaveTokenForPINWrapping;
GetAccessTokenInternal();
}
void DoWaitingForEnclaveTokenForPINWrapping(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
access_token_fetcher_.reset();
if (std::holds_alternative<Failure>(event)) {
FIDO_LOG(ERROR) << "Failed to get access token for enclave";
Stop(ActionOutcome::
kDoWaitingForEnclaveTokenForPINWrappingFailedEventFailure);
return;
}
CHECK(std::holds_alternative<AccessToken>(event)) << ToString(event);
std::string token = std::move(std::get_if<AccessToken>(&event)->value());
if (is_set_pin_ || is_pin_update_) {
SendPINSetRequest(std::move(token));
} else if (is_pin_renewal_) {
SendPINRenewalRequest(std::move(token));
} else {
SendPINAndSecretWrappingRequest(std::move(token));
}
}
void SendPINAndSecretWrappingRequest(std::string token) {
state_ = State::kWrappingPINAndSecret;
pending_transaction_ = enclave::Transact(
manager_->network_context_factory_, enclave::GetEnclaveIdentity(),
std::move(token),
/*reauthentication_token=*/std::nullopt,
ConcatEnclaveRequests(
BuildRecoveryKeyStorePINWrappingEnclaveRequest(
hashed_pin_->hashed, std::move(*cert_xml_),
std::move(*sig_xml_)),
BuildSecretWrappingEnclaveRequest(
GetNewSecretsToStore(*user_, *store_keys_args_for_joining_))),
enclave::EnclaveTransactionTypeForUMA::
kRecoveryKeyStoreWrapPINAndKeysWrap,
manager_->IdentityKeySigningCallback(),
base::BindOnce(&StateMachine::OnEnclaveResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void SendPINSetRequest(std::string token) {
state_ = State::kSettingPIN;
std::vector<uint8_t> wrapped_secret =
GetCurrentWrappedSecretForUser(user_).second;
pending_transaction_ = enclave::Transact(
manager_->network_context_factory_, enclave::GetEnclaveIdentity(),
std::move(token), std::move(rapt_),
BuildPINAndSecurityDomainSecretWrappingEnclaveRequest(
hashed_pin_->hashed,
ToSizedSpan<32>(wrapped_pin_proto_->claim_key()),
std::move(*cert_xml_), std::move(*sig_xml_), wrapped_secret),
enclave::EnclaveTransactionTypeForUMA::
kRecoveryKeyStoreWrapPINAndSecret,
manager_->IdentityKeySigningCallback(),
base::BindOnce(&StateMachine::OnEnclaveResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void SendPINRenewalRequest(std::string token) {
state_ = State::kRenewingPIN;
pending_transaction_ = enclave::Transact(
manager_->network_context_factory_, enclave::GetEnclaveIdentity(),
std::move(token), std::nullopt,
BuildPINRenewalRequest(
std::move(*cert_xml_), std::move(*sig_xml_),
GetCurrentWrappedSecretForUser(user_).second,
base::as_byte_span(user_->wrapped_pin().wrapped_pin())),
enclave::EnclaveTransactionTypeForUMA::kRecoveryKeyStoreRewrapPIN,
manager_->IdentityKeySigningCallback(),
base::BindOnce(&StateMachine::OnEnclaveResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void DoWrappingPINAndSecret(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (std::holds_alternative<Failure>(event)) {
Stop(ActionOutcome::kDoWrappingPINAndSecretFailedEventFailure);
return;
}
cbor::Value response =
std::move(std::get_if<EnclaveResponse>(&event)->value());
if (!IsAllOk(response, 2)) {
FIDO_LOG(ERROR) << "PIN wrapping resulted in error response: "
<< cbor::DiagnosticWriter::Write(response);
Stop(ActionOutcome::kDoWrappingPINAndSecretFailedErrorResponse);
return;
}
const cbor::Value& recovery_key_store_wrap_response =
response.GetArray()[0]
.GetMap()
.find(cbor::Value(enclave::kResponseSuccessKey))
->second;
recovery_key_store_wrap_response_ = ParseRecoveryKeyStoreWrapResponse(
hashed_pin_->metadata, recovery_key_store_wrap_response);
if (!recovery_key_store_wrap_response_) {
FIDO_LOG(ERROR)
<< "Failed to translate response into an UpdateVaultProto";
Stop(ActionOutcome::
kDoWrappingPINAndSecretFailedToTranslateResponseToProto);
return;
}
wrapping_response_ = std::move(response);
state_ = State::kWaitingForRecoveryKeyStore;
recovery_key_store_request_ =
manager_->recovery_key_store_conn_->UpdateRecoveryKeyStore(
*primary_account_info_, *recovery_key_store_wrap_response_->vault,
base::BindOnce(
[](base::WeakPtr<StateMachine> machine,
trusted_vault::RecoveryKeyStoreStatus status) {
if (!machine) {
return;
}
machine->Process(status);
},
weak_ptr_factory_.GetWeakPtr()));
}
void DoWaitingForRecoveryKeyStore(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
recovery_key_store_request_.reset();
CHECK(std::holds_alternative<trusted_vault::RecoveryKeyStoreStatus>(event))
<< ToString(event);
const auto* status =
std::get_if<trusted_vault::RecoveryKeyStoreStatus>(&event);
if (*status != trusted_vault::RecoveryKeyStoreStatus::kSuccess) {
FIDO_LOG(ERROR) << "Failed to upload to recovery key store";
Stop(ActionOutcome::
kDoWaitingForRecoveryKeyStoreFailedToUploadToRecoveryKeyStore);
return;
}
const bool updating_pin_member = is_pin_update_ || is_pin_renewal_;
if (!updating_pin_member && !is_set_pin_) {
CHECK(wrapped_pin_proto_->wrapped_pin().empty());
wrapped_pin_proto_->set_wrapped_pin(BuildWrappedPIN(
*hashed_pin_, ToSizedSpan<32>(wrapped_pin_proto_->claim_key()),
*recovery_key_store_wrap_response_,
store_keys_args_for_joining_->keys.back().key));
}
const std::string& vault_public_key =
recovery_key_store_wrap_response_->vault->application_keys()[0]
.asymmetric_key_pair()
.public_key();
const auto secure_box_pub_key =
trusted_vault::SecureBoxPublicKey::CreateByImport(
base::as_byte_span(vault_public_key));
std::string wrapped_pin_proto_serialized =
wrapped_pin_proto_->SerializeAsString();
*user_->mutable_wrapped_pin() = std::move(*wrapped_pin_proto_);
// If changing the PIN, there must be a previous PIN member public key.
// If enrolling with a PIN, it's possible Chrome is replacing an existing
// PIN that cannot be used, in which case we also need to set the previous
// PIN member public key.
CHECK(!updating_pin_member || action_->pin_public_key);
state_ = (updating_pin_member || is_set_pin_)
? State::kJoiningUpdatedPINToDomain
: State::kJoiningPINToDomain;
std::optional<trusted_vault::MemberKeysSource> member_keys_source =
std::move(member_keys_source_);
// If changing, renewing, or setting a PIN then `member_keys_source` will
// have been populated by the enclave. Otherwise a new PIN is being set and
// `store_keys_args_for_joining_` will contain the security domain secret,
// which is sufficient for calculating the member keys.
CHECK_EQ(member_keys_source.has_value(),
updating_pin_member || is_set_pin_);
if (!member_keys_source) {
member_keys_source = store_keys_args_for_joining_->keys;
}
join_request_ = manager_->trusted_vault_conn_->RegisterAuthenticationFactor(
*primary_account_info_, std::move(*member_keys_source),
*secure_box_pub_key,
trusted_vault::GpmPinMetadata(
action_->pin_public_key,
trusted_vault::UsableRecoveryPinMetadata(
std::move(wrapped_pin_proto_serialized),
/*expiry=*/base::Time())),
base::BindOnce(&StateMachine::OnJoinedSecurityDomain,
weak_ptr_factory_.GetWeakPtr()));
}
void DoJoiningPINToDomain(Event event) {
CHECK(std::holds_alternative<JoinStatus>(event)) << ToString(event);
wrapped_pin_proto_.reset();
const auto& join_status = std::get_if<JoinStatus>(&event)->value();
const trusted_vault::TrustedVaultRegistrationStatus status =
join_status.first;
const int key_version = join_status.second;
if (status != trusted_vault::TrustedVaultRegistrationStatus::kSuccess) {
Stop(
ActionOutcome::
kDoJoiningPINToDomainFailedTrustedVaultRegistrationStatusFailure);
return;
}
if (is_set_pin_) {
// If adding a PIN to an existing account, then we're done.
Stop(ActionOutcome::kSuccess);
return;
}
store_keys_args_for_joining_->keys.back().version = key_version;
if (!StoreWrappedSecrets(
user_, GetNewSecretsToStore(*user_, *store_keys_args_for_joining_),
base::span_from_ref(wrapping_response_->GetArray()[1]))) {
FIDO_LOG(ERROR) << "Secret wrapping resulted in malformed response: "
<< cbor::DiagnosticWriter::Write(*wrapping_response_);
Stop(ActionOutcome::
kDoJoiningPINToDomainFailedSecretWrappingMalformedResponse);
return;
}
user_->set_last_refreshed_pin_epoch_secs(
base::Time::Now().InSecondsFSinceUnixEpoch());
JoinSecurityDomain();
}
void DoJoiningUpdatedPINToDomain(Event event) {
CHECK(std::holds_alternative<JoinStatus>(event)) << ToString(event);
wrapped_pin_proto_.reset();
const auto& join_status = std::get_if<JoinStatus>(&event)->value();
const trusted_vault::TrustedVaultRegistrationStatus status =
join_status.first;
if (status == trusted_vault::TrustedVaultRegistrationStatus::kSuccess) {
Stop(ActionOutcome::kSuccess);
} else {
Stop(
ActionOutcome::
kDoJoiningUpdatedPINToDomainFailedTrustedVaultRegistrationStatusError);
return;
}
user_->set_last_refreshed_pin_epoch_secs(
base::Time::Now().InSecondsFSinceUnixEpoch());
manager_->WriteState(&local_state_);
}
#if BUILDFLAG(IS_MAC)
void DoJoiningICloudKeychainToDomain(Event event) {
CHECK(std::holds_alternative<JoinStatus>(event)) << ToString(event);
const auto& join_status = std::get_if<JoinStatus>(&event)->value();
const trusted_vault::TrustedVaultRegistrationStatus status =
join_status.first;
FIDO_LOG(EVENT) << "iCloud recovery key registration status: "
<< TrustedVaultRegistrationStatusToString(status);
state_ = State::kNextAction;
}
#endif // BUILDFLAG(IS_MAC)
void DoSettingPIN(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (std::holds_alternative<Failure>(event)) {
Stop(ActionOutcome::kDoSettingPINFailedEventFailure);
return;
}
cbor::Value response =
std::move(std::get_if<EnclaveResponse>(&event)->value());
if (!IsAllOk(response, 1)) {
FIDO_LOG(ERROR) << "PIN change resulted in error response: "
<< cbor::DiagnosticWriter::Write(response);
Stop(ActionOutcome::kDoSettingPINFailedPinChangeResultedInErrorResponse);
return;
}
std::optional<std::string> wrapped_pin = ParseWrappedPinFromCbor(response);
if (!wrapped_pin) {
Stop(ActionOutcome::kDoSettingPINFailedCanNotParseWrappedPinFromCbor);
return;
}
wrapped_pin_proto_->set_wrapped_pin(std::move(*wrapped_pin));
UploadVaultAndMemberFromResponse(hashed_pin_->metadata,
response.GetArray()[0]);
}
void DoRenewingPIN(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (std::holds_alternative<Failure>(event)) {
Stop(ActionOutcome::kDoRenewingPINFailedEventFailure);
return;
}
cbor::Value response =
std::move(std::get_if<EnclaveResponse>(&event)->value());
std::optional<device::enclave::RequestError> error =
GetRequestError(response, 0u);
if (error) {
switch (*error) {
case device::enclave::RequestError::kCohortNotYetDeprecated:
// This is the usual expected result of a PIN renewal.
FIDO_LOG(EVENT) << "Not renewing PIN because the enclave reports the "
"cohort is not yet deprecated";
user_->set_last_refreshed_pin_epoch_secs(
base::Time::Now().InSecondsFSinceUnixEpoch());
manager_->WriteState(&local_state_);
Stop(ActionOutcome::kDoRenewingPINFailedCohortNotYetDeprecated);
return;
case device::enclave::RequestError::kRecoveryKeyStoreDowngrade:
// This is expected when a client moves from a Finch keychain cohort
// experiment group to a control group.
FIDO_LOG(EVENT) << "Not renewing PIN because it would result in "
"downgrading the recovery store";
user_->set_last_refreshed_pin_epoch_secs(
base::Time::Now().InSecondsFSinceUnixEpoch());
manager_->WriteState(&local_state_);
Stop(ActionOutcome::kDoRenewingPINFailedRecoveryStoreDowngrade);
return;
default:
// `IsAllOk` below catches other errors the enclave may return that
// the client does not know about.
break;
}
}
if (!IsAllOk(response, 1)) {
FIDO_LOG(ERROR) << "PIN renewal resulted in error response: "
<< cbor::DiagnosticWriter::Write(response);
Stop(ActionOutcome::kDoRenewingPINFailedErrorResponse);
return;
}
// The PIN hash and claim keys haven't changed...
wrapped_pin_proto_ =
std::make_unique<EnclaveLocalState::WrappedPIN>(user_->wrapped_pin());
// ...but the wrapped PIN may contain new Vault cohort details so we need
// to update that.
std::optional<std::string> wrapped_pin = ParseWrappedPinFromCbor(response);
if (!wrapped_pin) {
Stop(ActionOutcome::kDoRenewingPINFailedParseWrappedPinFromCborFailure);
return;
}
wrapped_pin_proto_->set_wrapped_pin(std::move(*wrapped_pin));
UploadVaultAndMemberFromResponse(
PinMetadata::FromProto(*wrapped_pin_proto_), response.GetArray()[0]);
}
void DoWaitingForEnclaveTokenForUnregister(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
access_token_fetcher_.reset();
if (std::holds_alternative<Failure>(event)) {
FIDO_LOG(ERROR) << "Failed to get access token for enclave";
Stop(ActionOutcome::
kDoWaitingForEnclaveTokenForUnregisterFailedEventFailure);
return;
}
state_ = State::kUnregistering;
std::string token = std::move(std::get_if<AccessToken>(&event)->value());
pending_transaction_ =
enclave::Transact(manager_->network_context_factory_,
enclave::GetEnclaveIdentity(), std::move(token),
/*reauthentication_token=*/std::nullopt,
BuildUnregisterMessage(user_->device_id()),
enclave::EnclaveTransactionTypeForUMA::kDeviceForget,
enclave::SigningCallback(),
base::BindOnce(&StateMachine::OnEnclaveResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void DoUnregistering(Event event) {
if (std::holds_alternative<Failure>(event)) {
Stop(ActionOutcome::kDoUnregisteringFailedEventFailure);
return;
}
cbor::Value response =
std::move(std::get_if<EnclaveResponse>(&event)->value());
if (!IsAllOk(response, 1)) {
FIDO_LOG(ERROR) << "Unregister request resulted in error response: "
<< cbor::DiagnosticWriter::Write(response);
Stop(ActionOutcome::kDoUnregisteringFailedEnclaveResponseError);
return;
}
Stop(ActionOutcome::kSuccess);
}
// Start the process of uploading a Vault, and inserting it into the security
// domain, based on an enclave response. The `response` value should be an
// element from an enclave's response array. I.e. including the "ok" wrapping.
// It's assumed that `IsAllOk` has been checked and that the response is not
// an error. The `vault_` and `member_keys_source_` fields will be updated on
// success.
void UploadVaultAndMemberFromResponse(const PinMetadata& pin_metadata,
const cbor::Value& response) {
const cbor::Value& response_value =
response.GetMap()
.find(cbor::Value(enclave::kResponseSuccessKey))
->second;
if (!response_value.is_map()) {
FIDO_LOG(ERROR) << "response was not a map";
Stop(ActionOutcome::
kUploadVaultAndMemberFromResponseFailedResponseWasNotMap);
return;
}
const int32_t key_version = GetCurrentWrappedSecretForUser(user_).first;
std::optional<std::pair<EnclaveRecoveryKeyStoreWrapResponse,
trusted_vault::MemberKeysSource>>
result = ParseVaultAndMemberResponse(key_version, pin_metadata,
response_value.GetMap());
if (!result) {
Stop(ActionOutcome::
kUploadVaultAndMemberFromResponseFailedToParseResponse);
return;
}
std::tie(recovery_key_store_wrap_response_, member_keys_source_) =
std::move(*result);
state_ = State::kWaitingForRecoveryKeyStore;
recovery_key_store_request_ =
manager_->recovery_key_store_conn_->UpdateRecoveryKeyStore(
*primary_account_info_, *recovery_key_store_wrap_response_->vault,
base::BindOnce(
[](base::WeakPtr<StateMachine> machine,
trusted_vault::RecoveryKeyStoreStatus status) {
if (!machine) {
return;
}
machine->Process(status);
},
weak_ptr_factory_.GetWeakPtr()));
}
void JoinSecurityDomain() {
state_ = State::kJoiningDomain;
const auto secure_box_pub_key =
trusted_vault::SecureBoxPublicKey::CreateByImport(
base::as_byte_span(user_->member_public_key()));
join_request_ = manager_->trusted_vault_conn_->RegisterAuthenticationFactor(
*primary_account_info_, store_keys_args_for_joining_->keys,
*secure_box_pub_key, trusted_vault::LocalPhysicalDevice(),
base::BindOnce(&StateMachine::OnJoinedSecurityDomain,
weak_ptr_factory_.GetWeakPtr()));
}
void GetAccessTokenInternal() {
access_token_fetcher_ =
std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
signin::OAuthConsumerId::kEnclaveManager,
manager_->identity_manager_,
base::BindOnce(
[](base::WeakPtr<StateMachine> machine,
GoogleServiceAuthError error,
signin::AccessTokenInfo access_token_info) {
base::UmaHistogramEnumeration(
"WebAuthentication.Enclave.GetAccessTokenError",
error.state(), GoogleServiceAuthError::State::NUM_STATES);
if (!machine) {
return;
}
if (error.state() == GoogleServiceAuthError::NONE) {
machine->Process(AccessToken(access_token_info.token));
} else {
machine->Process(Failure());
}
},
weak_ptr_factory_.GetWeakPtr()),
signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable,
signin::ConsentLevel::kSignin);
}
void OnEnclaveResponse(
base::expected<cbor::Value, enclave::TransactError> response) {
if (!response.has_value()) {
Process(Failure());
} else {
Process(EnclaveResponse(std::move(response.value())));
}
}
void OnJoinedSecurityDomain(
trusted_vault::TrustedVaultRegistrationStatus status,
int key_version) {
Process(JoinStatus(std::make_pair(status, key_version)));
}
void HashPIN(std::string pin) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_BLOCKING, base::MayBlock()},
base::BindOnce(&HashPINSlowly, std::move(pin)),
base::BindOnce(
[](base::WeakPtr<StateMachine> machine,
std::unique_ptr<HashedPIN> hashed) {
if (!machine) {
return;
}
machine->Process(PINHashed(std::move(hashed)));
},
weak_ptr_factory_.GetWeakPtr()));
}
void VerifySystemUvAndGpmPinBeforeStoringOpportunisticallyRetrievedKey() {
// The keys were provided opportunistically so that a MagicArch flow can be
// avoided later. Download the GPM PIN metadata and check for system UV
// before storing these keys.
auto callback = base::BarrierCallback<OpportunisticRetrievalCheck>(
2,
base::BindOnce(
[](base::WeakPtr<StateMachine> machine,
std::vector<OpportunisticRetrievalCheck>
opportunistic_retrieval_checks) {
if (!machine) {
return;
}
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult
account_state;
SystemUv system_uv;
{
CHECK_EQ(opportunistic_retrieval_checks.size(), 2u);
auto& first = opportunistic_retrieval_checks.at(0);
auto& second = opportunistic_retrieval_checks.at(1);
const bool first_is_system_uv =
std::holds_alternative<SystemUv>(first);
system_uv =
std::get<SystemUv>(first_is_system_uv ? first : second);
account_state = std::get<
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult>(
std::move(first_is_system_uv ? second : first));
}
machine->Process(OpportunisticRetrievalCheckResult(
std::move(account_state), std::move(system_uv)));
},
weak_ptr_factory_.GetWeakPtr()));
AreUserVerifyingKeysSupported(base::BindOnce(
[](base::RepeatingCallback<void(OpportunisticRetrievalCheck)> cb,
bool result) {
std::move(cb).Run(result ? SystemUv::kSupported
: SystemUv::kNotSupported);
},
callback));
download_account_state_request_ =
manager_->trusted_vault_conn_
->DownloadAuthenticationFactorsRegistrationState(
*primary_account_info_,
base::BindOnce(
[](base::RepeatingCallback<void(
OpportunisticRetrievalCheck)> cb,
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult
result) {
std::move(cb).Run(std::move(result));
},
callback),
base::DoNothing());
}
void DoStoringOpportunisticallyRetrievedKey(Event event) {
CHECK(std::holds_alternative<OpportunisticRetrievalCheckResult>(event))
<< ToString(event);
const auto& pin_and_uv_info =
std::get_if<OpportunisticRetrievalCheckResult>(&event);
const trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult&
account_state = pin_and_uv_info->account_state;
const SystemUv& system_uv = pin_and_uv_info->system_uv;
const bool has_pin = account_state.gpm_pin_metadata &&
account_state.gpm_pin_metadata->usable_pin_metadata;
FIDO_LOG(EVENT) << "Opportunistic keys checks complete. "
<< "Has UV: " << static_cast<int>(system_uv)
<< ", has PIN: " << has_pin << ".";
if (system_uv == SystemUv::kNotSupported && !has_pin) {
// We shouldn't store keys if there is no system UV and no GPM PIN.
Stop(ActionOutcome::
kDoStoringOpportunisticallyRetrievedKeyFailedNoSystemUvNoGpmPin);
return;
}
std::unique_ptr<EnclaveLocalState::WrappedPIN> wrapped_pin;
if (account_state.gpm_pin_metadata.has_value() &&
account_state.gpm_pin_metadata->usable_pin_metadata) {
wrapped_pin = std::make_unique<EnclaveLocalState::WrappedPIN>();
if (!wrapped_pin->ParseFromString(
account_state.gpm_pin_metadata->usable_pin_metadata
->wrapped_pin) ||
CheckPINInvariants(*wrapped_pin).has_value()) {
Stop(
ActionOutcome::
kDoStoringOpportunisticallyRetrievedKeyFailedWrappedPinParsingProblem);
return;
}
}
// As next steps we are adding the current device to account and registering
// the GPM PIN.
action_->store_keys_args =
std::move(action_->opportunistic_store_keys_args);
action_->wrapped_pin = std::move(wrapped_pin);
if (account_state.gpm_pin_metadata) {
action_->pin_public_key =
std::move(account_state.gpm_pin_metadata->public_key);
}
manager_->store_keys_count_++;
state_ = State::kNextAction;
}
#if BUILDFLAG(IS_MAC)
void JoinICloudKeychainToDomain(
std::unique_ptr<trusted_vault::ICloudRecoveryKey> icloud_recovery_key) {
std::vector<trusted_vault::TrustedVaultKeyAndVersion> member_keys_source = {
TrustedVaultKeyAndVersion(manager_->secret_,
manager_->secret_version_)};
join_request_ = manager_->trusted_vault_conn_->RegisterAuthenticationFactor(
*primary_account_info_, std::move(member_keys_source),
icloud_recovery_key->key()->public_key(),
trusted_vault::ICloudKeychain(),
base::BindOnce(&StateMachine::OnJoinedSecurityDomain,
weak_ptr_factory_.GetWeakPtr()));
}
#endif // BUILDFLAG(IS_MAC)
// Constructed a wrapped version of the hashed PIN that will be part of the
// virtual member metadata. This inner CBOR structure contains everything that
// the enclave would need when processing a PIN and is authenticated (and
// encrypted) by the security domain secret.
static std::string BuildWrappedPIN(
const HashedPIN& hashed_pin,
base::span<const uint8_t, 32> claim_key,
const EnclaveRecoveryKeyStoreWrapResponse& vault_details,
base::span<const uint8_t> security_domain_secret) {
cbor::Value::MapValue map;
map.emplace(1, base::span<const uint8_t>(hashed_pin.hashed));
// Key 2 used to be the generation number and is now obsolete.
map.emplace(3, claim_key);
map.emplace(4, base::as_byte_span(
vault_details.vault->vault_parameters().counter_id()));
// The vault handle in the wrapped PIN doesn't include the first byte,
// which is the type of the vault entry.
map.emplace(5, base::as_byte_span(
vault_details.vault->vault_parameters().vault_handle())
.subspan<1>());
map.emplace(6, vault_details.cert_xml_serial_number);
map.emplace(7, vault_details.cohort_public_key);
const std::vector<uint8_t> cbor_bytes =
cbor::Writer::Write(cbor::Value(std::move(map))).value();
return VecToString(EncryptWrappedPIN(security_domain_secret, cbor_bytes));
}
void DownloadRecoveryKeyStoreKeys() {
state_ = State::kDownloadingRecoveryKeyStoreKeys;
cert_xml_loader_ = FetchURL(
manager_->url_loader_factory_.get(),
GetUrl(device::enclave::kCertXmlUrlFeature),
base::BindOnce(&StateMachine::FetchComplete,
weak_ptr_factory_.GetWeakPtr(), FetchedFile::kCertFile));
sig_xml_loader_ = FetchURL(
manager_->url_loader_factory_.get(),
GetUrl(device::enclave::kSigXmlUrlFeature),
base::BindOnce(&StateMachine::FetchComplete,
weak_ptr_factory_.GetWeakPtr(), FetchedFile::kSigFile));
}
const raw_ptr<EnclaveManager> manager_;
// local_state_ contains a copy of the EnclaveManager's state from when this
// StateMachine was created.
EnclaveLocalState local_state_;
// user_ points within `local_state_` to the state for the user specified in
// `primary_account_info_`.
const raw_ptr<EnclaveLocalState::User> user_;
const std::unique_ptr<CoreAccountInfo> primary_account_info_;
ActionOutcome outcome_ = ActionOutcome::kGenericError;
State state_ = State::kNextAction;
bool processing_ = false;
const std::unique_ptr<EnclaveManager::PendingAction> action_;
std::unique_ptr<StoreKeysArgs> store_keys_args_for_joining_;
base::flat_map<int32_t, std::vector<uint8_t>> new_security_domain_secrets_;
std::unique_ptr<trusted_vault::TrustedVaultConnection::Request> join_request_;
std::unique_ptr<trusted_vault::TrustedVaultConnection::Request>
download_account_state_request_;
std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher>
access_token_fetcher_;
std::unique_ptr<network::SimpleURLLoader> cert_xml_loader_;
std::unique_ptr<network::SimpleURLLoader> sig_xml_loader_;
std::unique_ptr<network::SimpleURLLoader> upload_loader_;
std::optional<std::string> cert_xml_;
std::optional<std::string> sig_xml_;
std::unique_ptr<HashedPIN> hashed_pin_;
std::optional<EnclaveRecoveryKeyStoreWrapResponse>
recovery_key_store_wrap_response_;
std::unique_ptr<trusted_vault::RecoveryKeyStoreConnection::Request>
recovery_key_store_request_;
std::optional<cbor::Value> wrapping_response_;
// True if a PIN is being hashed in order to add to an existing account.
bool is_set_pin_ = false;
// True if a PIN is being hashed in order to change it, rather than to set
// a new PIN on an account.
bool is_pin_update_ = false;
// True if the GPM PIN is being renewed without knowing or changing it.
bool is_pin_renewal_ = false;
// If changing a PIN, this holds a ReAuthentication Proof Token (RAPT), if
// the user is authenticating the request via doing a GAIA reauth.
std::optional<std::string> rapt_;
// If present, these keys will be used for adding the PIN to the domain.
std::optional<trusted_vault::MemberKeysSource> member_keys_source_;
// When uploading a PIN, this contains the pending `WrappedPIN`.
std::unique_ptr<EnclaveLocalState::WrappedPIN> wrapped_pin_proto_;
std::unique_ptr<device::enclave::EnclaveTransaction> pending_transaction_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<StateMachine> weak_ptr_factory_{this};
};
EnclaveManager::UVKeyOptions::UVKeyOptions() = default;
EnclaveManager::UVKeyOptions::~UVKeyOptions() = default;
EnclaveManager::UVKeyOptions::UVKeyOptions(UVKeyOptions&&) = default;
EnclaveManager::UVKeyOptions& EnclaveManager::UVKeyOptions::operator=(
EnclaveManager::UVKeyOptions&& other) = default;
// Observes the `IdentityManager` and tells the `EnclaveManager` when the
// primary account for the profile has changed.
class EnclaveManager::IdentityObserver
: public signin::IdentityManager::Observer {
public:
IdentityObserver(signin::IdentityManager* identity_manager,
EnclaveManager* manager)
: identity_manager_(identity_manager), manager_(manager) {
identity_manager_->AddObserver(this);
}
~IdentityObserver() override {
if (observing_) {
identity_manager_->RemoveObserver(this);
}
}
void OnPrimaryAccountChanged(
const signin::PrimaryAccountChangeEvent& event_details) override {
manager_->HandleIdentityChange();
}
void OnAccountsInCookieUpdated(
const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
const GoogleServiceAuthError& error) override {
manager_->HandleIdentityChange();
}
void OnIdentityManagerShutdown(
signin::IdentityManager* identity_manager) override {
if (observing_) {
identity_manager_->RemoveObserver(this);
observing_ = false;
}
}
private:
bool observing_ = true;
const raw_ptr<signin::IdentityManager> identity_manager_;
const raw_ptr<EnclaveManager> manager_;
};
EnclaveManager::EnclaveManager(
const base::FilePath& base_dir,
signin::IdentityManager* identity_manager,
device::NetworkContextFactory network_context_factory,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: file_path_(base_dir.Append(FILE_PATH_LITERAL("passkey_enclave_state"))),
identity_manager_(identity_manager),
network_context_factory_(network_context_factory),
url_loader_factory_(url_loader_factory),
trusted_vault_conn_(trusted_vault::NewFrontendTrustedVaultConnection(
trusted_vault::SecurityDomainId::kPasskeys,
identity_manager,
url_loader_factory_)),
trusted_vault_access_token_fetcher_frontend_(
std::make_unique<
trusted_vault::TrustedVaultAccessTokenFetcherFrontend>(
identity_manager_)),
recovery_key_store_conn_(std::make_unique<
trusted_vault::RecoveryKeyStoreConnectionImpl>(
url_loader_factory_->Clone(),
std::make_unique<trusted_vault::TrustedVaultAccessTokenFetcherImpl>(
trusted_vault_access_token_fetcher_frontend_->GetWeakPtr()))),
identity_observer_(
std::make_unique<IdentityObserver>(identity_manager_, this)) {
// Automatically load the enclave state shortly after startup so that any
// renewals will be considered without the user having to do something to
// trigger a WebAuthn operation.
LoadAfterDelay(base::Minutes(4), base::DoNothing());
// Also consider renewing the PIN every day, for users who keep Chrome open
// for long periods.
renewal_timer_.Start(FROM_HERE, base::Hours(24),
base::BindRepeating(&EnclaveManager::ConsiderPinRenewal,
weak_ptr_factory_.GetWeakPtr()));
}
EnclaveManager::~EnclaveManager() = default;
EnclaveManager* EnclaveManager::GetEnclaveManager() {
return this;
}
bool EnclaveManager::is_idle() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return !loading_ && !state_machine_;
}
bool EnclaveManager::IsLoaded() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return static_cast<bool>(local_state_);
}
bool EnclaveManager::IsRegistered() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return user_ && user_->registered();
}
bool EnclaveManager::has_pending_keys() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return pending_keys_ != nullptr;
}
bool EnclaveManager::IsReady() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return IsRegistered() && !user_->wrapped_security_domain_secrets().empty();
}
unsigned EnclaveManager::store_keys_count() const {
return store_keys_count_;
}
void EnclaveManager::LoadAfterDelay(base::TimeDelta delay,
base::OnceClosure closure) {
load_timer_.Start(
FROM_HERE, delay,
base::BindOnce(&EnclaveManager::Load, weak_ptr_factory_.GetWeakPtr(),
std::move(closure)));
}
void EnclaveManager::Load(base::OnceClosure closure) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (IsLoaded()) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(closure));
return;
}
load_duration_timer_ = std::make_unique<base::ElapsedTimer>();
load_callbacks_.emplace_back(std::move(closure));
load_callbacks_.emplace_back(
base::BindOnce(&EnclaveManager::NotifyObserversThatStateUpdated,
weak_ptr_factory_.GetWeakPtr()));
Act();
}
void EnclaveManager::RegisterIfNeeded(EnclaveManager::Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto action_callback = ToActionOutcomeCallback(
std::move(callback), ActionForUMA::kRegisterIfNeeded);
if (user_ && user_->registered()) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(action_callback), ActionOutcome::kSuccess));
return;
}
auto action = std::make_unique<PendingAction>();
action->callback = std::move(action_callback);
action->want_registration = true;
pending_actions_.emplace_back(std::move(action));
Act();
}
void EnclaveManager::SetupWithPIN(std::string pin,
EnclaveManager::Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto action = std::make_unique<PendingAction>();
action->callback =
ToActionOutcomeCallback(std::move(callback), ActionForUMA::kSetupWithPIN);
action->pin = std::move(pin);
action->setup_account = true;
pending_actions_.emplace_back(std::move(action));
Act();
}
std::unique_ptr<EnclaveManager::StoreKeysLock>
EnclaveManager::GetStoreKeysLock() {
store_keys_lock_depth_++;
return std::make_unique<StoreKeysLock>(GetWeakPtr());
}
bool EnclaveManager::AddDeviceToAccount(
std::optional<trusted_vault::GpmPinMetadata> pin_metadata,
EnclaveManager::Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(has_pending_keys());
std::unique_ptr<EnclaveLocalState::WrappedPIN> wrapped_pin;
if (pin_metadata.has_value() && pin_metadata->usable_pin_metadata) {
wrapped_pin = std::make_unique<EnclaveLocalState::WrappedPIN>();
if (!wrapped_pin->ParseFromString(
pin_metadata->usable_pin_metadata->wrapped_pin) ||
CheckPINInvariants(*wrapped_pin).has_value()) {
base::UmaHistogramEnumeration(
ToString(ActionForUMA::kAddDeviceToAccount),
ActionOutcome::kAddDeviceToAccountNotStartedWrappedPinParsingError);
return false;
}
}
auto action = std::make_unique<PendingAction>();
action->callback = ToActionOutcomeCallback(std::move(callback),
ActionForUMA::kAddDeviceToAccount);
action->store_keys_args = std::move(pending_keys_);
action->wrapped_pin = std::move(wrapped_pin);
if (pin_metadata) {
action->pin_public_key = std::move(pin_metadata->public_key);
}
pending_actions_.emplace_back(std::move(action));
Act();
return true;
}
void EnclaveManager::AddDeviceAndPINToAccount(
std::string pin,
std::optional<std::string> previous_pin_public_key,
EnclaveManager::Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(has_pending_keys());
auto action = std::make_unique<PendingAction>();
action->pin_public_key = std::move(previous_pin_public_key);
action->callback = ToActionOutcomeCallback(
std::move(callback), ActionForUMA::kAddDeviceAndPINToAccount);
action->store_keys_args = std::move(pending_keys_);
action->pin = std::move(pin);
pending_actions_.emplace_back(std::move(action));
Act();
}
void EnclaveManager::SetPIN(std::string pin,
std::string rapt,
EnclaveManager::Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(user_->registered());
auto action = std::make_unique<PendingAction>();
action->callback =
ToActionOutcomeCallback(std::move(callback), ActionForUMA::kSetPIN);
action->set_pin = std::move(pin);
action->rapt = std::move(rapt);
pending_actions_.emplace_back(std::move(action));
Act();
}
void EnclaveManager::ChangePIN(std::string updated_pin,
std::string rapt,
EnclaveManager::Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(user_->registered());
auto action = std::make_unique<PendingAction>();
action->callback =
ToActionOutcomeCallback(std::move(callback), ActionForUMA::kChangePIN);
action->updated_pin = std::move(updated_pin);
action->rapt = std::move(rapt);
pending_actions_.emplace_back(std::move(action));
Act();
}
void EnclaveManager::RenewPIN(EnclaveManager::Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(user_->registered());
CHECK(user_->has_wrapped_pin());
auto action = std::make_unique<PendingAction>();
// TODO(crbug.com/542277412): Use `ToActionOutcomeCallback` and migrate to
// `WebAuthentication.Enclave.ActionOutcome.RenewPIN` for consistency with
// other action outcome metrics.
action->callback = base::BindOnce(
[](EnclaveManager::Callback callback, ActionOutcome action_outcome) {
base::UmaHistogramEnumeration(
"WebAuthentication.Enclave.PinRenewalActionOutcome",
action_outcome);
std::move(callback).Run(action_outcome == ActionOutcome::kSuccess);
},
std::move(callback));
action->renew_pin = true;
pending_actions_.emplace_back(std::move(action));
Act();
}
#if BUILDFLAG(IS_MAC)
void EnclaveManager::AddICloudRecoveryKey(
std::unique_ptr<trusted_vault::ICloudRecoveryKey> icloud_recovery_key,
EnclaveManager::Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(user_->registered());
CHECK(!secret_.empty())
<< "AddICloudRecoveryKey must be called immediately after registration "
"and before discarding the security domain secret";
auto action = std::make_unique<PendingAction>();
action->callback = ToActionOutcomeCallback(
std::move(callback), ActionForUMA::kAddICloudRecoveryKey);
action->icloud_recovery_key = std::move(icloud_recovery_key);
pending_actions_.emplace_back(std::move(action));
Act();
}
#endif // BUILDFLAG(IS_MAC)
void EnclaveManager::Unenroll(EnclaveManager::Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto action = std::make_unique<PendingAction>();
action->callback = ToActionOutcomeCallback(
base::BindOnce(&EnclaveManager::UnregisterComplete,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
ActionForUMA::kUnenroll);
action->unregister = true;
if (!user_ || !IsRegistered()) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(action->callback), ActionOutcome::kSuccess));
return;
}
pending_actions_.emplace_back(std::move(action));
Act();
}
bool EnclaveManager::ConsiderSecurityDomainState(
const trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult&
state,
EnclaveManager::Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(user_);
bool ret = IsReady();
auto action_callback = ToActionOutcomeCallback(
std::move(callback), ActionForUMA::kConsiderSecurityDomainState);
if (IsSecurityDomainReset(state)) {
ClearRegistration();
FIDO_LOG(EVENT) << "The security domain has been reset.";
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(action_callback), ActionOutcome::kSuccess));
return false;
}
if (ret && state.gpm_pin_metadata.has_value() &&
state.gpm_pin_metadata->usable_pin_metadata) {
const auto& metadata = *state.gpm_pin_metadata;
auto wrapped_pin = std::make_unique<EnclaveLocalState::WrappedPIN>();
if (wrapped_pin->ParseFromString(
metadata.usable_pin_metadata->wrapped_pin) &&
!CheckPINInvariants(*wrapped_pin).has_value()) {
if (metadata.public_key.has_value() &&
(!user_->has_wrapped_pin() ||
user_->wrapped_pin().wrapped_pin() != wrapped_pin->wrapped_pin())) {
std::unique_ptr<PendingAction> action =
std::make_unique<PendingAction>();
action->callback = std::move(action_callback);
action->update_wrapped_pin = true;
action->wrapped_pin = std::move(wrapped_pin);
action->pin_public_key = *metadata.public_key;
FIDO_LOG(EVENT) << "The GPM PIN has been updated";
pending_actions_.emplace_back(std::move(action));
Act();
}
} else {
FIDO_LOG(ERROR) << "Wrapped PIN from security domain update is invalid: "
<< base::HexEncode(base::as_byte_span(
metadata.usable_pin_metadata->wrapped_pin));
base::UmaHistogramEnumeration(
ToString(ActionForUMA::kConsiderSecurityDomainState),
ActionOutcome::
kConsiderSecurityDomainStateNotStartedWrappedPinParsingError);
}
}
return ret;
}
void EnclaveManager::GetIdentityKeyForSignature(
base::OnceCallback<void(
scoped_refptr<unexportable_keys::RefCountedUnexportableSigningKey>)>
callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!user_ || user_->wrapped_identity_private_key().empty()) {
std::move(callback).Run(nullptr);
return;
}
if (identity_key_) {
std::move(callback).Run(identity_key_);
return;
}
auto key_callback = base::BindOnce(
[](base::WeakPtr<EnclaveManager> enclave_manager,
CoreAccountId account_id,
base::OnceCallback<void(
scoped_refptr<
unexportable_keys::RefCountedUnexportableSigningKey>)>
callback,
std::unique_ptr<crypto::UnexportableSigningKey> key) {
if (!enclave_manager ||
enclave_manager->primary_account_info_->account_id != account_id) {
std::move(callback).Run(nullptr);
return;
}
DCHECK_CALLED_ON_VALID_SEQUENCE(enclave_manager->sequence_checker_);
if (!key) {
enclave_manager->ClearRegistration();
std::move(callback).Run(nullptr);
return;
}
enclave_manager->identity_key_ =
unexportable_keys::MakeRefCountedUnexportableSigningKey(
std::move(key));
std::move(callback).Run(enclave_manager->identity_key_);
},
weak_ptr_factory_.GetWeakPtr(), primary_account_info_->account_id,
std::move(callback));
// Retrieve the key on a non-UI thread, and post a task back to the current
// thread that invokes `key_callback` with the obtained key.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
base::BindOnce(
[](std::string wrapped_identity_private_key)
-> std::unique_ptr<crypto::UnexportableSigningKey> {
std::unique_ptr<crypto::UnexportableKeyProvider> provider =
GetWebAuthnUnexportableKeyProvider();
if (!provider) {
return nullptr;
}
return provider->FromWrappedSigningKeySlowly(
ToVector(wrapped_identity_private_key));
},
user_->wrapped_identity_private_key()),
std::move(key_callback));
}
enclave::SigningCallback EnclaveManager::IdentityKeySigningCallback() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!user_->wrapped_identity_private_key().empty());
CHECK(user_->registered());
return base::BindOnce(
[](base::WeakPtr<EnclaveManager> enclave_manager,
enclave::SignedMessage message_to_be_signed,
base::OnceCallback<void(std::optional<enclave::ClientSignature>)>
result_callback) {
if (!enclave_manager || !enclave_manager->user_) {
std::move(result_callback).Run(std::nullopt);
return;
}
DCHECK_CALLED_ON_VALID_SEQUENCE(enclave_manager->sequence_checker_);
auto signing_callback = base::BindOnce(
[](std::string device_id,
enclave::SignedMessage message_to_be_signed,
base::OnceCallback<void(std::optional<enclave::ClientSignature>)>
result_callback,
scoped_refptr<
unexportable_keys::RefCountedUnexportableSigningKey> key) {
if (!key) {
std::move(result_callback).Run(std::nullopt);
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::TaskPriority::BEST_EFFORT, base::MayBlock()},
base::BindOnce(
[](std::string device_id,
enclave::SignedMessage message_to_be_signed,
scoped_refptr<unexportable_keys::
RefCountedUnexportableSigningKey>
key) -> std::optional<enclave::ClientSignature> {
std::optional<std::vector<uint8_t>> signature =
key->key().SignSlowly(message_to_be_signed);
if (!signature) {
return std::nullopt;
}
enclave::ClientSignature client_signature;
client_signature.device_id = ToVector(device_id);
client_signature.signature = std::move(*signature);
client_signature.key_type =
key->key().IsHardwareBacked()
? enclave::ClientKeyType::kHardware
: enclave::ClientKeyType::kSoftware;
return std::move(client_signature);
},
std::move(device_id), std::move(message_to_be_signed),
key),
std::move(result_callback));
},
enclave_manager->user_->device_id(),
std::move(message_to_be_signed), std::move(result_callback));
enclave_manager->GetIdentityKeyForSignature(
std::move(signing_callback));
},
weak_ptr_factory_.GetWeakPtr());
}
void EnclaveManager::GetUserVerifyingKeyForSignature(
UVKeyOptions options,
base::OnceCallback<void(
scoped_refptr<crypto::RefCountedUserVerifyingSigningKey>)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!user_ || user_->wrapped_uv_private_key().empty()) {
FIDO_LOG(ERROR) << "Attempted a UV signature but no key is available";
std::move(callback).Run(nullptr);
return;
}
#if BUILDFLAG(IS_WIN)
// On Windows, retrieving the UV key is slow so we cache it. On Mac, we avoid
// caching the key as we need to use a fresh LAContext every time we retrieve
// the key.
if (user_verifying_key_) {
std::move(callback).Run(user_verifying_key_);
return;
}
#endif // BUILDFLAG(IS_WIN)
auto user_verifying_key_provider =
GetUserVerifyingKeyProviderForSigning(std::move(options));
if (!user_verifying_key_provider) {
FIDO_LOG(ERROR)
<< "Attempted a UV signature but UV key provider is unavailable";
// This indicates the platform key provider was available, but now is not.
ClearRegistration();
std::move(callback).Run(nullptr);
return;
}
auto key_callback = base::BindOnce(
[](base::WeakPtr<EnclaveManager> enclave_manager,
CoreAccountId account_id,
base::OnceCallback<void(
scoped_refptr<crypto::RefCountedUserVerifyingSigningKey>)>
callback,
base::expected<std::unique_ptr<crypto::UserVerifyingSigningKey>,
crypto::UserVerifyingKeyCreationError> maybe_key) {
if (!enclave_manager ||
enclave_manager->primary_account_info_->account_id != account_id) {
FIDO_LOG(ERROR) << "Primary user no longer available for UV key "
"signature generation";
std::move(callback).Run(nullptr);
return;
}
if (!maybe_key.has_value()) {
FIDO_LOG(ERROR) << "UV key retrieval failed with error "
<< static_cast<int>(maybe_key.error());
enclave_manager->ClearRegistration();
std::move(callback).Run(nullptr);
return;
}
enclave_manager->user_verifying_key_ =
base::MakeRefCounted<crypto::RefCountedUserVerifyingSigningKey>(
std::move(maybe_key.value()));
std::move(callback).Run(enclave_manager->user_verifying_key_);
},
weak_ptr_factory_.GetWeakPtr(), primary_account_info_->account_id,
std::move(callback));
auto key_label =
UserVerifyingKeyLabelFromString(user_->wrapped_uv_private_key());
CHECK(key_label);
user_verifying_key_provider->GetUserVerifyingSigningKey(
std::move(*key_label), std::move(key_callback));
}
enclave::SigningCallback EnclaveManager::UserVerifyingKeySigningCallback(
UVKeyOptions options) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!user_->wrapped_uv_private_key().empty());
CHECK(user_->registered());
return base::BindOnce(
[](UVKeyOptions options, base::WeakPtr<EnclaveManager> enclave_manager,
enclave::SignedMessage message_to_be_signed,
base::OnceCallback<void(std::optional<enclave::ClientSignature>)>
result_callback) {
if (!enclave_manager) {
std::move(result_callback).Run(std::nullopt);
return;
}
DCHECK_CALLED_ON_VALID_SEQUENCE(enclave_manager->sequence_checker_);
auto signing_callback = base::BindOnce(
[](std::string device_id,
enclave::SignedMessage message_to_be_signed,
base::OnceCallback<void(std::optional<enclave::ClientSignature>)>
result_callback,
scoped_refptr<crypto::RefCountedUserVerifyingSigningKey>
uv_signing_key) {
if (!uv_signing_key) {
std::move(result_callback).Run(std::nullopt);
return;
}
uv_signing_key->key().Sign(
message_to_be_signed,
base::BindOnce(
[](std::string device_id, const bool is_hardware_backed,
base::OnceCallback<void(
std::optional<enclave::ClientSignature>)>
result_callback,
base::expected<std::vector<uint8_t>,
crypto::UserVerifyingKeySigningError>
maybe_signature) {
if (!maybe_signature.has_value()) {
FIDO_LOG(ERROR)
<< "UV key signature failed with error "
<< static_cast<int>(maybe_signature.error());
std::move(result_callback).Run(std::nullopt);
return;
}
enclave::ClientSignature client_signature;
client_signature.device_id = ToVector(device_id);
client_signature.signature =
std::move(maybe_signature.value());
client_signature.key_type =
is_hardware_backed
? enclave::ClientKeyType::kUserVerified
: enclave::ClientKeyType::kSoftwareUserVerified;
std::move(result_callback)
.Run(std::move(client_signature));
},
std::move(device_id),
uv_signing_key->key().IsHardwareBacked(),
std::move(result_callback)));
},
enclave_manager->user_->device_id(),
std::move(message_to_be_signed), std::move(result_callback));
enclave_manager->GetUserVerifyingKeyForSignature(
std::move(options), std::move(signing_callback));
},
std::move(options), weak_ptr_factory_.GetWeakPtr());
}
std::pair<std::unique_ptr<EnclaveManager::UvKeyCreationLock>,
device::enclave::UVKeyCreationCallback>
EnclaveManager::UserVerifyingKeyCreationCallback() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(user_->deferred_uv_key_creation());
CHECK(user_->registered());
return {
TakeUvKeyCreationLock(),
base::BindOnce(
[](base::WeakPtr<EnclaveManager> enclave_manager,
CoreAccountId account_id,
base::OnceCallback<void(base::span<const uint8_t>)>
public_key_callback) {
if (!enclave_manager) {
std::move(public_key_callback).Run(std::vector<uint8_t>());
return;
}
// Unregister the device with the enclave if there are any errors
// from this point, because UV key creation is a necessary step to
// have a usable state.
//
// The key provider is only used for creating a new key, not for
// signing, so passing empty options here is ok.
auto key_provider =
GetUserVerifyingKeyProviderForCreateAndDeleteOnly();
if (!key_provider) {
enclave_manager->OnDeferredUvKeyCreationFailure();
std::move(public_key_callback).Run(std::vector<uint8_t>());
return;
}
key_provider->GenerateUserVerifyingSigningKey(
device::enclave::kSigningAlgorithms,
base::BindOnce(
[](base::WeakPtr<EnclaveManager> enclave_manager,
base::OnceCallback<void(base::span<const uint8_t>)>
public_key_callback,
CoreAccountId account_id,
base::expected<
std::unique_ptr<crypto::UserVerifyingSigningKey>,
crypto::UserVerifyingKeyCreationError>
maybe_uv_key) {
if (!enclave_manager ||
enclave_manager->primary_account_info_->account_id !=
account_id) {
FIDO_LOG(ERROR)
<< "Primary user no longer available for "
"deferred UV key creation";
std::move(public_key_callback)
.Run(std::vector<uint8_t>());
return;
}
if (!maybe_uv_key.has_value()) {
FIDO_LOG(ERROR)
<< "Failed deferred UV key creation with error "
<< static_cast<int>(maybe_uv_key.error());
// If the user cancelled the verification, they should
// get a chance to try again on a future request.
// Otherwise the device is unregistered so they can
// attempt recovery later.
if (maybe_uv_key.error() !=
crypto::UserVerifyingKeyCreationError::
kUserCancellation) {
enclave_manager->OnDeferredUvKeyCreationFailure();
}
std::move(public_key_callback)
.Run(std::vector<uint8_t>());
return;
}
enclave_manager->user_verifying_key_ =
base::MakeRefCounted<
crypto::RefCountedUserVerifyingSigningKey>(
std::move(maybe_uv_key.value()));
const std::vector<uint8_t> uv_public_key =
enclave_manager->user_verifying_key_->key()
.GetPublicKey();
const std::string uv_public_key_str =
VecToString(uv_public_key);
auto* local_state =
StateForUser(enclave_manager->local_state_.get(),
*enclave_manager->primary_account_info_);
local_state->set_uv_public_key(uv_public_key_str);
local_state->set_wrapped_uv_private_key(
UserVerifyingLabelToString(
enclave_manager->user_verifying_key_->key()
.GetKeyLabel()));
local_state->set_deferred_uv_key_creation(false);
enclave_manager->WriteState(
enclave_manager->local_state_.get());
enclave_manager->OnDeferredUvKeyCreationSuccess();
std::move(public_key_callback).Run(uv_public_key);
},
enclave_manager, std::move(public_key_callback),
std::move(account_id)));
},
weak_ptr_factory_.GetWeakPtr(), primary_account_info_->account_id)};
}
void EnclaveManager::OnDeferredUvKeyCreationFailure() {
ClearRegistration();
deferred_uv_key_creation_successful_ = false;
}
void EnclaveManager::OnDeferredUvKeyCreationSuccess() {
deferred_uv_key_creation_successful_ = true;
}
std::unique_ptr<EnclaveManager::UvKeyCreationLock>
EnclaveManager::TakeUvKeyCreationLock() {
CHECK(!deferred_uv_key_creation_in_progress_);
deferred_uv_key_creation_in_progress_ = true;
return std::make_unique<UvKeyCreationLockImpl>(
(base::BindOnce(&EnclaveManager::OnUvKeyCreationLockReleased,
weak_ptr_factory_.GetWeakPtr())));
}
void EnclaveManager::OnUvKeyCreationLockReleased() {
CHECK(deferred_uv_key_creation_in_progress_);
deferred_uv_key_creation_in_progress_ = false;
// If the success bit hasn't been set, it means a transaction was destroyed
// before attempting UV key creation. By passing `true` to the pending
// transactions, the next one can attempt to create one.
bool success = deferred_uv_key_creation_successful_.has_value()
? *deferred_uv_key_creation_successful_
: true;
if (!pending_uv_key_requests_.empty()) {
std::vector<base::OnceCallback<void(bool)>> callbacks;
pending_uv_key_requests_.swap(callbacks);
for (auto& callback : callbacks) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), success));
}
}
}
void EnclaveManager::AddPendingUvRequest(
base::OnceCallback<void(bool)> callback) {
CHECK(deferred_uv_key_creation_in_progress_);
pending_uv_key_requests_.emplace_back(std::move(callback));
}
std::optional<std::vector<uint8_t>> EnclaveManager::GetWrappedSecret(
int32_t version) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(IsReady());
const auto it = user_->wrapped_security_domain_secrets().find(version);
if (it == user_->wrapped_security_domain_secrets().end()) {
return std::nullopt;
}
return ToVector(it->second);
}
std::pair<int32_t, std::vector<uint8_t>>
EnclaveManager::GetCurrentWrappedSecret() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(IsReady());
return GetCurrentWrappedSecretForUser(user_);
}
std::optional<std::pair<int32_t, std::vector<uint8_t>>>
EnclaveManager::TakeSecret() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (secret_.empty()) {
return std::nullopt;
}
return std::make_pair(secret_version_, std::move(secret_));
}
bool EnclaveManager::has_wrapped_pin() const {
CHECK(IsReady());
return user_->has_wrapped_pin();
}
bool EnclaveManager::wrapped_pin_is_arbitrary() const {
CHECK(has_wrapped_pin());
return user_->wrapped_pin().form() ==
EnclaveLocalState::WrappedPIN::FORM_ARBITRARY;
}
std::unique_ptr<webauthn_pb::EnclaveLocalState_WrappedPIN>
EnclaveManager::GetWrappedPIN() {
CHECK(has_wrapped_pin());
return std::make_unique<webauthn_pb::EnclaveLocalState_WrappedPIN>(
user_->wrapped_pin());
}
void EnclaveManager::SetWrappedPINDataForTesting(
std::vector<uint8_t> wrapped_pin_data) {
CHECK(has_wrapped_pin());
const_cast<webauthn_pb::EnclaveLocalState_User&>(*user_)
.mutable_wrapped_pin()
->set_wrapped_pin(VecToString(wrapped_pin_data));
}
EnclaveManager::UvKeyState EnclaveManager::uv_key_state(
PlatformUvSupport platform_uv_support) const {
CHECK(IsReady());
if (base::FeatureList::IsEnabled(
device::kWebAuthnCreatePinWhenSystemUvDisabled)) {
if (platform_uv_support == PlatformUvSupport::kNoUvKey) {
// Cannot use any UV keys if there is no UV key support.
return UvKeyState::kNone;
}
}
#if BUILDFLAG(IS_WIN)
if (user_->deferred_uv_key_creation()) {
return UvKeyState::kUsesSystemUIDeferredCreation;
}
#endif
if (user_->wrapped_uv_private_key().empty()) {
return UvKeyState::kNone;
}
#if BUILDFLAG(IS_MAC)
if (platform_uv_support == PlatformUvSupport::kUvKeyWithBiometrics) {
// Chrome will display an LAAuthenticationView with a Touch ID prompt.
return UvKeyState::kUsesChromeUI;
}
// Delegate prompting the user for their screen lock to macOS.
return UvKeyState::kUsesSystemUI;
#else
return UvKeyState::kUsesSystemUI;
#endif
}
std::unique_ptr<trusted_vault::TrustedVaultConnection::Request>
EnclaveManager::CheckGpmPinAvailability(GpmPinAvailabilityCallback callback) {
CoreAccountInfo account_info =
identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
return trusted_vault_conn_->DownloadAuthenticationFactorsRegistrationState(
account_info,
base::BindOnce(
[](GpmPinAvailabilityCallback callback,
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
result) {
if (!result.gpm_pin_metadata) {
std::move(callback).Run(GpmPinAvailability::kGpmPinUnset);
return;
}
if (!result.gpm_pin_metadata->usable_pin_metadata) {
std::move(callback).Run(
GpmPinAvailability::kGpmPinSetButNotUsable);
return;
}
EnclaveLocalState::WrappedPIN wrapped_pin;
bool pin_is_usable = wrapped_pin.ParseFromString(
result.gpm_pin_metadata
->usable_pin_metadata->wrapped_pin) &&
!CheckPINInvariants(wrapped_pin).has_value();
std::move(callback).Run(
pin_is_usable ? GpmPinAvailability::kGpmPinSetAndUsable
: GpmPinAvailability::kGpmPinSetButNotUsable);
},
std::move(callback)),
base::DoNothing());
}
// static
void EnclaveManager::AreUserVerifyingKeysSupported(Callback callback) {
if (base::FeatureList::IsEnabled(
device::kWebAuthnUseInsecureSoftwareUnexportableKeys)) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), true));
return;
}
#if BUILDFLAG(IS_CHROMEOS)
// ChromeOS doesn't have HW-backed UV keys, but uses a software provider.
std::move(callback).Run(true);
#else
crypto::AreUserVerifyingKeysSupported(
MakeUserVerifyingKeyConfig(/*options=*/{}), std::move(callback));
#endif
}
std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher>
EnclaveManager::GetAccessToken(
base::OnceCallback<void(std::optional<std::string>)> callback) {
return std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
signin::OAuthConsumerId::kEnclaveManager, identity_manager_,
base::BindOnce(
[](base::OnceCallback<void(std::optional<std::string>)> callback,
GoogleServiceAuthError error,
signin::AccessTokenInfo access_token_info) {
if (error.state() == GoogleServiceAuthError::NONE) {
std::move(callback).Run(std::move(access_token_info.token));
} else {
FIDO_LOG(ERROR)
<< "Failed to get access token: " << error.error_message();
std::move(callback).Run(std::nullopt);
}
},
std::move(callback)),
signin::PrimaryAccountAccessTokenFetcher::Mode::kImmediate,
signin::ConsentLevel::kSignin);
}
void EnclaveManager::AddObserver(Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observer_list_.AddObserver(observer);
}
void EnclaveManager::RemoveObserver(Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observer_list_.RemoveObserver(observer);
}
void EnclaveManager::StorePendingKeys(
const GaiaId& gaia_id,
std::vector<TrustedVaultKeyAndVersion> keys) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
pending_keys_ = std::make_unique<StoreKeysArgs>();
pending_keys_->gaia_id = gaia_id;
pending_keys_->keys = std::move(keys);
store_keys_count_++;
for (Observer& observer : observer_list_) {
observer.OnKeysStored(gaia_id);
}
}
void EnclaveManager::TemporarilyCachePendingOpportunisticKeys(
const GaiaId& gaia_id,
std::vector<TrustedVaultKeyAndVersion> keys) {
auto store_keys_args = std::make_unique<StoreKeysArgs>();
store_keys_args->gaia_id = gaia_id;
store_keys_args->keys = std::move(keys);
if (opportunistic_pending_keys_) {
// Some opportunistically retrieved key has already been cached. It will be
// overwritten by the current key.
webauthn::metrics::RecordGPMCachedOpportunisticallyRetrievedKeyEvent(
webauthn::metrics::
WebAuthenticationGPMCachedOpportunisticallyRetrievedKeyEvent::
kStoreKeysFromOpportunisticFlowCachedKeysHaveBeenOverwritten);
}
opportunistic_pending_keys_ = std::move(store_keys_args);
// Ensure that the cached keys could be discarded after the timeout.
int ttl_seconds =
device::kWebAuthnOpportunisticRetrievalTimeToKeepCachedKeySeconds.Get();
// Configuring the task for invalidating the cached key (if we overwrote the
// cached key - the previous key invalidation task will be cancelled at this
// point).
opportunistic_pending_keys_invalidation_task_.Reset(base::BindOnce(
[](base::WeakPtr<EnclaveManager> manager) {
if (manager && manager->opportunistic_pending_keys_) {
manager->opportunistic_pending_keys_.reset();
webauthn::metrics::RecordGPMCachedOpportunisticallyRetrievedKeyEvent(
webauthn::metrics::
WebAuthenticationGPMCachedOpportunisticallyRetrievedKeyEvent::
kStoreKeysFromOpportunisticFlowCachedKeysRemovedAfterTimeout);
}
},
weak_ptr_factory_.GetWeakPtr()));
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, opportunistic_pending_keys_invalidation_task_.callback(),
base::Seconds(ttl_seconds));
}
bool EnclaveManager::IsStoringKeysFromOutOfContextRetrievalEnabled() {
return base::FeatureList::IsEnabled(
device::kWebAuthnOpportunisticRetrieval) &&
base::FeatureList::IsEnabled(
device::
kWebAuthnDoNotAlwaysTerminateStateMachineDuringIdentityChange);
}
void EnclaveManager::StoreKeys(
const GaiaId& gaia_id,
std::vector<TrustedVaultKeyAndVersion> keys,
std::optional<trusted_vault::TrustedVaultUserActionTriggerForUMA>
user_action_trigger) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (user_action_trigger.has_value()) {
base::UmaHistogramEnumeration(
"PasswordManager.UserActionTriggerThatRetrievedPasskeySecret",
user_action_trigger.value());
}
if (IsStoringKeysFromOutOfContextRetrievalEnabled()) {
if (store_keys_lock_depth_) {
webauthn::metrics::RecordGPMRecoveryEvent(
webauthn::metrics::WebAuthenticationGPMRecoveryEvent::
kStoreKeysFromExplicitFlowStarted);
StorePendingKeys(gaia_id, std::move(keys));
} else {
CoreAccountInfo primary_account_info =
identity_manager_->GetPrimaryAccountInfo(
signin::ConsentLevel::kSignin);
if (primary_account_info.IsEmpty() ||
primary_account_info.gaia != gaia_id) {
// We can't store keys if the primary account is empty or has a
// different Gaia Id (because for storing keys we need to fetch a
// trusted vault access token, which can't be done if the account is
// empty or has a different Gaia Id). Upon identity change we will
// re-attempt to store these keys.
TemporarilyCachePendingOpportunisticKeys(gaia_id, std::move(keys));
webauthn::metrics::RecordGPMRecoveryEvent(
webauthn::metrics::WebAuthenticationGPMRecoveryEvent::
kStoreKeysFromOpportunisticFlowCachedKeysBecauseAccountDoesNotMatch);
return;
}
// TODO(crbug.com/450851888): Refactor the logic related to storing the
// keys from the out of context retrieval.
StoreKeysFromOutOfContextRetrieval(gaia_id, std::move(keys));
}
} else {
// Use the old implementation:
StorePendingKeys(gaia_id, std::move(keys));
}
}
void EnclaveManager::StoreKeysFromOutOfContextRetrieval(
const GaiaId& gaia_id,
std::vector<TrustedVaultKeyAndVersion> keys) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!store_keys_lock_depth_);
webauthn::metrics::RecordGPMRecoveryEvent(
webauthn::metrics::WebAuthenticationGPMRecoveryEvent::
kStoreKeysFromOpportunisticFlowStarted);
auto pending_keys = std::make_unique<StoreKeysArgs>();
pending_keys->gaia_id = gaia_id;
pending_keys->keys = std::move(keys);
if (IsRegistered()) {
FIDO_LOG(EVENT) << "Redundant opportunistic keys provided for version "
<< pending_keys->keys.back().version;
NotifyObserversAboutOutOfContextRecoveryOutcome(
OutOfContextRecoveryOutcome::
kStoreKeysFromOpportunisticFlowIgnoredRedundant);
return;
}
FIDO_LOG(EVENT) << "Opportunistic keys provided";
// We store opportunistically retrieved keys via
// `EnclaveManager::StateMachine` because the state machine always operates
// within the context of a specific Google account and will be destroyed by
// the EnclaveManager if the currently signed-in user changes. This is
// important because this helps to avoid a possible race condition between
// storing the keys and changing account (and such race condition could
// hypothetically lead to storing keys in a wrong account).
auto action = std::make_unique<PendingAction>();
action->callback =
base::BindOnce(&EnclaveManager::OpportunisticStoreKeysAddComplete,
weak_ptr_factory_.GetWeakPtr());
action->opportunistic_store_keys_args = std::move(pending_keys);
pending_actions_.emplace_back(std::move(action));
Act();
}
std::unique_ptr<enclave::ClaimedPIN> EnclaveManager::MakeClaimedPINSlowly(
std::string pin,
std::unique_ptr<webauthn_pb::EnclaveLocalState_WrappedPIN> wrapped_pin) {
uint8_t hashed[32];
crypto::kdf::Scrypt(
{.cost = static_cast<uint64_t>(wrapped_pin->hash_difficulty()),
.block_size = 8,
.parallelization = 1,
.max_memory_bytes = 1ul << 28},
base::as_byte_span(pin), base::as_byte_span(wrapped_pin->hash_salt()),
hashed, webauthn::MakeCryptoPassKey());
static constexpr uint8_t kAAD[] = {'P', 'I', 'N', ' ', 'c',
'l', 'a', 'i', 'm'};
crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM,
base::as_byte_span(wrapped_pin->claim_key()));
uint8_t nonce[12];
crypto::RandBytes(nonce);
std::vector<uint8_t> ciphertext = aead.Seal(hashed, nonce, kAAD);
ciphertext.insert(ciphertext.begin(), std::begin(nonce), std::end(nonce));
return std::make_unique<enclave::ClaimedPIN>(
std::move(ciphertext), ToVector(wrapped_pin->wrapped_pin()));
}
bool EnclaveManager::RunWhenStoppedForTesting(base::OnceClosure on_stop) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!state_machine_ && !loading_);
if (!currently_writing_) {
return false;
}
write_finished_callback_ = std::move(on_stop);
return true;
}
EnclaveLocalState& EnclaveManager::local_state_for_testing() {
return *local_state_;
}
void EnclaveManager::ClearCachedKeysForTesting() {
user_verifying_key_ = nullptr;
identity_key_ = nullptr;
}
void EnclaveManager::ResetForTesting() {
store_keys_count_ = 0;
user_verifying_key_ = nullptr;
identity_key_ = nullptr;
secret_.clear();
secret_version_ = -1;
pending_actions_.clear();
load_callbacks_.clear();
state_machine_.reset();
pending_keys_.reset();
currently_writing_ = false;
pending_write_ = std::nullopt;
identity_observer_.reset();
primary_account_info_.reset();
user_ = nullptr;
local_state_.reset();
loading_ = false;
}
void EnclaveManager::ClearRegistrationForTesting() {
ClearRegistration();
}
// static
void EnclaveManager::EnableInvariantChecksForTesting(bool enabled) {
g_invariant_override_ = !enabled;
}
void EnclaveManager::ConsiderPinRenewalForTesting() {
ConsiderPinRenewal();
}
unsigned EnclaveManager::renewal_checks_for_testing() const {
return renewal_checks_;
}
unsigned EnclaveManager::renewal_attempts_for_testing() const {
LOG(ERROR) << __func__ << " " << renewal_attempts_;
return renewal_attempts_;
}
// static
std::string EnclaveManager::MakeWrappedPINForTesting(
base::span<const uint8_t> security_domain_secret,
std::string_view pin) {
std::unique_ptr<HashedPIN> hashed = HashPINSlowly(pin);
std::unique_ptr<EnclaveLocalState::WrappedPIN> wrapped_pin =
hashed->ToWrappedPIN();
const uint8_t kFakeCounterId[8] = {};
const uint8_t kFakeVaultHandle[16] = {};
const uint8_t kFakeCohortPublicKey[16] = {};
const int32_t kFakeSerialNumber = 1;
cbor::Value::MapValue map;
map.emplace(1, base::span<const uint8_t>(hashed->hashed));
// 2 used to correspond to the generation.
map.emplace(3, ToSizedSpan<32>(wrapped_pin->claim_key()));
map.emplace(4, base::span<const uint8_t>(kFakeCounterId));
map.emplace(5, base::span<const uint8_t>(kFakeVaultHandle));
map.emplace(6, base::span<const uint8_t>(kFakeCohortPublicKey));
map.emplace(7, kFakeSerialNumber);
const std::vector<uint8_t> cbor_bytes =
cbor::Writer::Write(cbor::Value(std::move(map))).value();
wrapped_pin->set_wrapped_pin(
VecToString(EncryptWrappedPIN(security_domain_secret, cbor_bytes)));
return wrapped_pin->SerializeAsString();
}
// static
std::vector<uint8_t> EnclaveManager::EncryptWrappedPIN(
base::span<const uint8_t> security_domain_secret,
base::span<const uint8_t> cbor_bytes) {
// This is "KeychainApplicationKey:chrome:GPM PIN data wrapping key".
static constexpr uint8_t kKeyPurposePinDataKey[] = {
0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x41, 0x70, 0x70,
0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79,
0x3a, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x3a, 0x47, 0x50, 0x4d,
0x20, 0x50, 0x49, 0x4e, 0x20, 0x64, 0x61, 0x74, 0x61, 0x20, 0x77,
0x72, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x20, 0x6b, 0x65, 0x79};
const std::array<uint8_t, 32> derived_key = crypto::kdf::Hkdf<32>(
crypto::hash::kSha256, security_domain_secret,
/*salt=*/base::span<const uint8_t>(), kKeyPurposePinDataKey);
crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM, derived_key);
uint8_t nonce[12];
crypto::RandBytes(nonce);
std::vector<uint8_t> wrapped_pin = aead.Seal(
cbor_bytes, nonce, /*additional_data=*/base::span<const uint8_t>());
wrapped_pin.insert(wrapped_pin.begin(), std::begin(nonce), std::end(nonce));
return wrapped_pin;
}
void EnclaveManager::Act() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!local_state_) {
if (loading_) {
return;
}
loading_ = true;
if (!encryptor_) {
g_browser_process->os_crypt_async()->GetInstance(base::BindOnce(
&EnclaveManager::OnOsCryptReady, weak_ptr_factory_.GetWeakPtr()));
return;
}
base::OnceCallback<void(std::optional<std::string>)> decryption_callback =
base::BindOnce(
[](base::WeakPtr<EnclaveManager> manager,
std::optional<std::string> contents) {
if (!manager) {
return;
}
std::string decrypted;
if (!contents.has_value() ||
!manager->encryptor_->DecryptString(*contents, &decrypted)) {
manager->LoadComplete(std::nullopt);
return;
}
manager->LoadComplete(std::move(decrypted));
},
weak_ptr_factory_.GetWeakPtr());
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_BLOCKING, base::MayBlock()},
base::BindOnce(
[](base::FilePath path) -> std::optional<std::string> {
std::string contents;
if (!base::ReadFileToString(path, &contents)) {
return std::nullopt;
}
return std::move(contents);
},
file_path_),
std::move(decryption_callback));
return;
}
if (!load_callbacks_.empty()) {
std::vector<base::OnceClosure> callbacks = std::move(load_callbacks_);
load_callbacks_.clear();
for (auto& callback : callbacks) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(callback));
}
}
if (pending_actions_.empty() || state_machine_) {
return;
}
if (!user_) {
CancelAllActions();
return;
}
std::unique_ptr<PendingAction> action = std::move(pending_actions_.front());
pending_actions_.pop_front();
EnclaveLocalState copy;
copy.CopyFrom(*local_state_);
state_machine_ = std::make_unique<StateMachine>(
this, std::move(copy),
std::make_unique<CoreAccountInfo>(*primary_account_info_),
std::move(action));
}
void EnclaveManager::LoadComplete(std::optional<std::string> contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (load_duration_timer_) {
base::UmaHistogramTimes("WebAuthentication.EnclaveLoadDuration",
load_duration_timer_->Elapsed());
load_duration_timer_.reset();
}
loading_ = false;
if (contents) {
local_state_ = ParseStateFile(std::move(*contents));
} else {
local_state_ = std::make_unique<EnclaveLocalState>();
}
for (const auto& it : local_state_->users()) {
std::optional<int> error_line = CheckInvariants(it.second);
if (error_line.has_value()) {
FIDO_LOG(ERROR) << "State invariant failed on line " << *error_line;
local_state_ = std::make_unique<EnclaveLocalState>();
break;
}
}
HandleIdentityChange(/*is_post_load=*/true);
Act();
}
void EnclaveManager::HandleIdentityChange(bool is_post_load) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// This function is called when local state finishes loading. Prior to that
// identity changes are ignored.
if (!local_state_) {
if (opportunistic_pending_keys_ && !loading_) {
// Identity has changed, and we have pending opportunistic keys, but
// enclave manager is not loading - so let's load it. After loading the
// pending opportunistic keys will be stored.
Load(base::DoNothing());
}
return;
}
// If a state machine is running, there must be a current user.
CHECK(!state_machine_ || user_);
// In the old implementation `need_to_stop` is always `true` (but this is
// being fixed by enabling the flag
// `kWebAuthnDoNotAlwaysTerminateStateMachineDuringIdentityChange`).
bool need_to_stop = true;
if (base::FeatureList::IsEnabled(
device::
kWebAuthnDoNotAlwaysTerminateStateMachineDuringIdentityChange)) {
need_to_stop = false;
}
CoreAccountInfo primary_account_info =
identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
if (!primary_account_info.IsEmpty()) {
if (primary_account_info_ &&
primary_account_info_->account_id != primary_account_info.account_id) {
// If the signed-in user has changed, the state machine must be halted
// because otherwise it could act on the wrong account.
need_to_stop = true;
}
user_ = StateForUser(local_state_.get(), primary_account_info);
if (!user_) {
user_ = CreateStateForUser(local_state_.get(), primary_account_info);
}
if (pending_keys_ && pending_keys_->gaia_id != primary_account_info.gaia) {
pending_keys_.reset();
}
primary_account_info_ =
std::make_unique<CoreAccountInfo>(std::move(primary_account_info));
} else {
if (user_) {
// If the users signs out, the state machine is stopped because it only
// operates in the context of an account.
need_to_stop = true;
}
user_ = nullptr;
primary_account_info_.reset();
pending_keys_.reset();
}
if (need_to_stop) {
user_verifying_key_.reset();
identity_key_.reset();
}
const signin::AccountsInCookieJarInfo in_jar =
identity_manager_->GetAccountsInCookieJar();
base::flat_set<GaiaId> to_remove;
if (in_jar.AreAccountsFresh()) {
// If the user has signed out of any non-primary accounts, erase their
// enclave state.
const base::flat_set<GaiaId> gaia_ids_in_cookie_jar =
GetGaiaIDs(in_jar.GetAllAccounts());
const base::flat_set<GaiaId> gaia_ids_in_state =
GetGaiaIDs(local_state_->users());
to_remove = base::STLSetDifference<base::flat_set<GaiaId>>(
gaia_ids_in_state, gaia_ids_in_cookie_jar);
if (primary_account_info_) {
to_remove.erase(primary_account_info_->gaia);
}
if (!base::FeatureList::IsEnabled(
device::
kWebAuthnDoNotAlwaysTerminateStateMachineDuringIdentityChange)) {
// The old behavior assumes that `need_to_stop` is always `true` and
// always updates local state and writes it to disk (even if `to_remove`
// is empty).
//
// A `StateMachine` can also mutate the enclave state. Thus if we're
// about to mutate it ourselves, confirm that any `StateMachine` is
// about to be stopped and thus cannot overwrite these changes.
CHECK(need_to_stop);
for (const auto& gaia_id : to_remove) {
CHECK(local_state_->mutable_users()->erase(gaia_id.ToString()));
}
WriteState(local_state_.get());
}
}
if (need_to_stop && !is_post_load) {
CancelAllActions();
Stopped();
}
if (base::FeatureList::IsEnabled(
device::
kWebAuthnDoNotAlwaysTerminateStateMachineDuringIdentityChange)) {
// In the new implementation we update local state only if `to_remove` is
// not empty.
if (!to_remove.empty()) {
// Scheduling a state machine operation, which will update the local state
// and will write it to disk.
RemoveGaiaIdsFromLocalState(std::move(to_remove));
}
}
// Now, as the the logic of handling identity change has finished, we can try
// to store the cached opportunistically retrieved key (if the new primary
// account matches).
if (IsStoringKeysFromOutOfContextRetrievalEnabled() &&
primary_account_info_ && !primary_account_info_->IsEmpty() &&
opportunistic_pending_keys_ &&
opportunistic_pending_keys_->gaia_id == primary_account_info_->gaia) {
std::unique_ptr<StoreKeysArgs> store_keys_arg =
std::move(opportunistic_pending_keys_);
// Storing pending opportunistically retrieved keys. These keys were in a
// pending state because at the moment in time when they were retrieved
// the primary account was either empty or had a different Gaia Id. Now
// the primary account is available so we can store them.
StoreKeysFromOutOfContextRetrieval(store_keys_arg->gaia_id,
std::move(store_keys_arg->keys));
webauthn::metrics::RecordGPMCachedOpportunisticallyRetrievedKeyEvent(
webauthn::metrics::
WebAuthenticationGPMCachedOpportunisticallyRetrievedKeyEvent::
kStoreKeysFromOpportunisticFlowCachedKeysStoringAfterSignIn);
}
ConsiderPinRenewal();
}
void EnclaveManager::RemoveGaiaIdsFromLocalState(
base::flat_set<GaiaId> gaia_ids_to_remove) {
auto action = std::make_unique<PendingAction>();
action->gaia_ids_to_remove = std::move(gaia_ids_to_remove);
action->callback = base::DoNothing();
pending_actions_.emplace_back(std::move(action));
Act();
}
void EnclaveManager::Stopped() {
state_machine_.reset();
Act();
NotifyObserversThatStateUpdated();
}
void EnclaveManager::NotifyObserversThatStateUpdated() {
for (Observer& observer : observer_list_) {
observer.OnStateUpdated();
}
}
void EnclaveManager::CancelAllActions() {
std::deque<std::unique_ptr<PendingAction>> actions =
std::move(pending_actions_);
pending_actions_.clear();
for (const auto& action : actions) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(action->callback),
ActionOutcome::kActionCancelled));
}
}
void EnclaveManager::WriteState(EnclaveLocalState* new_state) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (const auto& it : new_state->users()) {
std::optional<int> error_line = CheckInvariants(it.second);
CHECK(!error_line.has_value())
<< "State invariant failed on line " << *error_line;
}
std::string serialized;
serialized.reserve(1024);
new_state->AppendToString(&serialized);
if (new_state != local_state_.get()) {
user_ = nullptr;
local_state_ = std::make_unique<EnclaveLocalState>();
CHECK(local_state_->ParseFromString(serialized));
user_ = StateForUser(local_state_.get(), *primary_account_info_);
}
const std::array<uint8_t, crypto::kSHA256Length> digest =
crypto::SHA256Hash(base::as_byte_span(serialized));
serialized.append(std::begin(kHashPrefix), std::end(kHashPrefix));
serialized.append(digest.begin(), digest.end());
if (currently_writing_) {
pending_write_ = std::move(serialized);
return;
}
DoWriteState(std::move(serialized));
}
void EnclaveManager::DoWriteState(std::string serialized) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(encryptor_);
currently_writing_ = true;
std::string encrypted;
if (!encryptor_->EncryptString(serialized, &encrypted)) {
WriteStateComplete(false);
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
base::BindOnce(
[](base::FilePath path, std::string encrypted) -> bool {
return base::ImportantFileWriter::WriteFileAtomically(path,
encrypted);
},
file_path_, std::move(encrypted)),
base::BindOnce(&EnclaveManager::WriteStateComplete,
weak_ptr_factory_.GetWeakPtr()));
}
void EnclaveManager::WriteStateComplete(bool success) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
currently_writing_ = false;
if (!success) {
FIDO_LOG(ERROR) << "Failed to write enclave state";
}
if (pending_write_) {
DoWriteState(std::move(*pending_write_));
pending_write_.reset();
return;
}
if (write_finished_callback_) {
std::move(write_finished_callback_).Run();
}
}
void EnclaveManager::ClearRegistration() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!user_) {
return;
}
user_verifying_key_.reset();
identity_key_.reset();
// Delete keys from the platform as a cleanup. Failures are ignored because
// there is nothing to be done in that case.
base::ThreadPool::PostTask(
FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
base::BindOnce(
[](std::vector<uint8_t> wrapped_identity_private_key) {
std::unique_ptr<crypto::UnexportableKeyProvider> provider =
GetWebAuthnUnexportableKeyProvider();
if (crypto::StatefulUnexportableKeyProvider* stateful_provider =
provider ? provider->AsStatefulUnexportableKeyProvider()
: nullptr) {
stateful_provider->DeleteWrappedKeysSlowly(
{wrapped_identity_private_key});
}
},
ToVector(user_->wrapped_identity_private_key())));
if (!user_->wrapped_uv_private_key().empty()) {
// The key provider is only used to delete, not sign, so passing empty
// options here is ok.
if (auto user_verifying_key_provider =
GetUserVerifyingKeyProviderForCreateAndDeleteOnly()) {
auto key_label =
UserVerifyingKeyLabelFromString(user_->wrapped_uv_private_key());
CHECK(key_label);
user_verifying_key_provider->DeleteUserVerifyingKey(std::move(*key_label),
base::DoNothing());
}
}
user_ = nullptr; // Prevent dangling raw_ptr error on next line.
CHECK(local_state_->mutable_users()->erase(
primary_account_info_->gaia.ToString()));
user_ = CreateStateForUser(local_state_.get(), *primary_account_info_);
WriteState(local_state_.get());
CancelAllActions();
}
void EnclaveManager::UnregisterComplete(EnclaveManager::Callback callback,
bool success) {
if (success) {
ClearRegistration();
}
std::move(callback).Run(success);
}
void EnclaveManager::SetSecret(int32_t key_version,
base::span<const uint8_t> secret) {
secret_version_ = key_version;
secret_ = std::vector<uint8_t>(secret.begin(), secret.end());
}
// A list of PIN-renewal events that are reported to UMA. Do not renumber
// as the values are persisted.
enum class PinRenewalEvent {
kConsidered = 0,
kNothingToRenew = 1,
kConcurrentRenewal = 2,
kNotYetTime = 3,
kStarted = 4,
kSuccess = 5,
kFailure = 6,
kMaxValue = kFailure,
};
static const char kPinRenewalHistogram[] = "WebAuthentication.PinRenewalEvent";
void EnclaveManager::ConsiderPinRenewal() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::UmaHistogramEnumeration(kPinRenewalHistogram,
PinRenewalEvent::kConsidered);
renewal_checks_++;
if (!user_ || !IsReady() || !user_->has_wrapped_pin()) {
base::UmaHistogramEnumeration(kPinRenewalHistogram,
PinRenewalEvent::kNothingToRenew);
return;
}
if (is_renewing_) {
base::UmaHistogramEnumeration(kPinRenewalHistogram,
PinRenewalEvent::kConcurrentRenewal);
return;
}
const auto now = base::Time::Now();
const base::Time last_refreshed = base::Time::FromSecondsSinceUnixEpoch(
user_->last_refreshed_pin_epoch_secs());
if (last_refreshed > now || now - last_refreshed > base::Days(kRefreshDays)) {
FIDO_LOG(EVENT) << "Renewing GPM PIN based on time since last renewal";
renewal_attempts_++;
is_renewing_ = true;
base::UmaHistogramEnumeration(kPinRenewalHistogram,
PinRenewalEvent::kStarted);
RenewPIN(base::BindOnce(&EnclaveManager::OnRenewalComplete,
weak_ptr_factory_.GetWeakPtr()));
} else {
base::UmaHistogramEnumeration(kPinRenewalHistogram,
PinRenewalEvent::kNotYetTime);
}
}
void EnclaveManager::OnRenewalComplete(bool success) {
base::UmaHistogramEnumeration(
kPinRenewalHistogram,
success ? PinRenewalEvent::kSuccess : PinRenewalEvent::kFailure);
is_renewing_ = false;
}
bool EnclaveManager::IsSecurityDomainReset(
const trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult&
state) {
// If the local state indicates that the user has joined the security domain,
// but the security domain is not initialized or does not match the key
// version, assume the security domain has been reset by another client.
return user_->joined() &&
state.state !=
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::
kError &&
(!state.key_version.has_value() ||
user_->wrapped_security_domain_secrets().find(*state.key_version) ==
user_->wrapped_security_domain_secrets().end());
}
void EnclaveManager::OnOsCryptReady(
scoped_refptr<os_crypt_async::Encryptor> encryptor) {
CHECK(!encryptor_);
encryptor_ = std::move(encryptor);
loading_ = false;
Act();
}
void EnclaveManager::OpportunisticStoreKeysAddComplete(
ActionOutcome action_outcome) {
// TODO(crbug.com/542277412): Migrate to
// `WebAuthentication.Enclave.ActionOutcome.OpportunisticStoreKeys` for
// consistency with other action outcome metrics.
base::UmaHistogramEnumeration(
"WebAuthentication.Enclave.OpportunisticStoreKeysOutcome",
action_outcome);
bool success = action_outcome == ActionOutcome::kSuccess;
FIDO_LOG(EVENT) << "Opportunistic keys device add result: "
<< EnclaveManager::StateMachine::ToString(action_outcome);
auto outcome =
success
? OutOfContextRecoveryOutcome::
kStoreKeysFromOpportunisticFlowSucceeded
: OutOfContextRecoveryOutcome::kStoreKeysFromOpportunisticFlowFailed;
NotifyObserversAboutOutOfContextRecoveryOutcome(outcome);
}
void EnclaveManager::NotifyObserversAboutOutOfContextRecoveryOutcome(
OutOfContextRecoveryOutcome outcome) {
webauthn::metrics::RecordGPMRecoveryEvent(
ToWebAuthenticationGPMRecoveryEvent(outcome));
for (Observer& observer : observer_list_) {
observer.OnOutOfContextRecoveryCompletion(outcome);
}
}
base::WeakPtr<EnclaveManager> EnclaveManager::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}