blob: 10252f7a274a2caa195dbb749c94c74d8030a41c [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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_document_service_impl.h"
#include <utility>
#include "base/bind.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "media/cdm/media_foundation_cdm_data.h"
#include "media/media_buildflags.h"
#if BUILDFLAG(ENABLE_CDM_STORAGE_ID)
#include "chrome/browser/media/cdm_storage_id.h"
#include "chrome/browser/media/media_storage_id_salt.h"
#include "content/public/browser/render_process_host.h"
#endif
#if BUILDFLAG(ENABLE_CDM_STORAGE_ID) || BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/render_frame_host.h"
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/components/settings/cros_settings_names.h"
#include "chrome/browser/ash/settings/cros_settings.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/crosapi/mojom/content_protection.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/media/platform_verification_chromeos.h"
#endif
#if BUILDFLAG(IS_WIN)
#include <windows.h>
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/system/sys_info.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/win/security_util.h"
#include "base/win/sid.h"
#include "chrome/browser/media/cdm_pref_service_helper.h"
#include "chrome/browser/media/media_foundation_service_monitor.h"
#include "media/cdm/win/media_foundation_cdm.h"
#include "sandbox/policy/win/lpac_capability.h"
#endif // BUILDFLAG(IS_WIN)
namespace {
#if BUILDFLAG(ENABLE_CDM_STORAGE_ID)
// Only support version 1 of Storage Id. However, the "latest" version can also
// be requested.
const uint32_t kRequestLatestStorageIdVersion = 0;
const uint32_t kCurrentStorageIdVersion = 1;
std::vector<uint8_t> GetStorageIdSaltFromProfile(
content::RenderFrameHost* rfh) {
DCHECK(rfh);
Profile* profile =
Profile::FromBrowserContext(rfh->GetProcess()->GetBrowserContext());
return MediaStorageIdSalt::GetSalt(profile->GetPrefs());
}
#endif // BUILDFLAG(ENABLE_CDM_STORAGE_ID)
#if BUILDFLAG(IS_WIN)
const char kCdmStore[] = "MediaFoundationCdmStore";
base::FilePath GetCdmStorePathRootForProfile(
const base::FilePath& profile_path) {
return profile_path.AppendASCII(kCdmStore).AppendASCII(
base::SysInfo::ProcessCPUArchitecture());
}
#endif // BUILDFLAG(IS_WIN)
} // namespace
#if BUILDFLAG(IS_WIN)
bool CreateCdmStorePathRootAndGrantAccessIfNeeded(
const base::FilePath& cdm_store_path_root) {
if (!media::MediaFoundationCdm::IsAvailable()) {
DLOG(ERROR) << "Granting access to LPAC process is not supported prior to "
"Windows 10.";
return false;
}
// If the path exist, we can assume the right permission are already
// set on it.
if (base::PathExists(cdm_store_path_root))
return true;
base::File::Error file_error;
if (!base::CreateDirectoryAndGetError(cdm_store_path_root, &file_error)) {
DLOG(ERROR) << "Create CDM store path failed with " << file_error;
return false;
}
auto sids = base::win::Sid::FromNamedCapabilityVector(
{sandbox::policy::kMediaFoundationCdmData});
return base::win::GrantAccessToPath(
cdm_store_path_root, *sids,
FILE_GENERIC_READ | FILE_GENERIC_WRITE | GENERIC_EXECUTE | DELETE,
CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE);
}
std::unique_ptr<media::MediaFoundationCdmData>
GetMediaFoundationCdmDataInternal(const base::FilePath profile_path,
std::unique_ptr<CdmPrefData> pref_data) {
DCHECK(pref_data);
auto cdm_store_path_root = GetCdmStorePathRootForProfile(profile_path);
if (!CreateCdmStorePathRootAndGrantAccessIfNeeded(cdm_store_path_root)) {
return nullptr;
}
std::unique_ptr<media::MediaFoundationCdmData> cdm_data;
return std::make_unique<media::MediaFoundationCdmData>(
pref_data->origin_id(), pref_data->client_token(), cdm_store_path_root);
}
#endif // BUILDFLAG(IS_WIN)
// static
void CdmDocumentServiceImpl::Create(
content::RenderFrameHost* render_frame_host,
mojo::PendingReceiver<media::mojom::CdmDocumentService> receiver) {
DVLOG(2) << __func__;
CHECK(render_frame_host);
// PlatformVerificationFlow and the pref service requires to be run/accessed
// on the UI thread.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// The object is bound to the lifetime of |render_frame_host| and the mojo
// connection. See DocumentService for details.
new CdmDocumentServiceImpl(*render_frame_host, std::move(receiver));
}
CdmDocumentServiceImpl::CdmDocumentServiceImpl(
content::RenderFrameHost& render_frame_host,
mojo::PendingReceiver<media::mojom::CdmDocumentService> receiver)
: DocumentService(render_frame_host, std::move(receiver)) {}
CdmDocumentServiceImpl::~CdmDocumentServiceImpl() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
void CdmDocumentServiceImpl::ChallengePlatform(
const std::string& service_id,
const std::string& challenge,
ChallengePlatformCallback callback) {
DVLOG(2) << __func__;
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// TODO(crbug.com/676224). This should be commented out at the mojom
// level so that it's only available for ChromeOS.
#if BUILDFLAG(IS_CHROMEOS)
bool success = platform_verification::PerformBrowserChecks(
render_frame_host().GetMainFrame());
if (!success) {
std::move(callback).Run(false, std::string(), std::string(), std::string());
return;
}
#endif
#if BUILDFLAG(IS_CHROMEOS_LACROS)
auto* lacros_service = chromeos::LacrosService::Get();
if (lacros_service &&
lacros_service->IsAvailable<crosapi::mojom::ContentProtection>() &&
lacros_service->GetInterfaceVersion(
crosapi::mojom::ContentProtection::Uuid_) >=
static_cast<int>(crosapi::mojom::ContentProtection::
kChallengePlatformMinVersion)) {
lacros_service->GetRemote<crosapi::mojom::ContentProtection>()
->ChallengePlatform(
service_id, challenge,
base::BindOnce(&CdmDocumentServiceImpl::OnPlatformChallenged,
weak_factory_.GetWeakPtr(), std::move(callback)));
return;
}
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (!platform_verification_flow_)
platform_verification_flow_ =
base::MakeRefCounted<ash::attestation::PlatformVerificationFlow>();
platform_verification_flow_->ChallengePlatformKey(
content::WebContents::FromRenderFrameHost(&render_frame_host()),
service_id, challenge,
base::BindOnce(&CdmDocumentServiceImpl::OnPlatformChallenged,
weak_factory_.GetWeakPtr(), std::move(callback)));
#else
// Not supported, so return failure.
std::move(callback).Run(false, std::string(), std::string(), std::string());
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
void CdmDocumentServiceImpl::OnPlatformChallenged(
ChallengePlatformCallback callback,
PlatformVerificationResult result,
const std::string& signed_data,
const std::string& signature,
const std::string& platform_key_certificate) {
DVLOG(2) << __func__ << ": " << result;
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (result != ash::attestation::PlatformVerificationFlow::SUCCESS) {
DCHECK(signed_data.empty());
DCHECK(signature.empty());
DCHECK(platform_key_certificate.empty());
LOG(ERROR) << "Platform verification failed.";
std::move(callback).Run(false, "", "", "");
return;
}
DCHECK(!signed_data.empty());
DCHECK(!signature.empty());
DCHECK(!platform_key_certificate.empty());
std::move(callback).Run(true, signed_data, signature,
platform_key_certificate);
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
void CdmDocumentServiceImpl::OnPlatformChallenged(
ChallengePlatformCallback callback,
crosapi::mojom::ChallengePlatformResultPtr result) {
if (!result) {
LOG(ERROR) << "Platform verification failed.";
std::move(callback).Run(false, "", "", "");
return;
}
std::move(callback).Run(true, std::move(result->signed_data),
std::move(result->signed_data_signature),
std::move(result->platform_key_certificate));
}
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
void CdmDocumentServiceImpl::GetStorageId(uint32_t version,
GetStorageIdCallback callback) {
DVLOG(2) << __func__ << " version: " << version;
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// TODO(crbug.com/676224). This should be commented out at the mojom
// level so that it's only available if Storage Id is available.
#if BUILDFLAG(ENABLE_CDM_STORAGE_ID)
// Check that the request is for a supported version.
if (version == kCurrentStorageIdVersion ||
version == kRequestLatestStorageIdVersion) {
ComputeStorageId(
GetStorageIdSaltFromProfile(&render_frame_host()), origin(),
base::BindOnce(&CdmDocumentServiceImpl::OnStorageIdResponse,
weak_factory_.GetWeakPtr(), std::move(callback)));
return;
}
#endif // BUILDFLAG(ENABLE_CDM_STORAGE_ID)
// Version not supported, so no Storage Id to return.
DVLOG(2) << __func__ << " not supported";
std::move(callback).Run(version, std::vector<uint8_t>());
}
#if BUILDFLAG(ENABLE_CDM_STORAGE_ID)
void CdmDocumentServiceImpl::OnStorageIdResponse(
GetStorageIdCallback callback,
const std::vector<uint8_t>& storage_id) {
DVLOG(2) << __func__ << " version: " << kCurrentStorageIdVersion
<< ", size: " << storage_id.size();
std::move(callback).Run(kCurrentStorageIdVersion, storage_id);
}
#endif // BUILDFLAG(ENABLE_CDM_STORAGE_ID)
#if BUILDFLAG(IS_CHROMEOS)
void CdmDocumentServiceImpl::IsVerifiedAccessEnabled(
IsVerifiedAccessEnabledCallback callback) {
// If we are in guest/incognito mode, then verified access is effectively
// disabled.
Profile* profile =
Profile::FromBrowserContext(render_frame_host().GetBrowserContext());
if (profile->IsOffTheRecord() || profile->IsGuestSession()) {
std::move(callback).Run(false);
return;
}
#if BUILDFLAG(IS_CHROMEOS_LACROS)
auto* lacros_service = chromeos::LacrosService::Get();
if (lacros_service &&
lacros_service->IsAvailable<crosapi::mojom::ContentProtection>() &&
lacros_service->GetInterfaceVersion(
crosapi::mojom::ContentProtection::Uuid_) >=
static_cast<int>(crosapi::mojom::ContentProtection::
kIsVerifiedAccessEnabledMinVersion)) {
lacros_service->GetRemote<crosapi::mojom::ContentProtection>()
->IsVerifiedAccessEnabled(std::move(callback));
} else {
std::move(callback).Run(false);
}
#else // BUILDFLAG(IS_CHROMEOS_LACROS)
bool enabled_for_device = false;
ash::CrosSettings::Get()->GetBoolean(
ash::kAttestationForContentProtectionEnabled, &enabled_for_device);
std::move(callback).Run(enabled_for_device);
#endif // else BUILDFLAG(IS_CHROMEOS_LACROS)
}
#endif // BUILDFLAG(IS_CHROMEOS)
#if BUILDFLAG(IS_WIN)
void CdmDocumentServiceImpl::GetMediaFoundationCdmData(
GetMediaFoundationCdmDataCallback callback) {
const url::Origin cdm_origin = origin();
if (cdm_origin.opaque()) {
mojo::ReportBadMessage("EME use is not allowed on opaque origin");
return;
}
Profile* profile =
Profile::FromBrowserContext(render_frame_host().GetBrowserContext());
PrefService* user_prefs = profile->GetPrefs();
std::unique_ptr<CdmPrefData> pref_data =
CdmPrefServiceHelper::GetCdmPrefData(user_prefs, cdm_origin);
if (!pref_data) {
std::move(callback).Run(nullptr);
return;
}
// PostTask because the task is doing IO operation that can block.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
base::BindOnce(&GetMediaFoundationCdmDataInternal, profile->GetPath(),
std::move(pref_data)),
std::move(callback));
}
void CdmDocumentServiceImpl::SetCdmClientToken(
const std::vector<uint8_t>& client_token) {
const url::Origin cdm_origin = origin();
if (cdm_origin.opaque()) {
mojo::ReportBadMessage("EME use is not allowed on opaque origin");
return;
}
PrefService* user_prefs =
Profile::FromBrowserContext(render_frame_host().GetBrowserContext())
->GetPrefs();
CdmPrefServiceHelper::SetCdmClientToken(user_prefs, cdm_origin, client_token);
}
void CdmDocumentServiceImpl::OnCdmEvent(media::CdmEvent event) {
DVLOG(1) << __func__ << ": event=" << static_cast<int>(event);
// CdmDocumentServiceImpl is shared by all CDMs in the same RenderFrame.
//
// We choose to only report a significant playback at most once and an error
// at most once because:
// 1. A site could create many CDM instances, e.g. to prefetch licenses. This
// could cause multiple errors to be reported.
// 2. The media::Renderer could be destroyed and then recreated as part of the
// suspend/resume process (e.g. paused for long time).This could cause
// multiple significant playback to be reported.
// In both cases, our data could be skewed if we don't throttle them.
//
// If an error happens after a significant playback both will be reported.
// This is fine since MediaFoundationServiceMonitor calculates a score.
switch (event) {
case media::CdmEvent::kSignificantPlayback:
if (!has_reported_significant_playback_) {
has_reported_significant_playback_ = true;
MediaFoundationServiceMonitor::GetInstance()->OnSignificantPlayback();
}
break;
case media::CdmEvent::kPlaybackError:
[[fallthrough]];
case media::CdmEvent::kCdmError:
if (!has_reported_cdm_error_) {
has_reported_cdm_error_ = true;
MediaFoundationServiceMonitor::GetInstance()->OnPlaybackOrCdmError();
}
break;
}
}
// This function goes over each folder located under the MediaFoundationCdm
// store root path and delete them as needed. A folder needs to be deleted for
// the following reason:
// - The origin id the folder is associated with is no longer present in the
// PrefService
// - The folder refers to an origin matched by `filter` AND the folder was last
// modified between `start` and `end`
void DeleteMediaFoundationCdmData(
const base::FilePath& profile_path,
const std::map<std::string, url::Origin> origin_id_mapping,
base::Time start,
base::Time end,
const base::RepeatingCallback<bool(const GURL&)>& filter) {
auto cdm_store_path_root = GetCdmStorePathRootForProfile(profile_path);
// Enumerate all folder under `cdm_store_path_root` which should give a list
// of folder whose names are origin ids. Each folder contains CDM data
// associated with that origin id.
//
base::FileEnumerator directory_enumerator(cdm_store_path_root,
/*recursive=*/false,
base::FileEnumerator::DIRECTORIES);
for (auto file_path = directory_enumerator.Next(); !file_path.value().empty();
file_path = directory_enumerator.Next()) {
// The folder name is a string representation of a base::UnguessableToken,
// using MaybeAsASCII() is fine.
std::string origin_id_string = file_path.BaseName().MaybeAsASCII();
if (origin_id_string.empty())
continue;
DVLOG(2) << __func__ << ": Processing: " << file_path;
absl::optional<url::Origin> origin = absl::nullopt;
if (origin_id_mapping.count(origin_id_string) != 0)
origin = origin_id_mapping.at(origin_id_string);
// If we couldn't find the origin, this mean the origin was not present in
// the PrefService and we should also delete the folder.
if (!origin) {
base::DeletePathRecursively(file_path);
continue;
}
// Null filter indicates that we should delete everything.
if (filter && !filter.Run(GURL(origin->Serialize())))
continue;
// Now go over every files under the current folder and delete them if
// needed.
base::FileEnumerator file_enumerator(file_path, /*recursive=*/true,
base::FileEnumerator::FILES);
// If at least one files was modified between `start` and `end`, we should
// delete the whole folder.
bool should_delete = false;
for (auto cdm_data_file_path = file_enumerator.Next();
!cdm_data_file_path.value().empty();
cdm_data_file_path = file_enumerator.Next()) {
DVLOG(2) << __func__ << ": - Processing: " << cdm_data_file_path;
base::File::Info file_info;
if (!base::GetFileInfo(cdm_data_file_path, &file_info)) {
DVLOG(ERROR) << "Failed to get FileInfo";
should_delete = true;
break;
}
if (file_info.last_modified >= start &&
(end.is_null() || file_info.last_modified <= end)) {
DVLOG(2) << "Deleting file. Last modified: " << file_info.last_modified;
should_delete = true;
break;
}
}
if (should_delete)
base::DeletePathRecursively(file_path);
}
}
void CdmDocumentServiceImpl::ClearCdmData(
Profile* profile,
base::Time start,
base::Time end,
const base::RepeatingCallback<bool(const GURL&)>& filter,
base::OnceClosure complete_cb) {
PrefService* user_prefs = profile->GetPrefs();
CdmPrefServiceHelper::ClearCdmPreferenceData(user_prefs, start, end, filter);
// Get the origin_id mapping here because the PrefService needs to be accessed
// from the UI thread.
auto origin_id_mapping = CdmPrefServiceHelper::GetOriginIdMapping(user_prefs);
// PostTask because is doing IO operation that can block.
base::ThreadPool::PostTaskAndReply(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
base::BindOnce(&DeleteMediaFoundationCdmData, profile->GetPath(),
std::move(origin_id_mapping), start, end, filter),
std::move(complete_cb));
}
#endif // BUILDFLAG(IS_WIN)