blob: 324abe0086b89302e7daff0cd9251f03d9c11426 [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/passkey_unlock_manager.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/webauthn/enclave_authenticator_browsertest_base.h"
#include "chrome/browser/webauthn/enclave_manager.h"
#include "chrome/browser/webauthn/enclave_manager_factory.h"
#include "chrome/browser/webauthn/passkey_unlock_manager_factory.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "device/fido/features.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "url/gurl.h"
// These tests are also disabled under MSAN. The enclave subprocess is written
// in Rust and FFI from Rust to C++ doesn't work in Chromium at this time
// (crbug.com/1369167).
#if !defined(MEMORY_SANITIZER)
namespace webauthn {
namespace {
constexpr char kAccountsGoogleHost[] = "accounts.google.com";
#if BUILDFLAG(IS_CHROMEOS)
constexpr char kEncryptionUnlockDesktopPath[] = "/encryption/unlock/chromeos";
#else
constexpr char kEncryptionUnlockDesktopPath[] = "/encryption/unlock/desktop";
#endif
// Custom request handler to serve the encryption unlock page.
std::unique_ptr<net::test_server::HttpResponse>
HandleEncryptionUnlockPageRequest(
const net::test_server::HttpRequest& request) {
GURL absolute_url = request.base_url.Resolve(request.relative_url);
if (absolute_url.host() == kAccountsGoogleHost &&
absolute_url.path() == kEncryptionUnlockDesktopPath) {
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
http_response->set_content_type("text/html");
http_response->set_content("<html><body>OK</body></html>");
return http_response;
}
return nullptr; // Let other handlers process if not matched.
}
class MockPasskeyUnlockManagerObserver : public PasskeyUnlockManager::Observer {
public:
MOCK_METHOD(void, OnPasskeyUnlockManagerStateChanged, (), (override));
MOCK_METHOD(void, OnPasskeyUnlockManagerShuttingDown, (), (override));
MOCK_METHOD(void, OnPasskeyUnlockManagerIsReady, (), (override));
};
class PasskeyUnlockManagerBrowserTest : public EnclaveAuthenticatorTestBase {
public:
PasskeyUnlockManagerBrowserTest() = default;
~PasskeyUnlockManagerBrowserTest() override = default;
webauthn::PasskeyUnlockManager* passkey_unlock_manager() {
return webauthn::PasskeyUnlockManagerFactory::GetForProfile(
browser()->profile());
}
protected:
void SetUpOnMainThread() override {
EnclaveAuthenticatorTestBase::SetUpOnMainThread();
// Make the browser's network stack route requests to the
// embedded_test_server.
host_resolver()->AddRule("*", "127.0.0.1");
embedded_test_server()->RegisterRequestHandler(
base::BindRepeating(&HandleEncryptionUnlockPageRequest));
ASSERT_TRUE(embedded_test_server()->Start());
}
private:
base::test::ScopedFeatureList feature_list_{device::kPasskeyUnlockErrorUi};
};
IN_PROC_BROWSER_TEST_F(PasskeyUnlockManagerBrowserTest, IsCreated) {
EXPECT_NE(passkey_unlock_manager(), nullptr);
}
// Test that calling `OpenTabWithPasskeyUnlockChallenge` opens a new tab with a
// Passkey unlock URL.
IN_PROC_BROWSER_TEST_F(PasskeyUnlockManagerBrowserTest,
OpensNewTabWithPasskeyUnlockUrl) {
TabStripModel* tab_strip_model = browser()->tab_strip_model();
int initial_tab_count = tab_strip_model->count();
PasskeyUnlockManager::OpenTabWithPasskeyUnlockChallenge(browser());
// Ensure that a new tab with an expected URL has been added.
EXPECT_EQ(initial_tab_count + 1, tab_strip_model->count());
content::WebContents* new_contents = tab_strip_model->GetActiveWebContents();
ASSERT_TRUE(new_contents);
#if BUILDFLAG(IS_CHROMEOS)
EXPECT_EQ(GURL("https://accounts.google.com/encryption/unlock/"
"chromeos?kdi=CAESDgoMaHdfcHJvdGVjdGVk"),
new_contents->GetVisibleURL());
#else
EXPECT_EQ(GURL("https://accounts.google.com/encryption/unlock/"
"desktop?kdi=CAESDgoMaHdfcHJvdGVjdGVk"),
new_contents->GetVisibleURL());
#endif
}
IN_PROC_BROWSER_TEST_F(PasskeyUnlockManagerBrowserTest,
NotifyObserversOnEnclaveStateUpdated) {
testing::NiceMock<MockPasskeyUnlockManagerObserver> observer;
passkey_unlock_manager()->AddObserver(&observer);
base::test::TestFuture<void> load_future;
EnclaveManager* enclave_manager =
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(
browser()->profile());
enclave_manager->Load(load_future.GetCallback());
ASSERT_TRUE(load_future.Wait());
base::test::TestFuture<void> event_future;
EXPECT_CALL(observer, OnPasskeyUnlockManagerStateChanged())
.WillOnce([&event_future]() {
// Signal the TestFuture when OnPasskeyUnlockManagerStateChanged is
// called.
event_future.SetValue();
});
// Simulate the operations that make the EnclaveManager ready. This causes a
// state change, which should be observed by the PasskeyUnlockManager.
SimulateSuccessfulGpmPinCreation("123456");
EXPECT_TRUE(event_future.Wait());
}
} // namespace
} // namespace webauthn
#endif // !defined(MEMORY_SANITIZER)