Chromium changes: https://chromium.googlesource.com/chromium/src/+/30e187d7d78b64c59c52a747b5373cc21b389871 commit 30e187d7d78b64c59c52a747b5373cc21b389871 Author: Giovanni Ortuño Urquidi <ortuno@chromium.org> Date: Tue Nov 4 09:44:08 2025 -0800 Remove base/hash/md5.h All usages have been migrated to crypto/obsolete/md5.h or upgraded to SHA-256. Fixed: 406729261 Change-Id: Ie643451d670c0ab701cddf2b3e820d50debd571e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7113738 Reviewed-by: Elly FJ <ellyjones@chromium.org> Commit-Queue: Giovanni Ortuno Urquidi <ortuno@chromium.org> Reviewed-by: Nico Weber <thakis@chromium.org> Owners-Override: Nico Weber <thakis@chromium.org> Cr-Commit-Position: refs/heads/main@{#1540135}
467 lines
15 KiB
C++
467 lines
15 KiB
C++
/* Copyright (c) 2021 The Brave Authors. All rights reserved.
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "brave/browser/default_protocol_handler_utils_win.h"
|
|
|
|
#include <shobjidl.h>
|
|
#include <winternl.h>
|
|
|
|
#include <wrl/client.h>
|
|
|
|
#include <array>
|
|
#include <memory>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "base/base64.h"
|
|
#include "base/compiler_specific.h"
|
|
#include "base/containers/span.h"
|
|
#include "base/containers/span_reader.h"
|
|
#include "base/files/file_path.h"
|
|
#include "base/logging.h"
|
|
#include "base/notreached.h"
|
|
#include "base/numerics/byte_conversions.h"
|
|
#include "base/path_service.h"
|
|
#include "base/strings/cstring_view.h"
|
|
#include "base/strings/strcat.h"
|
|
#include "base/strings/string_util.h"
|
|
#include "base/strings/string_util_win.h"
|
|
#include "base/strings/utf_string_conversions.h"
|
|
#include "base/threading/scoped_blocking_call.h"
|
|
#include "base/win/atl.h"
|
|
#include "base/win/registry.h"
|
|
#include "base/win/scoped_co_mem.h"
|
|
#include "base/win/scoped_com_initializer.h"
|
|
#include "base/win/win_util.h"
|
|
#include "base/win/windows_version.h"
|
|
#include "chrome/install_static/install_util.h"
|
|
#include "chrome/installer/util/shell_util.h"
|
|
#include "crypto/obsolete/md5.h"
|
|
#include "third_party/abseil-cpp/absl/strings/str_format.h"
|
|
|
|
// Most of source code in this file comes from firefox's SetDefaultBrowser -
|
|
// https://github.com/mozilla/gecko-dev/blob/master/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp
|
|
|
|
namespace brave {
|
|
std::array<uint8_t, crypto::obsolete::kMd5Size> Md5ForDefaultProtocolHandler(
|
|
base::span<const uint8_t> data) {
|
|
return crypto::obsolete::Md5::Hash(data);
|
|
}
|
|
} // namespace brave
|
|
|
|
namespace protocol_handler_utils {
|
|
|
|
namespace {
|
|
|
|
constexpr wchar_t kUserChoiceKey[] = L"UserChoice";
|
|
constexpr wchar_t kProgIDKey[] = L"ProgID";
|
|
constexpr wchar_t kHashKey[] = L"Hash";
|
|
|
|
inline uint32_t WordSwap(uint32_t v) {
|
|
return (v >> 16) | (v << 16);
|
|
}
|
|
|
|
std::wstring HashString(base::wcstring_view input) {
|
|
if (input.size() < 8u) {
|
|
// It seems this algo doesn't consider anything on the modulo of blocks, so
|
|
// there's nothing for us to do with small strings.
|
|
return std::wstring();
|
|
}
|
|
auto bytes = base::byte_span_with_nul_from_cstring_view(input);
|
|
|
|
// Compute an MD5 hash. md5[0] and md5[1] will be used as constant multipliers
|
|
// in the scramble below.
|
|
std::array<uint8_t, crypto::obsolete::kMd5Size> digest =
|
|
brave::Md5ForDefaultProtocolHandler(bytes);
|
|
auto md5 = base::as_byte_span(digest).first<8u>();
|
|
|
|
// The following loop effectively computes two checksums, scrambled like a
|
|
// hash after every DWORD is added.
|
|
struct MagicBlock {
|
|
std::pair<std::array<uint32_t, 5u>, std::array<uint32_t, 5u>> line;
|
|
};
|
|
|
|
auto magic_numbers_table = std::to_array<MagicBlock>(
|
|
{{{{base::U32FromNativeEndian(md5.first<4u>()) | 1, 0xCF98B111uL,
|
|
0x87085B9FuL, 0x12CEB96DuL, 0x257E1D83uL},
|
|
{base::U32FromNativeEndian(md5.first<4u>()) | 1, 0xEF0569FBuL,
|
|
0x689B6B9FuL, 0x79F8A395uL, 0xC3EFEA97uL}}},
|
|
{{{base::U32FromNativeEndian(md5.last<4u>()) | 1, 0xA27416F5uL,
|
|
0xD38396FFuL, 0x7C932B89uL, 0xBFA49F69uL},
|
|
{base::U32FromNativeEndian(md5.last<4u>()) | 1, 0xC31713DBuL,
|
|
0xDDCD1F0FuL, 0x59C3AF2DuL, 0x35BD1EC9uL}}}});
|
|
|
|
// The checksums.
|
|
uint32_t h0 = 0;
|
|
uint32_t h1 = 0;
|
|
// Accumulated total of the checksum after each uint32_t.
|
|
uint32_t h0_acc = 0;
|
|
uint32_t h1_acc = 0;
|
|
|
|
auto reader = base::SpanReader(bytes);
|
|
while (reader.remaining() >= 8u) {
|
|
// We clearly don't care about anything under 8u bytes as the mozilla
|
|
// implementation also doesn't seem to care.
|
|
|
|
for (const auto& magic_block : magic_numbers_table) {
|
|
const auto& [c0, c1] = magic_block.line;
|
|
uint32_t ui32 = base::U32FromNativeEndian(*reader.Read<4u>());
|
|
|
|
h0 += ui32;
|
|
// Scramble 0
|
|
h0 *= c0[0];
|
|
h0 = WordSwap(h0) * c0[1];
|
|
h0 = WordSwap(h0) * c0[2];
|
|
h0 = WordSwap(h0) * c0[3];
|
|
h0 = WordSwap(h0) * c0[4];
|
|
h0_acc += h0;
|
|
|
|
h1 += ui32;
|
|
// Scramble 1
|
|
h1 = WordSwap(h1) * c1[1] + h1 * c1[0];
|
|
h1 = (h1 >> 16) * c1[2] + h1 * c1[3];
|
|
h1 = WordSwap(h1) * c1[4] + h1;
|
|
h1_acc += h1;
|
|
}
|
|
}
|
|
|
|
uint32_t hash[2] = {h0 ^ h1, h0_acc ^ h1_acc};
|
|
return base::UTF8ToWide(base::Base64Encode(base::as_byte_span(hash)));
|
|
}
|
|
|
|
std::wstring FormatUserChoiceString(std::wstring_view ext,
|
|
std::wstring_view sid,
|
|
std::wstring_view prog_id,
|
|
SYSTEMTIME timestamp) {
|
|
timestamp.wSecond = 0;
|
|
timestamp.wMilliseconds = 0;
|
|
|
|
FILETIME file_time = {0};
|
|
if (!::SystemTimeToFileTime(×tamp, &file_time))
|
|
return std::wstring();
|
|
|
|
// This string is built into Windows as part of the UserChoice hash algorithm.
|
|
// It might vary across Windows SKUs (e.g. Windows 10 vs. Windows Server), or
|
|
// across builds of the same SKU, but this is the only currently known
|
|
// version. There isn't any known way of deriving it, so we assume this
|
|
// constant value. If we are wrong, we will not be able to generate correct
|
|
// UserChoice hashes.
|
|
constexpr wchar_t user_experience[] =
|
|
L"User Choice set via Windows User Experience "
|
|
L"{D18B6DD5-6124-4341-9318-804003BAFA0B}";
|
|
|
|
const std::wstring file_time_str = base::ASCIIToWide(absl::StrFormat(
|
|
"%08lx%08lx", file_time.dwHighDateTime, file_time.dwLowDateTime));
|
|
|
|
std::wstring user_choice =
|
|
base::StrCat({ext, sid, prog_id, file_time_str, user_experience});
|
|
// For using CharLowerW instead of base::ToLowerASCII().
|
|
// Otherwise, hash test with non-ascii inputs are failed.
|
|
::CharLowerW(user_choice.data());
|
|
return user_choice;
|
|
}
|
|
|
|
bool AddMillisecondsToSystemTime(SYSTEMTIME* system_time,
|
|
ULONGLONG increment_ms) {
|
|
FILETIME file_time;
|
|
ULARGE_INTEGER file_time_int;
|
|
if (!::SystemTimeToFileTime(system_time, &file_time))
|
|
return false;
|
|
|
|
file_time_int.LowPart = file_time.dwLowDateTime;
|
|
file_time_int.HighPart = file_time.dwHighDateTime;
|
|
|
|
// FILETIME is in units of 100ns.
|
|
file_time_int.QuadPart += increment_ms * 1000 * 10;
|
|
|
|
file_time.dwLowDateTime = file_time_int.LowPart;
|
|
file_time.dwHighDateTime = file_time_int.HighPart;
|
|
SYSTEMTIME tmp_system_time;
|
|
if (!::FileTimeToSystemTime(&file_time, &tmp_system_time))
|
|
return false;
|
|
|
|
*system_time = tmp_system_time;
|
|
return true;
|
|
}
|
|
|
|
// Compare two SYSTEMTIMEs as FILETIME after clearing everything
|
|
// below minutes.
|
|
bool CheckEqualMinutes(SYSTEMTIME system_time1, SYSTEMTIME system_time2) {
|
|
system_time1.wSecond = 0;
|
|
system_time1.wMilliseconds = 0;
|
|
|
|
system_time2.wSecond = 0;
|
|
system_time2.wMilliseconds = 0;
|
|
|
|
FILETIME file_time1;
|
|
FILETIME file_time2;
|
|
if (!::SystemTimeToFileTime(&system_time1, &file_time1) ||
|
|
!::SystemTimeToFileTime(&system_time2, &file_time2)) {
|
|
return false;
|
|
}
|
|
|
|
return (file_time1.dwLowDateTime == file_time2.dwLowDateTime) &&
|
|
(file_time1.dwHighDateTime == file_time2.dwHighDateTime);
|
|
}
|
|
|
|
std::wstring GetAssociationKeyPath(std::wstring_view protocol) {
|
|
const wchar_t* key_path;
|
|
if (protocol[0] == L'.') {
|
|
key_path =
|
|
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\";
|
|
} else {
|
|
key_path =
|
|
L"SOFTWARE\\Microsoft\\Windows\\Shell\\Associations\\"
|
|
L"UrlAssociations\\";
|
|
}
|
|
|
|
return base::StrCat({key_path, protocol});
|
|
}
|
|
|
|
bool SetUserChoice(std::wstring_view ext,
|
|
std::wstring_view sid,
|
|
std::wstring_view prog_id) {
|
|
SYSTEMTIME hash_timestamp;
|
|
::GetSystemTime(&hash_timestamp);
|
|
auto hash = GenerateUserChoiceHash(ext, sid, prog_id, hash_timestamp);
|
|
if (hash.empty())
|
|
return false;
|
|
|
|
// The hash changes at the end of each minute, so check that the hash should
|
|
// be the same by the time we're done writing.
|
|
constexpr ULONGLONG kWriteTimingThresholdMilliseconds = 100;
|
|
// Generating the hash could have taken some time, so start from now.
|
|
SYSTEMTIME write_end_timestamp;
|
|
::GetSystemTime(&write_end_timestamp);
|
|
if (!AddMillisecondsToSystemTime(&write_end_timestamp,
|
|
kWriteTimingThresholdMilliseconds)) {
|
|
return false;
|
|
}
|
|
if (!CheckEqualMinutes(hash_timestamp, write_end_timestamp)) {
|
|
LOG(ERROR) << "Hash is too close to expiration, sleeping until next hash.";
|
|
::Sleep(kWriteTimingThresholdMilliseconds * 2);
|
|
|
|
// For consistency, use the current time.
|
|
::GetSystemTime(&hash_timestamp);
|
|
hash = GenerateUserChoiceHash(ext, sid, prog_id, hash_timestamp);
|
|
if (hash.empty())
|
|
return false;
|
|
}
|
|
|
|
auto assoc_key_path = GetAssociationKeyPath(ext);
|
|
if (assoc_key_path.empty())
|
|
return false;
|
|
|
|
base::win::RegKey assoc_key(HKEY_CURRENT_USER);
|
|
if (assoc_key.OpenKey(assoc_key_path.c_str(), KEY_READ | KEY_WRITE) !=
|
|
ERROR_SUCCESS) {
|
|
LOG(ERROR) << "Can't open reg key: " << assoc_key_path;
|
|
return false;
|
|
}
|
|
|
|
// When Windows creates this key, it is read-only (Deny Set Value), so we need
|
|
// to delete it first.
|
|
LSTATUS ls = assoc_key.DeleteKey(kUserChoiceKey);
|
|
if (ls != ERROR_FILE_NOT_FOUND && ls != ERROR_SUCCESS) {
|
|
LOG(ERROR) << "Failed to delete UserChoice key"
|
|
<< " " << ls;
|
|
return false;
|
|
}
|
|
|
|
if (assoc_key.CreateKey(kUserChoiceKey, KEY_READ | KEY_WRITE) !=
|
|
ERROR_SUCCESS) {
|
|
LOG(ERROR) << "Failed to create UserChoice key";
|
|
return false;
|
|
}
|
|
|
|
if (assoc_key.WriteValue(kProgIDKey, prog_id.data()) != ERROR_SUCCESS) {
|
|
LOG(ERROR) << "Failed to write ProgID value";
|
|
return false;
|
|
}
|
|
|
|
if (assoc_key.WriteValue(kHashKey, hash.c_str()) != ERROR_SUCCESS) {
|
|
LOG(ERROR) << "Failed to write Hash value";
|
|
return false;
|
|
}
|
|
|
|
assoc_key.Close();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CheckProgIDExists(std::wstring_view prog_id) {
|
|
base::win::RegKey root(HKEY_CLASSES_ROOT);
|
|
return root.OpenKey(prog_id.data(), KEY_READ) == ERROR_SUCCESS;
|
|
}
|
|
|
|
std::wstring GetBrowserProgId() {
|
|
base::FilePath brave_exe;
|
|
if (!base::PathService::Get(base::FILE_EXE, &brave_exe)) {
|
|
LOG(ERROR) << "Error getting app exe path";
|
|
return std::wstring();
|
|
}
|
|
|
|
const auto suffix = ShellUtil::GetCurrentInstallationSuffix(brave_exe);
|
|
std::wstring brave_html =
|
|
base::StrCat({install_static::GetBrowserProgIdPrefix(), suffix});
|
|
|
|
// ProgIds cannot be longer than 39 characters.
|
|
// Ref: http://msdn.microsoft.com/en-us/library/aa911706.aspx.
|
|
// Make all new registrations comply with this requirement (existing
|
|
// registrations must be preserved).
|
|
std::wstring new_style_suffix;
|
|
if (ShellUtil::GetUserSpecificRegistrySuffix(&new_style_suffix) &&
|
|
suffix == new_style_suffix && brave_html.length() > 39) {
|
|
NOTREACHED();
|
|
}
|
|
return brave_html;
|
|
}
|
|
|
|
std::wstring GetProgIdForProtocol(std::wstring_view protocol) {
|
|
Microsoft::WRL::ComPtr<IApplicationAssociationRegistration> registration;
|
|
HRESULT hr =
|
|
::CoCreateInstance(CLSID_ApplicationAssociationRegistration, nullptr,
|
|
CLSCTX_INPROC, IID_PPV_ARGS(®istration));
|
|
if (FAILED(hr)) {
|
|
LOG(ERROR) << "Failed to create IApplicationAssociationRegistration";
|
|
return std::wstring();
|
|
}
|
|
base::win::ScopedCoMem<wchar_t> current_app;
|
|
const bool is_protocol = protocol[0] != L'.';
|
|
hr = registration->QueryCurrentDefault(
|
|
protocol.data(), is_protocol ? AT_URLPROTOCOL : AT_FILEEXTENSION,
|
|
AL_EFFECTIVE, ¤t_app);
|
|
if (FAILED(hr)) {
|
|
LOG(ERROR) << "Failed to query default app for protocol " << protocol;
|
|
return std::wstring();
|
|
}
|
|
return current_app.get();
|
|
}
|
|
|
|
// NOTE: The passed-in current user SID is used here, instead of getting the SID
|
|
// for the owner of the key. We are assuming that this key in HKCU is owned by
|
|
// the current user, since we want to replace that key ourselves. If the key is
|
|
// owned by someone else, then this check will fail; this is ok because we would
|
|
// likely not want to replace that other user's key anyway.
|
|
bool CheckUserChoiceHash(std::wstring_view protocol,
|
|
std::wstring_view user_sid) {
|
|
auto key_path = GetAssociationKeyPath(protocol);
|
|
if (key_path.empty())
|
|
return false;
|
|
|
|
base::win::RegKey user_choice_key(HKEY_CURRENT_USER, key_path.c_str(),
|
|
KEY_READ);
|
|
if (!user_choice_key.Valid())
|
|
return false;
|
|
|
|
if (user_choice_key.OpenKey(kUserChoiceKey, KEY_READ) != ERROR_SUCCESS)
|
|
return false;
|
|
|
|
FILETIME last_write_file_time;
|
|
|
|
// RegKey doesn't support for fetching last write time.
|
|
if (::RegQueryInfoKeyW(user_choice_key.Handle(), nullptr, nullptr, nullptr,
|
|
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
|
nullptr, &last_write_file_time) != ERROR_SUCCESS) {
|
|
return false;
|
|
}
|
|
|
|
SYSTEMTIME last_write_system_time;
|
|
if (!::FileTimeToSystemTime(&last_write_file_time, &last_write_system_time))
|
|
return false;
|
|
|
|
// Read ProgId
|
|
std::wstring prog_id;
|
|
if (user_choice_key.ReadValue(kProgIDKey, &prog_id) != ERROR_SUCCESS)
|
|
return false;
|
|
|
|
// Read Hash
|
|
std::wstring stored_hash;
|
|
if (user_choice_key.ReadValue(kHashKey, &stored_hash) != ERROR_SUCCESS)
|
|
return false;
|
|
|
|
auto computed_hash = GenerateUserChoiceHash(protocol, user_sid, prog_id,
|
|
last_write_system_time);
|
|
if (computed_hash.empty())
|
|
return false;
|
|
|
|
if (::CompareStringOrdinal(computed_hash.c_str(), -1, stored_hash.c_str(), -1,
|
|
FALSE) != CSTR_EQUAL) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::wstring GenerateUserChoiceHash(std::wstring_view ext,
|
|
std::wstring_view sid,
|
|
std::wstring_view prog_id,
|
|
SYSTEMTIME timestamp) {
|
|
auto user_choice = FormatUserChoiceString(ext, sid, prog_id, timestamp);
|
|
if (user_choice.empty()) {
|
|
LOG(ERROR) << "Didn't get user choice string for generating hash.";
|
|
return std::wstring();
|
|
}
|
|
|
|
return HashString(user_choice);
|
|
}
|
|
|
|
bool SetDefaultProtocolHandlerFor(std::wstring_view protocol) {
|
|
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
|
|
base::BlockingType::MAY_BLOCK);
|
|
|
|
if (IsDefaultProtocolHandlerFor(protocol)) {
|
|
VLOG(2) << "Already default handler for " << protocol;
|
|
return true;
|
|
}
|
|
|
|
const auto prog_id = GetBrowserProgId();
|
|
if (!CheckProgIDExists(prog_id)) {
|
|
LOG(ERROR) << "ProgId is not found - " << prog_id;
|
|
return false;
|
|
}
|
|
|
|
std::wstring user_sid;
|
|
if (!base::win::GetUserSidString(&user_sid)) {
|
|
LOG(ERROR) << "Can't get user sid";
|
|
return false;
|
|
}
|
|
|
|
if (!CheckUserChoiceHash(protocol, user_sid)) {
|
|
LOG(ERROR) << "UserChoice Hash mismatch";
|
|
return false;
|
|
}
|
|
|
|
// Applied FF's policy: unsupported below Windows 10 RS2
|
|
if (base::win::GetVersion() < base::win::Version::WIN10_RS2) {
|
|
VLOG(2) << "UserChoice hash matched, but Windows build is too old";
|
|
return false;
|
|
}
|
|
|
|
if (!SetUserChoice(protocol, user_sid, prog_id))
|
|
return false;
|
|
|
|
// Verify after set.
|
|
return GetProgIdForProtocol(protocol) == prog_id;
|
|
}
|
|
|
|
bool IsDefaultProtocolHandlerFor(std::wstring_view protocol) {
|
|
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
|
|
base::BlockingType::MAY_BLOCK);
|
|
|
|
const auto prog_id = GetBrowserProgId();
|
|
if (!CheckProgIDExists(prog_id)) {
|
|
LOG(ERROR) << "ProgId is not found - " << prog_id;
|
|
return false;
|
|
}
|
|
|
|
return prog_id == GetProgIdForProtocol(protocol);
|
|
}
|
|
|
|
} // namespace protocol_handler_utils
|