blob: 619727f1177683111f43a698ce1b16b6ff9dcc08 [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 <algorithm>
#include <map>
#include <numeric>
#include <vector>
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include "device/fido/fido_types.h"
using Mechanism = AuthenticatorRequestDialogModel::Mechanism;
namespace {
// Enum to represent the mechanism types for internal logic.
enum class SortableMechanismType {
kEnclavePasskey,
kPlatformPasskey,
kPassword,
kOther,
};
// Helper to determine the SortableMechanismType from a Mechanism.
SortableMechanismType GetSortableMechanismType(const Mechanism& mechanism) {
if (const auto* cred_variant =
std::get_if<Mechanism::Credential>(&mechanism.type)) {
if (cred_variant->value().source == device::AuthenticatorType::kEnclave) {
return SortableMechanismType::kEnclavePasskey;
}
// Other AuthenticatorTypes like kTouchID, kWinNative, kICloudKeychain,
// kPhone are considered Platform Passkeys for the logic.
return SortableMechanismType::kPlatformPasskey;
}
if (std::holds_alternative<Mechanism::Password>(mechanism.type)) {
return SortableMechanismType::kPassword;
}
return SortableMechanismType::kOther;
}
// Helper to get an effective timestamp for sorting and comparison.
base::Time GetEffectiveTimestamp(const Mechanism& mechanism) {
if (const auto* password_info =
std::get_if<Mechanism::Password>(&mechanism.type)) {
return password_info->value().last_used_time.value_or(base::Time::Min());
}
if (const auto* cred_variant =
std::get_if<Mechanism::Credential>(&mechanism.type)) {
return cred_variant->value().last_used_time.value_or(base::Time::Min());
}
return base::Time::Min();
}
// Helper function to deduplicate mechanisms for each account.
// Goes through all the accounts and finds the best password, enclave passkey
// and the passkey for each account. Then selects the best credential for the
// account based on the following rules:
// Rule 1: Enclave Passkey vs Platform Passkey -> Enclave Passkey.
// Rule 2: Enclave Passkey vs Password -> Most recent.
// Rule 3: Password vs Platform Passkey -> Platform Passkey.
std::vector<Mechanism> DeduplicateMechanismsByAccount(
const std::map<std::u16string, std::vector<const Mechanism*>>&
grouped_by_account) {
std::vector<Mechanism> deduplicated_mechanisms;
for (auto const& [account_name, account_mechanisms] : grouped_by_account) {
const Mechanism* selected_mechanism = nullptr;
const Mechanism* best_enclave_passkey = nullptr;
base::Time best_enclave_passkey_timestamp = base::Time::Min();
const Mechanism* best_platform_passkey = nullptr;
base::Time best_platform_passkey_timestamp = base::Time::Min();
const Mechanism* best_password = nullptr;
base::Time best_password_timestamp = base::Time::Min();
const Mechanism* best_unknown = nullptr;
for (const auto* mechanism : account_mechanisms) {
SortableMechanismType type = GetSortableMechanismType(*mechanism);
switch (type) {
case SortableMechanismType::kEnclavePasskey: {
base::Time current_enclave_passkey_timestamp =
GetEffectiveTimestamp(*mechanism);
if (best_enclave_passkey == nullptr ||
current_enclave_passkey_timestamp >
best_enclave_passkey_timestamp) {
best_enclave_passkey = mechanism;
best_enclave_passkey_timestamp = current_enclave_passkey_timestamp;
}
break;
}
case SortableMechanismType::kPlatformPasskey: {
base::Time current_platform_passkey_timestamp =
GetEffectiveTimestamp(*mechanism);
if (best_platform_passkey == nullptr ||
current_platform_passkey_timestamp >
best_platform_passkey_timestamp) {
best_platform_passkey = mechanism;
best_platform_passkey_timestamp =
current_platform_passkey_timestamp;
}
break;
}
case SortableMechanismType::kPassword: {
base::Time current_password_timestamp =
GetEffectiveTimestamp(*mechanism);
if (best_password == nullptr ||
best_password_timestamp < current_password_timestamp) {
best_password = mechanism;
best_password_timestamp = current_password_timestamp;
}
break;
}
case SortableMechanismType::kOther: {
best_unknown = mechanism;
break;
}
}
}
if (best_enclave_passkey) {
// Enclave Passkey is present.
if (best_password) {
// Enclave Passkey vs Password: Most recent.
if (best_password_timestamp > best_enclave_passkey_timestamp) {
selected_mechanism = best_password;
} else {
selected_mechanism = best_enclave_passkey;
}
} else {
// Only Enclave passkey (and possibly platform passkey, but enclave
// passkey wins by Rule 1).
selected_mechanism = best_enclave_passkey;
}
} else if (best_platform_passkey) {
// No Enclave passkey. If platform passkey exists, it wins over password
// by Rule 3.
selected_mechanism = best_platform_passkey;
} else if (best_password) {
// Only password left.
selected_mechanism = best_password;
} else {
// No passkey or password.
selected_mechanism = best_unknown;
}
if (selected_mechanism) {
if (account_mechanisms.size() > 1) {
WebAuthnMechanismDeduplicatedType type_to_log;
switch (GetSortableMechanismType(*selected_mechanism)) {
case SortableMechanismType::kEnclavePasskey:
type_to_log = WebAuthnMechanismDeduplicatedType::kEnclavePasskey;
break;
case SortableMechanismType::kPlatformPasskey:
type_to_log = WebAuthnMechanismDeduplicatedType::kPlatformPasskey;
break;
case SortableMechanismType::kPassword:
type_to_log = WebAuthnMechanismDeduplicatedType::kPassword;
break;
case SortableMechanismType::kOther:
type_to_log = WebAuthnMechanismDeduplicatedType::kOther;
break;
}
base::UmaHistogramEnumeration(
"WebAuthentication.MechanismSorter.SelectedMechanismType",
type_to_log);
}
deduplicated_mechanisms.emplace_back(
std::move(const_cast<Mechanism&>(*selected_mechanism)));
}
}
return deduplicated_mechanisms;
}
// Helper function to sort the deduplicated mechanisms.
std::vector<Mechanism> SortMechanisms(
std::vector<Mechanism> deduplicated_mechanisms) {
if (deduplicated_mechanisms.empty()) {
return deduplicated_mechanisms;
}
std::vector<size_t> indices(deduplicated_mechanisms.size());
std::iota(indices.begin(), indices.end(), 0);
std::sort(indices.begin(), indices.end(),
[&deduplicated_mechanisms](size_t a_idx, size_t b_idx) {
const auto& mechanism_a = deduplicated_mechanisms[a_idx];
const auto& mechanism_b = deduplicated_mechanisms[b_idx];
base::Time ts_a = GetEffectiveTimestamp(mechanism_a);
base::Time ts_b = GetEffectiveTimestamp(mechanism_b);
// Primary sort: Most recent (descending timestamp).
if (ts_a != ts_b) {
return ts_a > ts_b;
}
// Secondary sort: Alphabetical by name if all else is equal.
return mechanism_a.name < mechanism_b.name;
});
std::vector<Mechanism> actually_sorted_mechanisms;
actually_sorted_mechanisms.reserve(deduplicated_mechanisms.size());
for (size_t index : indices) {
// Move from the input `deduplicated_mechanisms` vector which is a copy.
actually_sorted_mechanisms.push_back(
std::move(deduplicated_mechanisms[index]));
}
return actually_sorted_mechanisms;
}
} // namespace
std::vector<Mechanism> MechanismSorter::ProcessMechanisms(
std::vector<Mechanism> mechanisms, /* mechanisms is a copy */
UIPresentation ui_presentation) {
if (ui_presentation != UIPresentation::kModalImmediate) {
// For non-immediate presentations, return mechanisms as is.
// Further default sorting could be added here if needed for other modes.
return mechanisms;
}
// 1. Group mechanisms by account name (Mechanism.name).
// Pointers in grouped_by_account point to elements in the `mechanisms`
// copy.
std::map<std::u16string, std::vector<const Mechanism*>> grouped_by_account;
for (const auto& mechanism : mechanisms) {
grouped_by_account[mechanism.name].push_back(&mechanism);
}
// 2. Deduplicate mechanisms.
// Pass the `mechanisms` copy to allow moving from it.
std::vector<Mechanism> deduplicated_mechanisms =
DeduplicateMechanismsByAccount(grouped_by_account);
base::UmaHistogramBoolean(
"WebAuthentication.MechanismSorter.DeduplicationHappened",
mechanisms.size() > deduplicated_mechanisms.size());
// 3. Sort the deduplicated mechanisms.
// `deduplicated_mechanisms` is already a new vector, pass by value.
std::vector<Mechanism> sorted_mechanisms =
SortMechanisms(std::move(deduplicated_mechanisms));
return sorted_mechanisms;
}