blob: 0be70bdd88c853111badcbe265ee420f7ea98acb [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/trusted_vault/recovery_key_store_connection_impl.h"
#include <memory>
#include "base/barrier_closure.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/trusted_vault/proto/recovery_key_store.pb.h"
#include "components/trusted_vault/proto_string_bytes_conversion.h"
#include "components/trusted_vault/recovery_key_store_certificate.h"
#include "components/trusted_vault/recovery_key_store_connection.h"
#include "components/trusted_vault/trusted_vault_access_token_fetcher.h"
#include "components/trusted_vault/trusted_vault_connection.h"
#include "components/trusted_vault/trusted_vault_histograms.h"
#include "components/trusted_vault/trusted_vault_request.h"
#include "net/base/url_util.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 "url/gurl.h"
namespace trusted_vault {
namespace {
// The "/0" suffix is required but ignored.
constexpr char kUpdateVaultUrl[] =
"https://cryptauthvault.googleapis.com/v1/vaults/0";
constexpr char kListVaultsUrl[] =
"https://cryptauthvault.googleapis.com/v1/"
"vaults?use_case=13&challenge_not_required=1";
constexpr char kRecoveryKeyStoreCertFileUrl[] =
"https://www.gstatic.com/cryptauthvault/v0/cert.xml";
constexpr char kRecoveryKeyStoreSigFileUrl[] =
"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 kMaxRecoveryKeyStoreCertFetchBodyBytes = 128 * 1024;
static constexpr net::NetworkTrafficAnnotationTag kCertXmlTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("trusted_vault_cert_request",
R"(
semantics {
sender: "Trusted Vault Service"
description:
"Request to the vault service in order to retrieve the public list "
"of certificates to perform encryption key operations for Chrome, "
"such as Google Password Manager passkey operations"
trigger:
"Periodically/upon certain non-user controlled events after user "
"signs in Chrome profile."
data: "None"
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2025-06-05"
user_data {
type: NONE
}
internal {
contacts {
email: "nsatragno@chromium.org"
}
contacts {
email: "chrome-webauthn@google.com"
}
}
}
policy {
cookies_allowed: NO
setting:
"This feature cannot be disabled in settings, but if user signs "
"out of Chrome, this request would not be made."
chrome_policy {
SigninAllowed {
policy_options {mode: MANDATORY}
SigninAllowed: false
}
}
})");
RecoveryKeyStoreStatus HttpStatusToRecoveryKeyStoreStatus(
TrustedVaultRequest::HttpStatus http_status) {
switch (http_status) {
case TrustedVaultRequest::HttpStatus::kSuccess:
return RecoveryKeyStoreStatus::kSuccess;
case TrustedVaultRequest::HttpStatus::kBadRequest:
case TrustedVaultRequest::HttpStatus::kNotFound:
case TrustedVaultRequest::HttpStatus::kConflict:
case TrustedVaultRequest::HttpStatus::kOtherError:
return RecoveryKeyStoreStatus::kOtherError;
case TrustedVaultRequest::HttpStatus::kNetworkError:
return RecoveryKeyStoreStatus::kNetworkError;
case TrustedVaultRequest::HttpStatus::kTransientAccessTokenFetchError:
return RecoveryKeyStoreStatus::kTransientAccessTokenFetchError;
case TrustedVaultRequest::HttpStatus::kPersistentAccessTokenFetchError:
return RecoveryKeyStoreStatus::kPersistentAccessTokenFetchError;
case TrustedVaultRequest::HttpStatus::
kPrimaryAccountChangeAccessTokenFetchError:
return RecoveryKeyStoreStatus::kPrimaryAccountChangeAccessTokenFetchError;
}
}
void ProcessUpdateVaultResponseResponse(
RecoveryKeyStoreConnection::UpdateRecoveryKeyStoreCallback callback,
TrustedVaultRequest::HttpStatus http_status,
const std::string& response_body) {
std::move(callback).Run(HttpStatusToRecoveryKeyStoreStatus(http_status));
}
// Represents a request to the listVaults RPC endpoint.
class ListRecoveryKeyStoresRequest : public TrustedVaultConnection::Request {
public:
ListRecoveryKeyStoresRequest(
const CoreAccountInfo& account_info,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
std::unique_ptr<TrustedVaultAccessTokenFetcher> access_token_fetcher,
RecoveryKeyStoreConnection::ListRecoveryKeyStoresCallback callback)
: account_id_(account_info.account_id),
url_loader_factory_(std::move(url_loader_factory)),
access_token_fetcher_(std::move(access_token_fetcher)),
callback_(std::move(callback)) {
StartOrContinueRequest();
}
private:
void StartOrContinueRequest(const std::string* next_page_token = nullptr) {
TrustedVaultRequest::RecordFetchStatusCallback record_fetch_status_to_uma =
base::BindRepeating(
&RecordRecoveryKeyStoreURLFetchResponse,
RecoveryKeyStoreURLFetchReasonForUMA::kListRecoveryKeyStores);
GURL request_url(kListVaultsUrl);
request_ = std::make_unique<TrustedVaultRequest>(
SecurityDomainId::kPasskeys, account_id_,
TrustedVaultRequest::HttpMethod::kGet,
next_page_token ? net::AppendQueryParameter(request_url, "page_token",
*next_page_token)
: request_url,
/*serialized_request_proto=*/std::nullopt,
/*max_retry_duration=*/base::Seconds(0), url_loader_factory_,
access_token_fetcher_->Clone(), std::move(record_fetch_status_to_uma));
// Unretained: this object owns `request_`. When `request_` is deleted, so
// is the `SimpleURLLoader` and thus any callback is canceled.
request_->FetchAccessTokenAndSendRequest(
base::BindOnce(&ListRecoveryKeyStoresRequest::ProcessResponse,
base::Unretained(this)));
}
void ProcessResponse(TrustedVaultRequest::HttpStatus http_status,
const std::string& response_body) {
if (http_status != TrustedVaultRequest::HttpStatus::kSuccess) {
FinishWithResultAndMaybeDestroySelf(
base::unexpected(HttpStatusToRecoveryKeyStoreStatus(http_status)));
return;
}
trusted_vault_pb::ListVaultsResponse response;
if (!response.ParseFromString(response_body)) {
FinishWithResultAndMaybeDestroySelf(
base::unexpected(RecoveryKeyStoreStatus::kOtherError));
return;
}
for (const auto& vault : response.vaults()) {
RecoveryKeyStoreEntry entry;
entry.backend_public_key =
ProtoStringToBytes(vault.vault_parameters().backend_public_key());
entry.vault_handle =
ProtoStringToBytes(vault.vault_parameters().vault_handle());
result_.push_back(std::move(entry));
}
if (!response.next_page_token().empty()) {
StartOrContinueRequest(&response.next_page_token());
return;
}
FinishWithResultAndMaybeDestroySelf(std::move(result_));
}
void FinishWithResultAndMaybeDestroySelf(
base::expected<std::vector<RecoveryKeyStoreEntry>, RecoveryKeyStoreStatus>
result) {
std::move(callback_).Run(std::move(result));
}
std::unique_ptr<TrustedVaultRequest> request_;
const CoreAccountId account_id_;
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
std::unique_ptr<TrustedVaultAccessTokenFetcher> access_token_fetcher_;
RecoveryKeyStoreConnection::ListRecoveryKeyStoresCallback callback_;
std::vector<RecoveryKeyStoreEntry> result_;
};
// Represents a request to fetch the recovery key store certificates.
class FetchRecoveryKeyStoreCertificatesRequest
: public TrustedVaultConnection::Request {
public:
// `clock` must outlive this.
FetchRecoveryKeyStoreCertificatesRequest(
base::Clock* clock,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
RecoveryKeyStoreConnection::FetchRecoveryKeyStoreCertificatesCallback
callback)
: clock_(clock),
url_loader_factory_(std::move(url_loader_factory)),
callback_(std::move(callback)) {
// Destroying `this` will also destroy the URL loader, which means these
// callbacks will never complete. Thus, unretained is fine.
file_download_closure_ = base::BarrierClosure(
2, base::BindOnce(
&FetchRecoveryKeyStoreCertificatesRequest::OnDownloadComplete,
base::Unretained(this)));
cert_xml_loader_ =
Fetch(kRecoveryKeyStoreCertFileUrl,
base::BindOnce(
&FetchRecoveryKeyStoreCertificatesRequest::OnFileReceived,
base::Unretained(this), &cert_xml_));
sig_xml_loader_ =
Fetch(kRecoveryKeyStoreSigFileUrl,
base::BindOnce(
&FetchRecoveryKeyStoreCertificatesRequest::OnFileReceived,
base::Unretained(this), &sig_xml_));
}
private:
// Fetches `url` and runs `callback` when done.
std::unique_ptr<network::SimpleURLLoader> Fetch(
const std::string_view url,
base::OnceCallback<void(std::optional<std::string>)> callback) {
auto network_request = std::make_unique<network::ResourceRequest>();
GURL request_url(url);
CHECK(request_url.is_valid());
network_request->url = std::move(request_url);
auto loader = network::SimpleURLLoader::Create(std::move(network_request),
kCertXmlTrafficAnnotation);
loader->SetTimeoutDuration(base::Seconds(10));
loader->SetURLLoaderFactoryOptions(
network::mojom::kURLLoadOptionBlockAllCookies);
loader->DownloadToString(url_loader_factory_.get(), std::move(callback),
kMaxRecoveryKeyStoreCertFetchBodyBytes);
return loader;
}
// Called after a fetch request finishes.
void OnFileReceived(std::string* destination,
std::optional<std::string> file) {
if (!file) {
FinishRequestAndMaybeDestroySelf(base::unexpected(
RecoveryKeyStoreCertificateFetchStatus::kNetworkError));
return;
}
*destination = std::move(*file);
file_download_closure_.Run();
}
// Called after both `sig_xml_` and `cert_xml_` have been downloaded.
void OnDownloadComplete() {
std::optional<RecoveryKeyStoreCertificate> certificate =
RecoveryKeyStoreCertificate::Parse(cert_xml_, sig_xml_, clock_->Now());
if (certificate) {
FinishRequestAndMaybeDestroySelf(std::move(*certificate));
return;
}
FinishRequestAndMaybeDestroySelf(
base::unexpected(RecoveryKeyStoreCertificateFetchStatus::kParseError));
}
void FinishRequestAndMaybeDestroySelf(
base::expected<RecoveryKeyStoreCertificate,
RecoveryKeyStoreCertificateFetchStatus> result) {
RecoveryKeyStoreCertificatesFetchStatusForUMA status =
RecoveryKeyStoreCertificatesFetchStatusForUMA::kSuccess;
if (!result.has_value()) {
switch (result.error()) {
case RecoveryKeyStoreCertificateFetchStatus::kNetworkError:
status = RecoveryKeyStoreCertificatesFetchStatusForUMA::kNetworkError;
break;
case RecoveryKeyStoreCertificateFetchStatus::kParseError:
status = RecoveryKeyStoreCertificatesFetchStatusForUMA::kParseError;
break;
case RecoveryKeyStoreCertificateFetchStatus::kSuccess:
NOTREACHED();
}
}
RecordRecoveryKeyStoreFetchCertificatesStatus(status);
std::move(callback_).Run(std::move(result));
}
raw_ptr<base::Clock> clock_;
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
RecoveryKeyStoreConnection::FetchRecoveryKeyStoreCertificatesCallback
callback_;
base::RepeatingClosure file_download_closure_;
std::unique_ptr<network::SimpleURLLoader> cert_xml_loader_;
std::unique_ptr<network::SimpleURLLoader> sig_xml_loader_;
std::string cert_xml_;
std::string sig_xml_;
};
} // namespace
RecoveryKeyStoreEntry::RecoveryKeyStoreEntry() = default;
RecoveryKeyStoreEntry::RecoveryKeyStoreEntry(RecoveryKeyStoreEntry&&) = default;
RecoveryKeyStoreEntry& RecoveryKeyStoreEntry::operator=(
RecoveryKeyStoreEntry&&) = default;
RecoveryKeyStoreEntry::~RecoveryKeyStoreEntry() = default;
bool RecoveryKeyStoreEntry::operator==(const RecoveryKeyStoreEntry&) const =
default;
RecoveryKeyStoreConnectionImpl::RecoveryKeyStoreConnectionImpl(
std::unique_ptr<network::PendingSharedURLLoaderFactory>
pending_url_loader_factory,
std::unique_ptr<TrustedVaultAccessTokenFetcher> access_token_fetcher)
: pending_url_loader_factory_(std::move(pending_url_loader_factory)),
access_token_fetcher_(std::move(access_token_fetcher)) {}
RecoveryKeyStoreConnectionImpl::~RecoveryKeyStoreConnectionImpl() = default;
std::unique_ptr<RecoveryKeyStoreConnectionImpl::Request>
RecoveryKeyStoreConnectionImpl::UpdateRecoveryKeyStore(
const CoreAccountInfo& account_info,
const trusted_vault_pb::Vault& vault,
UpdateRecoveryKeyStoreCallback callback) {
TrustedVaultRequest::RecordFetchStatusCallback record_fetch_status_to_uma =
base::BindRepeating(
&RecordRecoveryKeyStoreURLFetchResponse,
RecoveryKeyStoreURLFetchReasonForUMA::kUpdateRecoveryKeyStore);
auto request = std::make_unique<TrustedVaultRequest>(
SecurityDomainId::kPasskeys, account_info.account_id,
TrustedVaultRequest::HttpMethod::kPatch, GURL(kUpdateVaultUrl),
vault.SerializeAsString(),
/*max_retry_duration=*/base::Seconds(0), URLLoaderFactory(),
access_token_fetcher_->Clone(), std::move(record_fetch_status_to_uma));
request->FetchAccessTokenAndSendRequest(
base::BindOnce(&ProcessUpdateVaultResponseResponse, std::move(callback)));
return request;
}
std::unique_ptr<RecoveryKeyStoreConnectionImpl::Request>
RecoveryKeyStoreConnectionImpl::ListRecoveryKeyStores(
const CoreAccountInfo& account_info,
ListRecoveryKeyStoresCallback callback) {
return std::make_unique<ListRecoveryKeyStoresRequest>(
account_info, URLLoaderFactory(), access_token_fetcher_->Clone(),
std::move(callback));
}
std::unique_ptr<RecoveryKeyStoreConnectionImpl::Request>
RecoveryKeyStoreConnectionImpl::FetchRecoveryKeyStoreCertificates(
base::Clock* clock,
FetchRecoveryKeyStoreCertificatesCallback callback) {
return std::make_unique<FetchRecoveryKeyStoreCertificatesRequest>(
clock, URLLoaderFactory(), std::move(callback));
}
scoped_refptr<network::SharedURLLoaderFactory>
RecoveryKeyStoreConnectionImpl::URLLoaderFactory() {
// `url_loader_factory_` is created lazily, because it needs to be done on
// the backend sequence, while this class ctor is called on UI thread.
if (!url_loader_factory_) {
CHECK(pending_url_loader_factory_);
url_loader_factory_ = network::SharedURLLoaderFactory::Create(
std::move(pending_url_loader_factory_));
}
return url_loader_factory_;
}
} // namespace trusted_vault