blob: fcdaf69ff56b624b231d81a90838042498ff78e7 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/webauthn/mechanism_sorter.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/time/time.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include "device/fido/fido_types.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using Mechanism = AuthenticatorRequestDialogModel::Mechanism;
using CredentialInfo = Mechanism::CredentialInfo;
using PasswordInfo = Mechanism::PasswordInfo;
const auto kUserId = std::vector<uint8_t>{0x01, 0x02, 0x03};
// Helper to create a GPM Passkey mechanism.
Mechanism CreateEnclavePasskey(const std::u16string& user_name,
std::optional<base::Time> last_used_time) {
Mechanism::Credential cred_info(
{device::AuthenticatorType::kEnclave, kUserId, last_used_time});
return Mechanism(std::move(cred_info), user_name, user_name, kSmartphoneIcon,
base::DoNothing());
}
// Helper to create a Platform Passkey mechanism.
Mechanism CreatePlatformPasskey(const std::u16string& user_name,
std::optional<base::Time> last_used_time) {
Mechanism::Credential cred_info(
{device::AuthenticatorType::kICloudKeychain, kUserId, last_used_time});
return Mechanism(std::move(cred_info), user_name, user_name, kSmartphoneIcon,
base::DoNothing());
}
// Helper to create a Password mechanism.
Mechanism CreatePassword(const std::u16string& user_name,
base::Time last_used_time) {
Mechanism::Type password_data =
Mechanism::Password(Mechanism::PasswordInfo(last_used_time));
return Mechanism(std::move(password_data), user_name, user_name,
kSmartphoneIcon, base::DoNothing());
}
class MechanismSorterTest : public ::testing::Test {
public:
MechanismSorterTest() = default;
protected:
void ExpectNoDeduplication() {
histogram_tester_.ExpectUniqueSample(
"WebAuthentication.MechanismSorter.DeduplicationHappened", false, 1);
histogram_tester_.ExpectTotalCount(
"WebAuthentication.MechanismSorter.SelectedMechanismType", 0);
}
void ExpectDeduplicationRecorded(
WebAuthnMechanismDeduplicatedType deduplicated_type) {
histogram_tester_.ExpectUniqueSample(
"WebAuthentication.MechanismSorter.DeduplicationHappened", true, 1);
histogram_tester_.ExpectUniqueSample(
"WebAuthentication.MechanismSorter.SelectedMechanismType",
deduplicated_type, 1);
}
MechanismSorter sorter_;
base::HistogramTester histogram_tester_;
};
// Test that an empty list remains empty.
TEST_F(MechanismSorterTest, EmptyList) {
std::vector<Mechanism> mechanisms;
std::vector<Mechanism> result = sorter_.ProcessMechanisms(
std::move(mechanisms), UIPresentation::kModalImmediate);
EXPECT_TRUE(result.empty());
ExpectNoDeduplication();
}
// Test that a list with one enclave passkey remains unchanged.
TEST_F(MechanismSorterTest, SingleEnclaveMechanism) {
std::vector<Mechanism> mechanisms;
mechanisms.push_back(CreateEnclavePasskey(u"user1", base::Time::Now()));
std::vector<Mechanism> result = sorter_.ProcessMechanisms(
std::move(mechanisms), UIPresentation::kModalImmediate);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result[0].name, u"user1");
ExpectNoDeduplication();
}
// Test that a list with one platform passkey remains unchanged.
TEST_F(MechanismSorterTest, SinglePlatformMechanism) {
std::vector<Mechanism> mechanisms;
mechanisms.push_back(CreatePlatformPasskey(u"user1", std::nullopt));
std::vector<Mechanism> result = sorter_.ProcessMechanisms(
std::move(mechanisms), UIPresentation::kModalImmediate);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result[0].name, u"user1");
ExpectNoDeduplication();
}
// Test that a list with one password remains unchanged.
TEST_F(MechanismSorterTest, SinglePasswordMechanism) {
std::vector<Mechanism> mechanisms;
mechanisms.push_back(CreatePassword(u"user1", base::Time::Now()));
std::vector<Mechanism> result = sorter_.ProcessMechanisms(
std::move(mechanisms), UIPresentation::kModalImmediate);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result[0].name, u"user1");
ExpectNoDeduplication();
}
// Test deduplication: GPM Passkey preferred over Platform Passkey if newer.
TEST_F(MechanismSorterTest, DeduplicateGpmPasskeyVsPlatformPasskey_GpmNewer) {
std::vector<Mechanism> mechanisms;
base::Time time_now = base::Time::Now();
base::Time time_older = time_now - base::Minutes(1);
mechanisms.push_back(CreatePlatformPasskey(u"user1", time_older));
mechanisms.push_back(CreateEnclavePasskey(u"user1", time_now)); // Newer
std::vector<Mechanism> result = sorter_.ProcessMechanisms(
std::move(mechanisms), UIPresentation::kModalImmediate);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(std::get<Mechanism::Credential>(result[0].type).value().source,
device::AuthenticatorType::kEnclave);
ExpectDeduplicationRecorded(
WebAuthnMechanismDeduplicatedType::kEnclavePasskey);
}
// Test deduplication: GPM Passkey preferred over Password if GPM Passkey is
// newer.
TEST_F(MechanismSorterTest, DeduplicateGpmPasskeyVsGpmPassword_PasskeyNewer) {
std::vector<Mechanism> mechanisms;
base::Time time_now = base::Time::Now();
base::Time time_older = time_now - base::Minutes(1);
mechanisms.push_back(CreatePassword(u"user1", time_older));
mechanisms.push_back(CreateEnclavePasskey(u"user1", time_now)); // Newer
std::vector<Mechanism> result = sorter_.ProcessMechanisms(
std::move(mechanisms), UIPresentation::kModalImmediate);
ASSERT_EQ(result.size(), 1u);
EXPECT_TRUE(std::holds_alternative<Mechanism::Credential>(result[0].type));
EXPECT_EQ(std::get<Mechanism::Credential>(result[0].type).value().source,
device::AuthenticatorType::kEnclave);
ExpectDeduplicationRecorded(
WebAuthnMechanismDeduplicatedType::kEnclavePasskey);
}
// Test deduplication: GPM Password preferred over GPM Passkey if Password
// is newer.
TEST_F(MechanismSorterTest, DeduplicateGpmPasskeyVsGpmPassword_PasswordNewer) {
std::vector<Mechanism> mechanisms;
base::Time time_now = base::Time::Now();
base::Time time_older = time_now - base::Minutes(1);
mechanisms.push_back(CreateEnclavePasskey(u"user1", time_older));
mechanisms.push_back(CreatePassword(u"user1", time_now));
std::vector<Mechanism> result = sorter_.ProcessMechanisms(
std::move(mechanisms), UIPresentation::kModalImmediate);
ASSERT_EQ(result.size(), 1u);
EXPECT_TRUE(std::holds_alternative<Mechanism::Password>(result[0].type));
ExpectDeduplicationRecorded(WebAuthnMechanismDeduplicatedType::kPassword);
}
// Test deduplication: Platform Passkey preferred over Password.
TEST_F(MechanismSorterTest, DeduplicatePlatformPasskeyVsGpmPassword) {
std::vector<Mechanism> mechanisms;
base::Time time_now = base::Time::Now();
mechanisms.push_back(CreatePassword(u"user1", time_now));
mechanisms.push_back(CreatePlatformPasskey(u"user1", std::nullopt));
std::vector<Mechanism> result = sorter_.ProcessMechanisms(
std::move(mechanisms), UIPresentation::kModalImmediate);
ASSERT_EQ(result.size(), 1u);
EXPECT_TRUE(std::holds_alternative<Mechanism::Credential>(result[0].type));
EXPECT_NE(std::get<Mechanism::Credential>(result[0].type).value().source,
device::AuthenticatorType::kEnclave);
ExpectDeduplicationRecorded(
WebAuthnMechanismDeduplicatedType::kPlatformPasskey);
}
// Test sorting: Most recent first.
TEST_F(MechanismSorterTest, SortByTimestamp) {
std::vector<Mechanism> mechanisms;
base::Time time_now = base::Time::Now();
base::Time time_older = time_now - base::Minutes(1);
base::Time time_oldest = time_older - base::Minutes(1);
mechanisms.push_back(CreateEnclavePasskey(u"user_c", time_older));
mechanisms.push_back(CreateEnclavePasskey(u"user_a", time_now));
mechanisms.push_back(CreateEnclavePasskey(u"user_b", time_oldest));
std::vector<Mechanism> result = sorter_.ProcessMechanisms(
std::move(mechanisms), UIPresentation::kModalImmediate);
ASSERT_EQ(result.size(), 3u);
EXPECT_EQ(result[0].name, u"user_a");
EXPECT_EQ(result[1].name, u"user_c");
EXPECT_EQ(result[2].name, u"user_b");
ExpectNoDeduplication();
}
// Test sorting: Alphabetical by name if timestamps are equal.
TEST_F(MechanismSorterTest, SortByNameIfTimestampsEqual) {
std::vector<Mechanism> mechanisms;
base::Time same_time = base::Time::Now();
mechanisms.push_back(CreateEnclavePasskey(u"user_c", same_time));
mechanisms.push_back(CreateEnclavePasskey(u"user_a", same_time));
mechanisms.push_back(CreateEnclavePasskey(u"user_b", same_time));
std::vector<Mechanism> result = sorter_.ProcessMechanisms(
std::move(mechanisms), UIPresentation::kModalImmediate);
ASSERT_EQ(result.size(), 3u);
EXPECT_EQ(result[0].name, u"user_a");
EXPECT_EQ(result[1].name, u"user_b");
EXPECT_EQ(result[2].name, u"user_c");
ExpectNoDeduplication();
}
// Test that sorting/deduplication does not happen for non-kModalImmediate UI.
TEST_F(MechanismSorterTest, NoProcessingForOtherUIPresentations) {
std::vector<Mechanism> mechanisms;
base::Time time_now = base::Time::Now();
base::Time time_older = time_now - base::Minutes(1);
// Order is intentionally "wrong" for kModalImmediate
mechanisms.push_back(CreateEnclavePasskey(u"user1", time_older));
mechanisms.push_back(CreatePlatformPasskey(u"user1", std::nullopt));
mechanisms.push_back(CreateEnclavePasskey(u"user2", time_now));
std::vector<Mechanism> result =
sorter_.ProcessMechanisms(std::move(mechanisms), UIPresentation::kModal);
ASSERT_EQ(result.size(), 3u);
// Expect original order and content
EXPECT_EQ(result[0].name, u"user1");
EXPECT_EQ(result[1].name, u"user1");
EXPECT_EQ(result[2].name, u"user2");
histogram_tester_.ExpectTotalCount(
"WebAuthentication.MechanismSorter.DeduplicationHappened", 0);
histogram_tester_.ExpectTotalCount(
"WebAuthentication.MechanismSorter.SelectedMechanismType", 0);
}
// Test multiple deduplications in one call.
TEST_F(MechanismSorterTest, MultipleDeduplications) {
std::vector<Mechanism> mechanisms;
base::Time time_now = base::Time::Now();
base::Time time_older = time_now - base::Minutes(1);
// User 1: platform passkey wins over password.
mechanisms.push_back(CreatePassword(u"user1", time_now));
mechanisms.push_back(CreatePlatformPasskey(u"user1", time_older));
// User 2: newer enclave passkey wins over password.
mechanisms.push_back(CreatePassword(u"user2", time_older));
mechanisms.push_back(CreateEnclavePasskey(u"user2", time_now));
std::vector<Mechanism> result = sorter_.ProcessMechanisms(
std::move(mechanisms), UIPresentation::kModalImmediate);
ASSERT_EQ(result.size(), 2u);
histogram_tester_.ExpectUniqueSample(
"WebAuthentication.MechanismSorter.DeduplicationHappened", true, 1);
histogram_tester_.ExpectTotalCount(
"WebAuthentication.MechanismSorter.SelectedMechanismType", 2);
histogram_tester_.ExpectBucketCount(
"WebAuthentication.MechanismSorter.SelectedMechanismType",
WebAuthnMechanismDeduplicatedType::kPlatformPasskey, 1);
histogram_tester_.ExpectBucketCount(
"WebAuthentication.MechanismSorter.SelectedMechanismType",
WebAuthnMechanismDeduplicatedType::kEnclavePasskey, 1);
}
} // namespace