blob: d8c1590e465fbac4b9fdf34dba8345af63edee72 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/webauthn/enclave_manager.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <variant>
#include <vector>
#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/proto/enclave_local_state.pb.h"
#include "chrome/browser/webauthn/unexportable_key_utils.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/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/signin/public/identity_manager/scope_set.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_signing_key.h"
#include "components/unexportable_keys/unexportable_key_id.h"
#include "content/public/browser/render_frame_host.h"
#include "crypto/aead.h"
#include "crypto/hkdf.h"
#include "crypto/random.h"
#include "crypto/sha2.h"
#include "crypto/unexportable_key.h"
#include "crypto/user_verifying_key.h"
#include "device/fido/enclave/constants.h"
#include "device/fido/enclave/transact.h"
#include "device/fido/enclave/types.h"
#include "device/fido/features.h"
#include "device/fido/network_context_factory.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"
#include "third_party/boringssl/src/include/openssl/ec.h"
#include "third_party/boringssl/src/include/openssl/evp.h"
#include "third_party/boringssl/src/include/openssl/rand.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 webauthn_pb::EnclaveLocalState;
// 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<std::vector<uint8_t>> keys;
int last_key_version;
};
struct EnclaveManager::PendingAction {
EnclaveManager::Callback callback;
bool want_registration = false;
bool renew_pin = false;
std::unique_ptr<StoreKeysArgs> 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.
};
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};
static const char kPinRenewalFailureHistogram[] =
"WebAuthentication.PinRenewalFailureCause";
// 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) {
CBS cbs;
CBS_init(&cbs, spki.data(), spki.size());
bssl::UniquePtr<EVP_PKEY> pkey(EVP_parse_public_key(&cbs));
return static_cast<bool>(pkey);
}
bool IsValidUncompressedP256X962(base::span<const uint8_t> x962) {
if (x962.empty() || x962[0] != 4) {
return false;
}
const EC_GROUP* group = EC_group_p256();
bssl::UniquePtr<EC_POINT> point(EC_POINT_new(group));
return 1 == EC_POINT_oct2point(group, point.get(), x962.data(), x962.size(),
/*ctx=*/nullptr);
}
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;
}
// 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;
}
// 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 to wrap a PIN with the security domain secret.
cbor::Value::ArrayValue BuildPINWrappingEnclaveRequest(
base::span<const uint8_t> hashed_pin,
base::span<const uint8_t, 32> claim_key,
base::span<const uint8_t, enclave::kCounterIDLen> counter_id,
base::span<const uint8_t, enclave::kVaultHandleLen - 1>
vault_handle_without_type,
base::span<const uint8_t> wrapped_secret) {
cbor::Value::MapValue request;
request.emplace(enclave::kRequestCommandKey,
enclave::kPasskeysWrapPinCommandName);
request.emplace(enclave::kPinHash, hashed_pin);
if (base::FeatureList::IsEnabled(device::kWebAuthnSendPinGeneration)) {
request.emplace(enclave::kGeneration, 0);
}
request.emplace(enclave::kClaimKey, claim_key);
request.emplace(enclave::kRequestWrappedSecretKey, wrapped_secret);
request.emplace(enclave::kRequestCounterIDKey, counter_id);
request.emplace(enclave::kRequestVaultHandleWithoutTypeKey,
vault_handle_without_type);
cbor::Value::ArrayValue requests;
requests.emplace_back(std::move(request));
return requests;
}
// Build an enclave request to unwrap a security domain secret and encrypt it to
// a fresh recovery key store entry.
cbor::Value::ArrayValue BuildRecoveryKeyStorePINChangeEnclaveRequest(
base::span<const uint8_t> hashed_pin,
std::string cert_xml,
std::string sig_xml,
base::span<const uint8_t, enclave::kCounterIDLen> counter_id,
base::span<const uint8_t, enclave::kVaultHandleLen - 1>
vault_handle_without_type,
base::span<const uint8_t> wrapped_secret) {
cbor::Value::MapValue request;
request.emplace(enclave::kRequestCommandKey,
enclave::kRecoveryKeyStoreWrapAsMemberCommandName);
request.emplace(enclave::kRecoveryKeyStorePinHash, hashed_pin);
request.emplace(enclave::kRecoveryKeyStoreCertXml, ToVector(cert_xml));
request.emplace(enclave::kRecoveryKeyStoreSigXml, ToVector(sig_xml));
request.emplace(enclave::kRequestWrappedSecretKey, wrapped_secret);
request.emplace(enclave::kRequestCounterIDKey, counter_id);
request.emplace(enclave::kRequestVaultHandleWithoutTypeKey,
vault_handle_without_type);
cbor::Value::ArrayValue requests;
requests.emplace_back(std::move(request));
return requests;
}
// 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);
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::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 (UNSAFE_TODO(memcmp(calculated.data(), digest.data(),
crypto::kSHA256Length)) != 0) {
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) {
base::flat_set<GaiaId> result;
for (const gaia::ListedAccount& listed_account : listed_accounts) {
result.insert(listed_account.gaia_id);
}
return result;
}
base::flat_set<GaiaId> GetGaiaIDs(
const google::protobuf::Map<std::string, EnclaveLocalState::User>& users) {
base::flat_set<GaiaId> result;
for (const auto& it : users) {
result.insert(GaiaId(it.first));
}
return result;
}
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
}
// Fetch the contents of the given URL.
std::unique_ptr<network::SimpleURLLoader> FetchURL(
network::mojom::URLLoaderFactory* url_loader_factory,
std::string_view url,
base::OnceCallback<void(std::optional<std::string>)> callback) {
auto network_request = std::make_unique<network::ResourceRequest>();
GURL gurl(url);
CHECK(gurl.is_valid());
network_request->url = std::move(gurl);
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[0]),
cert_path.size());
CBB inner;
CBB_add_asn1(cbb.get(), &inner, CBS_ASN1_SEQUENCE);
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;
}
}
size_t final_len;
if (!CBB_finish(cbb.get(), nullptr, &final_len)) {
return std::nullopt;
}
cert_path.resize(final_len);
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.
std::optional<std::unique_ptr<trusted_vault_pb::Vault>>
RecoveryKeyStoreWrapResponseToProto(
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;
}
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));
return vault;
}
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 (int32_t i = args.last_key_version - args.keys.size() + 1;
i <= args.last_key_version; i++) {
if (existing.find(i) == existing.end()) {
new_secrets.emplace(i, args.keys[args.last_key_version - i]);
}
}
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>();
RAND_bytes(hashed->metadata.salt.data(), hashed->metadata.salt.size());
// 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';
});
CHECK(EVP_PBE_scrypt(pin.data(), pin.size(), hashed->metadata.salt.data(),
hashed->metadata.salt.size(), hashed->metadata.n, 8, 1,
/*max_mem=*/0, hashed->hashed, sizeof(hashed->hashed)));
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));
}
std::vector<uint8_t> 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::HkdfSha256<32>(
security_domain_secret, /*salt=*/base::span<const uint8_t>(),
kKeyPurposePinDataKey);
crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM);
aead.Init(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;
}
// 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<std::unique_ptr<trusted_vault_pb::Vault>,
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<std::unique_ptr<trusted_vault_pb::Vault>> vault =
RecoveryKeyStoreWrapResponseToProto(pin_metadata, it->second);
if (!vault) {
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(*vault), 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_;
};
} // 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(false);
}
}
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)
kSettingPIN,
kRenewingPIN,
kWaitingForEnclaveTokenForUnregister,
kUnregistering,
kSyncingWithSecurityDomain,
};
enum class FetchedFile {
kCertFile,
kSigFile,
};
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>;
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;
}
FIDO_LOG(EVENT) << ToString(initial_state) << " -" << event_str << "-> "
<< ToString(state_);
if (state_ == State::kStop) {
std::move(action_->callback).Run(success_);
manager_->Stopped();
// `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) {
std::move(action_->callback).Run(success_);
manager_->Stopped();
// `this` has been deleted now.
return;
}
processing_ = false;
}
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";
}
}
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", ")"});
},
},
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_->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);
store_keys_args_for_joining_->keys.emplace_back(
std::begin(security_domain_secret),
std::end(security_domain_secret));
// Zero is a special value that indicates that the epoch is unknown.
store_keys_args_for_joining_->last_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) {
CHECK_EQ(primary_account_info_->gaia, action_->store_keys_args->gaia_id);
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(GaiaConstants::kPasskeysEnclaveOAuth2Scope);
} 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()) {
state_ = State::kStop;
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()) {
state_ = State::kStop;
return;
}
is_pin_renewal_ = true;
if (base::FeatureList::IsEnabled(
device::kSyncSecurityDomainBeforePINRenewal)) {
SyncWithSecurityDomain();
} else {
DownloadRecoveryKeyStoreKeys();
}
return;
}
if (action_->unregister) {
if (!user_->registered()) {
success_ = true;
state_ = State::kStop;
return;
}
state_ = State::kWaitingForEnclaveTokenForUnregister;
GetAccessTokenInternal(GaiaConstants::kPasskeysEnclaveOAuth2Scope);
return;
}
if (action_->update_wrapped_pin) {
*user_->mutable_wrapped_pin() = std::move(*action_->wrapped_pin);
manager_->WriteState(&local_state_);
}
success_ = true;
state_ = State::kStop;
}
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)) {
state_ = State::kStop;
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_ = base::MakeRefCounted<
unexportable_keys::RefCountedUnexportableSigningKey>(
std::move(std::get_if<KeyReady>(&event)->value().second),
unexportable_keys::UnexportableKeyId());
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(GaiaConstants::kPasskeysEnclaveOAuth2Scope);
}
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";
state_ = State::kStop;
return;
}
CHECK(std::holds_alternative<AccessToken>(event)) << ToString(event);
state_ = State::kRegisteringWithEnclave;
std::string token = std::move(std::get_if<AccessToken>(&event)->value());
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::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)) {
state_ = State::kStop;
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);
state_ = State::kStop;
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);
state_ = State::kStop;
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";
state_ = State::kStop;
return;
}
state_ = State::kWrappingSecrets;
std::string token = std::move(std::get_if<AccessToken>(&event)->value());
enclave::Transact(
manager_->network_context_factory_, enclave::GetEnclaveIdentity(),
std::move(token),
/*reauthentication_token=*/std::nullopt,
cbor::Value(
BuildSecretWrappingEnclaveRequest(new_security_domain_secrets_)),
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";
state_ = State::kStop;
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);
state_ = State::kStop;
return;
}
if (!StoreWrappedSecrets(user_, new_security_domain_secrets,
response.GetArray())) {
FIDO_LOG(ERROR) << "Failed to store wrapped secrets";
state_ = State::kStop;
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_->last_key_version,
*store_keys_args_for_joining_->keys.rbegin());
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();
state_ = State::kStop;
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 trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult*
result = std::get_if<
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult>(&event);
if (result->state ==
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::
State::kError) {
state_ = State::kStop;
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.";
state_ = State::kStop;
if (is_pin_renewal_) {
base::UmaHistogramEnumeration(
kPinRenewalFailureHistogram,
PinRenewalFailureCause::kSecurityDomainReset);
}
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";
if (is_pin_renewal_) {
base::UmaHistogramEnumeration(
kPinRenewalFailureHistogram,
PinRenewalFailureCause::kSecurityDomainReportsNoPin);
}
state_ = State::kStop;
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.
state_ = State::kStop;
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.
state_ = State::kStop;
if (is_pin_renewal_) {
base::UmaHistogramEnumeration(kPinRenewalFailureHistogram,
PinRenewalFailureCause::kDuringDownload);
}
return;
}
state_ = State::kWaitingForEnclaveTokenForPINWrapping;
GetAccessTokenInternal(GaiaConstants::kPasskeysEnclaveOAuth2Scope);
}
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";
if (is_pin_renewal_) {
base::UmaHistogramEnumeration(
kPinRenewalFailureHistogram,
PinRenewalFailureCause::kGettingAccessToken);
}
state_ = State::kStop;
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;
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_))),
manager_->IdentityKeySigningCallback(),
base::BindOnce(&StateMachine::OnEnclaveResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void SendPINSetRequest(std::string token) {
uint8_t counter_id[enclave::kCounterIDLen];
crypto::RandBytes(counter_id);
uint8_t vault_handle_without_type[enclave::kVaultHandleLen - 1];
crypto::RandBytes(vault_handle_without_type);
state_ = State::kSettingPIN;
std::vector<uint8_t> wrapped_secret =
GetCurrentWrappedSecretForUser(user_).second;
enclave::Transact(
manager_->network_context_factory_, enclave::GetEnclaveIdentity(),
std::move(token), std::move(rapt_),
// The enclave needs to do two things:
// 1) Encrypt the PIN hash with the security domain secret,
// effectively "blessing" it as a valid PIN.
// 2) Encrypt the security domain secret to the recovery key store
// under the new PIN, so that the security domain can be recovered
// with that PIN in the future.
ConcatEnclaveRequests(
BuildPINWrappingEnclaveRequest(
hashed_pin_->hashed,
ToSizedSpan<32>(wrapped_pin_proto_->claim_key()), counter_id,
vault_handle_without_type, wrapped_secret),
BuildRecoveryKeyStorePINChangeEnclaveRequest(
hashed_pin_->hashed, std::move(*cert_xml_),
std::move(*sig_xml_), counter_id, vault_handle_without_type,
wrapped_secret)),
manager_->IdentityKeySigningCallback(),
base::BindOnce(&StateMachine::OnEnclaveResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void SendPINRenewalRequest(std::string token) {
state_ = State::kRenewingPIN;
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())),
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)) {
state_ = State::kStop;
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);
state_ = State::kStop;
return;
}
const cbor::Value& recovery_key_store_wrap_response =
response.GetArray()[0]
.GetMap()
.find(cbor::Value(enclave::kResponseSuccessKey))
->second;
std::optional<std::unique_ptr<trusted_vault_pb::Vault>> vault =
RecoveryKeyStoreWrapResponseToProto(hashed_pin_->metadata,
recovery_key_store_wrap_response);
if (!vault) {
FIDO_LOG(ERROR)
<< "Failed to translate response into an UpdateVaultProto";
state_ = State::kStop;
return;
}
vault_ = std::move(*vault);
wrapping_response_ = std::move(response);
state_ = State::kWaitingForRecoveryKeyStore;
recovery_key_store_request_ =
manager_->recovery_key_store_conn_->UpdateRecoveryKeyStore(
*primary_account_info_, *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) {
if (is_pin_renewal_) {
base::UmaHistogramEnumeration(kPinRenewalFailureHistogram,
PinRenewalFailureCause::kRKSUpload);
}
FIDO_LOG(ERROR) << "Failed to upload to recovery key store";
state_ = State::kStop;
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()),
vault_.get(), store_keys_args_for_joining_->keys.back()));
}
const std::string& vault_public_key =
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 = trusted_vault::GetTrustedVaultKeysWithVersions(
store_keys_args_for_joining_->keys,
store_keys_args_for_joining_->last_key_version);
}
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) {
state_ = State::kStop;
return;
}
if (is_set_pin_) {
// If adding a PIN to an existing account, then we're done.
success_ = true;
state_ = State::kStop;
return;
}
store_keys_args_for_joining_->last_key_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_);
state_ = State::kStop;
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;
state_ = State::kStop;
success_ =
status == trusted_vault::TrustedVaultRegistrationStatus::kSuccess;
if (!success_) {
if (is_pin_renewal_) {
base::UmaHistogramEnumeration(kPinRenewalFailureHistogram,
PinRenewalFailureCause::kJoiningToDomain);
}
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_);
state_ = State::kStop;
if (std::holds_alternative<Failure>(event)) {
return;
}
cbor::Value response =
std::move(std::get_if<EnclaveResponse>(&event)->value());
if (!IsAllOk(response, 2)) {
FIDO_LOG(ERROR) << "PIN change resulted in error response: "
<< cbor::DiagnosticWriter::Write(response);
return;
}
const cbor::Value& wrapped_pin_value =
response.GetArray()[0]
.GetMap()
.find(cbor::Value(enclave::kResponseSuccessKey))
->second;
if (!wrapped_pin_value.is_bytestring()) {
FIDO_LOG(ERROR) << "Wrapped PIN was not a bytestring";
return;
}
wrapped_pin_proto_->set_wrapped_pin(
VecToString(wrapped_pin_value.GetBytestring()));
UploadVaultAndMemberFromResponse(hashed_pin_->metadata,
response.GetArray()[1]);
}
void DoRenewingPIN(Event event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
state_ = State::kStop;
if (std::holds_alternative<Failure>(event)) {
base::UmaHistogramEnumeration(kPinRenewalFailureHistogram,
PinRenewalFailureCause::kEnclaveRequest1);
return;
}
cbor::Value response =
std::move(std::get_if<EnclaveResponse>(&event)->value());
if (!IsAllOk(response, 1)) {
base::UmaHistogramEnumeration(kPinRenewalFailureHistogram,
PinRenewalFailureCause::kEnclaveRequest2);
FIDO_LOG(ERROR) << "PIN renewal resulted in error response: "
<< cbor::DiagnosticWriter::Write(response);
return;
}
// The new wrapped PIN is the same as the current one.
wrapped_pin_proto_ =
std::make_unique<EnclaveLocalState::WrappedPIN>(user_->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";
state_ = State::kStop;
return;
}
state_ = State::kUnregistering;
std::string token = std::move(std::get_if<AccessToken>(&event)->value());
enclave::Transact(manager_->network_context_factory_,
enclave::GetEnclaveIdentity(), std::move(token),
/*reauthentication_token=*/std::nullopt,
BuildUnregisterMessage(user_->device_id()),
enclave::SigningCallback(),
base::BindOnce(&StateMachine::OnEnclaveResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void DoUnregistering(Event event) {
state_ = State::kStop;
if (std::holds_alternative<Failure>(event)) {
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);
return;
}
success_ = true;
}
// 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.
bool 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()) {
if (is_pin_renewal_) {
base::UmaHistogramEnumeration(
kPinRenewalFailureHistogram,
PinRenewalFailureCause::kEnclaveResponse1);
}
FIDO_LOG(ERROR) << "response was not a map";
return false;
}
const int32_t key_version = GetCurrentWrappedSecretForUser(user_).first;
std::optional<std::pair<std::unique_ptr<trusted_vault_pb::Vault>,
trusted_vault::MemberKeysSource>>
result = ParseVaultAndMemberResponse(key_version, pin_metadata,
response_value.GetMap());
if (!result) {
if (is_pin_renewal_) {
base::UmaHistogramEnumeration(
kPinRenewalFailureHistogram,
PinRenewalFailureCause::kEnclaveResponse2);
}
return false;
}
std::tie(vault_, member_keys_source_) = std::move(*result);
state_ = State::kWaitingForRecoveryKeyStore;
recovery_key_store_request_ =
manager_->recovery_key_store_conn_->UpdateRecoveryKeyStore(
*primary_account_info_, *vault_,
base::BindOnce(
[](base::WeakPtr<StateMachine> machine,
trusted_vault::RecoveryKeyStoreStatus status) {
if (!machine) {
return;
}
machine->Process(status);
},
weak_ptr_factory_.GetWeakPtr()));
return true;
}
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_,
trusted_vault::GetTrustedVaultKeysWithVersions(
store_keys_args_for_joining_->keys,
store_keys_args_for_joining_->last_key_version),
*secure_box_pub_key, trusted_vault::LocalPhysicalDevice(),
base::BindOnce(&StateMachine::OnJoinedSecurityDomain,
weak_ptr_factory_.GetWeakPtr()));
}
void GetAccessTokenInternal(const char* scope) {
access_token_fetcher_ =
std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
"passkeys_enclave", manager_->identity_manager_,
signin::ScopeSet{scope},
base::BindOnce(
[](base::WeakPtr<StateMachine> machine,
GoogleServiceAuthError error,
signin::AccessTokenInfo access_token_info) {
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()));
}
#if BUILDFLAG(IS_MAC)
void JoinICloudKeychainToDomain(
std::unique_ptr<trusted_vault::ICloudRecoveryKey> icloud_recovery_key) {
std::vector<trusted_vault::TrustedVaultKeyAndVersion> member_keys_source =
trusted_vault::GetTrustedVaultKeysWithVersions(
{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 trusted_vault_pb::Vault* vault,
base::span<const uint8_t> security_domain_secret) {
cbor::Value::MapValue map;
map.emplace(1, base::span<const uint8_t>(hashed_pin.hashed));
if (base::FeatureList::IsEnabled(device::kWebAuthnSendPinGeneration)) {
map.emplace(2, 0); // Generation number.
}
map.emplace(3, claim_key);
map.emplace(4, base::as_byte_span(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->vault_parameters().vault_handle())
.subspan<1>());
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(),
device::enclave::kRecoveryKeyStoreCertFileURL,
base::BindOnce(&StateMachine::FetchComplete,
weak_ptr_factory_.GetWeakPtr(), FetchedFile::kCertFile));
sig_xml_loader_ = FetchURL(
manager_->url_loader_factory_.get(),
device::enclave::kRecoveryKeyStoreSigFileURL,
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_;
bool success_ = false;
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::unique_ptr<trusted_vault_pb::Vault> vault_;
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_;
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;
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.
load_timer_.Start(
FROM_HERE, base::Minutes(4),
base::BindOnce(&EnclaveManager::Load, weak_ptr_factory_.GetWeakPtr(),
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::is_loaded() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return static_cast<bool>(local_state_);
}
bool EnclaveManager::is_registered() 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::is_ready() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return is_registered() && !user_->wrapped_security_domain_secrets().empty();
}
unsigned EnclaveManager::store_keys_count() const {
return store_keys_count_;
}
void EnclaveManager::Load(base::OnceClosure closure) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (is_loaded()) {
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));
Act();
}
void EnclaveManager::RegisterIfNeeded(EnclaveManager::Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (user_ && user_->registered()) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), true));
return;
}
auto action = std::make_unique<PendingAction>();
action->callback = std::move(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 = std::move(callback);
action->pin = std::move(pin);
action->setup_account = true;
pending_actions_.emplace_back(std::move(action));
Act();
}
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()) {
return false;
}
}
auto action = std::make_unique<PendingAction>();
action->callback = std::move(callback);
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_);
auto action = std::make_unique<PendingAction>();
action->pin_public_key = std::move(previous_pin_public_key);
action->callback = std::move(callback);
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 = std::move(callback);
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 = std::move(callback);
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>();
action->callback = 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 = std::move(callback);
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 =
base::BindOnce(&EnclaveManager::UnregisterComplete,
weak_ptr_factory_.GetWeakPtr(), std::move(callback));
action->unregister = true;
if (!user_ || !is_registered()) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(action->callback), true));
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 = is_ready();
if (IsSecurityDomainReset(state)) {
ClearRegistration();
FIDO_LOG(EVENT) << "The security domain has been reset.";
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), true));
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(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));
}
}
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_ = base::MakeRefCounted<
unexportable_keys::RefCountedUnexportableSigningKey>(
std::move(key), unexportable_keys::UnexportableKeyId());
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(is_ready());
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(is_ready());
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(is_ready());
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());
}
EnclaveManager::UvKeyState EnclaveManager::uv_key_state(
bool platform_has_biometrics) const {
CHECK(is_ready());
#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_has_biometrics) {
// 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
}
// 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>(
"passkeys_enclave", identity_manager_,
signin::ScopeSet{GaiaConstants::kPasskeysEnclaveOAuth2Scope},
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::StoreKeys(const GaiaId& gaia_id,
std::vector<std::vector<uint8_t>> keys,
int last_key_version) {
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);
pending_keys_->last_key_version = last_key_version;
store_keys_count_++;
for (Observer& observer : observer_list_) {
observer.OnKeysStored();
}
}
std::unique_ptr<enclave::ClaimedPIN> EnclaveManager::MakeClaimedPINSlowly(
std::string pin,
std::unique_ptr<webauthn_pb::EnclaveLocalState_WrappedPIN> wrapped_pin) {
uint8_t hashed[32];
const std::string& salt = wrapped_pin->hash_salt();
CHECK(EVP_PBE_scrypt(pin.data(), pin.size(),
reinterpret_cast<const uint8_t*>(salt.data()),
salt.size(), wrapped_pin->hash_difficulty(), 8, 1,
1ul << 28, hashed, sizeof(hashed)));
static constexpr uint8_t kAAD[] = {'P', 'I', 'N', ' ', 'c',
'l', 'a', 'i', 'm'};
crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM);
aead.Init(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] = {};
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));
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();
}
// 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_;
};
void EnclaveManager::Act() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!local_state_) {
if (loading_) {
return;
}
loading_ = true;
if (!encryptor_.has_value()) {
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_) {
return;
}
// If a state machine is running, there must be a current user.
CHECK(!state_machine_ || user_);
bool need_to_stop = true;
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();
}
user_verifying_key_.reset();
identity_key_.reset();
const signin::AccountsInCookieJarInfo in_jar =
identity_manager_->GetAccountsInCookieJar();
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 =
base::STLSetUnion<base::flat_set<GaiaId>>(
GetGaiaIDs(in_jar.GetPotentiallyInvalidSignedInAccounts()),
GetGaiaIDs(in_jar.GetSignedOutAccounts()));
const base::flat_set<GaiaId> gaia_ids_in_state =
GetGaiaIDs(local_state_->users());
base::flat_set<GaiaId> 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);
}
// 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();
}
ConsiderPinRenewal();
}
void EnclaveManager::Stopped() {
state_machine_.reset();
Act();
}
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), false));
}
}
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_.has_value());
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) {
if (auto provider = GetWebAuthnUnexportableKeyProvider()) {
provider->DeleteSigningKeySlowly(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_ || !is_ready() || !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(os_crypt_async::Encryptor encryptor) {
CHECK(!encryptor_.has_value());
encryptor_.emplace(std::move(encryptor));
loading_ = false;
Act();
}
base::WeakPtr<EnclaveManager> EnclaveManager::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}