// Copyright 2022 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/local_credential_management_win.h"
#include <cstdint>
#include <optional>
#include <vector>
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "chrome/test/base/testing_profile.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_task_environment.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/win/fake_webauthn_api.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
constexpr char kHasPlatformCredentialsPref[] =
"webauthn.has_platform_credentials";
constexpr char kRpId[] = "example.com";
constexpr uint8_t kCredId[] = {1, 2, 3, 4};
class LocalCredentialManagementTest : public testing::Test {
protected:
LocalCredentialManagementTest() = default;
void SetUp() override { api_.set_supports_silent_discovery(true); }
bool HasCredentials() {
base::test::TestFuture<bool> future;
local_cred_man_.HasCredentials(future.GetCallback());
EXPECT_TRUE(future.Wait());
return future.Get();
}
std::optional<std::vector<device::DiscoverableCredentialMetadata>>
Enumerate() {
base::test::TestFuture<
std::optional<std::vector<device::DiscoverableCredentialMetadata>>>
future;
local_cred_man_.Enumerate(future.GetCallback());
EXPECT_TRUE(future.Wait());
return future.Get();
}
// A `BrowserTaskEnvironment` needs to be in-scope in order to create a
// `TestingProfile`.
content::BrowserTaskEnvironment task_environment_;
TestingProfile profile_;
device::FakeWinWebAuthnApi api_;
LocalCredentialManagementWin local_cred_man_{&api_, &profile_};
};
TEST_F(LocalCredentialManagementTest, NoSupport) {
// With no support, `HasCredentials` should return false and `Enumerate`
// should return no value.
api_.set_supports_silent_discovery(false);
EXPECT_FALSE(HasCredentials());
EXPECT_FALSE(profile_.GetPrefs()->GetBoolean(kHasPlatformCredentialsPref));
EXPECT_FALSE(Enumerate().has_value());
}
TEST_F(LocalCredentialManagementTest, NoCredentials) {
// With support but no credentials `HasCredentials` should still return false,
// but `Enumerate` should return an empty list.
EXPECT_FALSE(HasCredentials());
EXPECT_FALSE(profile_.GetPrefs()->GetBoolean(kHasPlatformCredentialsPref));
const std::optional<std::vector<device::DiscoverableCredentialMetadata>>
result = Enumerate();
ASSERT_TRUE(result.has_value());
EXPECT_TRUE(result->empty());
}
TEST_F(LocalCredentialManagementTest, OneCredential) {
// With a credential injected, `HasCredentials` should return true and should
// cache that in the profile. Enumerate should return that credential.
api_.InjectDiscoverableCredential(kCredId, {kRpId, std::nullopt},
{{1, 2, 3, 4}, std::nullopt, std::nullopt});
EXPECT_TRUE(HasCredentials());
EXPECT_TRUE(profile_.GetPrefs()->GetBoolean(kHasPlatformCredentialsPref));
const std::optional<std::vector<device::DiscoverableCredentialMetadata>>
result = Enumerate();
ASSERT_TRUE(result.has_value());
ASSERT_EQ(result->size(), 1u);
EXPECT_EQ(result->at(0).rp_id, kRpId);
}
TEST_F(LocalCredentialManagementTest, CacheIsUsed) {
// If the cache is set to true then HasCredentials will return true even
// though there aren't any credentials.
profile_.GetPrefs()->SetBoolean(kHasPlatformCredentialsPref, true);
EXPECT_TRUE(HasCredentials());
EXPECT_TRUE(profile_.GetPrefs()->GetBoolean(kHasPlatformCredentialsPref));
const std::optional<std::vector<device::DiscoverableCredentialMetadata>>
result = Enumerate();
ASSERT_TRUE(result.has_value());
EXPECT_TRUE(result->empty());
// Calling `Enumerate` should have updated the cache to reflect the fact that
// there aren't any credentials.
EXPECT_FALSE(profile_.GetPrefs()->GetBoolean(kHasPlatformCredentialsPref));
}
TEST_F(LocalCredentialManagementTest, Sorting) {
constexpr uint8_t kCredId1[] = {1};
constexpr uint8_t kCredId2[] = {2};
constexpr uint8_t kCredId3[] = {3};
constexpr uint8_t kCredId4[] = {4};
constexpr uint8_t kCredId5[] = {5};
constexpr uint8_t kCredId6[] = {6};
constexpr uint8_t kCredId7[] = {7};
api_.InjectDiscoverableCredential(kCredId7, {"zzz.de", std::nullopt},
{{1, 2, 3, 4}, "username", std::nullopt});
api_.InjectDiscoverableCredential(kCredId2, {"zzz.de", std::nullopt},
{{1, 2, 3, 4}, "username", std::nullopt});
api_.InjectDiscoverableCredential(kCredId3,
{"www.example.co.uk", std::nullopt},
{{1, 2, 3, 4}, "user1", std::nullopt});
api_.InjectDiscoverableCredential(kCredId4,
{"foo.www.example.co.uk", std::nullopt},
{{1, 2, 3, 4}, "user1", std::nullopt});
api_.InjectDiscoverableCredential(kCredId5,
{"foo.example.co.uk", std::nullopt},
{{1, 2, 3, 4}, "user1", std::nullopt});
api_.InjectDiscoverableCredential(kCredId6, {"aardvark.us", std::nullopt},
{{1, 2, 3, 4}, "username", std::nullopt});
api_.InjectDiscoverableCredential(kCredId1, {"example.co.uk", std::nullopt},
{{1, 2, 3, 4}, "user2", std::nullopt});
const std::vector<device::DiscoverableCredentialMetadata> result =
Enumerate().value();
ASSERT_EQ(result.size(), 7u);
EXPECT_EQ(result[0].rp_id, "aardvark.us");
// Despite starting with other characters, all entries for example.co.uk
// should be sorted together.
EXPECT_EQ(result[1].rp_id, "example.co.uk");
EXPECT_EQ(result[2].rp_id, "foo.example.co.uk");
EXPECT_EQ(result[3].rp_id, "www.example.co.uk");
EXPECT_EQ(result[4].rp_id, "foo.www.example.co.uk");
EXPECT_EQ(result[5].rp_id, "zzz.de");
EXPECT_EQ(result[6].rp_id, "zzz.de");
// The two zzz.de entries have the same RP ID and user.name, thus they should
// be sorted by credential ID.
EXPECT_EQ(result[6].cred_id[0], 7);
}
} // namespace