blob: 0d5435810d2c809fc25bd43c1fbe35184d3c3db4 [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 "extensions/browser/api/feedback_private/feedback_service.h"
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/test/mock_callback.h"
#include "build/build_config.h"
#include "components/feedback/feedback_data.h"
#include "components/feedback/feedback_report.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/api/feedback_private/mock_feedback_service.h"
#include "extensions/browser/api_unittest.h"
#include "extensions/shell/browser/api/feedback_private/shell_feedback_private_delegate.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "base/base64.h"
#include "chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h"
#include "components/user_manager/fake_user_manager.h"
#include "components/user_manager/scoped_user_manager.h"
#include "components/user_manager/user_manager.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "third_party/boringssl/src/include/openssl/hpke.h"
#endif // BUILDFLAG(IS_CHROMEOS)
namespace extensions {
using feedback::FeedbackData;
using feedback::FeedbackUploader;
using testing::_;
using testing::StrictMock;
namespace {
const std::string kFakeKey = "fake key";
const std::string kFakeValue = "fake value";
const std::string kTabTitleValue = "some sensitive info";
#if BUILDFLAG(IS_CHROMEOS)
constexpr char kVariationsFetchHpkeKey[] =
"https://www.gstatic.com/chromeos-feedback-variations-encryption-key/"
"public_keyset.json";
const std::string kTestPublicKeyResponseBody =
"{\"primaryKeyId\":123,\"key\":[{\"keyData\":{\"typeUrl\":\"type."
"googleapis.com/"
"google.crypto.tink.HpkePublicKey\",\"value\":"
"\"EgYIARABGAIaIKeVK4N3icUhM5YF+Pp5S6PWAg9OlY8zP9oLL9qv4IYS\","
"\"keyMaterialType\":\"ASYMMETRIC_PUBLIC\"},\"status\":\"ENABLED\","
"\"keyId\":456,\"outputPrefixType\":\"RAW\"}]}";
const std::string kTestBase64HpkePrivateKey =
"IHbZB+CCrEXra2WGQx/jFQ+a0NSpVCauqy2uC9NH8Hs=";
// Command line variation string returned during testing.
const std::string kTestCommandLineVariations =
" --enable-features=\"*TestBlinkFeatureDefault,"
"TestFeatureForBrowserTest1\" "
"--disable-features=\"TestFeatureForBrowserTest2\" "
"--disable-field-trial-config";
#endif // BUILDFLAG(IS_CHROMEOS)
class MockFeedbackUploader : public FeedbackUploader {
public:
MockFeedbackUploader(
bool is_off_the_record,
const base::FilePath& state_path,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: FeedbackUploader(is_off_the_record, state_path, url_loader_factory) {}
MOCK_METHOD(void,
QueueReport,
(std::unique_ptr<std::string>, bool, int),
(override));
base::WeakPtr<FeedbackUploader> AsWeakPtr() override {
return weak_ptr_factory_.GetWeakPtr();
}
private:
base::WeakPtrFactory<MockFeedbackUploader> weak_ptr_factory_{this};
};
class MockFeedbackPrivateDelegate : public ShellFeedbackPrivateDelegate {
public:
MockFeedbackPrivateDelegate() {
ON_CALL(*this, FetchSystemInformation)
.WillByDefault([](content::BrowserContext* context,
system_logs::SysLogsFetcherCallback callback) {
auto sys_info = std::make_unique<system_logs::SystemLogsResponse>();
sys_info->emplace(kFakeKey, kFakeValue);
sys_info->emplace(feedback::FeedbackReport::kMemUsageWithTabTitlesKey,
kTabTitleValue);
std::move(callback).Run(std::move(sys_info));
});
#if BUILDFLAG(IS_CHROMEOS)
ON_CALL(*this, FetchExtraLogs)
.WillByDefault([](scoped_refptr<FeedbackData> feedback_data,
FetchExtraLogsCallback callback) {
std::move(callback).Run(feedback_data);
});
#endif // BUILDFLAG(IS_CHROMEOS)
}
~MockFeedbackPrivateDelegate() override = default;
MOCK_METHOD(void,
FetchSystemInformation,
(content::BrowserContext*, system_logs::SysLogsFetcherCallback),
(const, override));
#if BUILDFLAG(IS_CHROMEOS)
MOCK_METHOD(void,
FetchExtraLogs,
(scoped_refptr<feedback::FeedbackData>, FetchExtraLogsCallback),
(const, override));
#endif // BUILDFLAG(IS_CHROMEOS)
};
#if BUILDFLAG(IS_CHROMEOS)
const FeedbackCommon::AttachedFile* FindAttachment(
std::string_view name,
const scoped_refptr<FeedbackData>& feedback_data) {
size_t num_attachments = feedback_data->attachments();
for (size_t i = 0; i < num_attachments; i++) {
const FeedbackCommon::AttachedFile* file = feedback_data->attachment(i);
if (file->name == name) {
return file;
}
}
return nullptr;
}
void VerifyAttachment(std::string_view name,
std::string_view data,
const scoped_refptr<FeedbackData>& feedback_data) {
const auto* attachment = FindAttachment(name, feedback_data);
ASSERT_TRUE(attachment);
EXPECT_EQ(name, attachment->name);
EXPECT_EQ(data, attachment->data);
}
#endif // BUILDFLAG(IS_CHROMEOS)
} // namespace
class FeedbackServiceTest : public ApiUnitTest {
protected:
FeedbackServiceTest() {
#if BUILDFLAG(IS_CHROMEOS_ASH)
test_url_loader_factory_.AddResponse(
kVariationsFetchHpkeKey, kTestPublicKeyResponseBody, net::HTTP_OK);
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
test_shared_loader_factory_ =
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_);
CHECK(scoped_temp_dir_.CreateUniqueTempDir());
mock_uploader_ = std::make_unique<StrictMock<MockFeedbackUploader>>(
/*is_off_the_record=*/false, scoped_temp_dir_.GetPath(),
test_shared_loader_factory_);
feedback_data_ = base::MakeRefCounted<FeedbackData>(
mock_uploader_->AsWeakPtr(), nullptr);
#if BUILDFLAG(IS_CHROMEOS)
auto fake_user_manager = std::make_unique<user_manager::FakeUserManager>();
scoped_user_manager_ = std::make_unique<user_manager::ScopedUserManager>(
std::move(fake_user_manager));
#endif // BUILDFLAG(IS_CHROMEOS)
}
~FeedbackServiceTest() override = default;
void TestSendFeedbackConcerningTabTitles(bool send_tab_titles) {
feedback_data_->AddLog(kFakeKey, kFakeValue);
feedback_data_->AddLog(feedback::FeedbackReport::kMemUsageWithTabTitlesKey,
kTabTitleValue);
const FeedbackParams params{/*is_internal_email=*/false,
/*load_system_info=*/false,
/*send_tab_titles=*/send_tab_titles,
/*send_histograms=*/true,
/*send_bluetooth_logs=*/false,
/*send_wifi_debug_logs=*/false,
/*send_autofill_metadata=*/false};
EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
base::MockCallback<SendFeedbackCallback> mock_callback;
EXPECT_CALL(mock_callback, Run(true));
auto mock_delegate = std::make_unique<MockFeedbackPrivateDelegate>();
#if BUILDFLAG(IS_CHROMEOS)
EXPECT_CALL(*mock_delegate, FetchExtraLogs(_, _)).Times(1);
#endif // BUILDFLAG(IS_CHROMEOS)
auto feedback_service = base::MakeRefCounted<FeedbackService>(
browser_context(), mock_delegate.get());
RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
EXPECT_EQ(1u, feedback_data_->sys_info()->count(kFakeKey));
}
#if BUILDFLAG(IS_CHROMEOS)
void TestSendFeedbackConcerningWifiDebugLogs(bool send_wifi_debug_logs) {
const FeedbackParams params{/*is_internal_email=*/false,
/*load_system_info=*/true,
/*send_tab_titles=*/false,
/*send_histograms=*/false,
/*send_bluetooth_logs=*/false,
/*send_wifi_debug_logs=*/send_wifi_debug_logs,
/*send_autofill_metadata=*/false};
// Create a test file in sub directory "wifi".
const base::FilePath test_file_dir =
scoped_temp_dir_.GetPath().Append("wifi");
ASSERT_TRUE(base::CreateDirectory(test_file_dir));
const base::FilePath test_file =
test_file_dir.Append("wifi_firmware_dumps.tar.zst");
ASSERT_TRUE(base::WriteFile(test_file, "Test file content"));
EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
base::MockCallback<SendFeedbackCallback> mock_callback;
EXPECT_CALL(mock_callback, Run(true));
auto mock_delegate = std::make_unique<MockFeedbackPrivateDelegate>();
EXPECT_CALL(*mock_delegate, FetchSystemInformation(_, _)).Times(1);
EXPECT_CALL(*mock_delegate, FetchExtraLogs(_, _)).Times(1);
if (send_wifi_debug_logs) {
ash::DebugDaemonClient::InitializeFake();
}
auto feedback_service = base::MakeRefCounted<FeedbackService>(
browser_context(), mock_delegate.get());
feedback_service->SetLogFilesRootPathForTesting(scoped_temp_dir_.GetPath());
RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
if (ash::DebugDaemonClient::Get()) {
ash::DebugDaemonClient::Shutdown();
}
EXPECT_EQ(1u, feedback_data_->sys_info()->count(kFakeKey));
// Verify the attachment is added if and only if send_wifi_debug_logs is
// true.
constexpr char kWifiDumpName[] = "wifi_firmware_dumps.tar.zst";
if (send_wifi_debug_logs) {
VerifyAttachment(kWifiDumpName, "TestData", feedback_data_);
} else {
EXPECT_FALSE(FindAttachment(kWifiDumpName, feedback_data_));
}
}
void TestSendFeedbackConcerningBluetoothDebugLogs(bool send_bluetooth_logs) {
const FeedbackParams params{/*is_internal_email=*/false,
/*load_system_info=*/true,
/*send_tab_titles=*/false,
/*send_histograms=*/false,
/*send_bluetooth_logs=*/send_bluetooth_logs,
/*send_wifi_debug_logs=*/false,
/*send_autofill_metadata=*/false};
EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
base::MockCallback<SendFeedbackCallback> mock_callback;
EXPECT_CALL(mock_callback, Run(true));
auto mock_delegate = std::make_unique<MockFeedbackPrivateDelegate>();
EXPECT_CALL(*mock_delegate, FetchSystemInformation(_, _)).Times(1);
EXPECT_CALL(*mock_delegate, FetchExtraLogs(_, _)).Times(1);
if (send_bluetooth_logs) {
ash::DebugDaemonClient::InitializeFake();
}
auto feedback_service = base::MakeRefCounted<FeedbackService>(
browser_context(), mock_delegate.get());
feedback_service->SetLogFilesRootPathForTesting(scoped_temp_dir_.GetPath());
RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
if (ash::DebugDaemonClient::Get()) {
ash::DebugDaemonClient::Shutdown();
}
EXPECT_EQ(1u, feedback_data_->sys_info()->count(kFakeKey));
// Verify the attachment is added if and only if send_bluetooth_logs is
// true.
constexpr char kBluetoothDumpName[] = "bluetooth_firmware_dumps.tar.zst";
if (send_bluetooth_logs) {
VerifyAttachment(kBluetoothDumpName, "TestData", feedback_data_);
} else {
EXPECT_FALSE(FindAttachment(kBluetoothDumpName, feedback_data_));
}
}
#endif // BUILDFLAG(IS_CHROMEOS)
void RunUntilFeedbackIsSent(scoped_refptr<FeedbackService> feedback_service,
const FeedbackParams& params,
SendFeedbackCallback mock_callback) {
feedback_service->RedactThenSendFeedback(params, feedback_data_,
std::move(mock_callback));
base::ThreadPoolInstance::Get()->FlushForTesting();
task_environment()->RunUntilIdle();
}
#if BUILDFLAG(IS_CHROMEOS)
std::unique_ptr<user_manager::ScopedUserManager> scoped_user_manager_;
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
#endif // BUILDFLAG(IS_CHROMEOS)
base::ScopedTempDir scoped_temp_dir_;
network::TestURLLoaderFactory test_url_loader_factory_;
scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
std::unique_ptr<StrictMock<MockFeedbackUploader>> mock_uploader_;
scoped_refptr<FeedbackData> feedback_data_;
};
TEST_F(FeedbackServiceTest, SendFeedbackWithoutSysInfo) {
const FeedbackParams params{/*is_internal_email=*/false,
/*load_system_info=*/false,
/*send_tab_titles=*/true,
/*send_histograms=*/true,
/*send_bluetooth_logs=*/false,
/*send_wifi_debug_logs=*/false,
/*send_autofill_metadata=*/false};
EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
base::MockCallback<SendFeedbackCallback> mock_callback;
EXPECT_CALL(mock_callback, Run(true));
auto shell_delegate = std::make_unique<ShellFeedbackPrivateDelegate>();
auto feedback_service = base::MakeRefCounted<FeedbackService>(
browser_context(), shell_delegate.get());
RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
}
TEST_F(FeedbackServiceTest, SendFeedbackLoadSysInfo) {
const FeedbackParams params{/*is_internal_email=*/false,
/*load_system_info=*/true,
/*send_tab_titles=*/true,
/*send_histograms=*/true,
/*send_bluetooth_logs=*/false,
/*send_wifi_debug_logs=*/false,
/*send_autofill_metadata=*/false};
EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
base::MockCallback<SendFeedbackCallback> mock_callback;
EXPECT_CALL(mock_callback, Run(true));
auto mock_delegate = std::make_unique<MockFeedbackPrivateDelegate>();
EXPECT_CALL(*mock_delegate, FetchSystemInformation(_, _)).Times(1);
#if BUILDFLAG(IS_CHROMEOS)
EXPECT_CALL(*mock_delegate, FetchExtraLogs(_, _)).Times(1);
#endif // BUILDFLAG(IS_CHROMEOS)
auto feedback_service = base::MakeRefCounted<FeedbackService>(
browser_context(), mock_delegate.get());
RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
EXPECT_EQ(1u, feedback_data_->sys_info()->count(kFakeKey));
EXPECT_EQ(1u, feedback_data_->sys_info()->count(
feedback::FeedbackReport::kMemUsageWithTabTitlesKey));
}
// TODO(crbug.com/40908623): Re-enable this test
TEST_F(FeedbackServiceTest, DISABLED_SendFeedbackDoNotSendTabTitles) {
TestSendFeedbackConcerningTabTitles(false);
EXPECT_EQ(0u, feedback_data_->sys_info()->count(
feedback::FeedbackReport::kMemUsageWithTabTitlesKey));
}
TEST_F(FeedbackServiceTest, SendFeedbackDoSendTabTitles) {
TestSendFeedbackConcerningTabTitles(true);
EXPECT_EQ(1u, feedback_data_->sys_info()->count(
feedback::FeedbackReport::kMemUsageWithTabTitlesKey));
}
TEST_F(FeedbackServiceTest, SendFeedbackAutofillMetadata) {
const FeedbackParams params{/*is_internal_email=*/true,
/*load_system_info=*/false,
/*send_tab_titles=*/false,
/*send_histograms=*/true,
/*send_bluetooth_logs=*/false,
/*send_wifi_debug_logs=*/false,
/*send_autofill_metadata=*/true};
feedback_data_->set_autofill_metadata("Autofill Metadata");
EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
base::MockCallback<SendFeedbackCallback> mock_callback;
EXPECT_CALL(mock_callback, Run(true));
auto feedback_service =
base::MakeRefCounted<FeedbackService>(browser_context(), nullptr);
RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
}
#if BUILDFLAG(IS_CHROMEOS)
TEST_F(FeedbackServiceTest, SendFeedbackWithWifiDebugLogs) {
TestSendFeedbackConcerningWifiDebugLogs(/*send_wifi_debug_logs=*/true);
}
TEST_F(FeedbackServiceTest, SendFeedbackWithoutWifiDebugLogs) {
TestSendFeedbackConcerningWifiDebugLogs(/*send_wifi_debug_logs=*/false);
}
TEST_F(FeedbackServiceTest, SendFeedbackWithBluetoothDebugLogs) {
TestSendFeedbackConcerningBluetoothDebugLogs(/*send_bluetooth_logs=*/true);
}
TEST_F(FeedbackServiceTest, SendFeedbackWithoutBluetoothDebugLogs) {
TestSendFeedbackConcerningBluetoothDebugLogs(/*send_bluetooth_logs=*/false);
}
// Test that the feedback report contains the variations.binary encrypted
// properly.
TEST_F(FeedbackServiceTest, TestSendFeedbackWithVariationsBinary) {
const FeedbackParams params{/*is_internal_email=*/false,
/*load_system_info=*/true,
/*send_tab_titles=*/false,
/*send_histograms=*/false,
/*send_bluetooth_logs=*/false,
/*send_wifi_debug_logs=*/false,
/*send_autofill_metadata=*/false};
EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
base::MockCallback<SendFeedbackCallback> mock_callback;
EXPECT_CALL(mock_callback, Run(true));
auto mock_delegate = std::make_unique<MockFeedbackPrivateDelegate>();
EXPECT_CALL(*mock_delegate, FetchSystemInformation(_, _)).Times(1);
EXPECT_CALL(*mock_delegate, FetchExtraLogs(_, _)).Times(1);
auto feedback_service = base::MakeRefCounted<FeedbackService>(
browser_context(), mock_delegate.get());
feedback_service->SetUrlLoaderFactory(test_shared_loader_factory_);
RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
EXPECT_EQ(1u, feedback_data_->sys_info()->count(kFakeKey));
// Initialize Hpke private key.
bssl::ScopedEVP_HPKE_KEY base_key;
std::string decoded_hpke_private_key;
base::Base64Decode(kTestBase64HpkePrivateKey, &decoded_hpke_private_key);
std::vector<uint8_t> hpke_private_key;
hpke_private_key.assign(decoded_hpke_private_key.begin(),
decoded_hpke_private_key.end());
ASSERT_TRUE(EVP_HPKE_KEY_init(/*key=*/base_key.get(),
/*kem=*/EVP_hpke_x25519_hkdf_sha256(),
/*priv_key=*/hpke_private_key.data(),
/*priv_key_len=*/hpke_private_key.size()));
// Get the encrypted file.
constexpr char kVariationsBinary[] = "variations.binary";
const FeedbackCommon::AttachedFile* variatons_binary =
FindAttachment(kVariationsBinary, feedback_data_);
ASSERT_TRUE(variatons_binary);
std::vector<uint8_t> encrypted_data(variatons_binary->data.begin(),
variatons_binary->data.end());
// Setup recipient context.
bssl::ScopedEVP_HPKE_CTX recipient_ctx;
ASSERT_TRUE(EVP_HPKE_CTX_setup_recipient(/*ctx=*/recipient_ctx.get(),
/*key=*/base_key.get(),
/*kdf=*/EVP_hpke_hkdf_sha256(),
/*aead=*/EVP_hpke_aes_256_gcm(),
/*enc=*/encrypted_data.data(),
/*enc_len=*/X25519_PUBLIC_VALUE_LEN,
/*info=*/nullptr,
/*info_len=*/0));
// Decryption.
base::span<const uint8_t> ciphertext =
base::make_span(encrypted_data).subspan(X25519_PUBLIC_VALUE_LEN);
std::vector<uint8_t> plaintext(ciphertext.size());
size_t plaintext_len;
ASSERT_TRUE(EVP_HPKE_CTX_open(/*ctx=*/recipient_ctx.get(),
/*out=*/plaintext.data(),
/*out_len=*/&plaintext_len,
/*max_out_len=*/plaintext.size(),
/*in=*/ciphertext.data(),
/*in_len=*/ciphertext.size(),
/*ad=*/nullptr,
/*ad_len=*/0));
plaintext.resize(plaintext_len);
std::string decrypted_string(plaintext.begin(), plaintext.end());
// Final check.
EXPECT_EQ(kTestCommandLineVariations, decrypted_string);
}
#endif // BUILDFLAG(IS_CHROMEOS)
} // namespace extensions