blob: 48842b6a6284a1c1edd3fff7ba316ddd54441317 [file] [log] [blame]
// Copyright 2020 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/cable/v2_discovery.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "components/device_event_log/device_event_log.h"
#include "device/fido/cable/fido_tunnel_device.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/features.h"
#include "device/fido/fido_parsing_utils.h"
#include "third_party/boringssl/src/include/openssl/aes.h"
namespace device {
namespace cablev2 {
namespace {
// CableV2DiscoveryEvent enumerates several steps that occur while listening for
// BLE adverts. Do not change the assigned values since they are used in
// histograms, only append new values. Keep synced with enums.xml.
enum class CableV2DiscoveryEvent {
kStarted = 0,
kHavePairings = 1,
kHaveQRKeys = 2,
kHaveExtensionKeys = 3,
kTunnelMatch = 4,
kQRMatch = 5,
kExtensionMatch = 6,
kNoMatch = 7,
kMaxValue = 7,
};
void RecordEvent(CableV2DiscoveryEvent event) {
base::UmaHistogramEnumeration("WebAuthentication.CableV2.DiscoveryEvent",
event);
}
} // namespace
Discovery::Discovery(
FidoRequestType request_type,
network::mojom::NetworkContext* network_context,
absl::optional<base::span<const uint8_t, kQRKeySize>> qr_generator_key,
std::unique_ptr<AdvertEventStream> advert_stream,
std::vector<std::unique_ptr<Pairing>> pairings,
std::unique_ptr<EventStream<size_t>> contact_device_stream,
const std::vector<CableDiscoveryData>& extension_contents,
absl::optional<base::RepeatingCallback<void(std::unique_ptr<Pairing>)>>
pairing_callback,
absl::optional<base::RepeatingCallback<void(size_t)>>
invalidated_pairing_callback,
absl::optional<base::RepeatingCallback<void(Event)>> event_callback)
: FidoDeviceDiscovery(FidoTransportProtocol::kHybrid),
request_type_(request_type),
network_context_(network_context),
qr_keys_(KeysFromQRGeneratorKey(qr_generator_key)),
extension_keys_(KeysFromExtension(extension_contents)),
advert_stream_(std::move(advert_stream)),
pairings_(std::move(pairings)),
contact_device_stream_(std::move(contact_device_stream)),
pairing_callback_(std::move(pairing_callback)),
invalidated_pairing_callback_(std::move(invalidated_pairing_callback)),
event_callback_(std::move(event_callback)) {
static_assert(EXTENT(*qr_generator_key) == kQRSecretSize + kQRSeedSize, "");
advert_stream_->Connect(
base::BindRepeating(&Discovery::OnBLEAdvertSeen, base::Unretained(this)));
DCHECK(pairings_.empty() || contact_device_stream_);
if (contact_device_stream_) {
contact_device_stream_->Connect(base::BindRepeating(
&Discovery::OnContactDevice, base::Unretained(this)));
}
}
Discovery::~Discovery() = default;
void Discovery::StartInternal() {
DCHECK(!started_);
RecordEvent(CableV2DiscoveryEvent::kStarted);
if (!pairings_.empty()) {
RecordEvent(CableV2DiscoveryEvent::kHavePairings);
}
if (qr_keys_) {
RecordEvent(CableV2DiscoveryEvent::kHaveQRKeys);
}
if (!extension_keys_.empty()) {
RecordEvent(CableV2DiscoveryEvent::kHaveExtensionKeys);
}
started_ = true;
NotifyDiscoveryStarted(true);
std::vector<std::array<uint8_t, kAdvertSize>> pending_adverts(
std::move(pending_adverts_));
for (const auto& advert : pending_adverts) {
OnBLEAdvertSeen(advert);
}
}
void Discovery::OnBLEAdvertSeen(base::span<const uint8_t, kAdvertSize> advert) {
const std::array<uint8_t, kAdvertSize> advert_array =
fido_parsing_utils::Materialize<kAdvertSize>(advert);
if (!started_) {
// Server-linked devices may have started advertising already.
pending_adverts_.push_back(advert_array);
return;
}
if (base::FeatureList::IsEnabled(device::kWebAuthnNewHybridUI) &&
device_committed_) {
// A device has already been accepted. Ignore other adverts.
}
if (base::Contains(observed_adverts_, advert_array)) {
return;
}
observed_adverts_.insert(advert_array);
// Check whether the EID satisfies any pending tunnels.
for (std::vector<std::unique_ptr<FidoTunnelDevice>>::iterator i =
tunnels_pending_advert_.begin();
i != tunnels_pending_advert_.end(); i++) {
if (!(*i)->MatchAdvert(advert_array)) {
continue;
}
RecordEvent(CableV2DiscoveryEvent::kTunnelMatch);
FIDO_LOG(DEBUG) << " (" << base::HexEncode(advert)
<< " matches pending tunnel)";
std::unique_ptr<FidoTunnelDevice> device(std::move(*i));
tunnels_pending_advert_.erase(i);
device_committed_ = true;
if (event_callback_) {
event_callback_->Run(Event::kBLEAdvertReceived);
}
AddDevice(std::move(device));
return;
}
if (qr_keys_) {
// Check whether the EID matches a QR code.
absl::optional<CableEidArray> plaintext =
eid::Decrypt(advert_array, qr_keys_->eid_key);
if (plaintext) {
FIDO_LOG(DEBUG) << " (" << base::HexEncode(advert)
<< " matches QR code)";
RecordEvent(CableV2DiscoveryEvent::kQRMatch);
device_committed_ = true;
if (event_callback_) {
event_callback_->Run(Event::kBLEAdvertReceived);
}
AddDevice(std::make_unique<cablev2::FidoTunnelDevice>(
network_context_, pairing_callback_, event_callback_,
qr_keys_->qr_secret, qr_keys_->local_identity_seed, *plaintext));
return;
}
}
// Check whether the EID matches the extension.
for (const auto& extension : extension_keys_) {
absl::optional<CableEidArray> plaintext =
eid::Decrypt(advert_array, extension.eid_key);
if (plaintext) {
FIDO_LOG(DEBUG) << " (" << base::HexEncode(advert)
<< " matches extension)";
RecordEvent(CableV2DiscoveryEvent::kExtensionMatch);
device_committed_ = true;
AddDevice(std::make_unique<cablev2::FidoTunnelDevice>(
network_context_, base::DoNothing(), event_callback_,
extension.qr_secret, extension.local_identity_seed, *plaintext));
return;
}
}
RecordEvent(CableV2DiscoveryEvent::kNoMatch);
FIDO_LOG(DEBUG) << " (" << base::HexEncode(advert) << ": no v2 match)";
}
void Discovery::OnContactDevice(size_t pairing_index) {
DCHECK_LT(pairing_index, pairings_.size());
if (!pairings_[pairing_index]) {
return;
}
tunnels_pending_advert_.emplace_back(std::make_unique<FidoTunnelDevice>(
request_type_, network_context_, std::move(pairings_[pairing_index]),
base::BindOnce(&Discovery::PairingIsInvalid, weak_factory_.GetWeakPtr(),
pairing_index),
event_callback_));
}
void Discovery::PairingIsInvalid(size_t pairing_index) {
if (!invalidated_pairing_callback_) {
return;
}
invalidated_pairing_callback_->Run(pairing_index);
}
// static
absl::optional<Discovery::UnpairedKeys> Discovery::KeysFromQRGeneratorKey(
const absl::optional<base::span<const uint8_t, kQRKeySize>>
qr_generator_key) {
if (!qr_generator_key) {
return absl::nullopt;
}
UnpairedKeys ret;
static_assert(EXTENT(*qr_generator_key) == kQRSeedSize + kQRSecretSize, "");
ret.local_identity_seed = fido_parsing_utils::Materialize(
qr_generator_key->subspan<0, kQRSeedSize>());
ret.qr_secret = fido_parsing_utils::Materialize(
qr_generator_key->subspan<kQRSeedSize, kQRSecretSize>());
ret.eid_key = Derive<EXTENT(ret.eid_key)>(
ret.qr_secret, base::span<const uint8_t>(), DerivedValueType::kEIDKey);
return ret;
}
// static
std::vector<Discovery::UnpairedKeys> Discovery::KeysFromExtension(
const std::vector<CableDiscoveryData>& extension_contents) {
std::vector<Discovery::UnpairedKeys> ret;
for (auto const& data : extension_contents) {
if (data.version != CableDiscoveryData::Version::V2) {
continue;
}
if (data.v2->server_link_data.size() != kQRKeySize) {
FIDO_LOG(ERROR) << "caBLEv2 extension has incorrect length ("
<< data.v2->server_link_data.size() << ")";
continue;
}
absl::optional<Discovery::UnpairedKeys> keys = KeysFromQRGeneratorKey(
base::make_span<kQRKeySize>(data.v2->server_link_data));
if (keys.has_value()) {
ret.emplace_back(std::move(keys.value()));
}
}
return ret;
}
} // namespace cablev2
} // namespace device