[go: nahoru, domu]

blob: ff6316a1ada6c7e97fe0fa159ebdd1899d952f3f [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "device/fido/enclave/enclave_websocket_client.h"
#include <limits>
#include "components/device_event_log/device_event_log.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_parsing_utils.h"
#include "net/http/http_request_headers.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
namespace device::enclave {
namespace {
constexpr size_t kMaxIncomingMessageSize = 1 << 20;
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("passkey_enclave_client", R"(
semantics {
sender: "Cloud Enclave Passkey Authenticator Client"
description:
"Chrome can use a cloud-based authenticator running in a trusted "
"execution environment to fulfill WebAuthn getAssertion requests "
"for passkeys synced to Chrome from Google Password Manager. This "
"is used on desktop platforms where there is not a way to safely "
"unwrap the private keys with a lock screen knowledge factor. "
"This traffic creates an encrypted session with the enclave "
"service and carries the request and response over that session."
trigger:
"A web site initiates a WebAuthn request for passkeys on a device "
"that has been enrolled with the cloud authenticator, and there "
"is an available Google Password Manager passkey that can be used "
"to provide the assertion."
user_data {
type: PROFILE_DATA
type: CREDENTIALS
}
data: "This contains an encrypted WebAuthn assertion request as "
"well as an encrypted passkey which can only be unwrapped by the "
"enclave service."
internal {
contacts {
email: "chrome-webauthn@google.com"
}
}
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2023-07-05"
}
policy {
cookies_allowed: NO
setting: "Users can disable this authenticator 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"
}
}
}
})");
} // namespace
EnclaveWebSocketClient::EnclaveWebSocketClient(
const GURL& service_url,
std::string access_token,
raw_ptr<network::mojom::NetworkContext> network_context,
OnResponseCallback on_response)
: state_(State::kInitialized),
service_url_(service_url),
access_token_(std::move(access_token)),
network_context_(network_context),
on_response_(std::move(on_response)),
readable_watcher_(FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::MANUAL) {}
EnclaveWebSocketClient::~EnclaveWebSocketClient() = default;
void EnclaveWebSocketClient::Write(base::span<const uint8_t> data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (state_ == State::kDisconnected ||
data.size() > std::numeric_limits<uint32_t>::max()) {
FIDO_LOG(ERROR) << "Invalid WebSocket write.";
ClosePipe(SocketStatus::kError);
return;
}
if (state_ == State::kInitialized) {
Connect();
}
if (state_ != State::kOpen) {
pending_write_data_ = fido_parsing_utils::Materialize(data);
return;
}
InternalWrite(data);
}
void EnclaveWebSocketClient::Connect() {
// A disconnect handler is used so that the request can be completed in the
// event of an unexpected disconnection from the network service.
auto handshake_remote = handshake_receiver_.BindNewPipeAndPassRemote();
handshake_receiver_.set_disconnect_handler(base::BindOnce(
&EnclaveWebSocketClient::OnMojoPipeDisconnect, base::Unretained(this)));
state_ = State::kConnecting;
std::vector<network::mojom::HttpHeaderPtr> additional_headers;
additional_headers.emplace_back(network::mojom::HttpHeader::New(
net::HttpRequestHeaders::kAuthorization, "Bearer " + access_token_));
network_context_->CreateWebSocket(
service_url_, {}, net::SiteForCookies(), /*has_storage_access=*/false,
net::IsolationInfo(), std::move(additional_headers),
network::mojom::kBrowserProcessId, url::Origin::Create(service_url_),
network::mojom::kWebSocketOptionBlockAllCookies,
net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation),
std::move(handshake_remote),
/*url_loader_network_observer=*/mojo::NullRemote(),
/*auth_handler=*/mojo::NullRemote(),
/*header_client=*/mojo::NullRemote(),
/*throttling_profile_id=*/absl::nullopt);
}
void EnclaveWebSocketClient::InternalWrite(base::span<const uint8_t> data) {
CHECK(state_ == State::kOpen);
websocket_->SendMessage(network::mojom::WebSocketMessageType::BINARY,
data.size());
uint32_t num_bytes = static_cast<uint32_t>(data.size());
MojoResult result = writable_->WriteData(data.data(), &num_bytes,
MOJO_WRITE_DATA_FLAG_ALL_OR_NONE);
CHECK(result != MOJO_RESULT_OK ||
data.size() == static_cast<size_t>(num_bytes));
if (result != MOJO_RESULT_OK) {
FIDO_LOG(ERROR) << "Failed to write to WebSocket.";
ClosePipe(SocketStatus::kError);
}
}
void EnclaveWebSocketClient::OnOpeningHandshakeStarted(
network::mojom::WebSocketHandshakeRequestPtr request) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void EnclaveWebSocketClient::OnFailure(const std::string& message,
int net_error,
int response_code) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
FIDO_LOG(ERROR) << "Enclave service connection failed " << message << ", "
<< net_error << ", " << response_code;
ClosePipe(SocketStatus::kError);
}
void EnclaveWebSocketClient::OnConnectionEstablished(
mojo::PendingRemote<network::mojom::WebSocket> socket,
mojo::PendingReceiver<network::mojom::WebSocketClient> client_receiver,
network::mojom::WebSocketHandshakeResponsePtr response,
mojo::ScopedDataPipeConsumerHandle readable,
mojo::ScopedDataPipeProducerHandle writable) {
CHECK(!websocket_.is_bound());
CHECK(state_ == State::kConnecting);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
websocket_.Bind(std::move(socket));
readable_ = std::move(readable);
CHECK_EQ(readable_watcher_.Watch(
readable_.get(), MOJO_HANDLE_SIGNAL_READABLE,
MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED,
base::BindRepeating(&EnclaveWebSocketClient::ReadFromDataPipe,
base::Unretained(this))),
MOJO_RESULT_OK);
writable_ = std::move(writable);
client_receiver_.Bind(std::move(client_receiver));
// |handshake_receiver_| will disconnect soon. In order to catch network
// process crashes, we switch to watching |client_receiver_|.
handshake_receiver_.set_disconnect_handler(base::DoNothing());
client_receiver_.set_disconnect_handler(base::BindOnce(
&EnclaveWebSocketClient::OnMojoPipeDisconnect, base::Unretained(this)));
websocket_->StartReceiving();
state_ = State::kOpen;
if (pending_write_data_) {
InternalWrite(*pending_write_data_);
pending_write_data_ = absl::nullopt;
}
}
void EnclaveWebSocketClient::OnDataFrame(
bool finish,
network::mojom::WebSocketMessageType type,
uint64_t data_len) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK_EQ(state_, State::kOpen);
CHECK_EQ(pending_read_data_index_, pending_read_data_.size());
CHECK(!pending_read_finished_);
if (data_len == 0) {
if (finish) {
ProcessCompletedResponse();
}
return;
}
const size_t old_size = pending_read_data_index_;
const size_t new_size = old_size + data_len;
if ((type != network::mojom::WebSocketMessageType::BINARY &&
type != network::mojom::WebSocketMessageType::CONTINUATION) ||
data_len > std::numeric_limits<uint32_t>::max() || new_size < old_size ||
new_size > kMaxIncomingMessageSize) {
FIDO_LOG(ERROR) << "Invalid WebSocket frame (type: "
<< static_cast<int>(type) << ", len: " << data_len << ")";
ClosePipe(SocketStatus::kError);
return;
}
pending_read_data_.resize(new_size);
pending_read_finished_ = finish;
client_receiver_.Pause();
ReadFromDataPipe(MOJO_RESULT_OK, mojo::HandleSignalsState());
}
void EnclaveWebSocketClient::OnDropChannel(bool was_clean,
uint16_t code,
const std::string& reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(state_ == State::kOpen || state_ == State::kConnecting);
ClosePipe(SocketStatus::kSocketClosed);
}
void EnclaveWebSocketClient::OnClosingHandshake() {}
void EnclaveWebSocketClient::ReadFromDataPipe(MojoResult,
const mojo::HandleSignalsState&) {
const size_t todo = pending_read_data_.size() - pending_read_data_index_;
CHECK_GT(todo, 0u);
// Truncation to 32-bits cannot overflow because |pending_read_data_.size()|
// is bound by |kMaxIncomingMessageSize| when it is resized in |OnDataFrame|.
uint32_t todo_32 = static_cast<uint32_t>(todo);
static_assert(
kMaxIncomingMessageSize <= std::numeric_limits<decltype(todo_32)>::max(),
"");
const MojoResult result =
readable_->ReadData(&pending_read_data_.data()[pending_read_data_index_],
&todo_32, MOJO_READ_DATA_FLAG_NONE);
if (result == MOJO_RESULT_OK) {
pending_read_data_index_ += todo_32;
DCHECK_LE(pending_read_data_index_, pending_read_data_.size());
if (pending_read_data_index_ < pending_read_data_.size()) {
readable_watcher_.Arm();
} else {
client_receiver_.Resume();
if (pending_read_finished_) {
ProcessCompletedResponse();
}
}
} else if (result == MOJO_RESULT_SHOULD_WAIT) {
readable_watcher_.Arm();
} else {
FIDO_LOG(ERROR) << "Reading WebSocket frame failed: "
<< static_cast<int>(result);
ClosePipe(SocketStatus::kError);
}
}
void EnclaveWebSocketClient::ProcessCompletedResponse() {
on_response_.Run(SocketStatus::kOk, pending_read_data_);
pending_read_data_index_ = 0;
pending_read_finished_ = false;
pending_read_data_.clear();
}
void EnclaveWebSocketClient::ClosePipe(SocketStatus status) {
if (state_ == State::kDisconnected) {
return;
}
state_ = State::kDisconnected;
client_receiver_.reset();
pending_write_data_ = absl::nullopt;
pending_read_data_index_ = 0;
pending_read_finished_ = false;
pending_read_data_.clear();
on_response_.Run(status, std::vector<uint8_t>());
}
void EnclaveWebSocketClient::OnMojoPipeDisconnect() {
ClosePipe(SocketStatus::kSocketClosed);
}
} // namespace device::enclave