blob: e332c9b3679ccc7bda589aa7e7cc53b2cf85d990 [file] [log] [blame]
// Copyright 2021 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/cablev2_devices.h"
#include <algorithm>
#include <memory>
#include <vector>
#include "base/strings/string_number_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "chrome/test/base/testing_profile.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/sync/base/data_type.h"
#include "components/sync/protocol/sync_enums.pb.h"
#include "components/sync_device_info/device_info.h"
#include "content/public/test/browser_task_environment.h"
#include "device/fido/cable/cable_discovery_data.h"
#include "device/fido/cable/v2_constants.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/features.h"
#include "testing/gtest/include/gtest/gtest.h"
using cablev2::KnownDevices;
using cablev2::MergeDevices;
using device::cablev2::Pairing;
namespace {
enum class Channel {
UNKNOWN = 0,
STABLE = 1,
BETA = 2,
DEV = 3,
CANARY = 4,
DEVELOPER = 5,
};
constexpr uint8_t kPubKey1 = 1;
constexpr uint8_t kPubKey2 = 2;
constexpr uint8_t kPubKey3 = 3;
constexpr uint8_t kPubKey4 = 4;
constexpr uint8_t kPubKey5 = 5;
constexpr uint8_t kPubKey6 = 6;
constexpr uint8_t kPubKey7 = 7;
constexpr uint8_t kPubKey8 = 8;
const base::Time kTime9 = base::Time::FromTimeT(9);
const base::Time kTime10 = base::Time::FromTimeT(10);
const base::Time kTime11 = base::Time::FromTimeT(11);
std::unique_ptr<Pairing> SyncedDevice(const char* name,
uint8_t public_key_id,
Channel channel,
base::Time update) {
auto ret = std::make_unique<Pairing>();
ret->name = name;
ret->from_sync_deviceinfo = true;
ret->last_updated = update;
ret->channel_priority = static_cast<int>(channel);
ret->peer_public_key_x962 = {0};
ret->peer_public_key_x962[0] = public_key_id;
return ret;
}
std::unique_ptr<Pairing> LinkedDevice(const char* name, uint8_t public_key_id) {
auto ret = std::make_unique<Pairing>();
ret->name = name;
ret->peer_public_key_x962 = {0};
ret->peer_public_key_x962[0] = public_key_id;
return ret;
}
TEST(CableV2DeviceMerging, SyncOverridesForSameDevice) {
// If there's a record from the same device (i.e. same public key) obtained
// via syncing and linking, syncing takes priority.
auto known_devices = std::make_unique<KnownDevices>();
known_devices->synced_devices.emplace_back(
SyncedDevice("S1", kPubKey1, Channel::STABLE, kTime10));
known_devices->linked_devices.emplace_back(LinkedDevice("L1", kPubKey1));
std::vector<std::unique_ptr<Pairing>> result =
MergeDevices(std::move(known_devices), &icu::Locale::getUS());
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result[0]->name, "S1");
}
TEST(CableV2DeviceMerging, MostRecentSyncFirst) {
// If there are multiple pairings for the same device (i.e. same public key)
// then the most recent sync takes priority.
auto known_devices = std::make_unique<KnownDevices>();
std::vector<std::unique_ptr<Pairing>>& synced = known_devices->synced_devices;
synced.emplace_back(SyncedDevice("S1", kPubKey1, Channel::STABLE, kTime10));
synced.emplace_back(SyncedDevice("S1", kPubKey1, Channel::STABLE, kTime9));
synced.emplace_back(SyncedDevice("S1", kPubKey1, Channel::STABLE, kTime11));
std::vector<std::unique_ptr<Pairing>> result =
MergeDevices(std::move(known_devices), &icu::Locale::getUS());
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result[0]->name, "S1");
EXPECT_EQ(result[0]->last_updated, kTime11);
}
TEST(CableV2DeviceMerging, SyncOrderedByChannel) {
// Multiple Sync records from different installations on the same device are
// ordered by unstable channel first and then by update time. Pairings with
// the same name should be grouped together.
auto known_devices = std::make_unique<KnownDevices>();
std::vector<std::unique_ptr<Pairing>>& synced = known_devices->synced_devices;
synced.emplace_back(SyncedDevice("S1", kPubKey1, Channel::STABLE, kTime10));
synced.emplace_back(SyncedDevice("S1", kPubKey2, Channel::BETA, kTime10));
synced.emplace_back(SyncedDevice("S1", kPubKey3, Channel::DEV, kTime9));
synced.emplace_back(SyncedDevice("S1", kPubKey4, Channel::DEV, kTime10));
synced.emplace_back(SyncedDevice("S2", kPubKey5, Channel::STABLE, kTime10));
synced.emplace_back(SyncedDevice("S1", kPubKey6, Channel::DEV, kTime11));
synced.emplace_back(SyncedDevice("S1", kPubKey7, Channel::CANARY, kTime10));
synced.emplace_back(
SyncedDevice("S1", kPubKey8, Channel::DEVELOPER, kTime10));
std::vector<std::unique_ptr<Pairing>> result =
MergeDevices(std::move(known_devices), &icu::Locale::getUS());
std::vector<std::unique_ptr<Pairing>> expected;
expected.emplace_back(
SyncedDevice("S1", kPubKey1, Channel::DEVELOPER, kTime10));
expected.emplace_back(SyncedDevice("S1", kPubKey1, Channel::CANARY, kTime10));
expected.emplace_back(SyncedDevice("S1", kPubKey1, Channel::DEV, kTime11));
expected.emplace_back(SyncedDevice("S1", kPubKey1, Channel::DEV, kTime10));
expected.emplace_back(SyncedDevice("S1", kPubKey1, Channel::DEV, kTime9));
expected.emplace_back(SyncedDevice("S1", kPubKey1, Channel::BETA, kTime10));
expected.emplace_back(SyncedDevice("S1", kPubKey1, Channel::STABLE, kTime10));
expected.emplace_back(SyncedDevice("S2", kPubKey2, Channel::STABLE, kTime10));
ASSERT_EQ(result.size(), expected.size());
for (size_t i = 0; i < result.size(); i++) {
SCOPED_TRACE(i);
EXPECT_EQ(result[i]->name, expected[i]->name);
EXPECT_EQ(result[i]->channel_priority, expected[i]->channel_priority);
EXPECT_EQ(result[i]->last_updated, expected[i]->last_updated);
}
}
TEST(CableV2DeviceMerging, Alphabetical) {
// Names of different devices should be sorted alphabetically.
auto known_devices = std::make_unique<KnownDevices>();
std::vector<std::unique_ptr<Pairing>>& s = known_devices->synced_devices;
s.emplace_back(SyncedDevice("Charlie", kPubKey1, Channel::STABLE, kTime10));
s.emplace_back(SyncedDevice("Alice", kPubKey2, Channel::STABLE, kTime10));
s.emplace_back(SyncedDevice("Bob", kPubKey3, Channel::STABLE, kTime10));
std::vector<std::unique_ptr<Pairing>>& l = known_devices->linked_devices;
l.emplace_back(LinkedDevice("Denise", kPubKey4));
l.emplace_back(LinkedDevice("alicia", kPubKey5));
std::vector<std::unique_ptr<Pairing>> result =
MergeDevices(std::move(known_devices), &icu::Locale::getUS());
std::vector<std::string> expected = {
"Alice", "alicia", "Bob", "Charlie", "Denise",
};
ASSERT_EQ(result.size(), expected.size());
for (size_t i = 0; i < result.size(); i++) {
SCOPED_TRACE(i);
EXPECT_EQ(result[i]->name, expected[i]);
}
}
class CableV2DevicesProfileTest : public testing::Test {
// A `BrowserTaskEnvironment` needs to be in-scope in order to create a
// `TestingProfile`.
content::BrowserTaskEnvironment task_environment_;
};
TEST_F(CableV2DevicesProfileTest, InitiallyEmpty) {
TestingProfile profile;
std::unique_ptr<KnownDevices> known_devices =
KnownDevices::FromProfile(&profile);
EXPECT_EQ(known_devices->synced_devices.size(), 0u);
EXPECT_EQ(known_devices->linked_devices.size(), 0u);
}
std::unique_ptr<Pairing> PairingWithAllFields() {
auto ret = std::make_unique<Pairing>();
ret->tunnel_server_domain = device::cablev2::tunnelserver::KnownDomainID(1);
ret->contact_id = {1, 2, 3, 4, 5};
ret->id = {6, 7, 8, 9, 10};
ret->secret = {11, 12, 13, 14, 15};
ret->peer_public_key_x962 = {16, 17, 18, 19, 20};
ret->name = "Pairing";
ret->from_new_implementation = true;
return ret;
}
TEST_F(CableV2DevicesProfileTest, StoreAndFetch) {
TestingProfile profile;
cablev2::AddPairing(&profile, PairingWithAllFields());
std::unique_ptr<KnownDevices> known_devices =
KnownDevices::FromProfile(&profile);
EXPECT_EQ(known_devices->synced_devices.size(), 0u);
ASSERT_EQ(known_devices->linked_devices.size(), 1u);
std::unique_ptr<Pairing> expected = PairingWithAllFields();
const Pairing* const found = known_devices->linked_devices[0].get();
EXPECT_EQ(found->tunnel_server_domain, expected->tunnel_server_domain);
EXPECT_EQ(found->contact_id, expected->contact_id);
EXPECT_EQ(found->id, expected->id);
EXPECT_EQ(found->secret, expected->secret);
EXPECT_EQ(found->peer_public_key_x962, expected->peer_public_key_x962);
EXPECT_EQ(found->name, expected->name);
EXPECT_EQ(found->from_sync_deviceinfo, expected->from_sync_deviceinfo);
EXPECT_EQ(found->from_new_implementation, expected->from_new_implementation);
}
TEST_F(CableV2DevicesProfileTest, Delete) {
TestingProfile profile;
cablev2::AddPairing(&profile, PairingWithAllFields());
cablev2::DeletePairingByPublicKey(
profile.GetPrefs(), PairingWithAllFields()->peer_public_key_x962);
std::unique_ptr<KnownDevices> known_devices =
KnownDevices::FromProfile(&profile);
EXPECT_EQ(known_devices->synced_devices.size(), 0u);
EXPECT_EQ(known_devices->linked_devices.size(), 0u);
}
TEST_F(CableV2DevicesProfileTest, NameFiltering) {
// The pairing name is stored as sent by the device, but any newlines are
// removed when reading so that they don't mess up the UI.
TestingProfile profile;
cablev2::AddPairing(&profile,
LinkedDevice("w\nx\x0by\xe2\x80\xa8z", kPubKey1));
std::unique_ptr<KnownDevices> known_devices =
KnownDevices::FromProfile(&profile);
EXPECT_EQ(known_devices->synced_devices.size(), 0u);
ASSERT_EQ(known_devices->linked_devices.size(), 1u);
EXPECT_EQ(known_devices->linked_devices[0]->name, "wxyz");
}
TEST_F(CableV2DevicesProfileTest, NameCollision) {
// Adding a device that has a name equal to one of the existing names should
// cause the name to be changed to something that doesn't collide.
TestingProfile profile;
int pub_key = 10;
for (const char* name : {"collision", "collision (1)", "collision (2)\n"}) {
cablev2::AddPairing(&profile, LinkedDevice(name, pub_key++));
}
cablev2::AddPairing(&profile, LinkedDevice("collision", kPubKey1));
std::unique_ptr<KnownDevices> known_devices =
KnownDevices::FromProfile(&profile);
EXPECT_EQ(known_devices->synced_devices.size(), 0u);
ASSERT_EQ(known_devices->linked_devices.size(), 4u);
std::sort(known_devices->linked_devices.begin(),
known_devices->linked_devices.end(),
[](auto&& a, auto&& b) -> bool { return a->name < b->name; });
EXPECT_EQ(known_devices->linked_devices[3]->name, "collision (3)");
}
TEST_F(CableV2DevicesProfileTest, NameOverride) {
// A pairing from the old implementation can be overridden, by name, by the
// new implementation.
for (const bool old_impl_first : {true, false}) {
TestingProfile profile;
std::unique_ptr<Pairing> old_pairing = LinkedDevice("name", kPubKey1);
std::unique_ptr<Pairing> new_pairing = LinkedDevice("name", kPubKey2);
old_pairing->from_new_implementation = false;
new_pairing->from_new_implementation = true;
if (old_impl_first) {
cablev2::AddPairing(&profile, std::move(old_pairing));
}
cablev2::AddPairing(&profile, std::move(new_pairing));
if (!old_impl_first) {
cablev2::AddPairing(&profile, std::move(old_pairing));
}
std::unique_ptr<KnownDevices> known_devices =
KnownDevices::FromProfile(&profile);
EXPECT_EQ(known_devices->synced_devices.size(), 0u);
if (old_impl_first) {
ASSERT_EQ(known_devices->linked_devices.size(), 1u);
EXPECT_EQ(known_devices->linked_devices[0]->peer_public_key_x962[0],
kPubKey2);
} else {
ASSERT_EQ(known_devices->linked_devices.size(), 2u);
EXPECT_NE(known_devices->linked_devices[0]->name,
known_devices->linked_devices[1]->name);
}
}
}
TEST_F(CableV2DevicesProfileTest, Rename) {
TestingProfile profile;
PrefService* const prefs = profile.GetPrefs();
cablev2::AddPairing(&profile, LinkedDevice("one", kPubKey1));
cablev2::AddPairing(&profile, LinkedDevice("two", kPubKey2));
cablev2::AddPairing(&profile, LinkedDevice("three", kPubKey3));
std::unique_ptr<KnownDevices> known_devices =
KnownDevices::FromProfile(&profile);
std::array<uint8_t, device::kP256X962Length> public_key = {0};
public_key[0] = kPubKey2;
// Since a device with the name "three" already exists, the new name should
// be mapped to "three (1)" to avoid it.
EXPECT_TRUE(cablev2::RenamePairing(prefs, public_key, "three",
known_devices->Names()));
known_devices = KnownDevices::FromProfile(&profile);
EXPECT_EQ(known_devices->synced_devices.size(), 0u);
ASSERT_EQ(known_devices->linked_devices.size(), 3u);
std::sort(known_devices->linked_devices.begin(),
known_devices->linked_devices.end(),
[](auto&& a, auto&& b) -> bool { return a->name < b->name; });
EXPECT_EQ(known_devices->linked_devices[0]->name, "one");
EXPECT_EQ(known_devices->linked_devices[1]->name, "three");
EXPECT_EQ(known_devices->linked_devices[2]->name, "three (1)");
public_key[0] = kPubKey4;
// This shouldn't do anything and should return false because the public key
// is unknown.
EXPECT_FALSE(cablev2::RenamePairing(prefs, public_key, "three",
known_devices->Names()));
}
TEST_F(CableV2DevicesProfileTest, UpdateLinkSameName) {
TestingProfile profile;
cablev2::AddPairing(&profile, LinkedDevice("one", kPubKey1));
// This has the same public key as an existing pairing and thus the existing
// record should be updated. The name should not be remapped because there's
// no collision.
cablev2::AddPairing(&profile, LinkedDevice("one", kPubKey1));
std::unique_ptr<KnownDevices> known_devices =
KnownDevices::FromProfile(&profile);
EXPECT_EQ(known_devices->synced_devices.size(), 0u);
ASSERT_EQ(known_devices->linked_devices.size(), 1u);
EXPECT_EQ(known_devices->linked_devices[0]->name, "one");
}
TEST_F(CableV2DevicesProfileTest, UpdateLinkCollidingName) {
TestingProfile profile;
cablev2::AddPairing(&profile, LinkedDevice("one", kPubKey1));
cablev2::AddPairing(&profile, LinkedDevice("two", kPubKey2));
// This has the same public key as "one", and thus will replace it, but just
// because it's a replacement doesn't mean the name hasn't. In this case the
// new name needs to be remapped.
cablev2::AddPairing(&profile, LinkedDevice("two", kPubKey1));
std::unique_ptr<KnownDevices> known_devices =
KnownDevices::FromProfile(&profile);
EXPECT_EQ(known_devices->synced_devices.size(), 0u);
ASSERT_EQ(known_devices->linked_devices.size(), 2u);
std::sort(known_devices->linked_devices.begin(),
known_devices->linked_devices.end(),
[](auto&& a, auto&& b) -> bool { return a->name < b->name; });
EXPECT_EQ(known_devices->linked_devices[0]->name, "two");
EXPECT_EQ(known_devices->linked_devices[1]->name, "two (1)");
}
TEST_F(CableV2DevicesProfileTest, InvalidTunnelServerDomain) {
TestingProfile profile;
std::unique_ptr<Pairing> pairing = PairingWithAllFields();
pairing->tunnel_server_domain =
device::cablev2::tunnelserver::KnownDomainID(42);
cablev2::AddPairing(&profile, std::move(pairing));
std::unique_ptr<KnownDevices> known_devices =
KnownDevices::FromProfile(&profile);
EXPECT_TRUE(known_devices->linked_devices.empty());
}
TEST_F(CableV2DevicesProfileTest, MissingTunnelServerDomain) {
TestingProfile profile;
std::unique_ptr<Pairing> pairing = PairingWithAllFields();
cablev2::AddPairing(&profile, std::move(pairing));
{
ScopedListPrefUpdate update(profile.GetPrefs(),
"webauthn.cablev2_pairings");
for (base::Value& val : *update) {
val.GetDict().Remove("encoded_tunnel_server");
}
}
std::unique_ptr<KnownDevices> known_devices =
KnownDevices::FromProfile(&profile);
ASSERT_EQ(known_devices->linked_devices.size(), 1u);
EXPECT_EQ(known_devices->linked_devices.at(0)->tunnel_server_domain,
device::cablev2::kTunnelServer);
}
struct TestDeviceInfoConfig {
bool omit_paask_info = false;
uint32_t id = device::cablev2::sync::IDNow();
uint16_t tunnel_server_domain = 0;
};
syncer::DeviceInfo TestDeviceInfo(const TestDeviceInfoConfig& config) {
syncer::DeviceInfo::PhoneAsASecurityKeyInfo paask_info;
paask_info.contact_id = std::vector<uint8_t>({1, 2, 3});
std::ranges::fill(paask_info.peer_public_key_x962, 0);
paask_info.peer_public_key_x962[0] = 1;
std::ranges::fill(paask_info.secret, 0);
paask_info.secret[0] = 2;
paask_info.id = config.id;
paask_info.tunnel_server_domain = config.tunnel_server_domain;
std::optional<syncer::DeviceInfo::PhoneAsASecurityKeyInfo> paask_info_opt;
if (!config.omit_paask_info) {
paask_info_opt = paask_info;
}
return syncer::DeviceInfo(
/*guid=*/"guid",
/*client_name=*/"client_name",
/*chrome_version=*/"chrome_version",
/*sync_user_agent=*/"sync_user_agent",
sync_pb::SyncEnums_DeviceType_TYPE_LINUX,
syncer::DeviceInfo::OsType::kLinux,
syncer::DeviceInfo::FormFactor::kDesktop,
/*signin_scoped_device_id=*/"signin_scoped_device_id",
/*manufacturer_name=*/"manufacturer_name",
/*model_name=*/"model_name",
/*full_hardware_class=*/"full_hardware_class",
/*last_updated_timestamp=*/base::Time::Now(),
/*pulse_interval=*/base::TimeDelta(),
/*send_tab_to_self_receiving_enabled=*/
false,
/*send_tab_to_self_receiving_type=*/
sync_pb::
SyncEnums_SendTabReceivingType_SEND_TAB_RECEIVING_TYPE_CHROME_OR_UNSPECIFIED,
/*sharing_info=*/std::nullopt, paask_info_opt,
/*fcm_registration_token=*/"fcm_token", syncer::DataTypeSet(),
/*floating_workspace_last_signin_timestamp=*/base::Time::Now());
}
TEST(CableV2FromSyncInfoTest, Basic) {
TestDeviceInfoConfig config;
syncer::DeviceInfo info = TestDeviceInfo(config);
std::unique_ptr<device::cablev2::Pairing> pairing =
cablev2::PairingFromSyncedDevice(&info, base::Time::Now());
ASSERT_TRUE(pairing);
EXPECT_TRUE(pairing->from_sync_deviceinfo);
EXPECT_EQ(pairing->name, "client_name");
EXPECT_EQ(base::HexEncode(pairing->contact_id), "010203");
EXPECT_EQ(base::HexEncode(pairing->peer_public_key_x962),
"010000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000");
EXPECT_EQ(base::HexEncode(pairing->secret),
"0200000000000000000000000000000000000000000000000000000000000000");
EXPECT_EQ(pairing->channel_priority, 0);
}
TEST(CableV2FromSyncInfoTest, TooOld) {
TestDeviceInfoConfig config;
config.id -= device::cablev2::kMaxSyncInfoDaysForConsumer + 1;
syncer::DeviceInfo info = TestDeviceInfo(config);
std::unique_ptr<device::cablev2::Pairing> pairing =
cablev2::PairingFromSyncedDevice(&info, base::Time::Now());
EXPECT_FALSE(pairing);
}
TEST(CableV2FromSyncInfoTest, NoInfo) {
TestDeviceInfoConfig config;
config.omit_paask_info = true;
syncer::DeviceInfo info = TestDeviceInfo(config);
std::unique_ptr<device::cablev2::Pairing> pairing =
cablev2::PairingFromSyncedDevice(&info, base::Time::Now());
EXPECT_FALSE(pairing);
}
TEST(CableV2FromSyncInfoTest, UnknownTunnelServer) {
TestDeviceInfoConfig config;
config.tunnel_server_domain = 42;
syncer::DeviceInfo info = TestDeviceInfo(config);
std::unique_ptr<device::cablev2::Pairing> pairing =
cablev2::PairingFromSyncedDevice(&info, base::Time::Now());
EXPECT_FALSE(pairing);
}
} // namespace