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() {