[go: nahoru, domu]

Revert "Revert^2 "Revert "Reapply "Reland "webauthn: add EnclaveManager"""""

This reverts commit 8edb95bf324912599185daafabda4aa3df641f36.

Reason for revert:
LUCI Bisection has identified this change as the culprit of a build failure. See the analysis: https://ci.chromium.org/ui/p/chromium/bisection/compile-analysis/b/8757961552273000001

Sample failed build: https://ci.chromium.org/b/8757961552273000001

If this is a false positive, please report it at https://bugs.chromium.org/p/chromium/issues/entry?comment=Analysis%3A+https%3A%2F%2Fci.chromium.org%2Fui%2Fp%2Fchromium%2Fbisection%2Fcompile-analysis%2Fb%2F8757961552273000001&components=Tools%3ETest%3EFindit&labels=LUCI-Bisection-Wrong%2CPri-3%2CType-Bug&status=Available&summary=Wrongly+blamed+https%3A%2F%2Fchromium-review.googlesource.com%2Fc%2Fchromium%2Fsrc%2F%2B%2F5233429

Original change's description:
> Revert^2 "Revert "Reapply "Reland "webauthn: add EnclaveManager""""
>
> This reverts commit 401be556c01bda223bdbb902dc957494c629df33.
>
> Reason for revert: resolved by crrev.com/c/5232771
>
> Original change's description:
> > Revert "Revert "Reapply "Reland "webauthn: add EnclaveManager""""
> >
> > This reverts commit dbe53526028de702601002734dbd961b8005cb1e.
> >
> > Reason for revert:
> > LUCI Bisection has identified this change as the culprit of a build failure. See the analysis: https://ci.chromium.org/ui/p/chromium/bisection/compile-analysis/b/8757963839160801713
> >
> > Sample failed build: https://ci.chromium.org/b/8757963839160801713
> >
> > If this is a false positive, please report it at https://bugs.chromium.org/p/chromium/issues/entry?comment=Analysis%3A+https%3A%2F%2Fci.chromium.org%2Fui%2Fp%2Fchromium%2Fbisection%2Fcompile-analysis%2Fb%2F8757963839160801713&components=Tools%3ETest%3EFindit&labels=LUCI-Bisection-Wrong%2CPri-3%2CType-Bug&status=Available&summary=Wrongly+blamed+https%3A%2F%2Fchromium-review.googlesource.com%2Fc%2Fchromium%2Fsrc%2F%2B%2F5233385
> >
> > Original change's description:
> > > Revert "Reapply "Reland "webauthn: add EnclaveManager"""
> > >
> > > This reverts commit 837d08123460366085e05bb0b31c688fade1b70e.
> > >
> > > Reason for revert:
> > > EnclaveManagerTest is failing on the Linux MSan bot:
> > > https://ci.chromium.org/ui/p/chromium/builders/ci/Linux%20MSan%20Tests/45569/overview
> > >
> > > Original change's description:
> > > > Reapply "Reland "webauthn: add EnclaveManager""
> > > >
> > > > Landed in bb39fe0dd8f71795882de57f6ec4d0027528a7da but collided with a
> > > > change to Rust GN and we mutually broke each other.
> > > >
> > > > Reverted in df4f3f5e506ed21be7aecaa85de30be8110ad010. Then I reverted
> > > > the revert and set the CQ running figuring that it would land if it was
> > > > ok now. However, the CQ doesn't run in that case and it landed
> > > > immediately! So I reverted immediately again in
> > > > 48711548e9add0a0303944e8241f238051948ddc because it was late.
> > > >
> > > > Thus this this is the 3rd attempt.
> > > >
> > > > Bug: 1459620
> > > > Change-Id: I539fa2321a8b7b9919f8226f1fe25dfa8e78c78a
> > > > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5235058
> > > > Reviewed-by: Robert Sesek <rsesek@chromium.org>
> > > > Auto-Submit: Adam Langley <agl@chromium.org>
> > > > Commit-Queue: Robert Sesek <rsesek@chromium.org>
> > > > Cr-Commit-Position: refs/heads/main@{#1251743}
> > >
> > > Bug: 1459620
> > > Change-Id: I2a7fedef27edb00c224ee792d08f08615fdd181c
> > > No-Presubmit: true
> > > No-Tree-Checks: true
> > > No-Try: true
> > > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5233385
> > > Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
> > > Commit-Queue: Hiroki Nakagawa <nhiroki@chromium.org>
> > > Owners-Override: Hiroki Nakagawa <nhiroki@chromium.org>
> > > Cr-Commit-Position: refs/heads/main@{#1251875}
> > >
> >
> > Bug: 1459620
> > Change-Id: Ia2b2d2c4338b75bb91d3372be5292512ee539235
> > No-Presubmit: true
> > No-Tree-Checks: true
> > No-Try: true
> > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5235766
> > Owners-Override: luci-bisection@appspot.gserviceaccount.com <luci-bisection@appspot.gserviceaccount.com>
> > Commit-Queue: luci-bisection@appspot.gserviceaccount.com <luci-bisection@appspot.gserviceaccount.com>
> > Bot-Commit: luci-bisection@appspot.gserviceaccount.com <luci-bisection@appspot.gserviceaccount.com>
> > Cr-Commit-Position: refs/heads/main@{#1251880}
>
> Bug: 1459620
> Change-Id: I72edb671a3a32e22995e06205eab58c13b0e2d14
> No-Presubmit: true
> No-Tree-Checks: true
> No-Try: true
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5233429
> Auto-Submit: Eriko Kurimoto <elkurin@chromium.org>
> Owners-Override: Eriko Kurimoto <elkurin@chromium.org>
> Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
> Commit-Queue: Eriko Kurimoto <elkurin@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1251881}
>

Bug: 1459620
Change-Id: I2a7acbccd87bda19d2003a8f8539f0e91484ee66
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5235432
Commit-Queue: luci-bisection@appspot.gserviceaccount.com <luci-bisection@appspot.gserviceaccount.com>
Owners-Override: luci-bisection@appspot.gserviceaccount.com <luci-bisection@appspot.gserviceaccount.com>
Bot-Commit: luci-bisection@appspot.gserviceaccount.com <luci-bisection@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/heads/main@{#1251891}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 4e1242a6..9f6abba 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -4528,6 +4528,10 @@
       "webauthn/cablev2_devices.h",
       "webauthn/chrome_authenticator_request_delegate.cc",
       "webauthn/chrome_authenticator_request_delegate.h",
+      "webauthn/enclave_manager.cc",
+      "webauthn/enclave_manager.h",
+      "webauthn/enclave_manager_factory.cc",
+      "webauthn/enclave_manager_factory.h",
       "webauthn/local_credential_management.cc",
       "webauthn/local_credential_management.h",
       "webauthn/observable_authenticator_list.cc",
@@ -4587,12 +4591,14 @@
       "//chrome/browser/ui/webui/web_app_internals:mojo_bindings",
       "//chrome/browser/web_applications",
       "//chrome/browser/web_applications/app_service",
+      "//chrome/browser/webauthn/proto",
       "//chrome/common/apps/platform_apps",
       "//chrome/common/importer:interfaces",
       "//chrome/common/themes:autogenerated_theme_util",
       "//chrome/services/media_gallery_util/public/cpp",
       "//components/access_code_cast/common:metrics",
       "//components/app_constants",
+      "//components/cbor",
       "//components/commerce/core:cart_db_content_proto",
       "//components/commerce/core:coupon_db_content_proto",
       "//components/commerce/core:discounts_db_content_proto",
diff --git a/chrome/browser/webauthn/DEPS b/chrome/browser/webauthn/DEPS
index 8c2c087..99277687 100644
--- a/chrome/browser/webauthn/DEPS
+++ b/chrome/browser/webauthn/DEPS
@@ -2,4 +2,7 @@
   "+components/webauthn/android",
   "+components/externalauth/android",
   "+device/fido",
+  # EnclaveManager speaks CBOR to the passkeys enclave, which is a trustworth
+  # entity authenticated via the WebPKI and a public key baked into Chrome.
+  "+components/cbor",
 ]
diff --git a/chrome/browser/webauthn/enclave_manager.cc b/chrome/browser/webauthn/enclave_manager.cc
new file mode 100644
index 0000000..f31f49af
--- /dev/null
+++ b/chrome/browser/webauthn/enclave_manager.cc
@@ -0,0 +1,1197 @@
+// 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 "base/files/file_util.h"
+#include "base/files/important_file_writer.h"
+#include "base/functional/overloaded.h"
+#include "base/stl_util.h"
+#include "base/task/task_traits.h"
+#include "base/task/thread_pool.h"
+#include "chrome/browser/webauthn/proto/enclave_local_state.pb.h"
+#include "components/cbor/diagnostic_writer.h"
+#include "components/cbor/reader.h"
+#include "components/cbor/values.h"
+#include "components/cbor/writer.h"
+#include "components/device_event_log/device_event_log.h"
+#include "components/os_crypt/sync/os_crypt.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/trusted_vault/frontend_trusted_vault_connection.h"
+#include "components/trusted_vault/securebox.h"
+#include "components/trusted_vault/trusted_vault_connection.h"
+#include "components/trusted_vault/trusted_vault_server_constants.h"
+#include "crypto/sha2.h"
+#include "crypto/unexportable_key.h"
+#include "device/fido/enclave/constants.h"
+#include "device/fido/enclave/enclave_websocket_client.h"
+#include "device/fido/enclave/transact.h"
+#include "device/fido/enclave/types.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "google_apis/gaia/gaia_constants.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#include "services/network/public/cpp/shared_url_loader_factory.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"
+
+namespace enclave = device::enclave;
+using webauthn_pb::EnclaveLocalState;
+
+namespace {
+
+// Since protobuf maps `bytes` to `std::string` (rather than
+// `std::vector<uint8_t>`), functions for jumping between these representations
+// are needed.
+
+base::span<const uint8_t> ToSpan(const std::string& s) {
+  const uint8_t* data = reinterpret_cast<const uint8_t*>(s.data());
+  return base::span<const uint8_t>(data, s.size());
+}
+
+std::vector<uint8_t> ToVector(const std::string& s) {
+  const auto span = ToSpan(s);
+  return std::vector<uint8_t>(span.begin(), span.end());
+}
+
+std::string VecToString(base::span<const uint8_t> v) {
+  const char* data = reinterpret_cast<const char*>(v.data());
+  return std::string(data, data + v.size());
+}
+
+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(),
+                                 /*bn_ctx=*/nullptr);
+}
+
+// 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 (user.wrapped_hardware_private_key().empty() !=
+      user.hardware_public_key().empty()) {
+    return __LINE__;
+  }
+  if (!user.hardware_public_key().empty() &&
+      !IsValidSubjectPublicKeyInfo(ToSpan(user.hardware_public_key()))) {
+    return __LINE__;
+  }
+  if (user.wrapped_hardware_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(ToSpan(user.uv_public_key()))) {
+    return __LINE__;
+  }
+
+  if (user.registered() && user.wrapped_hardware_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(ToSpan(user.member_public_key()))) {
+    return __LINE__;
+  }
+
+  if (user.joined() && !user.registered()) {
+    return __LINE__;
+  }
+  if (!user.wrapped_security_domain_secrets().empty() != user.joined()) {
+    return __LINE__;
+  }
+
+  return absl::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,
+    crypto::UnexportableSigningKey* hardware_key) {
+  cbor::Value::MapValue pub_keys;
+  pub_keys.emplace(enclave::kHardwareKey,
+                   hardware_key->GetSubjectPublicKeyInfo());
+
+  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));
+
+  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));
+}
+
+EnclaveLocalState::User* StateForUser(EnclaveLocalState* local_state,
+                                      const CoreAccountInfo& account) {
+  auto it = local_state->mutable_users()->find(account.gaia);
+  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, 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 BuildWrappingMessage(
+    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(cbor::Value(std::move(request)));
+  }
+
+  return cbor::Value(std::move(requests));
+}
+
+// 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 `BuildWrappingMessage` to generate the enclave request.
+bool StoreWrappedSecrets(EnclaveLocalState::User* user,
+                         const base::flat_map<int32_t, std::vector<uint8_t>>
+                             new_security_domain_secrets,
+                         const cbor::Value& response) {
+  const cbor::Value::ArrayValue& responses = response.GetArray();
+  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";
+  }
+}
+
+// The list of algorithms that are acceptable as device identity keys.
+constexpr crypto::SignatureVerifier::SignatureAlgorithm kSigningAlgorithms[] = {
+    // This is in preference order and the enclave must support all the
+    // algorithms listed here.
+    crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256,
+    crypto::SignatureVerifier::SignatureAlgorithm::RSA_PKCS1_SHA256,
+};
+
+// 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 = ToSpan(contents_str);
+  if (contents.size() < crypto::kSHA256Length) {
+    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);
+  const std::array<uint8_t, crypto::kSHA256Length> calculated =
+      crypto::SHA256Hash(payload);
+  if (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<std::string> GetGaiaIDs(
+    const std::vector<gaia::ListedAccount>& listed_accounts) {
+  base::flat_set<std::string> result;
+  for (const gaia::ListedAccount& listed_account : listed_accounts) {
+    result.insert(listed_account.gaia_id);
+  }
+  return result;
+}
+
+base::flat_set<std::string> GetGaiaIDs(
+    const google::protobuf::Map<std::string, EnclaveLocalState::User>& users) {
+  base::flat_set<std::string> result;
+  for (const auto& it : users) {
+    result.insert(it.first);
+  }
+  return result;
+}
+
+}  // namespace
+
+EnclaveManager::EnclaveManager(
+    const base::FilePath& base_dir,
+    signin::IdentityManager* identity_manager,
+    raw_ptr<network::mojom::NetworkContext> network_context,
+    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
+    : file_path_(base_dir.Append(FILE_PATH_LITERAL("passkey_enclave_state"))),
+      identity_manager_(identity_manager),
+      network_context_(network_context),
+      url_loader_factory_(url_loader_factory),
+      trusted_vault_conn_(trusted_vault::NewFrontendTrustedVaultConnection(
+          trusted_vault::SecurityDomainId::kPasskeys,
+          identity_manager,
+          url_loader_factory_)),
+      identity_observer_(
+          std::make_unique<IdentityObserver>(identity_manager_, this)) {}
+
+EnclaveManager::~EnclaveManager() = default;
+
+bool EnclaveManager::is_idle() const {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  return state_ == State::kIdle;
+}
+
+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::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 {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  return store_keys_count_;
+}
+
+void EnclaveManager::Start() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (state_ == State::kInit) {
+    state_ = State::kIdle;
+    ActIfIdle();
+  }
+}
+
+void EnclaveManager::RegisterIfNeeded() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (user_ && user_->registered()) {
+    return;
+  }
+  want_registration_ = true;
+  ActIfIdle();
+}
+
+enclave::SigningCallback EnclaveManager::HardwareKeySigningCallback() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  CHECK(!user_->wrapped_hardware_private_key().empty());
+  CHECK(user_->registered());
+
+  return base::BindRepeating(
+      // TODO: this callback should also take a WeakPtr to the EnclaveManager so
+      // that the EnclaveManager can hold a cache of loaded keys and so that
+      // signing errors can be signaled up and cause the registration to be
+      // erased. (TPMs sometimes lose keys in practice.)
+      [](std::string wrapped_hardware_private_key, std::string device_id,
+         base::span<const uint8_t> message_to_be_signed)
+          -> enclave::ClientSignature {
+        // TODO: cache the key loading. TPMs are slow.
+        auto provider = crypto::GetSoftwareUnsecureUnexportableKeyProvider();
+        std::unique_ptr<crypto::UnexportableSigningKey> key =
+            provider->FromWrappedSigningKeySlowly(
+                ToVector(wrapped_hardware_private_key));
+        std::optional<std::vector<uint8_t>> signature =
+            key->SignSlowly(message_to_be_signed);
+        // TODO: this should be allowed to fail.
+        CHECK(signature);
+
+        enclave::ClientSignature ret;
+        ret.device_id = ToVector(device_id);
+        ret.signature = std::move(*signature);
+        ret.key_type = enclave::ClientKeyType::kHardware;
+
+        return ret;
+      },
+      user_->wrapped_hardware_private_key(), user_->device_id());
+}
+
+std::optional<std::vector<uint8_t>> EnclaveManager::GetWrappedKey(
+    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 absl::nullopt;
+  }
+  return ToVector(it->second);
+}
+
+std::vector<std::vector<uint8_t>> EnclaveManager::GetWrappedKeys() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  CHECK(is_ready());
+  std::vector<std::vector<uint8_t>> ret;
+  for (const auto& it : user_->wrapped_security_domain_secrets()) {
+    ret.emplace_back(ToVector(it.second));
+  }
+  return ret;
+}
+
+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);
+}
+
+// Holds the arguments to `StoreKeys` so that they can be processed when the
+// state machine is ready for them.
+struct EnclaveManager::StoreKeysArgs {
+  std::string gaia_id;
+  std::vector<std::vector<uint8_t>> keys;
+  int last_key_version;
+};
+
+void EnclaveManager::StoreKeys(const std::string& gaia_id,
+                               std::vector<std::vector<uint8_t>> keys,
+                               int last_key_version) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  store_keys_args_ = std::make_unique<StoreKeysArgs>();
+  store_keys_args_->gaia_id = gaia_id;
+  store_keys_args_->keys = std::move(keys);
+  store_keys_args_->last_key_version = last_key_version;
+  store_keys_count_++;
+
+  ActIfIdle();
+}
+
+bool EnclaveManager::RunWhenStoppedForTesting(base::OnceClosure on_stop) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  CHECK(state_ == State::kIdle || state_ == State::kInit);
+  if (!currently_writing_) {
+    return false;
+  }
+  write_finished_callback_ = std::move(on_stop);
+  return true;
+}
+
+const webauthn_pb::EnclaveLocalState& EnclaveManager::local_state_for_testing()
+    const {
+  return *local_state_;
+}
+
+// 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_->identity_updated_ = true;
+    manager_->ActIfIdle();
+  }
+
+  void OnAccountsInCookieUpdated(
+      const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
+      const GoogleServiceAuthError& error) override {
+    manager_->identity_updated_ = true;
+    manager_->ActIfIdle();
+  }
+
+  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_;
+};
+
+// static
+std::string EnclaveManager::ToString(State state) {
+  switch (state) {
+    case State::kInit:
+      return "Init";
+    case State::kIdle:
+      return "Idle";
+    case State::kLoading:
+      return "Loading";
+    case State::kNextAction:
+      return "NextAction";
+    case State::kGeneratingKey:
+      return "GeneratingKey";
+    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";
+  }
+}
+
+// static
+std::string EnclaveManager::ToString(const Event& event) {
+  return absl::visit(
+      base::Overloaded{
+          [](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 std::string("JoinStatus(") +
+                   TrustedVaultRegistrationStatusToString(status.value()) + ")";
+          },
+      },
+      event);
+}
+
+void EnclaveManager::ActIfIdle() {
+  if (is_idle()) {
+    state_ = State::kNextAction;
+    Loop(None());
+  }
+}
+
+void EnclaveManager::Loop(Event in_event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  for (;;) {
+    const State initial_state = state_;
+    Event event = std::move(in_event);
+    in_event = None();
+
+    switch (state_) {
+      case State::kInit:
+        // This state should never be observed. `Start` should set the state to
+        // `kIdle` before starting the event loop for the first time.
+        NOTREACHED();
+        break;
+
+      case State::kIdle:
+        CHECK(absl::holds_alternative<None>(event)) << ToString(event);
+        ResetActionState();
+        for (Observer& observer : observer_list_) {
+          observer.OnEnclaveManagerIdle();
+        }
+        return;
+
+      case State::kNextAction:
+        CHECK(absl::holds_alternative<None>(event)) << ToString(event);
+        DoNextAction(std::move(event));
+        break;
+
+      case State::kLoading: {
+        if (absl::holds_alternative<None>(event)) {
+          return;
+        }
+        DoLoading(std::move(event));
+        break;
+      }
+
+      case State::kGeneratingKey: {
+        if (absl::holds_alternative<None>(event)) {
+          return;
+        } else if (absl::holds_alternative<Failure>(event)) {
+          // The object that requested the registration will observe when this
+          // object idles again, and will notice that the user still isn't
+          // registered.
+          state_ = State::kNextAction;
+          return;
+        }
+        DoGeneratingKey(std::move(event));
+        break;
+      }
+
+      case State::kWaitingForEnclaveTokenForRegistration: {
+        if (absl::holds_alternative<None>(event)) {
+          return;
+        }
+        DoWaitingForEnclaveTokenForRegistration(std::move(event));
+        break;
+      }
+
+      case State::kRegisteringWithEnclave: {
+        if (absl::holds_alternative<None>(event)) {
+          return;
+        } else if (absl::holds_alternative<Failure>(event)) {
+          // The object that requested the registration will observe when this
+          // object idles again, and will notice that the user still isn't
+          // registered.
+          FIDO_LOG(ERROR) << "Failed to register with enclave";
+          state_ = State::kNextAction;
+          break;
+        }
+        DoRegisteringWithEnclave(std::move(event));
+
+        break;
+      }
+
+      case State::kWaitingForEnclaveTokenForWrapping: {
+        if (absl::holds_alternative<None>(event)) {
+          return;
+        }
+        DoWaitingForEnclaveTokenForWrapping(std::move(event));
+        break;
+      }
+
+      case State::kWrappingSecrets: {
+        if (absl::holds_alternative<None>(event)) {
+          return;
+        }
+        DoWrappingSecrets(std::move(event));
+
+        break;
+      }
+
+      case State::kJoiningDomain: {
+        if (absl::holds_alternative<None>(event)) {
+          return;
+        }
+
+        DoJoiningDomain(std::move(event));
+        break;
+      }
+    }
+
+    FIDO_LOG(EVENT) << ToString(initial_state) << " -" << ToString(event)
+                    << "-> " << ToString(state_);
+  }
+}
+
+void EnclaveManager::ResetActionState() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  store_keys_args_for_joining_.reset();
+  hardware_key_.reset();
+  new_security_domain_secrets_.clear();
+  join_request_.reset();
+  access_token_fetcher_.reset();
+}
+
+void EnclaveManager::DoNextAction(Event event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  if (!local_state_) {
+    StartLoadingState();
+    return;
+  }
+
+  if (identity_updated_) {
+    identity_updated_ = false;
+    HandleIdentityChange();
+  }
+
+  if (want_registration_ && user_ && !user_->registered()) {
+    want_registration_ = false;
+    StartEnclaveRegistration();
+    return;
+  }
+
+  if (user_ && user_->registered() && store_keys_args_) {
+    auto store_keys_args = std::move(store_keys_args_);
+    store_keys_args_.reset();
+
+    if (store_keys_args->gaia_id != primary_account_info_->gaia) {
+      FIDO_LOG(ERROR) << "Have keys for GAIA " << store_keys_args->gaia_id
+                      << " but primary account is "
+                      << primary_account_info_->gaia;
+    } else {
+      new_security_domain_secrets_ =
+          GetNewSecretsToStore(*user_, *store_keys_args);
+      if (!new_security_domain_secrets_.empty()) {
+        state_ = State::kWaitingForEnclaveTokenForWrapping;
+        store_keys_args_for_joining_ = std::move(store_keys_args);
+        GetAccessToken();
+        return;
+      } else if (!user_->joined() && !user_->member_public_key().empty()) {
+        store_keys_args_for_joining_ = std::move(store_keys_args);
+        JoinDomain();
+        return;
+      }
+    }
+  }
+
+  state_ = State::kIdle;
+}
+
+void EnclaveManager::StartLoadingState() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  state_ = State::kLoading;
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE, {base::TaskPriority::USER_BLOCKING, base::MayBlock()},
+      base::BindOnce(
+          [](base::FilePath path) -> Event {
+            std::string contents, decrypted;
+            if (!base::ReadFileToString(path, &contents) ||
+                !OSCrypt::DecryptString(contents, &decrypted)) {
+              return Failure();
+            }
+
+            return FileContents(std::move(decrypted));
+          },
+          file_path_),
+      base::BindOnce(&EnclaveManager::Loop, weak_ptr_factory_.GetWeakPtr()));
+}
+
+void EnclaveManager::HandleIdentityChange() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  ResetActionState();
+  CoreAccountInfo primary_account_info =
+      identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+  if (!primary_account_info.IsEmpty()) {
+    user_ = StateForUser(local_state_.get(), primary_account_info);
+    if (!user_) {
+      user_ = CreateStateForUser(local_state_.get(), primary_account_info);
+    }
+    primary_account_info_ =
+        std::make_unique<CoreAccountInfo>(std::move(primary_account_info));
+  } else {
+    user_ = nullptr;
+    primary_account_info_.reset();
+  }
+
+  const signin::AccountsInCookieJarInfo in_jar =
+      identity_manager_->GetAccountsInCookieJar();
+  if (!in_jar.accounts_are_fresh) {
+    return;
+  }
+
+  // If the user has signed out of any non-primary accounts, erase their enclave
+  // state.
+  const base::flat_set<std::string> gaia_ids_in_cookie_jar =
+      base::STLSetUnion<base::flat_set<std::string>>(
+          GetGaiaIDs(in_jar.signed_in_accounts),
+          GetGaiaIDs(in_jar.signed_out_accounts));
+  const base::flat_set<std::string> gaia_ids_in_state =
+      GetGaiaIDs(local_state_->users());
+  base::flat_set<std::string> to_remove =
+      base::STLSetDifference<base::flat_set<std::string>>(
+          gaia_ids_in_state, gaia_ids_in_cookie_jar);
+  if (primary_account_info_) {
+    to_remove.erase(primary_account_info_->gaia);
+  }
+  for (const auto& gaia_id : to_remove) {
+    CHECK(local_state_->mutable_users()->erase(gaia_id));
+  }
+  WriteState();
+}
+
+void EnclaveManager::StartEnclaveRegistration() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  state_ = State::kGeneratingKey;
+  std::optional<std::vector<uint8_t>> existing_key_id;
+  if (user_ && !user_->wrapped_hardware_private_key().empty()) {
+    existing_key_id = ToVector(user_->wrapped_hardware_private_key());
+  }
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
+      base::BindOnce(
+          [](std::optional<std::vector<uint8_t>> key_id) -> Event {
+            auto provider =
+                crypto::GetSoftwareUnsecureUnexportableKeyProvider();
+            if (!provider) {
+              return Failure();
+            }
+            if (key_id) {
+              std::unique_ptr<crypto::UnexportableSigningKey> key =
+                  provider->FromWrappedSigningKeySlowly(*key_id);
+              if (key) {
+                return KeyReady(std::move(key));
+              }
+            }
+            std::unique_ptr<crypto::UnexportableSigningKey> key =
+                provider->GenerateSigningKeySlowly(kSigningAlgorithms);
+            if (!key) {
+              return Failure();
+            }
+            return KeyReady(std::move(key));
+          },
+          std::move(existing_key_id)),
+      base::BindOnce(&EnclaveManager::Loop, weak_ptr_factory_.GetWeakPtr()));
+}
+
+void EnclaveManager::DoLoading(Event event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  const FileContents* contents = absl::get_if<FileContents>(&event);
+  if (contents) {
+    local_state_ = ParseStateFile(std::move(contents->value()));
+  } else if (absl::holds_alternative<Failure>(event)) {
+    local_state_ = std::make_unique<EnclaveLocalState>();
+  } else {
+    NOTREACHED() << "Unexpected event " << ToString(event);
+  }
+
+  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;
+    }
+  }
+
+  state_ = State::kNextAction;
+}
+
+void EnclaveManager::DoGeneratingKey(Event event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  CHECK(absl::holds_alternative<KeyReady>(event)) << ToString(event);
+
+  hardware_key_ = std::move(absl::get_if<KeyReady>(&event)->value());
+
+  const std::vector<uint8_t> spki = hardware_key_->GetSubjectPublicKeyInfo();
+  const std::string spki_str = VecToString(spki);
+  if (user_->hardware_public_key() != spki_str) {
+    std::array<uint8_t, crypto::kSHA256Length> device_id =
+        crypto::SHA256Hash(spki);
+    user_->set_hardware_public_key(spki_str);
+    user_->set_wrapped_hardware_private_key(
+        VecToString(hardware_key_->GetWrappedKey()));
+    user_->set_device_id(VecToString(device_id));
+
+    WriteState();
+  }
+
+  state_ = State::kWaitingForEnclaveTokenForRegistration;
+  GetAccessToken();
+}
+
+void EnclaveManager::DoWaitingForEnclaveTokenForRegistration(Event event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  access_token_fetcher_.reset();
+  if (absl::holds_alternative<Failure>(event)) {
+    FIDO_LOG(ERROR) << "Failed to get access token for enclave";
+    state_ = State::kNextAction;
+    return;
+  }
+  CHECK(absl::holds_alternative<AccessToken>(event)) << ToString(event);
+
+  state_ = State::kRegisteringWithEnclave;
+  std::string token = std::move(absl::get_if<AccessToken>(&event)->value());
+  enclave::Transact(
+      network_context_, enclave::GetEnclaveIdentity(), std::move(token),
+      BuildRegistrationMessage(user_->device_id(), hardware_key_.get()),
+      enclave::SigningCallback(),
+      base::BindOnce(
+          [](base::WeakPtr<EnclaveManager> client,
+             std::optional<cbor::Value> response) {
+            if (!client) {
+              return;
+            }
+            if (!response) {
+              client->Loop(Failure());
+            } else {
+              client->Loop(EnclaveResponse(std::move(*response)));
+            }
+          },
+          weak_ptr_factory_.GetWeakPtr()));
+}
+
+void EnclaveManager::DoRegisteringWithEnclave(Event event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  cbor::Value response =
+      std::move(absl::get_if<EnclaveResponse>(&event)->value());
+  if (!IsAllOk(response, 2)) {
+    FIDO_LOG(ERROR) << "Registration resulted in error response: "
+                    << cbor::DiagnosticWriter::Write(response);
+    state_ = State::kNextAction;
+    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::kNextAction;
+    return;
+  }
+
+  user_->set_registered(true);
+  WriteState();
+  state_ = State::kNextAction;
+}
+
+void EnclaveManager::DoWaitingForEnclaveTokenForWrapping(Event event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  access_token_fetcher_.reset();
+  if (absl::holds_alternative<Failure>(event)) {
+    FIDO_LOG(ERROR) << "Failed to get access token for enclave";
+    state_ = State::kNextAction;
+    return;
+  }
+
+  state_ = State::kWrappingSecrets;
+  std::string token = std::move(absl::get_if<AccessToken>(&event)->value());
+  enclave::Transact(network_context_, enclave::GetEnclaveIdentity(),
+                    std::move(token),
+                    BuildWrappingMessage(new_security_domain_secrets_),
+                    HardwareKeySigningCallback(),
+                    base::BindOnce(
+                        [](base::WeakPtr<EnclaveManager> client,
+                           std::optional<cbor::Value> response) {
+                          if (!client) {
+                            return;
+                          }
+                          if (!response) {
+                            client->Loop(Failure());
+                          } else {
+                            client->Loop(EnclaveResponse(std::move(*response)));
+                          }
+                        },
+                        weak_ptr_factory_.GetWeakPtr()));
+}
+
+void EnclaveManager::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 (absl::holds_alternative<Failure>(event)) {
+    FIDO_LOG(ERROR) << "Failed to wrap security domain secrets";
+    state_ = State::kNextAction;
+    return;
+  }
+
+  cbor::Value response =
+      std::move(absl::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::kNextAction;
+    return;
+  }
+
+  if (!StoreWrappedSecrets(user_, new_security_domain_secrets, response)) {
+    FIDO_LOG(ERROR) << "Failed to store wrapped secrets";
+    state_ = State::kNextAction;
+    return;
+  }
+
+  if (!user_->joined()) {
+    JoinDomain();
+  } else {
+    WriteState();
+    state_ = State::kNextAction;
+  }
+}
+
+void EnclaveManager::JoinDomain() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  state_ = State::kJoiningDomain;
+  const auto secure_box_pub_key =
+      trusted_vault::SecureBoxPublicKey::CreateByImport(
+          ToSpan(user_->member_public_key()));
+  join_request_ = trusted_vault_conn_->RegisterAuthenticationFactor(
+      *primary_account_info_, store_keys_args_for_joining_->keys,
+      store_keys_args_for_joining_->last_key_version, *secure_box_pub_key,
+      trusted_vault::AuthenticationFactorType::kPhysicalDevice,
+      /*authentication_factor_type_hint=*/absl::nullopt,
+      base::BindOnce(
+          [](base::WeakPtr<EnclaveManager> client,
+             trusted_vault::TrustedVaultRegistrationStatus status) {
+            if (!client) {
+              return;
+            }
+            client->Loop(JoinStatus(status));
+          },
+          weak_ptr_factory_.GetWeakPtr()));
+}
+
+void EnclaveManager::DoJoiningDomain(Event event) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  join_request_.reset();
+  store_keys_args_for_joining_.reset();
+
+  CHECK(absl::holds_alternative<JoinStatus>(event));
+  const trusted_vault::TrustedVaultRegistrationStatus status =
+      absl::get_if<JoinStatus>(&event)->value();
+
+  switch (status) {
+    case trusted_vault::TrustedVaultRegistrationStatus::kSuccess:
+    case trusted_vault::TrustedVaultRegistrationStatus::kAlreadyRegistered:
+      user_->set_joined(true);
+      break;
+    default:
+      user_->mutable_wrapped_security_domain_secrets()->clear();
+      break;
+  }
+
+  WriteState();
+  state_ = State::kNextAction;
+}
+
+void EnclaveManager::WriteState() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  for (const auto& it : local_state_->users()) {
+    std::optional<int> error_line = CheckInvariants(it.second);
+    CHECK(!error_line.has_value())
+        << "State invariant failed on line " << *error_line;
+  }
+
+  if (currently_writing_) {
+    need_write_ = true;
+    return;
+  }
+
+  DoWriteState();
+}
+
+void EnclaveManager::DoWriteState() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  std::string serialized;
+  serialized.reserve(1024);
+  local_state_->AppendToString(&serialized);
+  const std::array<uint8_t, crypto::kSHA256Length> digest =
+      crypto::SHA256Hash(base::as_bytes(base::make_span(serialized)));
+  serialized.append(digest.begin(), digest.end());
+
+  currently_writing_ = true;
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
+      base::BindOnce(
+          [](base::FilePath path, std::string contents) -> bool {
+            std::string encrypted;
+            return OSCrypt::EncryptString(contents, &encrypted) &&
+                   base::ImportantFileWriter::WriteFileAtomically(path,
+                                                                  contents);
+          },
+          file_path_, std::move(serialized)),
+      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 (need_write_) {
+    need_write_ = false;
+    DoWriteState();
+    return;
+  }
+
+  if (write_finished_callback_) {
+    std::move(write_finished_callback_).Run();
+  }
+}
+
+// static
+base::flat_map<int32_t, std::vector<uint8_t>>
+EnclaveManager::GetNewSecretsToStore(const EnclaveLocalState::User& user,
+                                     const 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;
+}
+
+void EnclaveManager::GetAccessToken() {
+  access_token_fetcher_ =
+      std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
+          "passkeys_enclave", identity_manager_,
+          signin::ScopeSet{GaiaConstants::kPasskeysEnclaveOAuth2Scope},
+          base::BindOnce(
+              [](base::WeakPtr<EnclaveManager> client,
+                 GoogleServiceAuthError error,
+                 signin::AccessTokenInfo access_token_info) {
+                if (!client) {
+                  return;
+                }
+                if (error.state() == GoogleServiceAuthError::NONE) {
+                  client->Loop(AccessToken(access_token_info.token));
+                } else {
+                  client->Loop(Failure());
+                }
+              },
+              weak_ptr_factory_.GetWeakPtr()),
+          signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable,
+          signin::ConsentLevel::kSignin);
+}
diff --git a/chrome/browser/webauthn/enclave_manager.h b/chrome/browser/webauthn/enclave_manager.h
new file mode 100644
index 0000000..f3cb640
--- /dev/null
+++ b/chrome/browser/webauthn/enclave_manager.h
@@ -0,0 +1,244 @@
+// 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.
+
+#ifndef CHROME_BROWSER_WEBAUTHN_ENCLAVE_MANAGER_H_
+#define CHROME_BROWSER_WEBAUTHN_ENCLAVE_MANAGER_H_
+
+#include <optional>
+#include <string>
+#include <variant>
+#include <vector>
+
+#include "base/containers/flat_map.h"
+#include "base/files/file_path.h"
+#include "base/functional/callback_forward.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/observer_list.h"
+#include "base/sequence_checker.h"
+#include "base/types/strong_alias.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/trusted_vault/trusted_vault_connection.h"
+#include "device/fido/enclave/types.h"
+#include "services/network/public/mojom/network_context.mojom-forward.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
+
+namespace cbor {
+class Value;
+}
+
+namespace crypto {
+class UnexportableSigningKey;
+}  // namespace crypto
+
+namespace network {
+class SharedURLLoaderFactory;
+}  // namespace network
+
+namespace signin {
+class IdentityManager;
+class PrimaryAccountAccessTokenFetcher;
+}  // namespace signin
+
+namespace trusted_vault {
+enum class TrustedVaultRegistrationStatus;
+}
+
+namespace webauthn_pb {
+class EnclaveLocalState;
+class EnclaveLocalState_User;
+}  // namespace webauthn_pb
+
+// EnclaveManager stores and manages the passkey enclave state. One instance
+// exists per-profile, owned by `EnclaveManagerFactory`.
+//
+// The state exposed from this class is per-primary-account. This class watches
+// the `IdentityManager` and, when the primary account changes, the result of
+// functions like `is_registered` will suddenly change too. If an account is
+// removed from the cookie jar (and it's not primary) then state for that
+// account will be erased.
+//
+// Calling `Start` for the first time will cause the persisted state to be read
+// from the disk. Each time all requested operations have completed, the class
+// becomes "idle": `is_idle` will return true, and `OnEnclaveManagerIdle`
+// will be called for all observers.
+//
+// When `is_ready` is true then this class can produce wrapped security domain
+// secrets and signing callbacks to use to perform passkey operations with the
+// enclave, which is the ultimate point of this class.
+class EnclaveManager : public KeyedService {
+ public:
+  class Observer : public base::CheckedObserver {
+   public:
+    virtual void OnEnclaveManagerIdle() = 0;
+  };
+
+  EnclaveManager(
+      const base::FilePath& base_dir,
+      signin::IdentityManager* identity_manager,
+      raw_ptr<network::mojom::NetworkContext> network_context,
+      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
+  ~EnclaveManager() override;
+  EnclaveManager(const EnclaveManager&) = delete;
+  EnclaveManager(const EnclaveManager&&) = delete;
+
+  // Returns true if there are no current operations pending.
+  bool is_idle() const;
+  // Returns true if the persistent state has been loaded from the disk. (Or
+  // else the loading failed and an empty state is being used.)
+  bool is_loaded() const;
+  // Returns true if the current user has been registered with the enclave.
+  bool is_registered() const;
+  // Returns true if the current user has joined the security domain and has one
+  // or more wrapped security domain secrets available. (This implies
+  // `is_registered`.)
+  bool is_ready() const;
+  // Returns the number of times that `StoreKeys` has been called.
+  unsigned store_keys_count() const;
+
+  // Start by loading the persisted state from disk. Harmless to call multiple
+  // times.
+  void Start();
+  // Register with the enclave if not already registered.
+  void RegisterIfNeeded();
+
+  // Get a callback to sign with the registered "hw" key. Only valid to call if
+  // `is_ready`.
+  device::enclave::SigningCallback HardwareKeySigningCallback();
+  // Fetch a wrapped security domain secret for the given epoch. Only valid to
+  // call if `is_ready`.
+  std::optional<std::vector<uint8_t>> GetWrappedKey(int32_t version);
+  // Fetch all wrapped security domain secrets, for when it's unknown which one
+  // a WebauthnCredentialSpecifics will need. Only valid to call if `is_ready`.
+  std::vector<std::vector<uint8_t>> GetWrappedKeys();
+
+  void AddObserver(Observer* observer);
+  void RemoveObserver(Observer* observer);
+
+  void StoreKeys(const std::string& gaia_id,
+                 std::vector<std::vector<uint8_t>> keys,
+                 int last_key_version);
+
+  // If background processes need to be stopped then return true and call
+  // `on_stop` when stopped. Otherwise return false.
+  bool RunWhenStoppedForTesting(base::OnceClosure on_stop);
+
+  const webauthn_pb::EnclaveLocalState& local_state_for_testing() const;
+
+ private:
+  struct StoreKeysArgs;
+  class IdentityObserver;
+
+  // The main part of this class is a state machine that uses the following
+  // states. It moves from state to state in response to `Event` values.
+  // Fields such as `want_registration_` and `identity_updated_` are set in
+  // order to record that the state machine needs to process those requests
+  // once the current processing has completed.
+  enum class State {
+    kInit,
+    kIdle,
+    kNextAction,
+    kLoading,
+    kGeneratingKey,
+    kWaitingForEnclaveTokenForRegistration,
+    kRegisteringWithEnclave,
+    kWaitingForEnclaveTokenForWrapping,
+    kWrappingSecrets,
+    kJoiningDomain,
+  };
+  static std::string ToString(State);
+
+  using None = base::StrongAlias<class None, absl::monostate>;
+  using Failure =
+      base::StrongAlias<class KeyGenerationFailure, absl::monostate>;
+  using FileContents = base::StrongAlias<class FileContents, std::string>;
+  using KeyReady =
+      base::StrongAlias<class KeyGenerated,
+                        std::unique_ptr<crypto::UnexportableSigningKey>>;
+  using EnclaveResponse = base::StrongAlias<class EnclaveResponse, cbor::Value>;
+  using JoinStatus =
+      base::StrongAlias<class JoinStatus,
+                        trusted_vault::TrustedVaultRegistrationStatus>;
+  using AccessToken = base::StrongAlias<class AccessToken, std::string>;
+  using Event = absl::variant<None,
+                              Failure,
+                              FileContents,
+                              KeyReady,
+                              EnclaveResponse,
+                              AccessToken,
+                              JoinStatus>;
+  static std::string ToString(const Event&);
+
+  // Moves to `kNextAction` if currently `kIdle`, which will trigger the next
+  // requested action.
+  void ActIfIdle();
+
+  // The main event loop function, and split out functions to handle each state.
+  void Loop(Event);
+  void ResetActionState();
+  void DoNextAction(Event);
+  void StartLoadingState();
+  void HandleIdentityChange();
+  void StartEnclaveRegistration();
+  void DoLoading(Event event);
+  void DoGeneratingKey(Event event);
+  void DoWaitingForEnclaveTokenForRegistration(Event event);
+  void DoRegisteringWithEnclave(Event event);
+  void DoWaitingForEnclaveTokenForWrapping(Event event);
+  void DoWrappingSecrets(Event event);
+  void JoinDomain();
+  void DoJoiningDomain(Event event);
+
+  // Can be called at any point to serialise the current value of `local_state_`
+  // to disk. Only a single write happens at a time. If a write is already
+  // happening, the request will be queued. If a request is already queued, this
+  // call will be ignored.
+  void WriteState();
+  void DoWriteState();
+  void WriteStateComplete(bool success);
+
+  void GetAccessToken();
+  static base::flat_map<int32_t, std::vector<uint8_t>> GetNewSecretsToStore(
+      const webauthn_pb::EnclaveLocalState_User& user,
+      const StoreKeysArgs& args);
+
+  const base::FilePath file_path_;
+  const raw_ptr<signin::IdentityManager> identity_manager_;
+  const raw_ptr<network::mojom::NetworkContext> network_context_;
+  const scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
+  const std::unique_ptr<trusted_vault::TrustedVaultConnection>
+      trusted_vault_conn_;
+
+  State state_ = State::kInit;
+  std::unique_ptr<webauthn_pb::EnclaveLocalState> local_state_;
+  raw_ptr<webauthn_pb::EnclaveLocalState_User> user_ = nullptr;
+  std::unique_ptr<CoreAccountInfo> primary_account_info_;
+  std::unique_ptr<IdentityObserver> identity_observer_;
+
+  bool need_write_ = false;
+  bool currently_writing_ = false;
+  base::OnceClosure write_finished_callback_;
+  std::unique_ptr<StoreKeysArgs> store_keys_args_;
+
+  // These members hold state that only exists for the duration of a sequence of
+  // non-idle states. Every time the state machine idles, all these members are
+  // reset.
+  std::unique_ptr<StoreKeysArgs> store_keys_args_for_joining_;
+  std::unique_ptr<crypto::UnexportableSigningKey> hardware_key_;
+  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<signin::PrimaryAccountAccessTokenFetcher>
+      access_token_fetcher_;
+
+  unsigned store_keys_count_ = 0;
+  bool want_registration_ = false;
+  bool identity_updated_ = true;
+
+  base::ObserverList<Observer> observer_list_;
+
+  SEQUENCE_CHECKER(sequence_checker_);
+  base::WeakPtrFactory<EnclaveManager> weak_ptr_factory_{this};
+};
+
+#endif  // CHROME_BROWSER_WEBAUTHN_ENCLAVE_MANAGER_H_
diff --git a/chrome/browser/webauthn/enclave_manager_factory.cc b/chrome/browser/webauthn/enclave_manager_factory.cc
new file mode 100644
index 0000000..35ef0b3
--- /dev/null
+++ b/chrome/browser/webauthn/enclave_manager_factory.cc
@@ -0,0 +1,48 @@
+// 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_factory.h"
+
+#include "chrome/browser/net/system_network_context_manager.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/webauthn/enclave_manager.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "content/public/browser/storage_partition.h"
+
+// static
+EnclaveManager* EnclaveManagerFactory::GetForProfile(Profile* profile) {
+  return static_cast<EnclaveManager*>(
+      GetInstance()->GetServiceForBrowserContext(profile, /*create=*/true));
+}
+
+// static
+EnclaveManagerFactory* EnclaveManagerFactory::GetInstance() {
+  static base::NoDestructor<EnclaveManagerFactory> instance;
+  return instance.get();
+}
+
+EnclaveManagerFactory::EnclaveManagerFactory()
+    : ProfileKeyedServiceFactory(
+          "EnclaveManager",
+          ProfileSelections::Builder()
+              .WithRegular(ProfileSelection::kRedirectedToOriginal)
+              .WithGuest(ProfileSelection::kNone)
+              .Build()) {
+  DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+EnclaveManagerFactory::~EnclaveManagerFactory() = default;
+
+std::unique_ptr<KeyedService>
+EnclaveManagerFactory::BuildServiceInstanceForBrowserContext(
+    content::BrowserContext* context) const {
+  Profile* const profile = Profile::FromBrowserContext(context);
+  return std::make_unique<EnclaveManager>(
+      /*base_dir=*/profile->GetPath(),
+      IdentityManagerFactory::GetForProfile(profile),
+      SystemNetworkContextManager::GetInstance()->GetContext(),
+      profile->GetDefaultStoragePartition()
+          ->GetURLLoaderFactoryForBrowserProcess());
+}
diff --git a/chrome/browser/webauthn/enclave_manager_factory.h b/chrome/browser/webauthn/enclave_manager_factory.h
new file mode 100644
index 0000000..ce5fc13b
--- /dev/null
+++ b/chrome/browser/webauthn/enclave_manager_factory.h
@@ -0,0 +1,30 @@
+// 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.
+
+#ifndef CHROME_BROWSER_WEBAUTHN_ENCLAVE_MANAGER_FACTORY_H_
+#define CHROME_BROWSER_WEBAUTHN_ENCLAVE_MANAGER_FACTORY_H_
+
+#include "base/no_destructor.h"
+#include "chrome/browser/profiles/profile_keyed_service_factory.h"
+
+class EnclaveManager;
+class Profile;
+
+class EnclaveManagerFactory : public ProfileKeyedServiceFactory {
+ public:
+  static EnclaveManager* GetForProfile(Profile* profile);
+  static EnclaveManagerFactory* GetInstance();
+
+ private:
+  friend base::NoDestructor<EnclaveManagerFactory>;
+
+  EnclaveManagerFactory();
+  ~EnclaveManagerFactory() override;
+
+  // BrowserContextKeyedServiceFactory:
+  std::unique_ptr<KeyedService> BuildServiceInstanceForBrowserContext(
+      content::BrowserContext* context) const override;
+};
+
+#endif  // CHROME_BROWSER_WEBAUTHN_ENCLAVE_MANAGER_FACTORY_H_
diff --git a/chrome/browser/webauthn/enclave_manager_unittest.cc b/chrome/browser/webauthn/enclave_manager_unittest.cc
new file mode 100644
index 0000000..b82ae49
--- /dev/null
+++ b/chrome/browser/webauthn/enclave_manager_unittest.cc
@@ -0,0 +1,436 @@
+// 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 "base/command_line.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/functional/callback.h"
+#include "base/json/json_reader.h"
+#include "base/path_service.h"
+#include "base/process/launch.h"
+#include "base/process/process.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/test/bind.h"
+#include "base/test/task_environment.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "chrome/browser/webauthn/proto/enclave_local_state.pb.h"
+#include "components/os_crypt/sync/os_crypt_mocker.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
+#include "components/trusted_vault/command_line_switches.h"
+#include "components/trusted_vault/proto/vault.pb.h"
+#include "components/trusted_vault/trusted_vault_server_constants.h"
+#include "device/fido/ctap_get_assertion_request.h"
+#include "device/fido/enclave/constants.h"
+#include "device/fido/enclave/enclave_authenticator.h"
+#include "device/fido/enclave/types.h"
+#include "net/base/port_util.h"
+#include "services/network/network_service.h"
+#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/public/mojom/network_context.mojom.h"
+#include "services/network/test/fake_test_cert_verifier_params_factory.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+// The communication with the enclave process would need to be ported to Windows
+// for these tests to run there.
+#if BUILDFLAG(IS_POSIX)
+
+namespace enclave = device::enclave;
+
+namespace {
+
+constexpr std::array<uint8_t, 32> kTestKey = {
+    0xc4, 0xdf, 0xa4, 0xed, 0xfc, 0xf9, 0x7c, 0xc0, 0x3a, 0xb1, 0xcb,
+    0x3c, 0x03, 0x02, 0x9b, 0x5a, 0x05, 0xec, 0x88, 0x48, 0x54, 0x42,
+    0xf1, 0x20, 0xb4, 0x75, 0x01, 0xde, 0x61, 0xf1, 0x39, 0x5d,
+};
+constexpr uint8_t kTestProtobuf[] = {
+    0x0a, 0x10, 0x71, 0xfd, 0xf9, 0x65, 0xa8, 0x7c, 0x61, 0xe2, 0xff, 0x27,
+    0x0c, 0x76, 0x25, 0x23, 0xe0, 0xa4, 0x12, 0x10, 0x77, 0xf2, 0x3c, 0x31,
+    0x3c, 0xe8, 0x94, 0x9a, 0x9f, 0xbc, 0xdf, 0x44, 0xfc, 0xf5, 0x41, 0x97,
+    0x1a, 0x0b, 0x77, 0x65, 0x62, 0x61, 0x75, 0x74, 0x68, 0x6e, 0x2e, 0x69,
+    0x6f, 0x22, 0x06, 0x56, 0x47, 0x56, 0x7a, 0x64, 0x41, 0x2a, 0x10, 0x60,
+    0x07, 0x19, 0x5b, 0x4e, 0x19, 0xf9, 0x6e, 0xc1, 0xfc, 0xfd, 0x0a, 0xf6,
+    0x0c, 0x00, 0x7e, 0x30, 0xf9, 0xa0, 0xea, 0xf3, 0xc8, 0x31, 0x3a, 0x04,
+    0x54, 0x65, 0x73, 0x74, 0x42, 0x04, 0x54, 0x65, 0x73, 0x74, 0x4a, 0xa6,
+    0x01, 0xdc, 0xc5, 0x16, 0x15, 0x91, 0x24, 0xd2, 0x31, 0xfc, 0x85, 0x8b,
+    0xe2, 0xec, 0x22, 0x09, 0x8f, 0x8d, 0x0f, 0xbe, 0x9b, 0x59, 0x71, 0x04,
+    0xcd, 0xaa, 0x3d, 0x32, 0x23, 0xbd, 0x25, 0x46, 0x14, 0x86, 0x9c, 0xfe,
+    0x74, 0xc8, 0xd3, 0x37, 0x70, 0xed, 0xb0, 0x25, 0xd4, 0x1b, 0xdd, 0xa4,
+    0x3c, 0x02, 0x13, 0x8c, 0x69, 0x03, 0xff, 0xd1, 0xb0, 0x72, 0x00, 0x29,
+    0xcf, 0x5f, 0x06, 0xb3, 0x94, 0xe2, 0xea, 0xca, 0x68, 0xdd, 0x0b, 0x07,
+    0x98, 0x7a, 0x2c, 0x8f, 0x08, 0xee, 0x7d, 0xad, 0x16, 0x35, 0xc7, 0x10,
+    0xf3, 0xa4, 0x90, 0x84, 0xd1, 0x8e, 0x2e, 0xdb, 0xb9, 0xfa, 0x72, 0x9a,
+    0xcf, 0x12, 0x1b, 0x3c, 0xca, 0xfa, 0x79, 0x4a, 0x1e, 0x1b, 0xe1, 0x15,
+    0xdf, 0xab, 0xee, 0x75, 0xbb, 0x5c, 0x5a, 0x94, 0x14, 0xeb, 0x72, 0xae,
+    0x37, 0x97, 0x03, 0xa8, 0xe7, 0x62, 0x9d, 0x2e, 0xfd, 0x28, 0xce, 0x03,
+    0x34, 0x20, 0xa7, 0xa2, 0x7b, 0x00, 0xc8, 0x12, 0x62, 0x12, 0x7f, 0x54,
+    0x73, 0x8c, 0x21, 0xc8, 0x85, 0x15, 0xce, 0x36, 0x14, 0xd9, 0x41, 0x22,
+    0xe8, 0xbf, 0x88, 0xf9, 0x45, 0xe4, 0x1c, 0x89, 0x7d, 0xa4, 0x23, 0x58,
+    0x00, 0x68, 0x98, 0xf5, 0x81, 0xef, 0xad, 0xf4, 0xda, 0x17, 0x70, 0xab,
+    0x03,
+};
+
+std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> GetTestEntity() {
+  auto ret = std::make_unique<sync_pb::WebauthnCredentialSpecifics>();
+  CHECK(ret->ParseFromArray(kTestProtobuf, sizeof(kTestProtobuf)));
+  return ret;
+}
+
+struct TempDir {
+ public:
+  TempDir() { CHECK(dir_.CreateUniqueTempDir()); }
+
+  base::FilePath GetPath() const { return dir_.GetPath(); }
+
+ private:
+  base::ScopedTempDir dir_;
+};
+
+std::pair<base::Process, uint16_t> StartEnclave(base::FilePath cwd) {
+  base::FilePath data_root;
+  CHECK(base::PathService::Get(base::DIR_OUT_TEST_DATA_ROOT, &data_root));
+  const base::FilePath enclave_bin_path =
+      data_root.AppendASCII("cloud_authenticator_test_service");
+  base::LaunchOptions subprocess_opts;
+  subprocess_opts.current_directory = cwd;
+
+  std::optional<base::Process> enclave_process;
+  uint16_t port;
+
+  for (int i = 0; i < 10; i++) {
+    int fds[2];
+    CHECK(!pipe(fds));
+    subprocess_opts.fds_to_remap.emplace_back(fds[1], 1);
+    enclave_process = base::LaunchProcess(base::CommandLine(enclave_bin_path),
+                                          subprocess_opts);
+    CHECK(enclave_process->IsValid());
+    close(fds[1]);
+
+    char port_str[6];
+    const ssize_t read_bytes =
+        HANDLE_EINTR(read(fds[0], port_str, sizeof(port_str)));
+    CHECK(read_bytes > 0);
+    port_str[read_bytes - 1] = 0;
+    unsigned u_port;
+    CHECK(base::StringToUint(port_str, &u_port)) << port_str;
+    port = base::checked_cast<uint16_t>(u_port);
+    close(fds[0]);
+
+    if (net::IsPortAllowedForScheme(port, "wss")) {
+      break;
+    }
+    LOG(INFO) << "Port " << port << " not allowed. Trying again.";
+
+    // The kernel randomly picked a port that Chromium will refuse to connect
+    // to. Try again.
+    enclave_process->Terminate(/*exit_code=*/1, /*wait=*/false);
+  }
+
+  return std::make_pair(std::move(*enclave_process), port);
+}
+
+enclave::ScopedEnclaveOverride TestEnclaveIdentity(uint16_t port) {
+  constexpr std::array<uint8_t, device::kP256X962Length> kTestPublicKey = {
+      0x04, 0x6b, 0x17, 0xd1, 0xf2, 0xe1, 0x2c, 0x42, 0x47, 0xf8, 0xbc,
+      0xe6, 0xe5, 0x63, 0xa4, 0x40, 0xf2, 0x77, 0x03, 0x7d, 0x81, 0x2d,
+      0xeb, 0x33, 0xa0, 0xf4, 0xa1, 0x39, 0x45, 0xd8, 0x98, 0xc2, 0x96,
+      0x4f, 0xe3, 0x42, 0xe2, 0xfe, 0x1a, 0x7f, 0x9b, 0x8e, 0xe7, 0xeb,
+      0x4a, 0x7c, 0x0f, 0x9e, 0x16, 0x2b, 0xce, 0x33, 0x57, 0x6b, 0x31,
+      0x5e, 0xce, 0xcb, 0xb6, 0x40, 0x68, 0x37, 0xbf, 0x51, 0xf5,
+  };
+  const std::string url = "ws://127.0.0.1:" + base::NumberToString(port);
+  enclave::EnclaveIdentity identity;
+  identity.url = GURL(url);
+  identity.public_key = kTestPublicKey;
+
+  return enclave::ScopedEnclaveOverride(std::move(identity));
+}
+
+trusted_vault_pb::JoinSecurityDomainsResponse MakeJoinSecurityDomainsResponse(
+    int current_epoch) {
+  trusted_vault_pb::JoinSecurityDomainsResponse response;
+  trusted_vault_pb::SecurityDomain* security_domain =
+      response.mutable_security_domain();
+  security_domain->set_name(
+      GetSecurityDomainName(trusted_vault::SecurityDomainId::kPasskeys));
+  security_domain->set_current_epoch(current_epoch);
+  return response;
+}
+
+std::unique_ptr<network::NetworkService> CreateNetwork(
+    mojo::Remote<network::mojom::NetworkContext>* network_context) {
+  network::mojom::NetworkContextParamsPtr params =
+      network::mojom::NetworkContextParams::New();
+  params->cert_verifier_params =
+      network::FakeTestCertVerifierParamsFactory::GetCertVerifierParams();
+
+  auto service = network::NetworkService::CreateForTesting();
+  service->CreateNetworkContext(network_context->BindNewPipeAndPassReceiver(),
+                                std::move(params));
+
+  return service;
+}
+
+class EnclaveManagerTest : public testing::Test, EnclaveManager::Observer {
+ public:
+  EnclaveManagerTest()
+      // `IdentityTestEnvironment` wants to run on an IO thread.
+      : task_env_(base::test::TaskEnvironment::MainThreadType::IO),
+        temp_dir_(),
+        process_and_port_(StartEnclave(temp_dir_.GetPath())),
+        enclave_override_(TestEnclaveIdentity(process_and_port_.second)),
+        network_service_(CreateNetwork(&network_context_)),
+        manager_(temp_dir_.GetPath(),
+                 identity_test_env_.identity_manager(),
+                 network_context_.get(),
+                 url_loader_factory_.GetSafeWeakWrapper()) {
+    OSCryptMocker::SetUp();
+
+    identity_test_env_.MakePrimaryAccountAvailable(
+        "test@gmail.com", signin::ConsentLevel::kSignin);
+    gaia_id_ = identity_test_env_.identity_manager()
+                   ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
+                   .gaia;
+    identity_test_env_.SetAutomaticIssueOfAccessTokens(true);
+    manager_.AddObserver(this);
+  }
+
+  ~EnclaveManagerTest() override {
+    if (manager_.RunWhenStoppedForTesting(task_env_.QuitClosure())) {
+      task_env_.RunUntilQuit();
+    }
+    CHECK(process_and_port_.first.Terminate(/*exit_code=*/1, /*wait=*/true));
+    OSCryptMocker::TearDown();
+  }
+
+ protected:
+  void RunUntilIdle() {
+    quit_closure_ = task_env_.QuitClosure();
+    task_env_.RunUntilQuit();
+  }
+
+  base::flat_set<std::string> GaiaAccountsInState() const {
+    const webauthn_pb::EnclaveLocalState& state =
+        manager_.local_state_for_testing();
+    base::flat_set<std::string> ret;
+    for (const auto& it : state.users()) {
+      ret.insert(it.first);
+    }
+    return ret;
+  }
+
+  void OnEnclaveManagerIdle() override {
+    if (manager_.is_idle() && quit_closure_.has_value()) {
+      auto quit_closure = std::move(quit_closure_.value());
+      quit_closure_.reset();
+      quit_closure.Run();
+    }
+  }
+
+  base::test::TaskEnvironment task_env_;
+  std::optional<base::RepeatingClosure> quit_closure_;
+  const TempDir temp_dir_;
+  const std::pair<base::Process, uint16_t> process_and_port_;
+  const enclave::ScopedEnclaveOverride enclave_override_;
+  network::TestURLLoaderFactory url_loader_factory_;
+  mojo::Remote<network::mojom::NetworkContext> network_context_;
+  std::unique_ptr<network::NetworkService> network_service_;
+  signin::IdentityTestEnvironment identity_test_env_;
+  std::string gaia_id_;
+  EnclaveManager manager_;
+};
+
+TEST_F(EnclaveManagerTest, TestInfrastructure) {
+  // Tests that the enclave starts up.
+}
+
+TEST_F(EnclaveManagerTest, Basic) {
+  ASSERT_FALSE(manager_.is_loaded());
+  ASSERT_FALSE(manager_.is_registered());
+  ASSERT_FALSE(manager_.is_ready());
+
+  manager_.Start();
+  ASSERT_FALSE(manager_.is_idle());
+  RunUntilIdle();
+  ASSERT_TRUE(manager_.is_idle());
+  ASSERT_TRUE(manager_.is_loaded());
+  ASSERT_FALSE(manager_.is_registered());
+  ASSERT_FALSE(manager_.is_ready());
+
+  manager_.RegisterIfNeeded();
+  ASSERT_FALSE(manager_.is_idle());
+  RunUntilIdle();
+  ASSERT_TRUE(manager_.is_idle());
+  ASSERT_TRUE(manager_.is_loaded());
+  ASSERT_TRUE(manager_.is_registered());
+  ASSERT_FALSE(manager_.is_ready());
+
+  url_loader_factory_.AddResponse(
+      GetFullJoinSecurityDomainsURLForTesting(
+          trusted_vault::ExtractTrustedVaultServiceURLFromCommandLine(),
+          trusted_vault::SecurityDomainId::kPasskeys)
+          .spec(),
+      MakeJoinSecurityDomainsResponse(/*current_epoch=*/1).SerializeAsString());
+  std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end());
+  manager_.StoreKeys(gaia_id_, {std::move(key)}, /*last_key_version=*/417);
+  ASSERT_FALSE(manager_.is_idle());
+  RunUntilIdle();
+  ASSERT_TRUE(manager_.is_idle());
+  ASSERT_TRUE(manager_.is_loaded());
+  ASSERT_TRUE(manager_.is_registered());
+  ASSERT_TRUE(manager_.is_ready());
+
+  auto ui_request = std::make_unique<enclave::CredentialRequest>();
+  ui_request->signing_callback = manager_.HardwareKeySigningCallback();
+  ui_request->wrapped_keys = {*manager_.GetWrappedKey(/*version=*/417)};
+  ui_request->entity = GetTestEntity();
+
+  enclave::EnclaveAuthenticator authenticator(
+      std::move(ui_request), /*save_passkey_callback=*/
+      base::BindRepeating(
+          [](sync_pb::WebauthnCredentialSpecifics) { NOTREACHED(); }),
+      network_context_.get());
+  device::CtapGetAssertionRequest ctap_request("test.com", R"({"foo": "bar"})");
+  ctap_request.allow_list.emplace_back(device::PublicKeyCredentialDescriptor(
+      device::CredentialType::kPublicKey, /*id=*/{1, 2, 3, 4}));
+
+  const base::StringPiece json_request_str =
+      R"({
+           "allowCredentials": [ ],
+           "challenge": "CYO8B30gOPIOVFAaU61J7PvoETG_sCZQ38Gzpu",
+           "rpId": "webauthn.io",
+           "userVerification": "preferred"
+         })";
+  base::Value json_request = base::JSONReader::Read(json_request_str).value();
+  device::CtapGetAssertionOptions ctap_options;
+  ctap_options.json =
+      base::MakeRefCounted<device::JSONRequest>(std::move(json_request));
+
+  auto quit_closure = task_env_.QuitClosure();
+  device::CtapDeviceResponseCode status;
+  std::vector<device::AuthenticatorGetAssertionResponse> responses;
+  authenticator.GetAssertion(
+      std::move(ctap_request), std::move(ctap_options),
+      base::BindLambdaForTesting(
+          [&quit_closure, &status, &responses](
+              device::CtapDeviceResponseCode in_status,
+              std::vector<device::AuthenticatorGetAssertionResponse>
+                  in_responses) {
+            status = in_status;
+            responses = std::move(in_responses);
+            quit_closure.Run();
+          }));
+  task_env_.RunUntilQuit();
+
+  ASSERT_EQ(status, device::CtapDeviceResponseCode::kSuccess);
+  ASSERT_EQ(responses.size(), 1u);
+}
+
+TEST_F(EnclaveManagerTest, SecretsArriveBeforeRegistration) {
+  manager_.Start();
+  manager_.RegisterIfNeeded();
+  ASSERT_FALSE(manager_.is_registered());
+
+  // Provide the domain secrets before the registration has completed. The
+  // system should still end up in the correct state.
+  url_loader_factory_.AddResponse(
+      GetFullJoinSecurityDomainsURLForTesting(
+          trusted_vault::ExtractTrustedVaultServiceURLFromCommandLine(),
+          trusted_vault::SecurityDomainId::kPasskeys)
+          .spec(),
+      MakeJoinSecurityDomainsResponse(/*current_epoch=*/1).SerializeAsString());
+  std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end());
+  manager_.StoreKeys(gaia_id_, {std::move(key)}, /*last_key_version=*/417);
+  RunUntilIdle();
+
+  ASSERT_TRUE(manager_.is_idle());
+  ASSERT_TRUE(manager_.is_loaded());
+  ASSERT_TRUE(manager_.is_registered());
+  ASSERT_TRUE(manager_.is_ready());
+}
+
+TEST_F(EnclaveManagerTest, RegistrationFailureAndRetry) {
+  const std::string gaia =
+      identity_test_env_.identity_manager()
+          ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
+          .gaia;
+
+  // Override the enclave with port=100, which will cause connection failures.
+  {
+    device::enclave::ScopedEnclaveOverride override(
+        TestEnclaveIdentity(/*port=*/100));
+    manager_.Start();
+    manager_.RegisterIfNeeded();
+    RunUntilIdle();
+  }
+  ASSERT_FALSE(manager_.is_registered());
+  const std::string public_key = manager_.local_state_for_testing()
+                                     .users()
+                                     .find(gaia)
+                                     ->second.hardware_public_key();
+  ASSERT_FALSE(public_key.empty());
+
+  manager_.RegisterIfNeeded();
+  RunUntilIdle();
+  ASSERT_TRUE(manager_.is_registered());
+
+  // The public key should not have changed because re-registration attempts
+  // must try the same public key again in case they actually worked the first
+  // time.
+  ASSERT_TRUE(public_key == manager_.local_state_for_testing()
+                                .users()
+                                .find(gaia)
+                                ->second.hardware_public_key());
+}
+
+TEST_F(EnclaveManagerTest, PrimaryUserChange) {
+  const std::string gaia1 =
+      identity_test_env_.identity_manager()
+          ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
+          .gaia;
+
+  manager_.Start();
+  manager_.RegisterIfNeeded();
+  RunUntilIdle();
+  ASSERT_TRUE(manager_.is_registered());
+  EXPECT_THAT(GaiaAccountsInState(), testing::UnorderedElementsAre(gaia1));
+
+  identity_test_env_.MakePrimaryAccountAvailable("test2@gmail.com",
+                                                 signin::ConsentLevel::kSignin);
+  const std::string gaia2 =
+      identity_test_env_.identity_manager()
+          ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
+          .gaia;
+  ASSERT_FALSE(manager_.is_registered());
+  manager_.RegisterIfNeeded();
+  RunUntilIdle();
+  ASSERT_TRUE(manager_.is_registered());
+  EXPECT_THAT(GaiaAccountsInState(),
+              testing::UnorderedElementsAre(gaia1, gaia2));
+
+  // Remove all accounts from the cookie jar. The primary account should be
+  // retained.
+  identity_test_env_.SetCookieAccounts({});
+  EXPECT_THAT(GaiaAccountsInState(), testing::UnorderedElementsAre(gaia2));
+
+  // When the primary account changes, the second account should be dropped
+  // because it was removed from the cookie jar.
+  identity_test_env_.MakePrimaryAccountAvailable("test3@gmail.com",
+                                                 signin::ConsentLevel::kSignin);
+  const std::string gaia3 =
+      identity_test_env_.identity_manager()
+          ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
+          .gaia;
+  EXPECT_THAT(GaiaAccountsInState(), testing::UnorderedElementsAre(gaia3));
+}
+
+}  // namespace
+
+#endif  // IS_POSIX
diff --git a/chrome/browser/webauthn/proto/BUILD.gn b/chrome/browser/webauthn/proto/BUILD.gn
new file mode 100644
index 0000000..e815f42
--- /dev/null
+++ b/chrome/browser/webauthn/proto/BUILD.gn
@@ -0,0 +1,11 @@
+# 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.
+
+import("//third_party/protobuf/proto_library.gni")
+
+proto_library("proto") {
+  sources = [ "enclave_local_state.proto" ]
+
+  extra_configs = [ "//build/config/compiler:wexit_time_destructors" ]
+}
diff --git a/chrome/browser/webauthn/proto/enclave_local_state.proto b/chrome/browser/webauthn/proto/enclave_local_state.proto
new file mode 100644
index 0000000..f425119f
--- /dev/null
+++ b/chrome/browser/webauthn/proto/enclave_local_state.proto
@@ -0,0 +1,53 @@
+// 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.
+
+syntax = "proto3";
+
+option optimize_for = LITE_RUNTIME;
+
+package webauthn_pb;
+
+message EnclaveLocalState {
+  // User contains state for a specific GAIA ID.
+  message User {
+    // The hardware-bound, not-user-verification-interlocked device key:
+    //
+    // These three members are either all empty or all non-empty.
+    bytes wrapped_hardware_private_key = 1;
+    // If non-empty, this contains a valid SubjectPublicKeyInfo.
+    bytes hardware_public_key = 2;
+    // This is currently SHA-256(hardware_public_key) but need not be.
+    bytes device_id = 5;
+
+    // The hardware-bound, user-verification-interlocked device key.
+    // (This is optional and might not be present if the device doesn't
+    // support UV-interlocked keys.)
+    //
+    // These two members are either both empty or both non-empty.
+    bytes wrapped_uv_private_key = 3;
+    bytes uv_public_key = 4;
+
+    // Whether this device has been registered with the enclave. If this is
+    // true then `hardware_public_key` and `member_public_key` must be
+    // non-empty.
+    bool registered = 6;
+
+    // The enclave-wrapped, security domain physical member key.
+    //
+    // These two members are either both empty or both non-empty.
+    bytes wrapped_member_private_key = 7;
+    // If non-empty, contains a P-256 point in uncompressed X9.62 format.
+    bytes member_public_key = 8;
+
+    // Whether this device has joined the hw_protected security domain. If this
+    // is true then `wrapped_security_domain_secrets` must be non-empty.
+    bool joined = 9;
+
+    // A map from security domain epoch to the enclave-wrapped security domain
+    // secret for that epoch.
+    map<int32, bytes> wrapped_security_domain_secrets = 10;
+  }
+
+  map<string, User> users = 1;
+}
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 2bf7a86..9177bad7 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -8095,6 +8095,7 @@
         "../browser/webauthn/authenticator_request_scheduler_unittest.cc",
         "../browser/webauthn/cablev2_devices_unittest.cc",
         "../browser/webauthn/chrome_authenticator_request_delegate_unittest.cc",
+        "../browser/webauthn/enclave_manager_unittest.cc",
         "../common/importer/mock_importer_bridge.cc",
         "../common/importer/mock_importer_bridge.h",
         "../renderer/media/webrtc_logging_agent_impl_unittest.cc",
@@ -8304,6 +8305,7 @@
         "//chrome/browser/ui/webui/side_panel/performance_controls:mojo_bindings",
         "//chrome/browser/ui/webui/side_panel/user_notes:mojo_bindings",
         "//chrome/browser/web_applications:web_applications_test_support",
+        "//chrome/browser/webauthn/proto",
         "//components/app_constants",
         "//components/color",
         "//components/commerce/core:cart_db_content_proto",
@@ -8361,6 +8363,12 @@
 
       data += [ "//ash/components/arc/test/data/icons/" ]
 
+      data_deps += [
+        # enclave_manager_unittest.cc runs this binary as part of its testing
+        # process.
+        "//third_party/cloud_authenticator/test/local_service:cloud_authenticator_test_service",
+      ]
+
       if (include_js2gtest_tests && is_chromeos_ash) {
         data += js2gtest_js_libraries
         deps += [
diff --git a/device/fido/enclave/enclave_websocket_client.cc b/device/fido/enclave/enclave_websocket_client.cc
index ff6316a..7485af2 100644
--- a/device/fido/enclave/enclave_websocket_client.cc
+++ b/device/fido/enclave/enclave_websocket_client.cc
@@ -279,10 +279,13 @@
 }
 
 void EnclaveWebSocketClient::ProcessCompletedResponse() {
-  on_response_.Run(SocketStatus::kOk, pending_read_data_);
+  std::vector<uint8_t> pending_read_data;
+  pending_read_data.swap(pending_read_data_);
   pending_read_data_index_ = 0;
   pending_read_finished_ = false;
-  pending_read_data_.clear();
+
+  on_response_.Run(SocketStatus::kOk, std::move(pending_read_data));
+  // `this` may have been deleted at this point.
 }
 
 void EnclaveWebSocketClient::ClosePipe(SocketStatus status) {
@@ -296,6 +299,7 @@
   pending_read_finished_ = false;
   pending_read_data_.clear();
   on_response_.Run(status, std::vector<uint8_t>());
+  // `this` may have been deleted at this point.
 }
 
 void EnclaveWebSocketClient::OnMojoPipeDisconnect() {