| // 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) |