blob: dc65bb7003b2225bbf2a40e1469fe42575e048ef [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 "content/browser/digital_credentials/cross_device_request_dispatcher.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/strings/string_number_conversions.h"
#include "base/types/optional_util.h"
#include "base/values.h"
#include "components/device_event_log/device_event_log.h"
#include "device/fido/cable/fido_tunnel_device.h"
#include "device/fido/fido_authenticator.h"
#include "device/fido/fido_discovery_base.h"
#include "device/fido/fido_types.h"
namespace content::digital_credentials::cross_device {
namespace {
RemoteError ErrorStringToRemoteError(const std::string& error_str) {
if (error_str == "USER_CANCELED") {
return RemoteError::kUserCanceled;
} else if (error_str == "DEVICE_ABORTED") {
return RemoteError::kDeviceAborted;
} else if (error_str == "NO_CREDENTIAL") {
return RemoteError::kNoCredential;
}
return RemoteError::kOther;
}
std::string RequestTypeToString(RequestInfo::RequestType type) {
switch (type) {
case RequestInfo::RequestType::kGet:
return "credential.get";
case RequestInfo::RequestType::kCreate:
return "credential.create";
}
}
std::vector<uint8_t> RequestToJSONBytes(RequestInfo request_info) {
base::Value::Dict digital;
digital.Set("digital", std::move(request_info.request));
base::Value::Dict toplevel;
toplevel.Set("origin", request_info.rp_origin.Serialize());
toplevel.Set("requestType", RequestTypeToString(request_info.request_type));
toplevel.Set("request", std::move(digital));
std::optional<std::string> json = base::WriteJson(toplevel);
// WriteJson must not fail in this context.
return std::vector<uint8_t>(json->begin(), json->end());
}
} // namespace
RequestDispatcher::RequestDispatcher(
std::unique_ptr<device::FidoDiscoveryBase> v1_discovery,
std::unique_ptr<device::FidoDiscoveryBase> v2_discovery,
RequestInfo request_info,
CompletionCallback callback)
: v1_discovery_(std::move(v1_discovery)),
v2_discovery_(std::move(v2_discovery)),
request_info_(std::move(request_info)),
callback_(std::move(callback)) {
FIDO_LOG(EVENT) << "Starting digital identity flow";
v1_discovery_->set_observer(this);
v2_discovery_->set_observer(this);
v1_discovery_->Start();
v2_discovery_->Start();
}
RequestDispatcher::~RequestDispatcher() = default;
void RequestDispatcher::AuthenticatorAdded(
device::FidoDiscoveryBase* discovery,
device::FidoAuthenticator* authenticator) {
DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
if (!callback_) {
return;
}
authenticator->InitializeAuthenticator(
base::BindOnce(&RequestDispatcher::OnAuthenticatorReady,
weak_factory_.GetWeakPtr(), authenticator));
}
void RequestDispatcher::OnAuthenticatorReady(
device::FidoAuthenticator* authenticator) {
DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
if (!callback_) {
return;
}
device::cablev2::FidoTunnelDevice* tunnel_device =
authenticator->GetTunnelDevice();
if (!tunnel_device) {
// Presumably all discovered FidoAuthenticators will be of the same type and
// so there's no point in waiting for more.
FIDO_LOG(ERROR) << "Non-tunnel device discovered";
std::move(callback_).Run(
base::unexpected(ProtocolError::kIncompatibleDevice));
return;
}
if (!tunnel_device->features().contains(
device::cablev2::Feature::kDigitialIdentities)) {
FIDO_LOG(ERROR)
<< "Hybrid device doesn't advertise support for digital identities";
std::move(callback_).Run(
base::unexpected(ProtocolError::kIncompatibleDevice));
return;
}
tunnel_device->DeviceTransactJSON(
RequestToJSONBytes(std::move(request_info_)),
base::BindOnce(&RequestDispatcher::OnComplete,
weak_factory_.GetWeakPtr()));
}
void RequestDispatcher::AuthenticatorRemoved(
device::FidoDiscoveryBase* discovery,
device::FidoAuthenticator* authenticator) {}
void RequestDispatcher::OnComplete(
std::optional<std::vector<uint8_t>> response) {
if (!response) {
FIDO_LOG(ERROR) << "No response for digital credential request";
std::move(callback_).Run(base::unexpected(ProtocolError::kTransportError));
return;
}
std::optional<base::Value::Dict> json = base::JSONReader::ReadDict(
std::string_view(reinterpret_cast<const char*>(response->data()),
response->size()),
base::JSON_PARSE_RFC);
if (!json) {
FIDO_LOG(ERROR) << "Invalid JSON response: " << base::HexEncode(*response);
std::move(callback_).Run(base::unexpected(ProtocolError::kInvalidResponse));
return;
}
std::string reserialized;
base::JSONWriter::WriteWithOptions(
*json, base::JsonOptions::OPTIONS_PRETTY_PRINT, &reserialized);
FIDO_LOG(EVENT) << "-> " << reserialized;
const base::Value::Dict* response_dict = json->FindDict("response");
if (!response_dict) {
FIDO_LOG(ERROR) << "no 'response' element in response";
std::move(callback_).Run(base::unexpected(ProtocolError::kInvalidResponse));
return;
}
const base::Value::Dict* digital = response_dict->FindDict("digital");
if (!digital) {
FIDO_LOG(ERROR) << "no 'digital' element in response";
std::move(callback_).Run(base::unexpected(ProtocolError::kInvalidResponse));
return;
}
const base::Value* error = digital->Find("error");
if (error) {
const std::string* error_str = error->GetIfString();
if (!error_str) {
FIDO_LOG(ERROR) << "error is not a string";
std::move(callback_).Run(
base::unexpected(ProtocolError::kInvalidResponse));
return;
}
std::move(callback_).Run(
base::unexpected(ErrorStringToRemoteError(*error_str)));
return;
}
const base::Value* data = digital->Find("data");
if (!data) {
FIDO_LOG(ERROR) << "response missing both 'error' and 'data'";
std::move(callback_).Run(base::unexpected(ProtocolError::kInvalidResponse));
return;
}
// The CTAP protocol standards defines the format of the mobile devices
// response contains a JSON object that has both a protocol and data. Mobile
// devices are being migrated to support the CTAP standards. First, try to
// read the proper format, otherwise, fallback to the legacy format.
if (data->is_dict()) {
const base::Value::Dict& data_dict = data->GetDict();
const base::Value* wallet_data = data_dict.Find("data");
if (wallet_data) {
FIDO_LOG(EVENT) << "Standard format is received from the mobile device.";
std::move(callback_).Run(
Response(DigitalIdentityProvider::DigitalCredential(
base::OptionalFromPtr(data_dict.FindString("protocol")),
wallet_data->Clone())));
return;
}
}
FIDO_LOG(EVENT) << "No proper standard format is received from the mobile "
"device. Fallback to legacy format.";
std::move(callback_).Run(Response(DigitalIdentityProvider::DigitalCredential(
/*protocol=*/std::nullopt, data->Clone())));
}
} // namespace content::digital_credentials::cross_device