// 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/media/cdm_pref_service_helper.h"
#include <optional>
#include "base/base64.h"
#include "base/containers/to_value_list.h"
#include "base/json/values_util.h"
#include "base/logging.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace {
const char kOriginId[] = "origin_id";
const char kOriginIdCreationTime[] = "origin_id_creation_time";
const char kClientToken[] = "client_token";
const char kClientTokenCreationTime[] = "client_token_creation_time";
bool TimeIsBetween(const base::Time& time,
const base::Time& start,
const base::Time& end) {
return time >= start && (end.is_null() || time <= end);
}
// Converts a base::Value::List of Time to std::vector<base::Time>
std::vector<base::Time> ListToTimes(const base::Value::List& time_list) {
std::vector<base::Time> times;
for (const base::Value& time_value : time_list) {
auto time = base::ValueToTime(time_value);
if (time) {
times.push_back(time.value());
} else {
DVLOG(ERROR) << "Could not convert time_value=" << time_value
<< " to time.";
}
}
return times;
}
// Data stored in the kMediaCdmOriginData Pref dictionary.
// {
// $origin_string: {
// # A unique random string for the real "origin_string".
// "origin_id": $origin_id
// "origin_id_creation_time": $origin_id_creation_time
// "hardware_secure_decryption_disable_times":
// $hw_secure_decryption_disable_times
// "client_token": $client_token (optional)
// "client_token_creation_time": $client_token_creation_time (optional)
// },
// more origin_string map...
// }
base::Value::Dict ToDictValue(const CdmPrefData& pref_data) {
// Origin ID
auto dict =
base::Value::Dict()
.Set(kOriginId, base::UnguessableTokenToValue(pref_data.origin_id()))
.Set(kOriginIdCreationTime,
base::TimeToValue(pref_data.origin_id_creation_time()));
// Optional Client Token
const std::optional<std::vector<uint8_t>> client_token =
pref_data.client_token();
if (client_token.has_value() && !client_token->empty()) {
std::string encoded_client_token = base::Base64Encode(client_token.value());
dict.Set(kClientToken, encoded_client_token);
dict.Set(kClientTokenCreationTime,
base::TimeToValue(pref_data.client_token_creation_time()));
}
dict.Set(prefs::kHardwareSecureDecryptionDisabledTimes,
base::ToValueList(pref_data.hw_secure_decryption_disable_times(),
&base::TimeToValue));
return dict;
}
// Convert `cdm_data_dict` to CdmPrefData. `cdm_data_dict` contains the origin
// id and the time it was first created as well as the client token and the time
// it was set/updated. Return nullptr if `cdm_data_dict` has any corruption,
// e.g. format error, missing fields, invalid value.
std::unique_ptr<CdmPrefData> FromDictValue(
const base::Value::Dict& cdm_data_dict) {
// Origin ID
const base::Value* origin_id_value = cdm_data_dict.Find(kOriginId);
if (!origin_id_value) {
return nullptr;
}
std::optional<base::UnguessableToken> origin_id =
base::ValueToUnguessableToken(*origin_id_value);
if (!origin_id) {
return nullptr;
}
const base::Value* time_value = cdm_data_dict.Find(kOriginIdCreationTime);
if (!time_value) {
return nullptr;
}
std::optional<base::Time> origin_id_time = base::ValueToTime(time_value);
if (!origin_id_time || origin_id_time.value().is_null()) {
return nullptr;
}
#if BUILDFLAG(IS_WIN)
std::vector<base::Time> hw_secure_disabled_times;
const base::Value::List* hw_secure_disabled_time_values =
cdm_data_dict.FindList(prefs::kHardwareSecureDecryptionDisabledTimes);
if (!hw_secure_disabled_time_values) {
return nullptr;
}
hw_secure_disabled_times = ListToTimes(*hw_secure_disabled_time_values);
auto cdm_pref_data = std::make_unique<CdmPrefData>(
origin_id.value(), origin_id_time.value(), hw_secure_disabled_times);
#else
auto cdm_pref_data =
std::make_unique<CdmPrefData>(origin_id.value(), origin_id_time.value());
#endif // BUILDFLAG(IS_WIN)
// Client Token
const std::string* encoded_client_token =
cdm_data_dict.FindString(kClientToken);
if (encoded_client_token) {
std::string decoded_client_token;
if (!base::Base64Decode(*encoded_client_token, &decoded_client_token)) {
return nullptr;
}
std::vector<uint8_t> client_token(decoded_client_token.begin(),
decoded_client_token.end());
time_value = cdm_data_dict.Find(kClientTokenCreationTime);
// If we have a client token but no creation time, this is an error.
if (!time_value) {
return nullptr;
}
std::optional<base::Time> client_token_time = base::ValueToTime(time_value);
if (!client_token_time) {
return nullptr;
}
cdm_pref_data->SetClientToken(client_token, client_token_time.value());
}
return cdm_pref_data;
}
} // namespace
CdmPrefData::CdmPrefData(const base::UnguessableToken& origin_id,
base::Time origin_id_time)
: origin_id_(origin_id), origin_id_creation_time_(origin_id_time) {
DCHECK(origin_id_);
}
CdmPrefData::CdmPrefData(
const base::UnguessableToken& origin_id,
base::Time origin_id_time,
std::vector<base::Time> hw_secure_decryption_disable_times)
: origin_id_(origin_id),
origin_id_creation_time_(origin_id_time),
hw_secure_decryption_disable_times_(hw_secure_decryption_disable_times) {
CHECK(origin_id_);
}
CdmPrefData::~CdmPrefData() = default;
const base::UnguessableToken& CdmPrefData::origin_id() const {
return origin_id_;
}
base::Time CdmPrefData::origin_id_creation_time() const {
return origin_id_creation_time_;
}
const std::optional<std::vector<uint8_t>> CdmPrefData::client_token() const {
return client_token_;
}
base::Time CdmPrefData::client_token_creation_time() const {
return client_token_creation_time_;
}
std::vector<base::Time> CdmPrefData::hw_secure_decryption_disable_times()
const {
return hw_secure_decryption_disable_times_;
}
void CdmPrefData::SetClientToken(const std::vector<uint8_t>& client_token,
const base::Time creation_time) {
VLOG(1) << __func__;
client_token_ = client_token;
client_token_creation_time_ = creation_time;
}
CdmPrefServiceHelper::CdmPrefServiceHelper() = default;
CdmPrefServiceHelper::~CdmPrefServiceHelper() = default;
void CdmPrefServiceHelper::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(prefs::kMediaCdmOriginData);
}
// Removes the CDM preference data from origin dict if the session's creation
// time falls in [`start`, `end`] and `filter` returns true on its origin.
// `start` can be null, which would indicate that we should delete everything
// since the beginning of time. `end` can also be null, in which case we can
// just ignore it. If only `client_token_creation_time` falls between `start`
// and `end`, we only clear that field. If `origin_id_creation_time` falls
// between `start` and `end`, we clear the whole entry.
void CdmPrefServiceHelper::ClearCdmPreferenceData(
PrefService* user_prefs,
base::Time start,
base::Time end,
const base::RepeatingCallback<bool(const GURL&)>& filter) {
DVLOG(1) << __func__ << " From [" << start << ", " << end << "]";
ScopedDictPrefUpdate update(user_prefs, prefs::kMediaCdmOriginData);
std::vector<std::string> origins_to_delete;
for (auto [origin, origin_value] : *update) {
// Null filter indicates that we should delete everything.
if (filter && !filter.Run(GURL(origin))) {
continue;
}
auto* origin_dict = origin_value.GetIfDict();
if (!origin_dict) {
DVLOG(ERROR) << "Could not parse the preference data. Removing entry.";
origins_to_delete.push_back(origin);
continue;
}
std::unique_ptr<CdmPrefData> cdm_pref_data = FromDictValue(*origin_dict);
if (!cdm_pref_data) {
origins_to_delete.push_back(origin);
continue;
}
if (TimeIsBetween(cdm_pref_data->origin_id_creation_time(), start, end)) {
DVLOG(1) << "Clearing cdm pref data for " << origin;
origins_to_delete.push_back(origin);
} else if (TimeIsBetween(cdm_pref_data->client_token_creation_time(), start,
end)) {
origin_dict->Remove(kClientToken);
origin_dict->Remove(kClientTokenCreationTime);
}
}
// Remove CDM preference data.
for (const auto& origin_str : origins_to_delete) {
update->Remove(origin_str);
}
DVLOG(1) << __func__ << "Done removing CDM preference data";
}
std::unique_ptr<CdmPrefData> CdmPrefServiceHelper::GetCdmPrefData(
PrefService* user_prefs,
const url::Origin& cdm_origin) {
VLOG(1) << __func__;
// Access to the PrefService must be made from the UI thread.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const base::Value::Dict& dict =
user_prefs->GetDict(prefs::kMediaCdmOriginData);
DCHECK(!cdm_origin.opaque());
if (cdm_origin.opaque()) {
mojo::ReportBadMessage("EME use is not allowed on opaque origin");
return nullptr;
}
const std::string serialized_cdm_origin = cdm_origin.Serialize();
DCHECK(!serialized_cdm_origin.empty());
const base::Value::Dict* cdm_data_dict = dict.FindDict(serialized_cdm_origin);
std::unique_ptr<CdmPrefData> cdm_pref_data;
if (cdm_data_dict) {
cdm_pref_data = FromDictValue(*cdm_data_dict);
}
// Create an new entry or overwrite the existing one in case we weren't able
// to get a valid origin ID from `FromDictValue()`.
if (!cdm_pref_data) {
ScopedDictPrefUpdate update(user_prefs, prefs::kMediaCdmOriginData);
#if BUILDFLAG(IS_WIN)
// Initialize hardware secure decryption disabled times to match local
// state's hardware secure decryption disabled times. This prevents sites
// with no prior hardware secure playback from re-experiecing errors/crashes
// if there were previous errors that are recorded globally. See
// go/hardware-secure-per-site-fallback for details.
cdm_pref_data = std::make_unique<CdmPrefData>(
base::UnguessableToken::Create(), base::Time::Now(),
ListToTimes(g_browser_process->local_state()->GetList(
prefs::kGlobalHardwareSecureDecryptionDisabledTimes)));
#else
cdm_pref_data = std::make_unique<CdmPrefData>(
base::UnguessableToken::Create(), base::Time::Now());
#endif // BUILDFLAG(IS_WIN)
update->Set(serialized_cdm_origin, ToDictValue(*cdm_pref_data));
}
return cdm_pref_data;
}
void CdmPrefServiceHelper::SetCdmClientToken(
PrefService* user_prefs,
const url::Origin& cdm_origin,
const std::vector<uint8_t>& client_token) {
VLOG(1) << __func__;
// Access to the PrefService must be made from the UI thread.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(!cdm_origin.opaque());
const std::string serialized_cdm_origin = cdm_origin.Serialize();
DCHECK(!serialized_cdm_origin.empty());
ScopedDictPrefUpdate update(user_prefs, prefs::kMediaCdmOriginData);
base::Value::Dict& dict = update.Get();
base::Value::Dict* dict_value = dict.FindDict(serialized_cdm_origin);
if (!dict_value) {
// If there is no preference associated with the origin at this point, this
// means that the preference data was deleted by the user recently. No need
// to save the client token in that case.
return;
}
std::unique_ptr<CdmPrefData> cdm_pref_data = FromDictValue(*dict_value);
if (!cdm_pref_data) {
DVLOG(ERROR) << "The CDM preference data for origin \""
<< serialized_cdm_origin
<< "\" could not be parsed. Removing entry from preferences.";
dict.Remove(serialized_cdm_origin);
return;
}
cdm_pref_data->SetClientToken(client_token, base::Time::Now());
dict.Set(serialized_cdm_origin, ToDictValue(*cdm_pref_data));
}
std::map<std::string, url::Origin> CdmPrefServiceHelper::GetOriginIdMapping(
PrefService* user_prefs) {
std::map<std::string, url::Origin> mapping;
const base::Value::Dict& dict =
user_prefs->GetDict(prefs::kMediaCdmOriginData);
for (auto key_value : dict) {
const base::Value* origin_id_value =
key_value.second.GetDict().Find(kOriginId);
if (!origin_id_value) {
continue;
}
std::optional<base::UnguessableToken> origin_id =
base::ValueToUnguessableToken(*origin_id_value);
if (!origin_id) {
continue;
}
const url::Origin origin = url::Origin::Create(GURL(key_value.first));
mapping[origin_id->ToString()] = origin;
}
return mapping;
}