0

[webauthn] Synced GPM passkeys on conditional UI

Display synced GPM passkeys on webauthn's conditional UI. Credentials
are shown for users who have opted in to syncing them. Credentials may
also show up on the platform authenticator credential picker. This is
okay as we'll update that UI surface before launching the feature.

For now, credentials are rendered with the subtext "Use lock screen". A
follow-up will change this to display the device name.

Tapping a GPM passkey dispatches to a sync paired phone. At the moment,
this is the first applicable phone on the list. A follow-up will have
Chrome remember the last used phone instead.

Finally, with the feature enabled IsConditionalMediationAvailable will
always return true on all desktop platforms since they are all capable
of showing GPM passkeys.

This feature is guarded by the WebAuthenticationListSyncedPasskeys flag
and requires SyncWebauthnCredentials to be enabled as well.

Bug: 1428655
Change-Id: I7297935f905f443eae4b4b0621128c2bc47f0966
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4621635
Reviewed-by: Martin Kreichgauer <martinkr@google.com>
Commit-Queue: Nina Satragno <nsatragno@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1160857}
This commit is contained in:
Nina Satragno
2023-06-21 21:07:16 +00:00
committed by Chromium LUCI CQ
parent bdc63cb93b
commit 775ab71c48
15 changed files with 410 additions and 74 deletions

@ -134,8 +134,9 @@ class AuthenticatorDialogViewTest : public DialogBrowserTest {
AuthenticatorTransport::kHybrid};
std::array<uint8_t, device::kP256X962Length> public_key = {0};
AuthenticatorRequestDialogModel::PairedPhone phone("Phone", 0,
public_key);
AuthenticatorRequestDialogModel::PairedPhone phone(
AuthenticatorRequestDialogModel::PairedPhone::PairingSource::kQR,
"Phone", 0, public_key);
dialog_model_->set_cable_transport_info(
/*extension_is_v2=*/absl::nullopt,
/*paired_phones=*/{phone},

@ -61,6 +61,7 @@ class AuthenticatorDialogTest : public DialogBrowserTest {
};
AuthenticatorRequestDialogModel::PairedPhone phone(
AuthenticatorRequestDialogModel::PairedPhone::PairingSource::kQR,
"Elisa's Pixel 6 Pro", 0,
std::array<uint8_t, device::kP256X962Length>{0});

@ -38,6 +38,7 @@
#include "device/fido/fido_types.h"
#include "device/fido/pin.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_elider.h"
@ -180,9 +181,11 @@ AuthenticatorRequestDialogModel::Mechanism::Mechanism(Mechanism&&) = default;
AuthenticatorRequestDialogModel::PairedPhone::PairedPhone(const PairedPhone&) =
default;
AuthenticatorRequestDialogModel::PairedPhone::PairedPhone(
PairingSource pairing_source,
const std::string& name,
size_t contact_id,
const std::array<uint8_t, device::kP256X962Length> public_key_x962) {
this->pairing_source = pairing_source;
this->name = name;
this->contact_id = contact_id;
this->public_key_x962 = public_key_x962;
@ -403,21 +406,18 @@ void AuthenticatorRequestDialogModel::StartPlatformAuthenticatorFlow() {
// If the platform authenticator reports known credentials, show them in the
// UI.
if (!transport_availability_.recognized_platform_authenticator_credentials
.empty()) {
if (!transport_availability_.recognized_credentials.empty()) {
if (transport_availability_.has_empty_allow_list) {
// For discoverable credential requests, show an account picker.
ephemeral_state_.creds_ =
transport_availability_
.recognized_platform_authenticator_credentials;
transport_availability_.recognized_credentials;
SetCurrentStep(ephemeral_state_.creds_.size() == 1
? Step::kPreSelectSingleAccount
: Step::kPreSelectAccount);
} else {
// For requests with an allow list, pre-select a random credential.
ephemeral_state_.creds_ = {
transport_availability_
.recognized_platform_authenticator_credentials.front()};
transport_availability_.recognized_credentials.front()};
#if BUILDFLAG(IS_MAC)
if (base::FeatureList::IsEnabled(
device::kWebAuthnSkipSingleAccountMacOS) &&
@ -767,7 +767,11 @@ void AuthenticatorRequestDialogModel::OnAccountPreselectedIndex(size_t index) {
DCHECK(account_preselected_callback_);
account_preselected_callback_.Run(cred.cred_id);
ephemeral_state_.creds_.clear();
HideDialogAndDispatchToPlatformAuthenticator(source);
if (source == device::AuthenticatorType::kPhone) {
ContactPrioritySyncedPhone();
} else {
HideDialogAndDispatchToPlatformAuthenticator(source);
}
}
void AuthenticatorRequestDialogModel::SetSelectedAuthenticatorForTesting(
@ -1028,6 +1032,19 @@ void AuthenticatorRequestDialogModel::StartICloudKeychain(
device::AuthenticatorType::kICloudKeychain);
}
void AuthenticatorRequestDialogModel::ContactPrioritySyncedPhone() {
PairedPhone phone = *GetPrioritySyncedPhone();
const auto phone_mechanism_it =
base::ranges::find_if(mechanisms_, [&phone](const auto& mechanism) {
return absl::holds_alternative<Mechanism::Phone>(mechanism.type) &&
absl::get<Mechanism::Phone>(mechanism.type).value() ==
phone.name;
});
CHECK(phone_mechanism_it != mechanisms_.end());
ContactPhone(phone.name,
std::distance(mechanisms_.begin(), phone_mechanism_it));
}
void AuthenticatorRequestDialogModel::ContactPhone(const std::string& name,
size_t mechanism_index) {
current_mechanism_ = mechanism_index;
@ -1081,8 +1098,7 @@ void AuthenticatorRequestDialogModel::ContactPhoneAfterBleIsPowered(
}
void AuthenticatorRequestDialogModel::StartConditionalMediationRequest() {
ephemeral_state_.creds_ =
transport_availability_.recognized_platform_authenticator_credentials;
ephemeral_state_.creds_ = transport_availability_.recognized_credentials;
auto* render_frame_host = content::RenderFrameHost::FromID(frame_host_id_);
auto* web_contents = GetWebContents();
@ -1149,6 +1165,17 @@ void AuthenticatorRequestDialogModel::ContactNextPhoneByName(
DCHECK(found_name);
}
absl::optional<AuthenticatorRequestDialogModel::PairedPhone>
AuthenticatorRequestDialogModel::GetPrioritySyncedPhone() {
// TODO(crbug.com/1428655): return the last recently used phone instead.
for (const PairedPhone& phone : paired_phones_) {
if (phone.pairing_source == PairedPhone::PairingSource::kSyncDeviceInfo) {
return phone;
}
}
return absl::nullopt;
}
void AuthenticatorRequestDialogModel::PopulateMechanisms() {
const bool is_get_assertion = transport_availability_.request_type ==
device::FidoRequestType::kGetAssertion;

@ -213,9 +213,16 @@ class AuthenticatorRequestDialogModel {
// PairedPhone represents a paired caBLEv2 device.
struct PairedPhone {
// Indicates the source of the pairing information.
enum class PairingSource {
kQR,
kSyncDeviceInfo,
};
PairedPhone() = delete;
PairedPhone(const PairedPhone&);
PairedPhone(
PairingSource paired_source,
const std::string& name,
size_t contact_id,
const std::array<uint8_t, device::kP256X962Length> public_key_x962);
@ -225,6 +232,10 @@ class AuthenticatorRequestDialogModel {
static bool CompareByName(const PairedPhone& a, const PairedPhone& b);
// pairing_source indicates the source of this pairing. Pairings from sync
// device info also sync their passkey metadata to Chrome, so they should be
// the ones dispatched to when preselecting one such credential.
PairingSource pairing_source;
// name is the human-friendly name of the phone. It may be unreasonably
// long, however, and should be elided to fit within UIs.
std::string name;
@ -687,6 +698,10 @@ class AuthenticatorRequestDialogModel {
void StartICloudKeychain(size_t mechanism_index);
// Contacts the "priority" paired phone from sync. At least one sync phone
// must be available to call this.
void ContactPrioritySyncedPhone();
// Contacts a paired phone. The phone is specified by name.
void ContactPhone(const std::string& name, size_t mechanism_index);
void ContactPhoneAfterOffTheRecordInterstitial(std::string name);
@ -698,6 +713,10 @@ class AuthenticatorRequestDialogModel {
void ContactNextPhoneByName(const std::string& name);
// Returns a phone that has been paired through Chrome Sync, or absl::nullopt
// if there isn't one.
absl::optional<PairedPhone> GetPrioritySyncedPhone();
// PopulateMechanisms fills in |mechanisms_|.
void PopulateMechanisms();

@ -575,13 +575,11 @@ TEST_F(AuthenticatorRequestDialogModelTest, Mechanisms) {
if (base::Contains(test.params,
TransportAvailabilityParam::kOneRecognizedCred)) {
transports_info.recognized_platform_authenticator_credentials = {
kCred1};
transports_info.recognized_credentials = {kCred1};
} else if (base::Contains(
test.params,
TransportAvailabilityParam::kTwoRecognizedCreds)) {
transports_info.recognized_platform_authenticator_credentials = {
kCred1, kCred2};
transports_info.recognized_credentials = {kCred1, kCred2};
}
transports_info.has_empty_allow_list = base::Contains(
test.params, TransportAvailabilityParam::kEmptyAllowList);
@ -641,7 +639,10 @@ TEST_F(AuthenticatorRequestDialogModelTest, Mechanisms) {
for (const auto& name : test.phone_names) {
std::array<uint8_t, device::kP256X962Length> public_key = {0};
public_key[0] = base::checked_cast<uint8_t>(phones.size());
phones.emplace_back(name, /*contact_id=*/0, public_key);
phones.emplace_back(
AuthenticatorRequestDialogModel::PairedPhone::PairingSource::kQR,
name,
/*contact_id=*/0, public_key);
}
model.set_cable_transport_info(has_v2_cable_extension,
std::move(phones), base::DoNothing(),
@ -838,7 +839,8 @@ TEST_F(AuthenticatorRequestDialogModelTest, Cable2ndFactorFlows) {
std::array<uint8_t, device::kP256X962Length> public_key = {0};
std::vector<AuthenticatorRequestDialogModel::PairedPhone> phones(
{{"phone", /*contact_id=*/0, public_key}});
{{AuthenticatorRequestDialogModel::PairedPhone::PairingSource::kQR,
"phone", /*contact_id=*/0, public_key}});
model.set_cable_transport_info(/*extension_is_v2=*/absl::nullopt,
std::move(phones), base::DoNothing(),
absl::nullopt);
@ -1122,8 +1124,7 @@ TEST_F(AuthenticatorRequestDialogModelTest, ConditionalUIRecognizedCredential) {
transports_info.available_transports = kAllTransports;
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
transports_info.recognized_platform_authenticator_credentials = {kCred1,
kCred2};
transports_info.recognized_credentials = {kCred1, kCred2};
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/true);
EXPECT_EQ(model.current_step(), Step::kConditionalMediation);
@ -1165,6 +1166,54 @@ TEST_F(AuthenticatorRequestDialogModelTest, ConditionalUICancelRequest) {
model.RemoveObserver(&mock_observer);
}
// Tests that selecting a phone passkey on Conditional UI contacts the priority
// synced phone.
TEST_F(AuthenticatorRequestDialogModelTest, ConditionalUIPhonePasskey) {
AuthenticatorRequestDialogModel model(/*render_frame_host=*/nullptr);
model.SetAccountPreselectedCallback(base::DoNothing());
// Set up a linked phone and a phone from sync.
const size_t kLinkedPhoneId = 0;
const size_t kSyncedPhoneId = 1;
std::vector<AuthenticatorRequestDialogModel::PairedPhone> phones({
{AuthenticatorRequestDialogModel::PairedPhone::PairingSource::kQR,
"Linked phone", kLinkedPhoneId,
/*public_key_x962=*/{{0}}},
{AuthenticatorRequestDialogModel::PairedPhone::PairingSource::
kSyncDeviceInfo,
"Synced phone", kSyncedPhoneId,
/*public_key_x962=*/{{0}}},
});
// Store the contacted phone id.
absl::optional<size_t> phone_id;
base::RepeatingCallback<void(size_t)> callback =
base::BindLambdaForTesting([&phone_id](size_t value) {
ASSERT_FALSE(phone_id);
phone_id = value;
});
model.set_cable_transport_info(/*extension_is_v2=*/absl::nullopt,
std::move(phones), std::move(callback),
absl::nullopt);
// Set up a single credential from a phone.
device::DiscoverableCredentialMetadata credential = kCred1;
credential.source = device::AuthenticatorType::kPhone;
TransportAvailabilityInfo tai;
tai.recognized_credentials = {credential};
tai.is_ble_powered = true;
tai.request_type = device::FidoRequestType::kGetAssertion;
tai.available_transports = {AuthenticatorTransport::kHybrid};
model.StartFlow(std::move(tai), /*is_conditional_mediation=*/true);
EXPECT_EQ(model.current_step(), Step::kConditionalMediation);
// Preselect the credential.
model.OnAccountPreselected(credential.cred_id);
EXPECT_EQ(model.current_step(), Step::kCableActivate);
EXPECT_EQ(phone_id, kSyncedPhoneId);
}
#if BUILDFLAG(IS_WIN)
// Tests that cancelling the Windows Platform authenticator during a Conditional
// UI request restarts it.
@ -1221,8 +1270,7 @@ TEST_F(AuthenticatorRequestDialogModelTest, PreSelectWithEmptyAllowList) {
transports_info.has_empty_allow_list = true;
transports_info.has_platform_authenticator_credential = device::
FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
transports_info.recognized_platform_authenticator_credentials = {kCred1,
kCred2};
transports_info.recognized_credentials = {kCred1, kCred2};
model.StartFlow(std::move(transports_info),
/*is_conditional_mediation=*/false);
EXPECT_EQ(model.current_step(), Step::kPreSelectAccount);
@ -1239,7 +1287,9 @@ TEST_F(AuthenticatorRequestDialogModelTest, PreSelectWithEmptyAllowList) {
TEST_F(AuthenticatorRequestDialogModelTest, ContactPriorityPhone) {
AuthenticatorRequestDialogModel model(/*render_frame_host=*/nullptr);
std::vector<AuthenticatorRequestDialogModel::PairedPhone> phones(
{{"phone", /*contact_id=*/0, /*public_key_x962=*/{{0}}}});
{{AuthenticatorRequestDialogModel::PairedPhone::PairingSource::kQR,
"phone", /*contact_id=*/0,
/*public_key_x962=*/{{0}}}});
model.set_cable_transport_info(/*extension_is_v2=*/absl::nullopt,
std::move(phones), base::DoNothing(),
absl::nullopt);
@ -1268,7 +1318,9 @@ TEST_F(AuthenticatorRequestDialogModelTest, BluetoothPermissionPrompt) {
AuthenticatorRequestDialogModel model(/*render_frame_host=*/nullptr);
std::vector<AuthenticatorRequestDialogModel::PairedPhone> phones(
{{"phone", /*contact_id=*/0, /*public_key_x962=*/{{0}}}});
{{AuthenticatorRequestDialogModel::PairedPhone::PairingSource::kQR,
"phone", /*contact_id=*/0,
/*public_key_x962=*/{{0}}}});
model.set_cable_transport_info(/*extension_is_v2=*/absl::nullopt,
std::move(phones), base::DoNothing(),
absl::nullopt);

@ -4,6 +4,7 @@
#include "chrome/browser/webauthn/chrome_authenticator_request_delegate.h"
#include <algorithm>
#include <memory>
#include <utility>
@ -35,6 +36,7 @@
#include "chrome/browser/ui/page_action/page_action_icon_type.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include "chrome/browser/webauthn/cablev2_devices.h"
#include "chrome/browser/webauthn/passkey_model_factory.h"
#include "chrome/browser/webauthn/webauthn_pref_names.h"
#include "chrome/browser/webauthn/webauthn_switches.h"
#include "chrome/common/chrome_switches.h"
@ -46,7 +48,10 @@
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/security_state/core/security_state.h"
#include "components/sync/base/features.h"
#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
#include "components/user_prefs/user_prefs.h"
#include "components/webauthn/core/browser/passkey_model.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/device_service.h"
@ -54,9 +59,11 @@
#include "content/public/browser/web_contents.h"
#include "crypto/random.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/features.h"
#include "device/fido/fido_authenticator.h"
#include "device/fido/fido_discovery_factory.h"
#include "device/fido/fido_types.h"
#include "device/fido/public_key_credential_descriptor.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "extensions/common/constants.h"
@ -607,6 +614,7 @@ void ChromeAuthenticatorRequestDelegate::ConfigureCable(
known_devices->synced_devices =
g_observer->GetCablePairingsFromSyncedDevices();
}
can_use_synced_phone_passkeys_ = !known_devices->synced_devices.empty();
paired_phones = cablev2::MergeDevices(std::move(known_devices),
&icu::Locale::getDefault());
@ -621,8 +629,13 @@ void ChromeAuthenticatorRequestDelegate::ConfigureCable(
if (!paired_phones.empty()) {
for (size_t i = 0; i < paired_phones.size(); i++) {
const auto& phone = paired_phones[i];
paired_phone_entries.emplace_back(phone->name, i,
phone->peer_public_key_x962);
paired_phone_entries.emplace_back(
phone->from_sync_deviceinfo
? AuthenticatorRequestDialogModel::PairedPhone::PairingSource::
kSyncDeviceInfo
: AuthenticatorRequestDialogModel::PairedPhone::PairingSource::
kQR,
phone->name, i, phone->peer_public_key_x962);
phone_names_.push_back(phone->name);
phone_public_keys_.push_back(phone->peer_public_key_x962);
}
@ -769,10 +782,15 @@ void ChromeAuthenticatorRequestDelegate::SetUserEntityForMakeCredentialRequest(
void ChromeAuthenticatorRequestDelegate::OnTransportAvailabilityEnumerated(
device::FidoRequestHandlerBase::TransportAvailabilityInfo data) {
if (base::FeatureList::IsEnabled(device::kWebAuthnListSyncedPasskeys) &&
base::FeatureList::IsEnabled(syncer::kSyncWebauthnCredentials) &&
!IsVirtualEnvironmentEnabled() && can_use_synced_phone_passkeys_) {
GetPhoneContactableGpmPasskeysForRpId(dialog_model_->relying_party_id(),
&data.recognized_credentials);
}
if (is_conditional_ && !credential_filter_.empty()) {
std::vector<device::DiscoverableCredentialMetadata> filtered_list;
for (auto& platform_credential :
data.recognized_platform_authenticator_credentials) {
for (auto& platform_credential : data.recognized_credentials) {
for (auto& filter_credential : credential_filter_) {
if (platform_credential.cred_id == filter_credential.id) {
filtered_list.push_back(platform_credential);
@ -780,8 +798,7 @@ void ChromeAuthenticatorRequestDelegate::OnTransportAvailabilityEnumerated(
}
}
}
data.recognized_platform_authenticator_credentials =
std::move(filtered_list);
data.recognized_credentials = std::move(filtered_list);
}
if (g_observer) {
@ -977,3 +994,26 @@ void ChromeAuthenticatorRequestDelegate::OnCableEvent(
dialog_model_->OnCableEvent(event);
}
void ChromeAuthenticatorRequestDelegate::GetPhoneContactableGpmPasskeysForRpId(
const std::string& rp_id,
std::vector<device::DiscoverableCredentialMetadata>* passkeys) {
webauthn::PasskeyModel* passkey_model =
PasskeyModelFactory::GetInstance()->GetForProfile(
Profile::FromBrowserContext(GetBrowserContext()));
CHECK(passkey_model);
for (const sync_pb::WebauthnCredentialSpecifics& passkey :
passkey_model->GetAllPasskeys()) {
if (passkey.rp_id() != dialog_model_->relying_party_id()) {
continue;
}
passkeys->emplace_back(
device::AuthenticatorType::kPhone, passkey.rp_id(),
std::vector<uint8_t>(passkey.credential_id().begin(),
passkey.credential_id().end()),
device::PublicKeyCredentialUserEntity(
std::vector<uint8_t>(passkey.user_id().begin(),
passkey.user_id().end()),
passkey.user_name(), passkey.user_display_name()));
}
}

@ -216,6 +216,7 @@ class ChromeAuthenticatorRequestDelegate
// "leaks" to be reported.
void SetPassEmptyUsbDeviceManagerForTesting(bool value);
private:
FRIEND_TEST_ALL_PREFIXES(ChromeAuthenticatorRequestDelegateTest,
TestTransportPrefType);
FRIEND_TEST_ALL_PREFIXES(ChromeAuthenticatorRequestDelegateTest,
@ -237,6 +238,11 @@ class ChromeAuthenticatorRequestDelegate
void OnInvalidatedCablePairing(size_t failed_contact_index);
void OnCableEvent(device::cablev2::Event event);
// Adds GPM passkeys matching |rp_id| to |passkeys|.
void GetPhoneContactableGpmPasskeysForRpId(
const std::string& rp_id,
std::vector<device::DiscoverableCredentialMetadata>* passkeys);
const content::GlobalRenderFrameHostId render_frame_host_id_;
const std::unique_ptr<AuthenticatorRequestDialogModel> dialog_model_;
base::OnceClosure cancel_callback_;
@ -271,7 +277,10 @@ class ChromeAuthenticatorRequestDelegate
// don't show errors on the desktop too.
bool cable_device_ready_ = false;
private:
// can_use_synced_phone_passkeys_ is true if there is a phone pairing
// available that can service requests for synced GPM passkeys.
bool can_use_synced_phone_passkeys_ = false;
base::WeakPtrFactory<ChromeAuthenticatorRequestDelegate> weak_ptr_factory_{
this};
};

@ -14,17 +14,22 @@
#include "build/build_config.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate_factory.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include "chrome/browser/webauthn/passkey_model_factory.h"
#include "chrome/browser/webauthn/webauthn_pref_names.h"
#include "chrome/browser/webauthn/webauthn_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "components/prefs/pref_service.h"
#include "components/sync/base/features.h"
#include "components/webauthn/core/browser/passkey_model.h"
#include "components/webauthn/core/browser/test_passkey_model.h"
#include "content/public/browser/authenticator_request_client_delegate.h"
#include "content/public/browser/browser_context.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/web_contents_tester.h"
#include "device/fido/cable/cable_discovery_data.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/features.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_discovery_factory.h"
@ -36,6 +41,7 @@
#include "net/ssl/ssl_info.h"
#include "net/test/cert_test_util.h"
#include "net/test/test_data_directory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
@ -55,8 +61,87 @@ namespace {
static constexpr char kRelyingPartyID[] = "example.com";
class Observer : public testing::NiceMock<
ChromeAuthenticatorRequestDelegate::TestObserver> {
public:
MOCK_METHOD(void,
Created,
(ChromeAuthenticatorRequestDelegate * delegate),
(override));
MOCK_METHOD(std::vector<std::unique_ptr<device::cablev2::Pairing>>,
GetCablePairingsFromSyncedDevices,
(),
(override));
MOCK_METHOD(void,
OnTransportAvailabilityEnumerated,
(ChromeAuthenticatorRequestDelegate * delegate,
device::FidoRequestHandlerBase::TransportAvailabilityInfo* tai),
(override));
MOCK_METHOD(void,
UIShown,
(ChromeAuthenticatorRequestDelegate * delegate),
(override));
MOCK_METHOD(void,
CableV2ExtensionSeen,
(base::span<const uint8_t> server_link_data),
(override));
};
class MockCableDiscoveryFactory : public device::FidoDiscoveryFactory {
public:
void set_cable_data(
device::FidoRequestType request_type,
std::vector<device::CableDiscoveryData> data,
const absl::optional<std::array<uint8_t, device::cablev2::kQRKeySize>>&
qr_generator_key,
std::vector<std::unique_ptr<device::cablev2::Pairing>> pairings)
override {
cable_data = std::move(data);
qr_key = qr_generator_key;
v2_pairings = std::move(pairings);
}
void set_android_accessory_params(
mojo::Remote<device::mojom::UsbDeviceManager>,
std::string aoa_request_description) override {
this->aoa_configured = true;
}
std::vector<device::CableDiscoveryData> cable_data;
absl::optional<std::array<uint8_t, device::cablev2::kQRKeySize>> qr_key;
std::vector<std::unique_ptr<device::cablev2::Pairing>> v2_pairings;
bool aoa_configured = false;
};
class ChromeAuthenticatorRequestDelegateTest
: public ChromeRenderViewHostTestHarness {};
: public ChromeRenderViewHostTestHarness {
public:
ChromeAuthenticatorRequestDelegateTest() {
scoped_feature_list_.InitWithFeatures(
{device::kWebAuthnListSyncedPasskeys, syncer::kSyncWebauthnCredentials},
/*disabled_features=*/{});
}
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
PasskeyModelFactory::GetInstance()->SetTestingFactoryAndUse(
profile(),
base::BindRepeating(
[](content::BrowserContext*) -> std::unique_ptr<KeyedService> {
return std::make_unique<webauthn::TestPasskeyModel>();
}));
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(&observer_);
}
void TearDown() override {
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(nullptr);
ChromeRenderViewHostTestHarness::TearDown();
}
protected:
Observer observer_;
base::test::ScopedFeatureList scoped_feature_list_;
};
class TestAuthenticatorModelObserver final
: public AuthenticatorRequestDialogModel::Observer {
@ -156,32 +241,6 @@ TEST_F(ChromeAuthenticatorRequestDelegateTest, IndividualAttestation) {
}
TEST_F(ChromeAuthenticatorRequestDelegateTest, CableConfiguration) {
class DiscoveryFactory : public device::FidoDiscoveryFactory {
public:
void set_cable_data(
device::FidoRequestType request_type,
std::vector<device::CableDiscoveryData> data,
const absl::optional<std::array<uint8_t, device::cablev2::kQRKeySize>>&
qr_generator_key,
std::vector<std::unique_ptr<device::cablev2::Pairing>> pairings)
override {
cable_data = std::move(data);
qr_key = qr_generator_key;
v2_pairings = std::move(pairings);
}
void set_android_accessory_params(
mojo::Remote<device::mojom::UsbDeviceManager>,
std::string aoa_request_description) override {
this->aoa_configured = true;
}
std::vector<device::CableDiscoveryData> cable_data;
absl::optional<std::array<uint8_t, device::cablev2::kQRKeySize>> qr_key;
std::vector<std::unique_ptr<device::cablev2::Pairing>> v2_pairings;
bool aoa_configured = false;
};
const std::array<uint8_t, 16> eid = {1, 2, 3, 4};
const std::array<uint8_t, 32> prekey = {5, 6, 7, 8};
const device::CableDiscoveryData v1_extension(
@ -326,7 +385,7 @@ TEST_F(ChromeAuthenticatorRequestDelegateTest, CableConfiguration) {
SCOPED_TRACE(windows_has_hybrid);
#endif
DiscoveryFactory discovery_factory;
MockCableDiscoveryFactory discovery_factory;
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
delegate.SetRelyingPartyId(/*rp_id=*/"example.com");
delegate.SetPassEmptyUsbDeviceManagerForTesting(true);
@ -463,6 +522,114 @@ TEST_F(ChromeAuthenticatorRequestDelegateTest, VirtualEnvironmentAttestation) {
EXPECT_TRUE(cb.value());
}
// Tests that synced GPM passkeys are injected in the transport availability
// info.
TEST_F(ChromeAuthenticatorRequestDelegateTest, GPMPasskeys) {
GURL url("https://example.com");
content::WebContentsTester::For(web_contents())->NavigateAndCommit(url);
ChromeWebAuthnCredentialsDelegateFactory::CreateForWebContents(
web_contents());
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
delegate.SetPassEmptyUsbDeviceManagerForTesting(true);
delegate.SetRelyingPartyId("example.com");
// Set up a paired phone from sync.
auto phone = std::make_unique<device::cablev2::Pairing>();
phone->name = "Miku's Pixel 7 XL";
phone->contact_id = {1, 2, 3, 4};
phone->id = {5, 6, 7, 8};
phone->from_sync_deviceinfo = true;
std::vector<std::unique_ptr<device::cablev2::Pairing>> phones;
phones.emplace_back(std::move(phone));
EXPECT_CALL(observer_, GetCablePairingsFromSyncedDevices)
.WillOnce(testing::Return(testing::ByMove(std::move(phones))));
MockCableDiscoveryFactory discovery_factory;
delegate.ConfigureCable(
url::Origin::Create(url), device::FidoRequestType::kGetAssertion,
/*resident_key_requirement=*/absl::nullopt,
/*pairings_from_extension=*/std::vector<device::CableDiscoveryData>(),
&discovery_factory);
// Add a synced passkey for example.com and another for othersite.com.
webauthn::PasskeyModel* passkey_model =
PasskeyModelFactory::GetForProfile(profile());
ASSERT_TRUE(passkey_model);
sync_pb::WebauthnCredentialSpecifics passkey;
passkey.set_sync_id(std::string(16, 'a'));
passkey.set_credential_id(std::string(16, 'b'));
passkey.set_rp_id("example.com");
passkey.set_user_id(std::string({5, 6, 7, 8}));
passkey.set_user_name("hmiku");
passkey.set_user_display_name("Hatsune Miku");
sync_pb::WebauthnCredentialSpecifics passkey_other_rp_id = passkey;
passkey_other_rp_id.set_rp_id("othersite.com");
passkey_model->AddNewPasskeyForTesting(std::move(passkey));
passkey_model->AddNewPasskeyForTesting(std::move(passkey_other_rp_id));
AuthenticatorRequestDialogModel::TransportAvailabilityInfo tai;
EXPECT_CALL(observer_, OnTransportAvailabilityEnumerated)
.WillOnce([&tai](const auto* _, const auto* new_tai) {
tai = std::move(*new_tai);
});
delegate.OnTransportAvailabilityEnumerated(tai);
// The GPM passkey for example.com should have been added to the recognized
// credentials list.
ASSERT_EQ(tai.recognized_credentials.size(), 1u);
const device::DiscoverableCredentialMetadata credential =
tai.recognized_credentials.at(0);
EXPECT_EQ(credential.cred_id, std::vector<uint8_t>(16, 'b'));
EXPECT_EQ(credential.rp_id, "example.com");
EXPECT_EQ(credential.source, device::AuthenticatorType::kPhone);
EXPECT_EQ(credential.user.display_name, "Hatsune Miku");
EXPECT_EQ(credential.user.name, "hmiku");
EXPECT_EQ(credential.user.id, std::vector<uint8_t>({5, 6, 7, 8}));
}
// Tests that synced GPM passkeys are not discovered if there are no sync paired
// phones.
TEST_F(ChromeAuthenticatorRequestDelegateTest, GPMPasskeys_NoSyncPairedPhones) {
GURL url("https://example.com");
content::WebContentsTester::For(web_contents())->NavigateAndCommit(url);
ChromeWebAuthnCredentialsDelegateFactory::CreateForWebContents(
web_contents());
ChromeAuthenticatorRequestDelegate delegate(main_rfh());
delegate.SetPassEmptyUsbDeviceManagerForTesting(true);
delegate.SetRelyingPartyId("example.com");
// Return an empty list of synced devices.
EXPECT_CALL(observer_, GetCablePairingsFromSyncedDevices);
MockCableDiscoveryFactory discovery_factory;
delegate.ConfigureCable(
url::Origin::Create(url), device::FidoRequestType::kGetAssertion,
/*resident_key_requirement=*/absl::nullopt,
/*pairings_from_extension=*/std::vector<device::CableDiscoveryData>(),
&discovery_factory);
// Add a synced passkey for example.com.
webauthn::PasskeyModel* passkey_model =
PasskeyModelFactory::GetForProfile(profile());
ASSERT_TRUE(passkey_model);
sync_pb::WebauthnCredentialSpecifics passkey;
passkey.set_sync_id(std::string(16, 'a'));
passkey.set_credential_id(std::string(16, 'b'));
passkey.set_rp_id("example.com");
passkey.set_user_id(std::string({5, 6, 7, 8}));
passkey_model->AddNewPasskeyForTesting(std::move(passkey));
AuthenticatorRequestDialogModel::TransportAvailabilityInfo tai;
EXPECT_CALL(observer_, OnTransportAvailabilityEnumerated)
.WillOnce([&tai](const auto* _, const auto* new_tai) {
tai = std::move(*new_tai);
});
delegate.OnTransportAvailabilityEnumerated(tai);
// The GPM passkey should not be present in the recognized credentials list.
EXPECT_TRUE(tai.recognized_credentials.empty());
}
#if BUILDFLAG(IS_MAC)
std::string TouchIdMetadataSecret(ChromeWebAuthenticationDelegate& delegate,
content::BrowserContext* browser_context) {

@ -6454,6 +6454,7 @@ test("unit_tests") {
"//components/variations/service:service",
"//components/vector_icons",
"//components/version_info:generate_version_info",
"//components/webauthn/core/browser:browser",
"//components/webrtc",
"//content/public/app",
"//content/test:test_support",

@ -1187,6 +1187,13 @@ void AuthenticatorCommonImpl::IsConditionalMediationAvailable(
url::Origin caller_origin,
blink::mojom::Authenticator::IsConditionalMediationAvailableCallback
callback) {
// Passkeys from a phone can always be discovered through conditional
// mediation. To avoid leaking bluetooth or sync status, always advertise the
// feature is available.
if (base::FeatureList::IsEnabled(device::kWebAuthnListSyncedPasskeys)) {
std::move(callback).Run(true);
return;
}
// Conditional mediation is always supported if the virtual environment is
// providing a platform authenticator.
absl::optional<bool> embedder_isuvpaa_override =

@ -1518,6 +1518,14 @@ TEST_F(AuthenticatorImplTest, GetAssertionResponseWithAttestedCredentialData) {
AuthenticatorStatus::NOT_ALLOWED_ERROR);
}
TEST_F(AuthenticatorImplTest, GPMPasskeys_IsConditionalMediationAvailable) {
// Conditional mediation should always be available if gpm passkeys are
// enabled.
base::test::ScopedFeatureList scoped_feature_list{
device::kWebAuthnListSyncedPasskeys};
ASSERT_TRUE(AuthenticatorIsConditionalMediationAvailable());
}
#if BUILDFLAG(IS_WIN)
TEST_F(AuthenticatorImplTest, IsUVPAA) {
virtual_device_factory_->set_discover_win_webauthn_api_authenticator(true);
@ -7179,10 +7187,9 @@ class ResidentKeyTestAuthenticatorRequestDelegate
EXPECT_EQ(info.has_platform_authenticator_credential,
device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential);
EXPECT_TRUE(
base::Contains(info.recognized_platform_authenticator_credentials,
*config_.preselected_credential_id,
&device::DiscoverableCredentialMetadata::cred_id));
EXPECT_TRUE(base::Contains(
info.recognized_credentials, *config_.preselected_credential_id,
&device::DiscoverableCredentialMetadata::cred_id));
std::move(account_preselected_callback_)
.Run(*config_.preselected_credential_id);
request_callback_.Run(*config_.preselected_authenticator_id);
@ -9104,7 +9111,7 @@ TEST_F(ICloudKeychainAuthenticatorImplTest, Discovery) {
tai) {
tai_seen = true;
CHECK_EQ(tai.has_icloud_keychain, feature_enabled);
CHECK_EQ(tai.recognized_platform_authenticator_credentials.size(),
CHECK_EQ(tai.recognized_credentials.size(),
feature_enabled ? 1u : 0u);
CHECK_EQ(tai.has_icloud_keychain_credential,
feature_enabled
@ -9114,9 +9121,7 @@ TEST_F(ICloudKeychainAuthenticatorImplTest, Discovery) {
RecognizedCredential::kNoRecognizedCredential);
if (feature_enabled) {
CHECK_EQ(tai.recognized_platform_authenticator_credentials[0]
.user.name.value(),
"name");
CHECK_EQ(tai.recognized_credentials[0].user.name.value(), "name");
}
});

@ -113,4 +113,9 @@ BASE_FEATURE(kWebAuthnWindowsUIv6,
"WebAuthenticationWindowsUIv6",
base::FEATURE_ENABLED_BY_DEFAULT);
// Not yet enabled by default.
BASE_FEATURE(kWebAuthnListSyncedPasskeys,
"WebAuthenticationListSyncedPasskeys",
base::FEATURE_DISABLED_BY_DEFAULT);
} // namespace device

@ -86,6 +86,10 @@ BASE_DECLARE_FEATURE(kWebAuthnSkipSingleAccountMacOS);
COMPONENT_EXPORT(DEVICE_FIDO)
BASE_DECLARE_FEATURE(kWebAuthnWindowsUIv6);
// List synced GPM passkeys on webauthn credential pickers.
COMPONENT_EXPORT(DEVICE_FIDO)
BASE_DECLARE_FEATURE(kWebAuthnListSyncedPasskeys);
} // namespace device
#endif // DEVICE_FIDO_FEATURES_H_

@ -433,8 +433,7 @@ void FidoRequestHandlerBase::OnHavePlatformCredentialStatus(
}
}
auto& out_creds = transport_availability_info_
.recognized_platform_authenticator_credentials;
auto& out_creds = transport_availability_info_.recognized_credentials;
if (out_creds.empty()) {
out_creds = std::move(creds);
} else if (!creds.empty()) {

@ -93,12 +93,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoRequestHandlerBase
RecognizedCredential has_icloud_keychain_credential =
RecognizedCredential::kNoRecognizedCredential;
// The set of recognized platform credential user entities that can fulfill
// a GetAssertion request. Not all platform authenticators report this, so
// the set might be empty even if
// |has_platform_authenticator_credential| is |kHasRecognizedCredential|.
std::vector<DiscoverableCredentialMetadata>
recognized_platform_authenticator_credentials;
// The set of recognized credential user entities that can fulfill a
// GetAssertion request. Not all authenticators report this, so the set
// might be empty even if |has_platform_authenticator_credential| is
// |kHasRecognizedCredential|.
std::vector<DiscoverableCredentialMetadata> recognized_credentials;
bool is_ble_powered = false;
bool can_power_on_ble_adapter = false;