| // 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 <utility> |
| #include <variant> |
| |
| #include "base/base64.h" |
| #include "base/containers/flat_map.h" |
| #include "base/files/file_util.h" |
| #include "base/files/important_file_writer.h" |
| #include "base/functional/overloaded.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/stl_util.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/types/strong_alias.h" |
| #include "build/build_config.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/proto/recovery_key_store.pb.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 "components/unexportable_keys/ref_counted_unexportable_signing_key.h" |
| #include "components/unexportable_keys/unexportable_key_id.h" |
| #include "crypto/aead.h" |
| #include "crypto/hkdf.h" |
| #include "crypto/random.h" |
| #include "crypto/sha2.h" |
| #include "crypto/unexportable_key.h" |
| #include "crypto/user_verifying_key.h" |
| #include "device/fido/enclave/constants.h" |
| #include "device/fido/enclave/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 "net/base/url_util.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "third_party/abseil-cpp/absl/types/variant.h" |
| #include "third_party/boringssl/src/include/openssl/bytestring.h" |
| #include "third_party/boringssl/src/include/openssl/ec.h" |
| #include "third_party/boringssl/src/include/openssl/evp.h" |
| #include "third_party/boringssl/src/include/openssl/rand.h" |
| |
| #if BUILDFLAG(IS_WIN) |
| #include "base/strings/strcat.h" |
| #endif |
| |
| namespace enclave = device::enclave; |
| using webauthn_pb::EnclaveLocalState; |
| |
| // Holds the arguments to `StoreKeys` so that they can be processed when the |
| // state machine is ready for them. |
| struct EnclaveManager::StoreKeysArgs { |
| std::string gaia_id; |
| std::vector<std::vector<uint8_t>> keys; |
| int last_key_version; |
| }; |
| |
| struct EnclaveManager::PendingActions { |
| bool want_registration; |
| std::unique_ptr<StoreKeysArgs> store_keys_args; |
| std::string pin; |
| }; |
| |
| namespace { |
| |
| // These URLs distribute the public keys for the recovery key store. |
| constexpr char kCertFileURL[] = |
| "https://www.gstatic.com/cryptauthvault/v0/cert.xml"; |
| constexpr char kSigFileURL[] = |
| "https://www.gstatic.com/cryptauthvault/v0/cert.sig.xml"; |
| |
| // The maximum number of bytes that will be downloaded from the above two URLs. |
| constexpr size_t kMaxFetchBodyBytes = 128 * 1024; |
| |
| // This URL is used for uploading to the recovery key store. The "name" |
| // parameter isn't used by Vault and so is a constant "0". |
| constexpr char kRecoveryKeyStoreURL[] = |
| "https://cryptauthvault.googleapis.com/v1/vaults/0"; |
| |
| const net::NetworkTrafficAnnotationTag kTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("recovery_key_store_fetch", R"( |
| semantics { |
| sender: "Google Password Manager" |
| description: |
| "If a user enrolls a Google Password Manager PIN, it is hashed and " |
| "sent to the Recovery Key Store so that they can recover their " |
| "credentials with it in the future. This key store involves " |
| "dedicated hardware to limit the number of guesses permitted. The " |
| "PIN hash is encrypted directly to this hardware and these network " |
| "fetches cover downloading the neccessary public key and uploading " |
| "the encrypted package to the key store." |
| trigger: |
| "A user enrolls a PIN in Google Password Manager." |
| user_data { |
| type: ACCESS_TOKEN |
| } |
| data: "An encrypted PIN." |
| internal { |
| contacts { |
| email: "chrome-webauthn@google.com" |
| } |
| } |
| destination: GOOGLE_OWNED_SERVICE |
| last_reviewed: "2024-02-08" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: "Users can disable this feature by opening settings " |
| "and signing out of the Google account in their profile, or by " |
| "disabling password sync on the profile. Password sync can be " |
| "disabled from the Sync and Google Services screen." |
| chrome_policy { |
| SyncDisabled { |
| SyncDisabled: true |
| } |
| SyncTypesListDisabled { |
| SyncTypesListDisabled: { |
| entries: "passwords" |
| } |
| } |
| } |
| })"); |
| |
| // This prefix is the protobuf encoding for a 32-byte value with tag 1024. |
| // This means that, with the hash appended, the serialised state file is still a |
| // valid protobuf, which is handly for debugging. |
| static const uint8_t kHashPrefix[] = {0x82, 0x40, 32}; |
| |
| // 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__; |
| } |
| |
| for (const auto& it : user.wrapped_pins()) { |
| const EnclaveLocalState::WrappedPIN& wrapped_pin = it.second; |
| // The nonce is 12 bytes, and the tag is 16 bytes, so this establishes |
| // a lower-bound of one byte of plaintext. |
| if (wrapped_pin.wrapped_pin().size() < 12 + 1 + 16) { |
| return __LINE__; |
| } |
| if (wrapped_pin.claim_key().size() != 32) { |
| return __LINE__; |
| } |
| if (wrapped_pin.generation() < 0) { |
| return __LINE__; |
| } |
| if (wrapped_pin.form() == wrapped_pin.FORM_UNSPECIFIED) { |
| return __LINE__; |
| } |
| if (wrapped_pin.hash() == wrapped_pin.HASH_UNSPECIFIED) { |
| return __LINE__; |
| } |
| if (wrapped_pin.hash_difficulty() <= 0) { |
| return __LINE__; |
| } |
| if (wrapped_pin.hash_salt().empty()) { |
| return __LINE__; |
| } |
| } |
| |
| return std::nullopt; |
| } |
| |
| // Build an enclave request that registers a new device and requests a new |
| // wrapped asymmetric key which will be used to join the security domain. |
| cbor::Value BuildRegistrationMessage( |
| const std::string& device_id, |
| const crypto::UnexportableSigningKey& 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(std::move(request)); |
| } |
| |
| return cbor::Value(std::move(requests)); |
| } |
| |
| // Build an enclave request to wrap a PIN and a security domain secret. |
| cbor::Value BuildPINAndSecretWrappingMessage( |
| base::span<const uint8_t> hashed_pin, |
| std::string cert_xml, |
| std::string sig_xml, |
| base::span<const uint8_t> security_domain_secret) { |
| cbor::Value::ArrayValue requests; |
| |
| cbor::Value::MapValue request1; |
| request1.emplace(enclave::kRequestCommandKey, |
| enclave::kRecoveryKeyStoreWrapCommandName); |
| request1.emplace(enclave::kRecoveryKeyStorePinHash, std::move(hashed_pin)); |
| request1.emplace(enclave::kRecoveryKeyStoreCertXml, ToVector(cert_xml)); |
| request1.emplace(enclave::kRecoveryKeyStoreSigXml, ToVector(sig_xml)); |
| requests.emplace_back(std::move(request1)); |
| |
| cbor::Value::MapValue request2; |
| request2.emplace(enclave::kRequestCommandKey, enclave::kWrapKeyCommandName); |
| request2.emplace(enclave::kWrappingPurpose, |
| enclave::kKeyPurposeSecurityDomainSecret); |
| request2.emplace(enclave::kWrappingKeyToWrap, security_domain_secret); |
| requests.emplace_back(std::move(request2)); |
| |
| 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, |
| base::span<const cbor::Value> responses) { |
| CHECK_EQ(new_security_domain_secrets.size(), responses.size()); |
| |
| size_t i = 0; |
| for (const auto& it : new_security_domain_secrets) { |
| const cbor::Value& wrapped_value = |
| responses[i++] |
| .GetMap() |
| .find(cbor::Value(enclave::kResponseSuccessKey)) |
| ->second; |
| if (!wrapped_value.is_bytestring()) { |
| return false; |
| } |
| const std::vector<uint8_t>& wrapped = wrapped_value.GetBytestring(); |
| if (wrapped.empty()) { |
| return false; |
| } |
| user->mutable_wrapped_security_domain_secrets()->insert( |
| {it.first, VecToString(wrapped)}); |
| } |
| |
| return true; |
| } |
| |
| const char* TrustedVaultRegistrationStatusToString( |
| trusted_vault::TrustedVaultRegistrationStatus status) { |
| switch (status) { |
| case trusted_vault::TrustedVaultRegistrationStatus::kSuccess: |
| return "Success"; |
| case trusted_vault::TrustedVaultRegistrationStatus::kAlreadyRegistered: |
| return "AlreadyRegistered"; |
| case trusted_vault::TrustedVaultRegistrationStatus::kLocalDataObsolete: |
| return "LocalDataObsolete"; |
| case trusted_vault::TrustedVaultRegistrationStatus:: |
| kTransientAccessTokenFetchError: |
| return "TransientAccessTokenFetchError"; |
| case trusted_vault::TrustedVaultRegistrationStatus:: |
| kPersistentAccessTokenFetchError: |
| return "PersistentAccessTokenFetchError"; |
| case trusted_vault::TrustedVaultRegistrationStatus:: |
| kPrimaryAccountChangeAccessTokenFetchError: |
| return "PrimaryAccountChangeAccessTokenFetchError"; |
| case trusted_vault::TrustedVaultRegistrationStatus::kNetworkError: |
| return "NetworkError"; |
| case trusted_vault::TrustedVaultRegistrationStatus::kOtherError: |
| return "OtherError"; |
| } |
| } |
| |
| // 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 + sizeof(kHashPrefix)) { |
| FIDO_LOG(ERROR) << "Enclave state too small to be valid"; |
| return ret; |
| } |
| |
| const base::span<const uint8_t> digest = contents.last(crypto::kSHA256Length); |
| const base::span<const uint8_t> payload = contents.first( |
| contents.size() - crypto::kSHA256Length - sizeof(kHashPrefix)); |
| const std::array<uint8_t, crypto::kSHA256Length> calculated = |
| crypto::SHA256Hash(payload); |
| if (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; |
| } |
| |
| std::optional<crypto::UserVerifyingKeyLabel> CreateUserVerifyingKeyLabel() { |
| #if BUILDFLAG(IS_WIN) |
| std::vector<uint8_t> random(16); |
| crypto::RandBytes(random); |
| return base::StrCat({"enclave-uvkey-", base::Base64Encode(random)}); |
| #else |
| return std::nullopt; |
| #endif |
| } |
| |
| std::string UserVerifyingLabelToString(crypto::UserVerifyingKeyLabel label) { |
| #if BUILDFLAG(IS_WIN) |
| return label; |
| #else |
| return std::string(); |
| #endif |
| } |
| |
| std::optional<crypto::UserVerifyingKeyLabel> UserVerifyingKeyLabelFromString( |
| std::string saved_label) { |
| #if BUILDFLAG(IS_WIN) |
| return saved_label; |
| #else |
| return std::nullopt; |
| #endif |
| } |
| |
| // Fetch the contents of the given URL. |
| std::unique_ptr<network::SimpleURLLoader> FetchURL( |
| network::mojom::URLLoaderFactory* url_loader_factory, |
| std::string_view url, |
| base::OnceCallback<void(std::optional<std::string>)> callback) { |
| auto network_request = std::make_unique<network::ResourceRequest>(); |
| GURL gurl(url); |
| CHECK(gurl.is_valid()); |
| network_request->url = std::move(gurl); |
| |
| auto loader = network::SimpleURLLoader::Create(std::move(network_request), |
| kTrafficAnnotation); |
| loader->SetTimeoutDuration(base::Seconds(10)); |
| loader->SetURLLoaderFactoryOptions( |
| network::mojom::kURLLoadOptionBlockAllCookies); |
| loader->DownloadToString(url_loader_factory, std::move(callback), |
| kMaxFetchBodyBytes); |
| return loader; |
| } |
| |
| // Takes a CBOR array of bytestrings and returns those bytestrings assembled |
| // into an ASN.1 SEQUENCE. |
| std::optional<std::string> CBORListOfBytestringToASN1Sequence( |
| const cbor::Value& array) { |
| if (!array.is_array()) { |
| return std::nullopt; |
| } |
| |
| const std::vector<cbor::Value>& bytestrings = array.GetArray(); |
| base::CheckedNumeric<size_t> total_bytes_checked = 0; |
| for (const auto& bytestring : bytestrings) { |
| if (!bytestring.is_bytestring()) { |
| return std::nullopt; |
| } |
| total_bytes_checked += bytestring.GetBytestring().size(); |
| } |
| |
| // 16 bytes is more than sufficient for the ASN.1 header that needs to be |
| // prepended. (If it were not then `CBB_finish` would fail, below, so this is |
| // not a memory-safety-load-bearing assumption.) |
| total_bytes_checked += 16; |
| |
| if (!total_bytes_checked.IsValid()) { |
| return std::nullopt; |
| } |
| const size_t total_bytes = total_bytes_checked.ValueOrDie(); |
| |
| std::string cert_path; |
| cert_path.resize(total_bytes); |
| bssl::ScopedCBB cbb; |
| CBB_init_fixed(cbb.get(), reinterpret_cast<uint8_t*>(&cert_path[0]), |
| cert_path.size()); |
| CBB inner; |
| CBB_add_asn1(cbb.get(), &inner, CBS_ASN1_SEQUENCE); |
| for (const auto& bytestring : bytestrings) { |
| const std::vector<uint8_t>& bytes = bytestring.GetBytestring(); |
| if (!CBB_add_bytes(&inner, bytes.data(), bytes.size())) { |
| return std::nullopt; |
| } |
| } |
| size_t final_len; |
| if (!CBB_finish(cbb.get(), nullptr, &final_len)) { |
| return std::nullopt; |
| } |
| cert_path.resize(final_len); |
| return cert_path; |
| } |
| |
| // Convert the response to an enclave "recovery_key_store/wrap" command, into a |
| // protobuf that can be sent to the recovery key store service. |
| std::optional<std::unique_ptr<trusted_vault_pb::Vault>> |
| RecoveryKeyStoreWrapResponseToProto( |
| base::span<const uint8_t> scrypt_salt, |
| int scrypt_n, |
| bool is_six_digits, |
| const cbor::Value& recovery_key_store_wrap_response) { |
| if (!recovery_key_store_wrap_response.is_map()) { |
| return std::nullopt; |
| } |
| const cbor::Value::MapValue& response = |
| recovery_key_store_wrap_response.GetMap(); |
| cbor::Value::MapValue::const_iterator it; |
| |
| #define GET_BYTESTRING(name) \ |
| it = response.find(cbor::Value(#name)); \ |
| if (it == response.end() || !it->second.is_bytestring()) { \ |
| return std::nullopt; \ |
| } \ |
| const std::vector<uint8_t>& name = it->second.GetBytestring(); |
| |
| GET_BYTESTRING(cohort_public_key); |
| GET_BYTESTRING(encrypted_recovery_key); |
| GET_BYTESTRING(vault_handle); |
| GET_BYTESTRING(counter_id); |
| GET_BYTESTRING(app_public_key); |
| GET_BYTESTRING(wrapped_app_private_key); |
| GET_BYTESTRING(wrapped_wrapping_key); |
| |
| #undef GET_BYTESTRING |
| |
| it = response.find(cbor::Value("max_attempts")); |
| if (it == response.end() || !it->second.is_unsigned()) { |
| return std::nullopt; |
| } |
| const int64_t max_attempts = it->second.GetUnsigned(); |
| if (max_attempts > std::numeric_limits<int32_t>::max()) { |
| return std::nullopt; |
| } |
| |
| // "certs_in_path" contains an array of bytestrings. Each is an X.509 |
| // certificate in the verified path from leaf to root, omitting the root |
| // itself. The protobuf wants this in an ASN.1 SEQUENCE. |
| it = response.find(cbor::Value("certs_in_path")); |
| if (it == response.end()) { |
| return std::nullopt; |
| } |
| std::optional<std::string> cert_path = |
| CBORListOfBytestringToASN1Sequence(it->second); |
| if (!cert_path) { |
| return std::nullopt; |
| } |
| |
| auto vault = std::make_unique<trusted_vault_pb::Vault>(); |
| auto* params = vault->mutable_vault_parameters(); |
| params->set_backend_public_key(VecToString(cohort_public_key)); |
| params->set_counter_id(VecToString(counter_id)); |
| params->set_max_attempts(base::checked_cast<int32_t>(max_attempts)); |
| params->set_vault_handle(VecToString(vault_handle)); |
| |
| vault->set_recovery_key(VecToString(encrypted_recovery_key)); |
| |
| auto* app_key = vault->add_application_keys(); |
| // This key name mirrors what Android sets. |
| app_key->set_key_name("security_domain_member_key_encrypted_locally"); |
| auto* asymmetric_key_pair = app_key->mutable_asymmetric_key_pair(); |
| asymmetric_key_pair->set_public_key(VecToString(app_public_key)); |
| asymmetric_key_pair->set_wrapped_private_key( |
| VecToString(wrapped_app_private_key)); |
| asymmetric_key_pair->set_wrapping_key(VecToString(wrapped_wrapping_key)); |
| |
| trusted_vault_pb::VaultMetadata metadata; |
| metadata.set_lskf_type(is_six_digits |
| ? trusted_vault_pb::VaultMetadata::PIN |
| : trusted_vault_pb::VaultMetadata::PASSWORD); |
| metadata.set_hash_type(trusted_vault_pb::VaultMetadata::SCRYPT); |
| metadata.set_hash_salt(VecToString(scrypt_salt)); |
| metadata.set_hash_difficulty(scrypt_n); |
| metadata.set_cert_path(std::move(*cert_path)); |
| |
| std::string metadata_bytes; |
| if (!metadata.SerializeToString(&metadata_bytes)) { |
| return std::nullopt; |
| } |
| vault->set_vault_metadata(std::move(metadata_bytes)); |
| |
| return vault; |
| } |
| |
| base::flat_map<int32_t, std::vector<uint8_t>> GetNewSecretsToStore( |
| const EnclaveLocalState::User& user, |
| const EnclaveManager::StoreKeysArgs& args) { |
| const auto& existing = user.wrapped_security_domain_secrets(); |
| base::flat_map<int32_t, std::vector<uint8_t>> new_secrets; |
| for (int32_t i = args.last_key_version - args.keys.size() + 1; |
| i <= args.last_key_version; i++) { |
| if (existing.find(i) == existing.end()) { |
| new_secrets.emplace(i, args.keys[args.last_key_version - i]); |
| } |
| } |
| |
| return new_secrets; |
| } |
| |
| } // namespace |
| |
| // StateMachine performs a sequence of actions, as specified by the public |
| // `set_` functions, when `Start` is called. It always operates within the |
| // context of a specific Google account and will be destroyed by the |
| // EnclaveManager if the currently signed-in user changes. It works on a copy of |
| // the EnclaveLocalState and writes updated versions to the EnclaveManager |
| // once they are ready. A StateMachine is owned by the EnclaveManager and at |
| // most one exists at any given time. |
| class EnclaveManager::StateMachine { |
| public: |
| explicit StateMachine(EnclaveManager* manager, |
| webauthn_pb::EnclaveLocalState local_state, |
| std::unique_ptr<CoreAccountInfo> primary_account_info) |
| : manager_(manager), |
| local_state_(std::move(local_state)), |
| user_(StateForUser(&local_state_, *primary_account_info)), |
| primary_account_info_(std::move(primary_account_info)) {} |
| |
| void set_want_registration() { want_registration_ = true; } |
| |
| void set_store_keys_args( |
| std::unique_ptr<EnclaveManager::StoreKeysArgs> args) { |
| store_keys_args_ = std::move(args); |
| } |
| |
| void set_pending_pin(std::string pending_pin) { |
| pending_pin_ = std::move(pending_pin); |
| } |
| |
| void Start() { |
| if (state_ == State::kInit) { |
| state_ = State::kNextAction; |
| Process(None()); |
| } |
| } |
| |
| private: |
| // 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, |
| kStop, |
| kNextAction, |
| kGeneratingKeys, |
| kWaitingForEnclaveTokenForRegistration, |
| kRegisteringWithEnclave, |
| kWaitingForEnclaveTokenForWrapping, |
| kWrappingSecrets, |
| kJoiningDomain, |
| kHashingPIN, |
| kDownloadingRecoveryKeyStoreKeys, |
| kWaitingForEnclaveTokenForPINWrapping, |
| kWrappingPIN, |
| kWaitingForRecoveryKeyStoreTokenForUpload, |
| kWaitingForRecoveryKeyStore, |
| kJoiningPINToDomain, |
| }; |
| |
| enum class FetchedFile { |
| kCertFile, |
| kSigFile, |
| }; |
| |
| struct HashedPIN { |
| ~HashedPIN() { memset(hashed, 0, sizeof(hashed)); } |
| |
| int n = 0; // The scrypt `N` parameter. |
| bool is_six_digits = false; |
| uint8_t salt[16]; |
| uint8_t hashed[32]; |
| }; |
| |
| 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::pair<std::unique_ptr<crypto::UserVerifyingSigningKey>, |
| std::unique_ptr<crypto::UnexportableSigningKey>>>; |
| using EnclaveResponse = base::StrongAlias<class EnclaveResponse, cbor::Value>; |
| using JoinStatus = |
| base::StrongAlias<class JoinStatus, |
| std::pair<trusted_vault::TrustedVaultRegistrationStatus, |
| /*key_version=*/int>>; |
| using AccessToken = base::StrongAlias<class AccessToken, std::string>; |
| using FileFetched = |
| base::StrongAlias<class FileFetched, |
| std::pair<FetchedFile, std::optional<std::string>>>; |
| using PINHashed = |
| base::StrongAlias<class PINHashed, std::unique_ptr<HashedPIN>>; |
| using Response = base::StrongAlias<class Response, std::string>; |
| using Event = absl::variant<None, |
| Failure, |
| FileContents, |
| KeyReady, |
| EnclaveResponse, |
| AccessToken, |
| JoinStatus, |
| FileFetched, |
| PINHashed, |
| Response>; |
| |
| void Process(Event event) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| CHECK(!processing_) << ToString(state_); |
| processing_ = true; |
| |
| const State initial_state = state_; |
| const std::string event_str = ToString(event); |
| |
| switch (state_) { |
| case State::kInit: |
| // This state should never be observed. `Start` should set the |
| // state to `kNextAction` before starting the event Process for the |
| // first time. |
| NOTREACHED(); |
| break; |
| |
| case State::kStop: |
| // This should never be observed here as this special case is handled |
| // below. |
| NOTREACHED(); |
| break; |
| |
| case State::kNextAction: |
| CHECK(absl::holds_alternative<None>(event)) << ToString(event); |
| DoNextAction(); |
| break; |
| |
| case State::kGeneratingKeys: |
| DoGeneratingKeys(std::move(event)); |
| break; |
| |
| case State::kWaitingForEnclaveTokenForRegistration: |
| DoWaitingForEnclaveTokenForRegistration(std::move(event)); |
| break; |
| |
| case State::kRegisteringWithEnclave: |
| DoRegisteringWithEnclave(std::move(event)); |
| break; |
| |
| case State::kWaitingForEnclaveTokenForWrapping: |
| DoWaitingForEnclaveTokenForWrapping(std::move(event)); |
| break; |
| |
| case State::kWrappingSecrets: |
| DoWrappingSecrets(std::move(event)); |
| break; |
| |
| case State::kJoiningDomain: |
| DoJoiningDomain(std::move(event)); |
| break; |
| |
| case State::kHashingPIN: |
| DoHashingPIN(std::move(event)); |
| break; |
| |
| case State::kDownloadingRecoveryKeyStoreKeys: |
| DoDownloadingRecoveryKeyStoreKeys(std::move(event)); |
| break; |
| |
| case State::kWaitingForEnclaveTokenForPINWrapping: |
| DoWaitingForEnclaveTokenForPINWrapping(std::move(event)); |
| break; |
| |
| case State::kWrappingPIN: |
| DoWrappingPIN(std::move(event)); |
| break; |
| |
| case State::kWaitingForRecoveryKeyStoreTokenForUpload: |
| DoWaitingForRecoveryKeyStoreTokenForUpload(std::move(event)); |
| break; |
| |
| case State::kWaitingForRecoveryKeyStore: |
| DoWaitingForRecoveryKeyStore(std::move(event)); |
| break; |
| |
| case State::kJoiningPINToDomain: |
| DoJoiningPINToDomain(std::move(event)); |
| break; |
| } |
| |
| FIDO_LOG(EVENT) << ToString(initial_state) << " -" << event_str << "-> " |
| << ToString(state_); |
| |
| if (state_ == State::kStop) { |
| manager_->Stopped(); |
| // `this` has been deleted now. |
| return; |
| } |
| |
| // The only internal state transition (i.e. where one state moves to another |
| // without waiting for an external event) allowed is to `kNextAction`. |
| if (state_ != State::kNextAction) { |
| processing_ = false; |
| return; |
| } |
| |
| const State prior_state = state_; |
| DoNextAction(); |
| FIDO_LOG(EVENT) << ToString(prior_state) << " --> " << ToString(state_); |
| |
| if (state_ == State::kStop) { |
| manager_->Stopped(); |
| // `this` has been deleted now. |
| return; |
| } |
| |
| processing_ = false; |
| } |
| |
| static std::string ToString(State state) { |
| switch (state) { |
| case State::kInit: |
| return "Init"; |
| case State::kStop: |
| return "Stop"; |
| case State::kNextAction: |
| return "NextAction"; |
| case State::kGeneratingKeys: |
| return "GeneratingKeys"; |
| case State::kWaitingForEnclaveTokenForRegistration: |
| return "WaitingForEnclaveTokenForRegistration"; |
| case State::kRegisteringWithEnclave: |
| return "RegisteringWithEnclave"; |
| case State::kWaitingForEnclaveTokenForWrapping: |
| return "WaitingForEnclaveTokenForWrapping"; |
| case State::kWrappingSecrets: |
| return "WrappingSecrets"; |
| case State::kJoiningDomain: |
| return "JoiningDomain"; |
| case State::kHashingPIN: |
| return "HashingPIN"; |
| case State::kDownloadingRecoveryKeyStoreKeys: |
| return "DownloadingRecoveryKeyStoreKeys"; |
| case State::kWaitingForEnclaveTokenForPINWrapping: |
| return "WaitingForEnclaveTokenForPINWrapping"; |
| case State::kWrappingPIN: |
| return "WrappingPIN"; |
| case State::kWaitingForRecoveryKeyStoreTokenForUpload: |
| return "WaitingForRecoveryKeyStoreTokenForUpload"; |
| case State::kWaitingForRecoveryKeyStore: |
| return "WaitingForRecoveryKeyStore"; |
| case State::kJoiningPINToDomain: |
| return "JoiningPINToDomain"; |
| } |
| } |
| |
| static std::string 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 base::StrCat( |
| {"JoinStatus(", |
| TrustedVaultRegistrationStatusToString(status.value().first), |
| ", ", base::NumberToString(status.value().second), ")"}); |
| }, |
| [](const FileFetched& fetched) { |
| const FetchedFile fetched_file = fetched.value().first; |
| const std::optional<std::string>& contents = |
| fetched.value().second; |
| return base::StrCat( |
| {"FileFetched(", ToString(fetched_file), ", ", |
| (contents ? base::StringPrintf("%zu bytes", contents->size()) |
| : "error"), |
| ")"}); |
| }, |
| [](const PINHashed&) { return std::string("PINHashed"); }, |
| [](const Response& response) { |
| const std::string& response_str = response.value(); |
| return base::StringPrintf("Response(%zu bytes)", |
| response_str.size()); |
| }}, |
| event); |
| } |
| |
| static std::string ToString(FetchedFile fetched_file) { |
| switch (fetched_file) { |
| case FetchedFile::kCertFile: |
| return "cert.xml"; |
| case FetchedFile::kSigFile: |
| return "cert.sig.xml"; |
| } |
| } |
| |
| void DoNextAction() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if ((want_registration_ || store_keys_args_ || !pending_pin_.empty()) && |
| !user_->registered()) { |
| want_registration_ = false; |
| StartEnclaveRegistration(); |
| return; |
| } |
| |
| if (user_->registered() && store_keys_args_) { |
| CHECK_EQ(primary_account_info_->gaia, store_keys_args_->gaia_id); |
| auto store_keys_args = std::move(store_keys_args_); |
| store_keys_args_.reset(); |
| |
| 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); |
| GetAccessTokenInternal(GaiaConstants::kPasskeysEnclaveOAuth2Scope); |
| } else if (!user_->joined() && !user_->member_public_key().empty()) { |
| store_keys_args_for_joining_ = std::move(store_keys_args); |
| JoinSecurityDomain(); |
| } |
| return; |
| } |
| |
| if (user_->registered() && !pending_pin_.empty()) { |
| state_ = State::kHashingPIN; |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::TaskPriority::USER_BLOCKING, base::MayBlock()}, |
| base::BindOnce( |
| [](std::string pin) -> std::unique_ptr<HashedPIN> { |
| auto hashed = std::make_unique<HashedPIN>(); |
| RAND_bytes(hashed->salt, sizeof(hashed->salt)); |
| // This is the primary work factor in scrypt. This value matches |
| // the original recommended parameters. Those are a little out |
| // of data in 2024, but Android is using 4096. Since this work |
| // factor falls on the server when MagicArch is used, I've stuck |
| // with this norm. |
| hashed->n = 16384; |
| hashed->is_six_digits = |
| pin.size() == 6 && |
| base::ranges::all_of(pin, [](char c) -> bool { |
| return c >= '0' && c <= '9'; |
| }); |
| CHECK(EVP_PBE_scrypt(pin.data(), pin.size(), hashed->salt, |
| sizeof(hashed->salt), hashed->n, 8, 1, |
| /*max_mem=*/0, hashed->hashed, |
| sizeof(hashed->hashed))); |
| return hashed; |
| }, |
| std::move(pending_pin_)), |
| base::BindOnce( |
| [](base::WeakPtr<StateMachine> machine, |
| std::unique_ptr<HashedPIN> hashed) { |
| if (!machine) { |
| return; |
| } |
| machine->Process(PINHashed(std::move(hashed))); |
| }, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| state_ = State::kStop; |
| } |
| |
| void FetchComplete(FetchedFile file, std::optional<std::string> contents) { |
| Process(FileFetched(std::make_pair(file, std::move(contents)))); |
| } |
| |
| void StartEnclaveRegistration() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| state_ = State::kGeneratingKeys; |
| |
| manager_->user_verifying_key_.reset(); |
| |
| user_verifying_key_provider_ = crypto::GetUserVerifyingKeyProvider(); |
| std::optional<crypto::UserVerifyingKeyLabel> key_label; |
| // TODO(enclave): Reusing the label makes sense on Windows because it will |
| // overwrite the existing key with a new one. This might be different on |
| // other platforms. |
| if (!user_->wrapped_uv_private_key().empty()) { |
| key_label = |
| UserVerifyingKeyLabelFromString(user_->wrapped_uv_private_key()); |
| } |
| if (!key_label) { |
| key_label = CreateUserVerifyingKeyLabel(); |
| } |
| if (!user_verifying_key_provider_ || !key_label) { |
| // Null `user_verifying_key_provider_` means the current |
| // platform does not support user-verifying keys. nullopt for |key_label| |
| // means Chrome does not support them on this OS. |
| GenerateHardwareKey(nullptr); |
| return; |
| } |
| user_verifying_key_provider_->GenerateUserVerifyingSigningKey( |
| *key_label, kSigningAlgorithms, |
| base::BindOnce(&StateMachine::GenerateHardwareKey, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void GenerateHardwareKey( |
| std::unique_ptr<crypto::UserVerifyingSigningKey> uv_key) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(state_ == State::kGeneratingKeys); |
| std::optional<std::vector<uint8_t>> existing_key_id; |
| if (!user_->wrapped_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, |
| std::unique_ptr<crypto::UserVerifyingSigningKey> uv_key) |
| -> 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::make_pair(std::move(uv_key), std::move(key))); |
| } |
| } |
| std::unique_ptr<crypto::UnexportableSigningKey> key = |
| provider->GenerateSigningKeySlowly(kSigningAlgorithms); |
| if (!key) { |
| return Failure(); |
| } |
| return KeyReady( |
| std::make_pair(std::move(uv_key), std::move(key))); |
| }, |
| std::move(existing_key_id), std::move(uv_key)), |
| base::BindOnce(&StateMachine::Process, weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void DoGeneratingKeys(Event event) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (absl::holds_alternative<Failure>(event)) { |
| state_ = State::kStop; |
| return; |
| } |
| CHECK(absl::holds_alternative<KeyReady>(event)) << ToString(event); |
| |
| bool state_dirty = false; |
| |
| auto uv_key = std::move(absl::get_if<KeyReady>(&event)->value().first); |
| manager_->user_verifying_key_ = |
| (uv_key == nullptr) |
| ? nullptr |
| : base::MakeRefCounted<crypto::RefCountedUserVerifyingSigningKey>( |
| std::move(uv_key)); |
| |
| manager_->hardware_key_ = base::MakeRefCounted< |
| unexportable_keys::RefCountedUnexportableSigningKey>( |
| std::move(absl::get_if<KeyReady>(&event)->value().second), |
| unexportable_keys::UnexportableKeyId()); |
| |
| EnclaveLocalState::User* const user_state = user_; |
| if (manager_->user_verifying_key_) { |
| const std::vector<uint8_t> uv_public_key = |
| manager_->user_verifying_key_->key().GetPublicKey(); |
| const std::string uv_public_key_str = VecToString(uv_public_key); |
| if (user_state->uv_public_key() != uv_public_key_str) { |
| user_state->set_uv_public_key(uv_public_key_str); |
| user_state->set_wrapped_uv_private_key(UserVerifyingLabelToString( |
| manager_->user_verifying_key_->key().GetKeyLabel())); |
| state_dirty = true; |
| } |
| } |
| |
| const std::vector<uint8_t> spki = |
| manager_->hardware_key_->key().GetSubjectPublicKeyInfo(); |
| const std::string spki_str = VecToString(spki); |
| if (user_state->hardware_public_key() != spki_str) { |
| std::array<uint8_t, crypto::kSHA256Length> device_id = |
| crypto::SHA256Hash(spki); |
| user_state->set_hardware_public_key(spki_str); |
| user_state->set_wrapped_hardware_private_key( |
| VecToString(manager_->hardware_key_->key().GetWrappedKey())); |
| user_state->set_device_id(VecToString(device_id)); |
| state_dirty = true; |
| } |
| |
| if (state_dirty) { |
| manager_->WriteState(&local_state_); |
| } |
| |
| state_ = State::kWaitingForEnclaveTokenForRegistration; |
| GetAccessTokenInternal(GaiaConstants::kPasskeysEnclaveOAuth2Scope); |
| } |
| |
| void DoWaitingForEnclaveTokenForRegistration(Event event) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| access_token_fetcher_.reset(); |
| if (absl::holds_alternative<Failure>(event)) { |
| FIDO_LOG(ERROR) << "Failed to get access token for enclave"; |
| state_ = State::kStop; |
| 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(manager_->network_context_, enclave::GetEnclaveIdentity(), |
| std::move(token), |
| BuildRegistrationMessage(user_->device_id(), |
| manager_->hardware_key_->key()), |
| enclave::SigningCallback(), |
| base::BindOnce(&StateMachine::OnEnclaveResponse, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void DoRegisteringWithEnclave(Event event) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (absl::holds_alternative<Failure>(event)) { |
| state_ = State::kStop; |
| return; |
| } |
| |
| 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::kStop; |
| return; |
| } |
| |
| if (!SetSecurityDomainMemberKey( |
| user_, response.GetArray()[1] |
| .GetMap() |
| .find(cbor::Value(enclave::kResponseSuccessKey)) |
| ->second)) { |
| FIDO_LOG(ERROR) << "Wrapped member key was invalid: " |
| << cbor::DiagnosticWriter::Write(response); |
| state_ = State::kNextAction; |
| return; |
| } |
| |
| user_->set_registered(true); |
| manager_->WriteState(&local_state_); |
| state_ = State::kNextAction; |
| } |
| |
| void DoWaitingForEnclaveTokenForWrapping(Event event) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| access_token_fetcher_.reset(); |
| if (absl::holds_alternative<Failure>(event)) { |
| FIDO_LOG(ERROR) << "Failed to get access token for enclave"; |
| state_ = State::kStop; |
| return; |
| } |
| |
| state_ = State::kWrappingSecrets; |
| std::string token = std::move(absl::get_if<AccessToken>(&event)->value()); |
| enclave::Transact( |
| manager_->network_context_, enclave::GetEnclaveIdentity(), |
| std::move(token), BuildWrappingMessage(new_security_domain_secrets_), |
| manager_->HardwareKeySigningCallback(), |
| base::BindOnce( |
| [](base::WeakPtr<StateMachine> machine, |
| std::optional<cbor::Value> response) { |
| if (!machine) { |
| return; |
| } |
| if (!response) { |
| machine->Process(Failure()); |
| } else { |
| machine->Process(EnclaveResponse(std::move(*response))); |
| } |
| }, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void DoWrappingSecrets(Event event) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| const auto new_security_domain_secrets = |
| std::move(new_security_domain_secrets_); |
| new_security_domain_secrets_.clear(); |
| |
| if (absl::holds_alternative<Failure>(event)) { |
| FIDO_LOG(ERROR) << "Failed to wrap security domain secrets"; |
| state_ = State::kStop; |
| 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::kStop; |
| return; |
| } |
| |
| if (!StoreWrappedSecrets(user_, new_security_domain_secrets, |
| response.GetArray())) { |
| FIDO_LOG(ERROR) << "Failed to store wrapped secrets"; |
| state_ = State::kStop; |
| return; |
| } |
| |
| if (!user_->joined()) { |
| JoinSecurityDomain(); |
| } else { |
| manager_->WriteState(&local_state_); |
| state_ = State::kNextAction; |
| } |
| } |
| |
| void DoJoiningDomain(Event event) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| join_request_.reset(); |
| store_keys_args_for_joining_.reset(); |
| |
| CHECK(absl::holds_alternative<JoinStatus>(event)); |
| const trusted_vault::TrustedVaultRegistrationStatus status = |
| absl::get_if<JoinStatus>(&event)->value().first; |
| |
| 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; |
| } |
| |
| manager_->WriteState(&local_state_); |
| state_ = State::kNextAction; |
| } |
| |
| void DoHashingPIN(Event event) { |
| // The new PIN has been hashed. Next we fetch the public keys of the |
| // recovery key store. |
| CHECK(absl::holds_alternative<PINHashed>(event)); |
| hashed_pin_ = std::move(absl::get_if<PINHashed>(&event)->value()); |
| |
| cert_xml_loader_ = FetchURL( |
| manager_->url_loader_factory_.get(), kCertFileURL, |
| base::BindOnce(&StateMachine::FetchComplete, |
| weak_ptr_factory_.GetWeakPtr(), FetchedFile::kCertFile)); |
| sig_xml_loader_ = FetchURL( |
| manager_->url_loader_factory_.get(), kSigFileURL, |
| base::BindOnce(&StateMachine::FetchComplete, |
| weak_ptr_factory_.GetWeakPtr(), FetchedFile::kSigFile)); |
| state_ = State::kDownloadingRecoveryKeyStoreKeys; |
| } |
| |
| void DoDownloadingRecoveryKeyStoreKeys(Event event) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| CHECK(absl::holds_alternative<FileFetched>(event)) << ToString(event); |
| auto& file_fetched = absl::get_if<FileFetched>(&event)->value(); |
| const FetchedFile fetched_file = file_fetched.first; |
| std::optional<std::string>& contents = file_fetched.second; |
| |
| switch (fetched_file) { |
| case FetchedFile::kCertFile: |
| cert_xml_loader_.reset(); |
| cert_xml_ = std::move(contents); |
| break; |
| |
| case FetchedFile::kSigFile: |
| sig_xml_loader_.reset(); |
| sig_xml_ = std::move(contents); |
| break; |
| } |
| |
| if (cert_xml_loader_ || sig_xml_loader_) { |
| // One of the fetches is still running. |
| return; |
| } |
| |
| if (!cert_xml_ || !sig_xml_) { |
| // One (or both) fetches failed. |
| state_ = State::kStop; |
| return; |
| } |
| |
| state_ = State::kWaitingForEnclaveTokenForPINWrapping; |
| GetAccessTokenInternal(GaiaConstants::kPasskeysEnclaveOAuth2Scope); |
| } |
| |
| void DoWaitingForEnclaveTokenForPINWrapping(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::kStop; |
| return; |
| } |
| CHECK(absl::holds_alternative<AccessToken>(event)) << ToString(event); |
| |
| // Also generate the security domain secret now so that we can wrap it in |
| // the same enclave request. |
| crypto::RandBytes(security_domain_secret_); |
| |
| // We have everything needed to make the enclave request to wrap the hashed |
| // PIN for transmission to the recovery key store. |
| state_ = State::kWrappingPIN; |
| std::string token = std::move(absl::get_if<AccessToken>(&event)->value()); |
| enclave::Transact(manager_->network_context_, enclave::GetEnclaveIdentity(), |
| std::move(token), |
| BuildPINAndSecretWrappingMessage( |
| hashed_pin_->hashed, std::move(*cert_xml_), |
| std::move(*sig_xml_), security_domain_secret_), |
| manager_->HardwareKeySigningCallback(), |
| base::BindOnce(&StateMachine::OnEnclaveResponse, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void DoWrappingPIN(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) << "PIN wrapping resulted in error response: " |
| << cbor::DiagnosticWriter::Write(response); |
| state_ = State::kStop; |
| return; |
| } |
| |
| const cbor::Value& recovery_key_store_wrap_response = |
| response.GetArray()[0] |
| .GetMap() |
| .find(cbor::Value(enclave::kResponseSuccessKey)) |
| ->second; |
| |
| std::optional<std::unique_ptr<trusted_vault_pb::Vault>> vault = |
| RecoveryKeyStoreWrapResponseToProto(hashed_pin_->salt, hashed_pin_->n, |
| hashed_pin_->is_six_digits, |
| recovery_key_store_wrap_response); |
| if (!vault) { |
| FIDO_LOG(ERROR) |
| << "Failed to translate response into an UpdateVaultProto"; |
| state_ = State::kStop; |
| return; |
| } |
| vault_ = std::move(*vault); |
| |
| wrapping_response_ = std::move(response); |
| |
| state_ = State::kWaitingForRecoveryKeyStoreTokenForUpload; |
| GetAccessTokenInternal(GaiaConstants::kCryptAuthOAuth2Scope); |
| } |
| |
| void DoWaitingForRecoveryKeyStoreTokenForUpload(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 cryptauth"; |
| state_ = State::kStop; |
| return; |
| } |
| CHECK(absl::holds_alternative<AccessToken>(event)) << ToString(event); |
| |
| std::string token = std::move(absl::get_if<AccessToken>(&event)->value()); |
| auto request = std::make_unique<network::ResourceRequest>(); |
| GURL base_url(kRecoveryKeyStoreURL); |
| request->url = net::AppendQueryParameter(base_url, "alt", "proto"); |
| request->method = "PATCH"; |
| request->headers.SetHeader("Authorization", |
| base::StrCat({"Bearer ", token})); |
| |
| upload_loader_ = network::SimpleURLLoader::Create(std::move(request), |
| kTrafficAnnotation); |
| upload_loader_->SetTimeoutDuration(base::Seconds(10)); |
| upload_loader_->SetURLLoaderFactoryOptions( |
| network::mojom::kURLLoadOptionBlockAllCookies); |
| std::string serialized_vault; |
| CHECK(vault_->SerializeToString(&serialized_vault)); |
| upload_loader_->AttachStringForUpload(serialized_vault, |
| "application/x-protobuf"); |
| |
| state_ = State::kWaitingForRecoveryKeyStore; |
| upload_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| manager_->url_loader_factory_.get(), |
| base::BindOnce( |
| [](base::WeakPtr<StateMachine> machine, |
| std::optional<std::string> response) { |
| if (!machine) { |
| return; |
| } |
| if (response) { |
| machine->Process(Response(std::move(*response))); |
| } else { |
| machine->Process(Failure()); |
| } |
| }, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void DoWaitingForRecoveryKeyStore(Event event) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| access_token_fetcher_.reset(); |
| if (absl::holds_alternative<Failure>(event)) { |
| FIDO_LOG(ERROR) << "Failed to upload to recovery key store"; |
| state_ = State::kNextAction; |
| return; |
| } |
| CHECK(absl::holds_alternative<Response>(event)) << ToString(event); |
| |
| const std::string& response_str = absl::get_if<Response>(&event)->value(); |
| trusted_vault_pb::Vault vault; |
| if (!vault.ParseFromString(response_str)) { |
| FIDO_LOG(ERROR) << "Failed to parse Vault: " |
| << base::HexEncode(base::as_byte_span(response_str)); |
| state_ = State::kNextAction; |
| return; |
| } |
| |
| wrapped_pin_ = BuildWrappedPIN(*hashed_pin_, /*generation=*/0, vault_.get(), |
| security_domain_secret_); |
| store_keys_args_for_joining_ = std::make_unique<StoreKeysArgs>(); |
| // Key version zero is a special value that indicates that the security |
| // domain is being created. |
| store_keys_args_for_joining_->last_key_version = 0; |
| store_keys_args_for_joining_->keys.emplace_back( |
| std::begin(security_domain_secret_), std::end(security_domain_secret_)); |
| |
| const auto secure_box_pub_key = |
| trusted_vault::SecureBoxPublicKey::CreateByImport(ToSpan( |
| vault_->application_keys()[0].asymmetric_key_pair().public_key())); |
| |
| state_ = State::kJoiningPINToDomain; |
| join_request_ = manager_->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::GpmPin(wrapped_pin_->SerializeAsString()), |
| base::BindOnce(&StateMachine::OnJoinedSecurityDomain, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void DoJoiningPINToDomain(Event event) { |
| CHECK(absl::holds_alternative<JoinStatus>(event)) << ToString(event); |
| |
| const auto& join_status = absl::get_if<JoinStatus>(&event)->value(); |
| const trusted_vault::TrustedVaultRegistrationStatus status = |
| join_status.first; |
| const int key_version = join_status.second; |
| |
| if (status != trusted_vault::TrustedVaultRegistrationStatus::kSuccess) { |
| state_ = State::kStop; |
| return; |
| } |
| |
| user_->mutable_wrapped_pins()->clear(); |
| user_->mutable_wrapped_pins()->insert( |
| {key_version, std::move(*wrapped_pin_)}); |
| wrapped_pin_.reset(); |
| |
| base::flat_map<int32_t, std::vector<uint8_t>> new_security_domain_secrets; |
| new_security_domain_secrets.insert({key_version, |
| {std::begin(security_domain_secret_), |
| std::end(security_domain_secret_)}}); |
| if (!StoreWrappedSecrets(user_, new_security_domain_secrets, |
| base::span<const cbor::Value>( |
| &wrapping_response_->GetArray()[1], 1ul))) { |
| FIDO_LOG(ERROR) << "Secret wrapping resulted in malformed resposne: " |
| << cbor::DiagnosticWriter::Write(*wrapping_response_); |
| state_ = State::kStop; |
| return; |
| } |
| |
| store_keys_args_for_joining_->last_key_version = key_version; |
| JoinSecurityDomain(); |
| } |
| |
| void JoinSecurityDomain() { |
| state_ = State::kJoiningDomain; |
| const auto secure_box_pub_key = |
| trusted_vault::SecureBoxPublicKey::CreateByImport( |
| ToSpan(user_->member_public_key())); |
| join_request_ = manager_->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::PhysicalDevice(), |
| base::BindOnce(&StateMachine::OnJoinedSecurityDomain, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void GetAccessTokenInternal(const char* scope) { |
| access_token_fetcher_ = |
| std::make_unique<signin::PrimaryAccountAccessTokenFetcher>( |
| "passkeys_enclave", manager_->identity_manager_, |
| signin::ScopeSet{scope}, |
| base::BindOnce( |
| [](base::WeakPtr<StateMachine> machine, |
| GoogleServiceAuthError error, |
| signin::AccessTokenInfo access_token_info) { |
| if (!machine) { |
| return; |
| } |
| if (error.state() == GoogleServiceAuthError::NONE) { |
| machine->Process(AccessToken(access_token_info.token)); |
| } else { |
| machine->Process(Failure()); |
| } |
| }, |
| weak_ptr_factory_.GetWeakPtr()), |
| signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable, |
| signin::ConsentLevel::kSignin); |
| } |
| |
| void OnEnclaveResponse(std::optional<cbor::Value> response) { |
| if (!response) { |
| Process(Failure()); |
| } else { |
| Process(EnclaveResponse(std::move(*response))); |
| } |
| } |
| |
| void OnJoinedSecurityDomain( |
| trusted_vault::TrustedVaultRegistrationStatus status, |
| int key_version) { |
| Process(JoinStatus(std::make_pair(status, key_version))); |
| } |
| |
| // Constructed a wrapped version of the hashed PIN that will be part of the |
| // virtual member metadata. The inner CBOR structure contains everything that |
| // the enclave would need when processing a PIN and is authenticated (and |
| // encrypted) by the security domain secret. |
| static webauthn_pb::EnclaveLocalState::WrappedPIN BuildWrappedPIN( |
| const HashedPIN& hashed_pin, |
| int64_t generation, |
| const trusted_vault_pb::Vault* vault, |
| base::span<const uint8_t> security_domain_secret) { |
| uint8_t claim_key[32]; |
| crypto::RandBytes(claim_key); |
| |
| cbor::Value::MapValue map; |
| map.emplace(1, base::span<const uint8_t>(hashed_pin.hashed)); |
| map.emplace(2, generation); |
| map.emplace(3, base::span<const uint8_t>(claim_key)); |
| map.emplace(4, ToSpan(vault->vault_parameters().counter_id())); |
| map.emplace(5, ToSpan(vault->vault_parameters().vault_handle())); |
| const std::vector<uint8_t> cbor_bytes = |
| cbor::Writer::Write(cbor::Value(std::move(map))).value(); |
| |
| // This is "KeychainApplicationKey:chrome:GPM PIN data wrapping key". |
| static constexpr uint8_t kKeyPurposePinDataKey[] = { |
| 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x41, 0x70, 0x70, |
| 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, |
| 0x3a, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x3a, 0x47, 0x50, 0x4d, |
| 0x20, 0x50, 0x49, 0x4e, 0x20, 0x64, 0x61, 0x74, 0x61, 0x20, 0x77, |
| 0x72, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x20, 0x6b, 0x65, 0x79}; |
| const std::vector<uint8_t> derived_key = crypto::HkdfSha256( |
| security_domain_secret, /*salt=*/base::span<const uint8_t>(), |
| kKeyPurposePinDataKey, 32); |
| crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM); |
| aead.Init(derived_key); |
| uint8_t nonce[12]; |
| crypto::RandBytes(nonce); |
| std::vector<uint8_t> wrapped_pin = aead.Seal( |
| cbor_bytes, nonce, /*additional_data=*/base::span<const uint8_t>()); |
| wrapped_pin.insert(wrapped_pin.begin(), std::begin(nonce), std::end(nonce)); |
| |
| webauthn_pb::EnclaveLocalState::WrappedPIN ret; |
| ret.set_wrapped_pin(VecToString(std::move(wrapped_pin))); |
| ret.set_claim_key(VecToString(claim_key)); |
| ret.set_generation(generation); |
| ret.set_form( |
| hashed_pin.is_six_digits |
| ? webauthn_pb::EnclaveLocalState::WrappedPIN::FORM_SIX_DIGITS |
| : webauthn_pb::EnclaveLocalState::WrappedPIN::FORM_ARBITRARY); |
| ret.set_hash(webauthn_pb::EnclaveLocalState::WrappedPIN::HASH_SCRYPT); |
| ret.set_hash_difficulty(hashed_pin.n); |
| ret.set_hash_salt(VecToString(hashed_pin.salt)); |
| return ret; |
| } |
| |
| const raw_ptr<EnclaveManager> manager_; |
| // local_state_ contains a copy of the EnclaveManager's state from when this |
| // StateMachine was created. |
| webauthn_pb::EnclaveLocalState local_state_; |
| // user_ points within `local_state_` to the state for the user specified in |
| // `primary_account_info_`. |
| const raw_ptr<webauthn_pb::EnclaveLocalState::User> user_; |
| const std::unique_ptr<CoreAccountInfo> primary_account_info_; |
| |
| State state_ = State::kInit; |
| bool processing_ = false; |
| |
| std::unique_ptr<StoreKeysArgs> store_keys_args_; |
| std::string pending_pin_; |
| bool want_registration_ = false; |
| |
| std::unique_ptr<StoreKeysArgs> store_keys_args_for_joining_; |
| std::unique_ptr<crypto::UserVerifyingKeyProvider> |
| user_verifying_key_provider_; |
| 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_; |
| std::unique_ptr<network::SimpleURLLoader> cert_xml_loader_; |
| std::unique_ptr<network::SimpleURLLoader> sig_xml_loader_; |
| std::unique_ptr<network::SimpleURLLoader> upload_loader_; |
| std::optional<std::string> cert_xml_; |
| std::optional<std::string> sig_xml_; |
| std::unique_ptr<HashedPIN> hashed_pin_; |
| std::unique_ptr<trusted_vault_pb::Vault> vault_; |
| std::array<uint8_t, 32> security_domain_secret_; |
| std::optional<webauthn_pb::EnclaveLocalState::WrappedPIN> wrapped_pin_; |
| std::optional<cbor::Value> wrapping_response_; |
| |
| SEQUENCE_CHECKER(sequence_checker_); |
| base::WeakPtrFactory<StateMachine> weak_ptr_factory_{this}; |
| }; |
| |
| 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 !loading_ && !state_machine_; |
| } |
| |
| bool EnclaveManager::is_loaded() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return static_cast<bool>(local_state_); |
| } |
| |
| bool EnclaveManager::is_registered() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return user_ && user_->registered(); |
| } |
| |
| bool EnclaveManager::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_; |
| } |
| |
| bool EnclaveManager::is_uv_key_available() const { |
| return user_ && !user_->wrapped_uv_private_key().empty(); |
| } |
| |
| void EnclaveManager::Start() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| Act(); |
| } |
| |
| void EnclaveManager::RegisterIfNeeded() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (user_ && user_->registered()) { |
| return; |
| } |
| if (!pending_actions_) { |
| pending_actions_ = std::make_unique<PendingActions>(); |
| } |
| pending_actions_->want_registration = true; |
| Act(); |
| } |
| |
| void EnclaveManager::SetupWithPIN(std::string pin) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!pending_actions_) { |
| pending_actions_ = std::make_unique<PendingActions>(); |
| } |
| pending_actions_->pin = std::move(pin); |
| Act(); |
| } |
| |
| void EnclaveManager::GetHardwareKeyForSignature( |
| base::OnceCallback<void( |
| scoped_refptr<unexportable_keys::RefCountedUnexportableSigningKey>)> |
| callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!user_ || user_->wrapped_hardware_private_key().empty()) { |
| std::move(callback).Run(nullptr); |
| return; |
| } |
| |
| if (hardware_key_) { |
| std::move(callback).Run(hardware_key_); |
| return; |
| } |
| |
| auto key_callback = base::BindOnce( |
| [](base::WeakPtr<EnclaveManager> enclave_manager, |
| CoreAccountId account_id, |
| base::OnceCallback<void( |
| scoped_refptr< |
| unexportable_keys::RefCountedUnexportableSigningKey>)> |
| callback, |
| std::unique_ptr<crypto::UnexportableSigningKey> key) { |
| if (!enclave_manager || |
| enclave_manager->primary_account_info_->account_id != account_id) { |
| std::move(callback).Run(nullptr); |
| return; |
| } |
| DCHECK_CALLED_ON_VALID_SEQUENCE(enclave_manager->sequence_checker_); |
| if (!key) { |
| // TODO(enclave): The key is gone. Clear registration state. |
| std::move(callback).Run(nullptr); |
| return; |
| } |
| enclave_manager->hardware_key_ = base::MakeRefCounted< |
| unexportable_keys::RefCountedUnexportableSigningKey>( |
| std::move(key), unexportable_keys::UnexportableKeyId()); |
| std::move(callback).Run(enclave_manager->hardware_key_); |
| }, |
| weak_ptr_factory_.GetWeakPtr(), primary_account_info_->account_id, |
| std::move(callback)); |
| |
| // Retrieve the key on a non-UI thread, and post a task back to the current |
| // thread that invokes `key_callback` with the obtained key. |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()}, |
| base::BindOnce( |
| [](std::string wrapped_hardware_private_key) |
| -> std::unique_ptr<crypto::UnexportableSigningKey> { |
| auto provider = |
| crypto::GetSoftwareUnsecureUnexportableKeyProvider(); |
| return provider->FromWrappedSigningKeySlowly( |
| ToVector(wrapped_hardware_private_key)); |
| }, |
| user_->wrapped_hardware_private_key()), |
| std::move(key_callback)); |
| } |
| |
| enclave::SigningCallback EnclaveManager::HardwareKeySigningCallback() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(!user_->wrapped_hardware_private_key().empty()); |
| CHECK(user_->registered()); |
| |
| return base::BindOnce( |
| [](base::WeakPtr<EnclaveManager> enclave_manager, |
| enclave::SignedMessage message_to_be_signed, |
| base::OnceCallback<void(std::optional<enclave::ClientSignature>)> |
| result_callback) { |
| if (!enclave_manager || !enclave_manager->user_) { |
| std::move(result_callback).Run(std::nullopt); |
| return; |
| } |
| DCHECK_CALLED_ON_VALID_SEQUENCE(enclave_manager->sequence_checker_); |
| |
| auto signing_callback = base::BindOnce( |
| [](std::string device_id, |
| enclave::SignedMessage message_to_be_signed, |
| base::OnceCallback<void(std::optional<enclave::ClientSignature>)> |
| result_callback, |
| scoped_refptr< |
| unexportable_keys::RefCountedUnexportableSigningKey> key) { |
| if (!key) { |
| std::move(result_callback).Run(std::nullopt); |
| return; |
| } |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, |
| {base::TaskPriority::BEST_EFFORT, base::MayBlock()}, |
| base::BindOnce( |
| [](std::string device_id, |
| enclave::SignedMessage message_to_be_signed, |
| scoped_refptr<unexportable_keys:: |
| RefCountedUnexportableSigningKey> |
| key) -> std::optional<enclave::ClientSignature> { |
| std::optional<std::vector<uint8_t>> signature = |
| key->key().SignSlowly(message_to_be_signed); |
| if (!signature) { |
| return std::nullopt; |
| } |
| enclave::ClientSignature client_signature; |
| client_signature.device_id = ToVector(device_id); |
| client_signature.signature = std::move(*signature); |
| client_signature.key_type = |
| enclave::ClientKeyType::kHardware; |
| return std::move(client_signature); |
| }, |
| std::move(device_id), std::move(message_to_be_signed), |
| key), |
| std::move(result_callback)); |
| }, |
| enclave_manager->user_->device_id(), |
| std::move(message_to_be_signed), std::move(result_callback)); |
| |
| enclave_manager->GetHardwareKeyForSignature( |
| std::move(signing_callback)); |
| }, |
| weak_ptr_factory_.GetWeakPtr()); |
| } |
| |
| void EnclaveManager::GetUserVerifyingKeyForSignature( |
| base::OnceCallback<void( |
| scoped_refptr<crypto::RefCountedUserVerifyingSigningKey>)> callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!user_ || user_->wrapped_uv_private_key().empty()) { |
| std::move(callback).Run(nullptr); |
| return; |
| } |
| |
| if (user_verifying_key_) { |
| std::move(callback).Run(user_verifying_key_); |
| return; |
| } |
| |
| auto user_verifying_key_provider = crypto::GetUserVerifyingKeyProvider(); |
| if (!user_verifying_key_provider) { |
| // This indicates the platform key provider was available, but now is not. |
| // TODO(enclave): Clear registration state. |
| std::move(callback).Run(nullptr); |
| return; |
| } |
| |
| crypto::UserVerifyingKeyProvider* provider_temp = |
| user_verifying_key_provider.get(); |
| |
| auto key_callback = base::BindOnce( |
| [](base::WeakPtr<EnclaveManager> enclave_manager, |
| CoreAccountId account_id, |
| std::unique_ptr<crypto::UserVerifyingKeyProvider> provider, |
| base::OnceCallback<void( |
| scoped_refptr<crypto::RefCountedUserVerifyingSigningKey>)> |
| callback, |
| std::unique_ptr<crypto::UserVerifyingSigningKey> key) { |
| provider.reset(); |
| if (!enclave_manager || |
| enclave_manager->primary_account_info_->account_id != account_id) { |
| std::move(callback).Run(nullptr); |
| return; |
| } |
| if (!key) { |
| // TODO(enclave): The key is gone. Clear registration state. |
| std::move(callback).Run(nullptr); |
| return; |
| } |
| enclave_manager->user_verifying_key_ = |
| base::MakeRefCounted<crypto::RefCountedUserVerifyingSigningKey>( |
| std::move(key)); |
| std::move(callback).Run(enclave_manager->user_verifying_key_); |
| }, |
| weak_ptr_factory_.GetWeakPtr(), primary_account_info_->account_id, |
| std::move(user_verifying_key_provider), std::move(callback)); |
| |
| auto key_label = |
| UserVerifyingKeyLabelFromString(user_->wrapped_uv_private_key()); |
| CHECK(key_label); |
| |
| provider_temp->GetUserVerifyingSigningKey(std::move(*key_label), |
| std::move(key_callback)); |
| } |
| |
| enclave::SigningCallback EnclaveManager::UserVerifyingKeySigningCallback() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(!user_->wrapped_uv_private_key().empty()); |
| CHECK(user_->registered()); |
| |
| return base::BindOnce( |
| [](base::WeakPtr<EnclaveManager> enclave_manager, |
| enclave::SignedMessage message_to_be_signed, |
| base::OnceCallback<void(std::optional<enclave::ClientSignature>)> |
| result_callback) { |
| if (!enclave_manager) { |
| std::move(result_callback).Run(std::nullopt); |
| return; |
| } |
| DCHECK_CALLED_ON_VALID_SEQUENCE(enclave_manager->sequence_checker_); |
| |
| auto signing_callback = base::BindOnce( |
| [](std::string device_id, |
| enclave::SignedMessage message_to_be_signed, |
| base::OnceCallback<void(std::optional<enclave::ClientSignature>)> |
| result_callback, |
| scoped_refptr<crypto::RefCountedUserVerifyingSigningKey> |
| uv_signing_key) { |
| if (!uv_signing_key) { |
| std::move(result_callback).Run(std::nullopt); |
| return; |
| } |
| uv_signing_key->key() |
| .Sign( |
| message_to_be_signed, |
| base::BindOnce( |
| [](std::string device_id, |
| base::OnceCallback<void( |
| std::optional<enclave::ClientSignature>)> |
| result_callback, |
| std::optional<std::vector<uint8_t>> signature) { |
| if (!signature) { |
| std::move(result_callback).Run(std::nullopt); |
| return; |
| } |
| enclave::ClientSignature client_signature; |
| client_signature.device_id = ToVector(device_id); |
| client_signature.signature = std::move(*signature); |
| client_signature.key_type = |
| enclave::ClientKeyType::kUserVerified; |
| std::move(result_callback) |
| .Run(std::move(client_signature)); |
| }, |
| std::move(device_id), std::move(result_callback))); |
| }, |
| enclave_manager->user_->device_id(), |
| std::move(message_to_be_signed), std::move(result_callback)); |
| |
| enclave_manager->GetUserVerifyingKeyForSignature( |
| std::move(signing_callback)); |
| }, |
| weak_ptr_factory_.GetWeakPtr()); |
| } |
| |
| std::optional<std::vector<uint8_t>> EnclaveManager::GetWrappedSecret( |
| int32_t version) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(is_ready()); |
| const auto it = user_->wrapped_security_domain_secrets().find(version); |
| if (it == user_->wrapped_security_domain_secrets().end()) { |
| return std::nullopt; |
| } |
| return ToVector(it->second); |
| } |
| |
| std::vector<std::vector<uint8_t>> EnclaveManager::GetWrappedSecrets() { |
| 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; |
| } |
| |
| std::pair<int32_t, std::vector<uint8_t>> |
| EnclaveManager::GetCurrentWrappedSecret() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(is_ready()); |
| CHECK(!user_->wrapped_security_domain_secrets().empty()); |
| |
| std::optional<int32_t> max_version; |
| for (const auto& it : user_->wrapped_security_domain_secrets()) { |
| if (!max_version.has_value() || *max_version < it.first) { |
| max_version = it.first; |
| } |
| } |
| const auto it = user_->wrapped_security_domain_secrets().find(*max_version); |
| return std::make_pair(it->first, ToVector(it->second)); |
| } |
| |
| std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher> |
| EnclaveManager::GetAccessToken( |
| base::OnceCallback<void(std::optional<std::string>)> callback) { |
| return std::make_unique<signin::PrimaryAccountAccessTokenFetcher>( |
| "passkeys_enclave", identity_manager_, |
| signin::ScopeSet{GaiaConstants::kPasskeysEnclaveOAuth2Scope}, |
| base::BindOnce( |
| [](base::OnceCallback<void(std::optional<std::string>)> callback, |
| GoogleServiceAuthError error, |
| signin::AccessTokenInfo access_token_info) { |
| if (error.state() == GoogleServiceAuthError::NONE) { |
| std::move(callback).Run(std::move(access_token_info.token)); |
| } else { |
| FIDO_LOG(ERROR) |
| << "Failed to get access token: " << error.error_message(); |
| std::move(callback).Run(std::nullopt); |
| } |
| }, |
| std::move(callback)), |
| signin::PrimaryAccountAccessTokenFetcher::Mode::kImmediate, |
| signin::ConsentLevel::kSignin); |
| } |
| |
| void EnclaveManager::AddObserver(Observer* observer) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| observer_list_.AddObserver(observer); |
| } |
| |
| void EnclaveManager::RemoveObserver(Observer* observer) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| observer_list_.RemoveObserver(observer); |
| } |
| |
| void EnclaveManager::StoreKeys(const std::string& gaia_id, |
| std::vector<std::vector<uint8_t>> keys, |
| int last_key_version) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| auto 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_++; |
| |
| if (!pending_actions_) { |
| pending_actions_ = std::make_unique<PendingActions>(); |
| } |
| pending_actions_->store_keys_args = std::move(store_keys_args); |
| Act(); |
| } |
| |
| std::unique_ptr<enclave::ClaimedPIN> EnclaveManager::MakeClaimedPINSlowly( |
| std::string pin, |
| const webauthn_pb::EnclaveLocalState::WrappedPIN& wrapped_pin) { |
| uint8_t hashed[32]; |
| const std::string& salt = wrapped_pin.hash_salt(); |
| CHECK(EVP_PBE_scrypt(pin.data(), pin.size(), |
| reinterpret_cast<const uint8_t*>(salt.data()), |
| salt.size(), wrapped_pin.hash_difficulty(), 8, 1, |
| 1ul << 28, hashed, sizeof(hashed))); |
| |
| static constexpr uint8_t kAAD[] = {'P', 'I', 'N', ' ', 'c', |
| 'l', 'a', 'i', 'm'}; |
| crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM); |
| aead.Init(ToSpan(wrapped_pin.claim_key())); |
| uint8_t nonce[12]; |
| crypto::RandBytes(nonce); |
| std::vector<uint8_t> ciphertext = aead.Seal(hashed, nonce, kAAD); |
| ciphertext.insert(ciphertext.begin(), std::begin(nonce), std::end(nonce)); |
| |
| return std::make_unique<enclave::ClaimedPIN>( |
| std::move(ciphertext), ToVector(wrapped_pin.wrapped_pin())); |
| } |
| |
| bool EnclaveManager::RunWhenStoppedForTesting(base::OnceClosure on_stop) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(!state_machine_ && !loading_); |
| if (!currently_writing_) { |
| return false; |
| } |
| write_finished_callback_ = std::move(on_stop); |
| return true; |
| } |
| |
| const webauthn_pb::EnclaveLocalState& EnclaveManager::local_state_for_testing() |
| const { |
| return *local_state_; |
| } |
| |
| // static |
| std::string_view EnclaveManager::recovery_key_store_url_for_testing() { |
| return kRecoveryKeyStoreURL; |
| } |
| |
| // static |
| std::string_view EnclaveManager::recovery_key_store_cert_url_for_testing() { |
| return kCertFileURL; |
| } |
| |
| // static |
| std::string_view EnclaveManager::recovery_key_store_sig_url_for_testing() { |
| return kSigFileURL; |
| } |
| |
| // Observes the `IdentityManager` and tells the `EnclaveManager` when the |
| // primary account for the profile has changed. |
| class EnclaveManager::IdentityObserver |
| : public signin::IdentityManager::Observer { |
| public: |
| IdentityObserver(signin::IdentityManager* identity_manager, |
| EnclaveManager* manager) |
| : identity_manager_(identity_manager), manager_(manager) { |
| identity_manager_->AddObserver(this); |
| } |
| |
| ~IdentityObserver() override { |
| if (observing_) { |
| identity_manager_->RemoveObserver(this); |
| } |
| } |
| |
| void OnPrimaryAccountChanged( |
| const signin::PrimaryAccountChangeEvent& event_details) override { |
| manager_->HandleIdentityChange(); |
| } |
| |
| void OnAccountsInCookieUpdated( |
| const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info, |
| const GoogleServiceAuthError& error) override { |
| manager_->HandleIdentityChange(); |
| } |
| |
| void OnIdentityManagerShutdown( |
| signin::IdentityManager* identity_manager) override { |
| if (observing_) { |
| identity_manager_->RemoveObserver(this); |
| observing_ = false; |
| } |
| } |
| |
| private: |
| bool observing_ = true; |
| const raw_ptr<signin::IdentityManager> identity_manager_; |
| const raw_ptr<EnclaveManager> manager_; |
| }; |
| |
| void EnclaveManager::Act() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!local_state_) { |
| if (loading_) { |
| return; |
| } |
| |
| loading_ = true; |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::TaskPriority::USER_BLOCKING, base::MayBlock()}, |
| base::BindOnce( |
| [](base::FilePath path) -> std::optional<std::string> { |
| std::string contents, decrypted; |
| if (!base::ReadFileToString(path, &contents) || |
| !OSCrypt::DecryptString(contents, &decrypted)) { |
| return std::nullopt; |
| } |
| |
| return std::move(decrypted); |
| }, |
| file_path_), |
| base::BindOnce(&EnclaveManager::LoadComplete, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| if (!user_ || !pending_actions_) { |
| Stopped(); |
| return; |
| } |
| |
| if (!state_machine_) { |
| EnclaveLocalState copy; |
| copy.CopyFrom(*local_state_); |
| state_machine_ = std::make_unique<StateMachine>( |
| this, std::move(copy), |
| std::make_unique<CoreAccountInfo>(*primary_account_info_)); |
| } |
| |
| if (pending_actions_->want_registration) { |
| state_machine_->set_want_registration(); |
| } |
| if (pending_actions_->store_keys_args) { |
| state_machine_->set_store_keys_args( |
| std::move(pending_actions_->store_keys_args)); |
| } |
| if (!pending_actions_->pin.empty()) { |
| state_machine_->set_pending_pin(std::move(pending_actions_->pin)); |
| } |
| |
| pending_actions_.reset(); |
| state_machine_->Start(); |
| } |
| |
| void EnclaveManager::LoadComplete(std::optional<std::string> contents) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| loading_ = false; |
| if (contents) { |
| local_state_ = ParseStateFile(std::move(*contents)); |
| } else { |
| local_state_ = std::make_unique<EnclaveLocalState>(); |
| } |
| |
| for (const auto& it : local_state_->users()) { |
| std::optional<int> error_line = CheckInvariants(it.second); |
| if (error_line.has_value()) { |
| FIDO_LOG(ERROR) << "State invariant failed on line " << *error_line; |
| local_state_ = std::make_unique<EnclaveLocalState>(); |
| break; |
| } |
| } |
| |
| HandleIdentityChange(/*is_post_load=*/true); |
| Act(); |
| } |
| |
| void EnclaveManager::HandleIdentityChange(bool is_post_load) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // This function is called when local state finishes loading. Prior to that |
| // identity changes are ignored. |
| if (!local_state_) { |
| return; |
| } |
| |
| // If a state machine is running, there must be a current user. |
| CHECK(!state_machine_ || user_); |
| bool need_to_stop = true; |
| |
| CoreAccountInfo primary_account_info = |
| identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); |
| if (!primary_account_info.IsEmpty()) { |
| if (primary_account_info_ && |
| primary_account_info_->account_id != primary_account_info.account_id) { |
| // If the signed-in user has changed, the state machine must be halted |
| // because otherwise it could act on the wrong account. |
| need_to_stop = true; |
| } |
| user_ = StateForUser(local_state_.get(), primary_account_info); |
| if (!user_) { |
| user_ = CreateStateForUser(local_state_.get(), primary_account_info); |
| } |
| primary_account_info_ = |
| std::make_unique<CoreAccountInfo>(std::move(primary_account_info)); |
| } else { |
| if (user_) { |
| // If the users signs out, the state machine is stopped because it only |
| // operates in the context of an account. |
| need_to_stop = true; |
| } |
| user_ = nullptr; |
| primary_account_info_.reset(); |
| } |
| |
| user_verifying_key_.reset(); |
| hardware_key_.reset(); |
| |
| const signin::AccountsInCookieJarInfo in_jar = |
| identity_manager_->GetAccountsInCookieJar(); |
| if (in_jar.accounts_are_fresh) { |
| // 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); |
| } |
| // A `StateMachine` can also mutate the enclave state. Thus if we're about |
| // to mutate it ourselves, confirm that any `StateMachine` is about to be |
| // stopped and thus cannot overwrite these changes. |
| CHECK(need_to_stop); |
| for (const auto& gaia_id : to_remove) { |
| CHECK(local_state_->mutable_users()->erase(gaia_id)); |
| } |
| WriteState(local_state_.get()); |
| } |
| |
| if (need_to_stop && !is_post_load) { |
| Stopped(); |
| } |
| } |
| |
| void EnclaveManager::Stopped() { |
| state_machine_.reset(); |
| pending_actions_.reset(); |
| |
| for (Observer& observer : observer_list_) { |
| observer.OnEnclaveManagerIdle(); |
| } |
| } |
| |
| void EnclaveManager::WriteState(EnclaveLocalState* new_state) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| for (const auto& it : new_state->users()) { |
| std::optional<int> error_line = CheckInvariants(it.second); |
| CHECK(!error_line.has_value()) |
| << "State invariant failed on line " << *error_line; |
| } |
| |
| std::string serialized; |
| serialized.reserve(1024); |
| new_state->AppendToString(&serialized); |
| |
| if (new_state != local_state_.get()) { |
| user_ = nullptr; |
| local_state_ = std::make_unique<EnclaveLocalState>(); |
| CHECK(local_state_->ParseFromString(serialized)); |
| user_ = StateForUser(local_state_.get(), *primary_account_info_); |
| } |
| |
| const std::array<uint8_t, crypto::kSHA256Length> digest = |
| crypto::SHA256Hash(base::as_bytes(base::make_span(serialized))); |
| serialized.append(std::begin(kHashPrefix), std::end(kHashPrefix)); |
| serialized.append(digest.begin(), digest.end()); |
| |
| if (currently_writing_) { |
| pending_write_ = std::move(serialized); |
| return; |
| } |
| |
| DoWriteState(std::move(serialized)); |
| } |
| |
| void EnclaveManager::DoWriteState(std::string serialized) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| 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 (pending_write_) { |
| DoWriteState(std::move(*pending_write_)); |
| pending_write_.reset(); |
| return; |
| } |
| |
| if (write_finished_callback_) { |
| std::move(write_finished_callback_).Run(); |
| } |
| } |