| // Copyright 2013 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 "media/cdm/library_cdm/clear_key_cdm/clear_key_cdm.h" |
| |
| #include <algorithm> |
| #include <cstring> |
| #include <sstream> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/files/file.h" |
| #include "base/logging.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "media/base/cdm_callback_promise.h" |
| #include "media/base/cdm_key_information.h" |
| #include "media/base/decoder_buffer.h" |
| #include "media/base/decrypt_config.h" |
| #include "media/base/encryption_pattern.h" |
| #include "media/cdm/api/content_decryption_module_ext.h" |
| #include "media/cdm/json_web_key.h" |
| #include "media/cdm/library_cdm/cdm_host_proxy.h" |
| #include "media/cdm/library_cdm/cdm_host_proxy_impl.h" |
| #include "media/cdm/library_cdm/clear_key_cdm/cdm_file_io_test.h" |
| #include "media/cdm/library_cdm/clear_key_cdm/cdm_proxy_test.h" |
| #include "media/cdm/library_cdm/clear_key_cdm/cdm_video_decoder.h" |
| #include "media/media_buildflags.h" |
| |
| #if defined(CLEAR_KEY_CDM_USE_FFMPEG_DECODER) |
| #include "base/at_exit.h" |
| #include "base/files/file_path.h" |
| #include "base/path_service.h" |
| #include "media/base/media.h" |
| #include "media/cdm/library_cdm/clear_key_cdm/ffmpeg_cdm_audio_decoder.h" |
| #include "media/cdm/library_cdm/clear_key_cdm/ffmpeg_cdm_video_decoder.h" |
| |
| #if !defined COMPONENT_BUILD |
| static base::AtExitManager g_at_exit_manager; |
| #endif |
| #endif // CLEAR_KEY_CDM_USE_FFMPEG_DECODER |
| |
| const char kClearKeyCdmVersion[] = "0.1.0.1"; |
| const char kExternalClearKeyKeySystem[] = "org.chromium.externalclearkey"; |
| |
| // Variants of External Clear Key key system to test different scenarios. |
| const char kExternalClearKeyDecryptOnlyKeySystem[] = |
| "org.chromium.externalclearkey.decryptonly"; |
| const char kExternalClearKeyMessageTypeTestKeySystem[] = |
| "org.chromium.externalclearkey.messagetypetest"; |
| const char kExternalClearKeyFileIOTestKeySystem[] = |
| "org.chromium.externalclearkey.fileiotest"; |
| const char kExternalClearKeyOutputProtectionTestKeySystem[] = |
| "org.chromium.externalclearkey.outputprotectiontest"; |
| const char kExternalClearKeyPlatformVerificationTestKeySystem[] = |
| "org.chromium.externalclearkey.platformverificationtest"; |
| const char kExternalClearKeyCrashKeySystem[] = |
| "org.chromium.externalclearkey.crash"; |
| const char kExternalClearKeyVerifyCdmHostTestKeySystem[] = |
| "org.chromium.externalclearkey.verifycdmhosttest"; |
| const char kExternalClearKeyStorageIdTestKeySystem[] = |
| "org.chromium.externalclearkey.storageidtest"; |
| const char kExternalClearKeyDifferentGuidTestKeySystem[] = |
| "org.chromium.externalclearkey.differentguid"; |
| const char kExternalClearKeyCdmProxyTestKeySystem[] = |
| "org.chromium.externalclearkey.cdmproxytest"; |
| |
| const int64_t kSecondsPerMinute = 60; |
| const int64_t kMsPerSecond = 1000; |
| const int64_t kMaxTimerDelayMs = 1 * kSecondsPerMinute * kMsPerSecond; |
| |
| // CDM unit test result header. Must be in sync with UNIT_TEST_RESULT_HEADER in |
| // media/test/data/eme_player_js/globals.js. |
| const char kUnitTestResultHeader[] = "UNIT_TEST_RESULT"; |
| |
| const char kDummyIndividualizationRequest[] = "dummy individualization request"; |
| |
| static bool g_is_cdm_module_initialized = false; |
| |
| // Copies |input_buffer| into a DecoderBuffer. If the |input_buffer| is |
| // empty, an empty (end-of-stream) DecoderBuffer is returned. |
| static scoped_refptr<media::DecoderBuffer> CopyDecoderBufferFrom( |
| const cdm::InputBuffer_2& input_buffer) { |
| if (!input_buffer.data) { |
| DCHECK(!input_buffer.data_size); |
| return media::DecoderBuffer::CreateEOSBuffer(); |
| } |
| |
| // TODO(xhwang): Get rid of this copy. |
| scoped_refptr<media::DecoderBuffer> output_buffer = |
| media::DecoderBuffer::CopyFrom(input_buffer.data, input_buffer.data_size); |
| output_buffer->set_timestamp( |
| base::TimeDelta::FromMicroseconds(input_buffer.timestamp)); |
| |
| if (input_buffer.encryption_scheme == cdm::EncryptionScheme::kUnencrypted) |
| return output_buffer; |
| |
| DCHECK_GT(input_buffer.iv_size, 0u); |
| DCHECK_GT(input_buffer.key_id_size, 0u); |
| std::vector<media::SubsampleEntry> subsamples; |
| for (uint32_t i = 0; i < input_buffer.num_subsamples; ++i) { |
| subsamples.push_back( |
| media::SubsampleEntry(input_buffer.subsamples[i].clear_bytes, |
| input_buffer.subsamples[i].cipher_bytes)); |
| } |
| |
| const std::string key_id_string( |
| reinterpret_cast<const char*>(input_buffer.key_id), |
| input_buffer.key_id_size); |
| const std::string iv_string(reinterpret_cast<const char*>(input_buffer.iv), |
| input_buffer.iv_size); |
| if (input_buffer.encryption_scheme == cdm::EncryptionScheme::kCenc) { |
| output_buffer->set_decrypt_config(media::DecryptConfig::CreateCencConfig( |
| key_id_string, iv_string, subsamples)); |
| } else { |
| DCHECK_EQ(input_buffer.encryption_scheme, cdm::EncryptionScheme::kCbcs); |
| output_buffer->set_decrypt_config(media::DecryptConfig::CreateCbcsConfig( |
| key_id_string, iv_string, subsamples, |
| media::EncryptionPattern(input_buffer.pattern.crypt_byte_block, |
| input_buffer.pattern.skip_byte_block))); |
| } |
| |
| return output_buffer; |
| } |
| |
| static std::string GetUnitTestResultMessage(bool success) { |
| std::string message(kUnitTestResultHeader); |
| message += success ? '1' : '0'; |
| return message; |
| } |
| |
| static cdm::Exception ConvertException( |
| media::CdmPromise::Exception exception_code) { |
| switch (exception_code) { |
| case media::CdmPromise::Exception::NOT_SUPPORTED_ERROR: |
| return cdm::kExceptionNotSupportedError; |
| case media::CdmPromise::Exception::INVALID_STATE_ERROR: |
| return cdm::kExceptionInvalidStateError; |
| case media::CdmPromise::Exception::TYPE_ERROR: |
| return cdm::kExceptionTypeError; |
| case media::CdmPromise::Exception::QUOTA_EXCEEDED_ERROR: |
| return cdm::kExceptionQuotaExceededError; |
| } |
| NOTREACHED(); |
| return cdm::kExceptionInvalidStateError; |
| } |
| |
| static media::CdmSessionType ConvertSessionType(cdm::SessionType session_type) { |
| switch (session_type) { |
| case cdm::kTemporary: |
| return media::CdmSessionType::TEMPORARY_SESSION; |
| case cdm::kPersistentLicense: |
| return media::CdmSessionType::PERSISTENT_LICENSE_SESSION; |
| case cdm::kPersistentKeyRelease: |
| return media::CdmSessionType::PERSISTENT_RELEASE_MESSAGE_SESSION; |
| } |
| NOTREACHED(); |
| return media::CdmSessionType::TEMPORARY_SESSION; |
| } |
| |
| static media::EmeInitDataType ConvertInitDataType( |
| cdm::InitDataType init_data_type) { |
| switch (init_data_type) { |
| case cdm::kCenc: |
| return media::EmeInitDataType::CENC; |
| case cdm::kKeyIds: |
| return media::EmeInitDataType::KEYIDS; |
| case cdm::kWebM: |
| return media::EmeInitDataType::WEBM; |
| } |
| NOTREACHED(); |
| return media::EmeInitDataType::UNKNOWN; |
| } |
| |
| cdm::KeyStatus ConvertKeyStatus(media::CdmKeyInformation::KeyStatus status) { |
| switch (status) { |
| case media::CdmKeyInformation::KeyStatus::USABLE: |
| return cdm::kUsable; |
| case media::CdmKeyInformation::KeyStatus::INTERNAL_ERROR: |
| return cdm::kInternalError; |
| case media::CdmKeyInformation::KeyStatus::EXPIRED: |
| return cdm::kExpired; |
| case media::CdmKeyInformation::KeyStatus::OUTPUT_RESTRICTED: |
| return cdm::kOutputRestricted; |
| case media::CdmKeyInformation::KeyStatus::OUTPUT_DOWNSCALED: |
| return cdm::kOutputDownscaled; |
| case media::CdmKeyInformation::KeyStatus::KEY_STATUS_PENDING: |
| return cdm::kStatusPending; |
| case media::CdmKeyInformation::KeyStatus::RELEASED: |
| return cdm::kReleased; |
| } |
| NOTREACHED(); |
| return cdm::kInternalError; |
| } |
| |
| cdm::MessageType ConvertMessageType(media::CdmMessageType message_type) { |
| switch (message_type) { |
| case media::CdmMessageType::LICENSE_REQUEST: |
| return cdm::kLicenseRequest; |
| case media::CdmMessageType::LICENSE_RENEWAL: |
| return cdm::kLicenseRenewal; |
| case media::CdmMessageType::LICENSE_RELEASE: |
| return cdm::kLicenseRelease; |
| case media::CdmMessageType::INDIVIDUALIZATION_REQUEST: |
| return cdm::kIndividualizationRequest; |
| } |
| |
| NOTREACHED(); |
| return cdm::kLicenseRequest; |
| } |
| |
| // Shallow copy all the key information from |keys_info| into |keys_vector|. |
| // |keys_vector| is only valid for the lifetime of |keys_info| because it |
| // contains pointers into the latter. |
| void ConvertCdmKeysInfo(const media::CdmKeysInfo& keys_info, |
| std::vector<cdm::KeyInformation>* keys_vector) { |
| keys_vector->reserve(keys_info.size()); |
| for (const auto& key_info : keys_info) { |
| cdm::KeyInformation key = {}; |
| key.key_id = key_info->key_id.data(); |
| key.key_id_size = key_info->key_id.size(); |
| key.status = ConvertKeyStatus(key_info->status); |
| key.system_code = key_info->system_code; |
| keys_vector->push_back(key); |
| } |
| } |
| |
| void INITIALIZE_CDM_MODULE() { |
| DVLOG(1) << __func__; |
| #if defined(CLEAR_KEY_CDM_USE_FFMPEG_DECODER) |
| media::InitializeMediaLibrary(); |
| #endif // CLEAR_KEY_CDM_USE_FFMPEG_DECODER |
| |
| g_is_cdm_module_initialized = true; |
| } |
| |
| void DeinitializeCdmModule() { |
| DVLOG(1) << __func__; |
| } |
| |
| void* CreateCdmInstance(int cdm_interface_version, |
| const char* key_system, |
| uint32_t key_system_size, |
| GetCdmHostFunc get_cdm_host_func, |
| void* user_data) { |
| DVLOG(1) << "CreateCdmInstance()"; |
| |
| if (!g_is_cdm_module_initialized) { |
| DVLOG(1) << "CDM module not initialized."; |
| return nullptr; |
| } |
| |
| std::string key_system_string(key_system, key_system_size); |
| if (key_system_string != kExternalClearKeyKeySystem && |
| key_system_string != kExternalClearKeyDecryptOnlyKeySystem && |
| key_system_string != kExternalClearKeyMessageTypeTestKeySystem && |
| key_system_string != kExternalClearKeyFileIOTestKeySystem && |
| key_system_string != kExternalClearKeyOutputProtectionTestKeySystem && |
| key_system_string != kExternalClearKeyPlatformVerificationTestKeySystem && |
| key_system_string != kExternalClearKeyCrashKeySystem && |
| key_system_string != kExternalClearKeyVerifyCdmHostTestKeySystem && |
| key_system_string != kExternalClearKeyStorageIdTestKeySystem && |
| key_system_string != kExternalClearKeyDifferentGuidTestKeySystem && |
| key_system_string != kExternalClearKeyCdmProxyTestKeySystem) { |
| DVLOG(1) << "Unsupported key system:" << key_system_string; |
| return nullptr; |
| } |
| |
| // We support CDM_9, CDM_10 and CDM_11. |
| using CDM_9 = cdm::ContentDecryptionModule_9; |
| using CDM_10 = cdm::ContentDecryptionModule_10; |
| using CDM_11 = cdm::ContentDecryptionModule_11; |
| |
| if (cdm_interface_version == CDM_9::kVersion) { |
| CDM_9::Host* host = static_cast<CDM_9::Host*>( |
| get_cdm_host_func(CDM_9::Host::kVersion, user_data)); |
| if (!host) |
| return nullptr; |
| |
| DVLOG(1) << __func__ << ": Create ClearKeyCdm with CDM_9::Host."; |
| return static_cast<CDM_9*>(new media::ClearKeyCdm(host, key_system_string)); |
| } |
| |
| if (cdm_interface_version == CDM_10::kVersion) { |
| CDM_10::Host* host = static_cast<CDM_10::Host*>( |
| get_cdm_host_func(CDM_10::Host::kVersion, user_data)); |
| if (!host) |
| return nullptr; |
| |
| DVLOG(1) << __func__ << ": Create ClearKeyCdm with CDM_10::Host."; |
| return static_cast<CDM_10*>( |
| new media::ClearKeyCdm(host, key_system_string)); |
| } |
| |
| if (cdm_interface_version == CDM_11::kVersion) { |
| CDM_11::Host* host = static_cast<CDM_11::Host*>( |
| get_cdm_host_func(CDM_11::Host::kVersion, user_data)); |
| if (!host) |
| return nullptr; |
| |
| DVLOG(1) << __func__ << ": Create ClearKeyCdm with CDM_11::Host."; |
| return static_cast<CDM_11*>( |
| new media::ClearKeyCdm(host, key_system_string)); |
| } |
| |
| return nullptr; |
| } |
| |
| const char* GetCdmVersion() { |
| return kClearKeyCdmVersion; |
| } |
| |
| static bool g_verify_host_files_result = false; |
| |
| // Makes sure files and corresponding signature files are readable but not |
| // writable. |
| bool VerifyCdmHost_0(const cdm::HostFile* host_files, uint32_t num_files) { |
| DVLOG(1) << __func__ << ": " << num_files; |
| |
| // We should always have the CDM and at least one common file. |
| // The common CDM host file (e.g. chrome) might not exist since we are running |
| // in browser_tests. |
| const uint32_t kMinNumHostFiles = 2; |
| |
| // We should always have the CDM. |
| const int kNumCdmFiles = 1; |
| |
| if (num_files < kMinNumHostFiles) { |
| LOG(ERROR) << "Too few host files: " << num_files; |
| g_verify_host_files_result = false; |
| return true; |
| } |
| |
| int num_opened_files = 0; |
| for (uint32_t i = 0; i < num_files; ++i) { |
| const int kBytesToRead = 10; |
| std::vector<char> buffer(kBytesToRead); |
| |
| base::File file(static_cast<base::PlatformFile>(host_files[i].file)); |
| if (!file.IsValid()) |
| continue; |
| |
| num_opened_files++; |
| |
| int bytes_read = file.Read(0, buffer.data(), buffer.size()); |
| if (bytes_read != kBytesToRead) { |
| LOG(ERROR) << "File bytes read: " << bytes_read; |
| g_verify_host_files_result = false; |
| return true; |
| } |
| |
| // TODO(xhwang): Check that the files are not writable. |
| // TODO(xhwang): Also verify the signature file when it's available. |
| } |
| |
| // We should always have CDM files opened. |
| if (num_opened_files < kNumCdmFiles) { |
| LOG(ERROR) << "Too few opened files: " << num_opened_files; |
| g_verify_host_files_result = false; |
| return true; |
| } |
| |
| g_verify_host_files_result = true; |
| return true; |
| } |
| |
| namespace media { |
| |
| namespace { |
| |
| cdm::InputBuffer_2 ToInputBuffer_2(cdm::InputBuffer_1 encrypted_buffer) { |
| cdm::InputBuffer_2 buffer = {}; |
| buffer.data = encrypted_buffer.data; |
| buffer.data_size = encrypted_buffer.data_size; |
| buffer.key_id = encrypted_buffer.key_id; |
| buffer.key_id_size = encrypted_buffer.key_id_size; |
| buffer.iv = encrypted_buffer.iv; |
| buffer.iv_size = encrypted_buffer.iv_size; |
| buffer.subsamples = encrypted_buffer.subsamples; |
| buffer.num_subsamples = encrypted_buffer.num_subsamples; |
| buffer.timestamp = encrypted_buffer.timestamp; |
| // InputBuffer_1 must be either 'cenc' or unencrypted. |
| buffer.encryption_scheme = (buffer.iv_size == 0) |
| ? cdm::EncryptionScheme::kUnencrypted |
| : cdm::EncryptionScheme::kCenc; |
| buffer.pattern = {0, 0}; |
| return buffer; |
| } |
| |
| } // namespace |
| |
| template <typename HostInterface> |
| ClearKeyCdm::ClearKeyCdm(HostInterface* host, const std::string& key_system) |
| : host_interface_version_(HostInterface::kVersion), |
| cdm_host_proxy_(new CdmHostProxyImpl<HostInterface>(host)), |
| cdm_(new ClearKeyPersistentSessionCdm( |
| cdm_host_proxy_.get(), |
| base::Bind(&ClearKeyCdm::OnSessionMessage, base::Unretained(this)), |
| base::Bind(&ClearKeyCdm::OnSessionClosed, base::Unretained(this)), |
| base::Bind(&ClearKeyCdm::OnSessionKeysChange, base::Unretained(this)), |
| base::Bind(&ClearKeyCdm::OnSessionExpirationUpdate, |
| base::Unretained(this)))), |
| key_system_(key_system) { |
| DCHECK(g_is_cdm_module_initialized); |
| } |
| |
| ClearKeyCdm::~ClearKeyCdm() = default; |
| |
| void ClearKeyCdm::Initialize(bool allow_distinctive_identifier, |
| bool allow_persistent_state) { |
| // Implementation doesn't use distinctive identifier and will only need |
| // to check persistent state permission. |
| allow_persistent_state_ = allow_persistent_state; |
| |
| // CdmProxy must be created during initialization time. OnInitialized() will |
| // be called in OnCdmProxyTestComplete(). |
| if (key_system_ == kExternalClearKeyCdmProxyTestKeySystem) { |
| StartCdmProxyTest(); |
| return; |
| } |
| |
| cdm_host_proxy_->OnInitialized(true); |
| } |
| |
| void ClearKeyCdm::Initialize(bool allow_distinctive_identifier, |
| bool allow_persistent_state, |
| bool use_hw_secure_codecs) { |
| Initialize(allow_distinctive_identifier, allow_persistent_state); |
| } |
| |
| void ClearKeyCdm::GetStatusForPolicy(uint32_t promise_id, |
| const cdm::Policy& policy) { |
| // Pretend the device is HDCP 2.0 compliant. |
| const cdm::HdcpVersion kDeviceHdcpVersion = cdm::kHdcpVersion2_0; |
| |
| if (policy.min_hdcp_version <= kDeviceHdcpVersion) { |
| cdm_host_proxy_->OnResolveKeyStatusPromise(promise_id, cdm::kUsable); |
| return; |
| } |
| |
| cdm_host_proxy_->OnResolveKeyStatusPromise(promise_id, |
| cdm::kOutputRestricted); |
| } |
| |
| void ClearKeyCdm::CreateSessionAndGenerateRequest( |
| uint32_t promise_id, |
| cdm::SessionType session_type, |
| cdm::InitDataType init_data_type, |
| const uint8_t* init_data, |
| uint32_t init_data_size) { |
| DVLOG(1) << __func__; |
| |
| if (session_type != cdm::kTemporary && !allow_persistent_state_) { |
| OnPromiseFailed(promise_id, CdmPromise::Exception::INVALID_STATE_ERROR, 0, |
| "Persistent state not allowed."); |
| return; |
| } |
| |
| std::unique_ptr<media::NewSessionCdmPromise> promise( |
| new media::CdmCallbackPromise<std::string>( |
| base::Bind(&ClearKeyCdm::OnSessionCreated, base::Unretained(this), |
| promise_id), |
| base::Bind(&ClearKeyCdm::OnPromiseFailed, base::Unretained(this), |
| promise_id))); |
| cdm_->CreateSessionAndGenerateRequest( |
| ConvertSessionType(session_type), ConvertInitDataType(init_data_type), |
| std::vector<uint8_t>(init_data, init_data + init_data_size), |
| std::move(promise)); |
| |
| // Run unit tests if applicable. Unit test results are reported in the form of |
| // a session message. Therefore it can only be called after session creation. |
| if (key_system_ == kExternalClearKeyFileIOTestKeySystem) { |
| StartFileIOTest(); |
| } else if (key_system_ == kExternalClearKeyOutputProtectionTestKeySystem) { |
| StartOutputProtectionTest(); |
| } else if (key_system_ == |
| kExternalClearKeyPlatformVerificationTestKeySystem) { |
| StartPlatformVerificationTest(); |
| } else if (key_system_ == kExternalClearKeyVerifyCdmHostTestKeySystem) { |
| ReportVerifyCdmHostTestResult(); |
| } else if (key_system_ == kExternalClearKeyStorageIdTestKeySystem) { |
| StartStorageIdTest(); |
| } else if (key_system_ == kExternalClearKeyCdmProxyTestKeySystem) { |
| ReportCdmProxyTestResult(); |
| } |
| } |
| |
| void ClearKeyCdm::LoadSession(uint32_t promise_id, |
| cdm::SessionType session_type, |
| const char* session_id, |
| uint32_t session_id_length) { |
| DVLOG(1) << __func__; |
| DCHECK_EQ(session_type, cdm::kPersistentLicense); |
| DCHECK(allow_persistent_state_); |
| std::string web_session_str(session_id, session_id_length); |
| |
| std::unique_ptr<media::NewSessionCdmPromise> promise( |
| new media::CdmCallbackPromise<std::string>( |
| base::Bind(&ClearKeyCdm::OnSessionCreated, base::Unretained(this), |
| promise_id), |
| base::Bind(&ClearKeyCdm::OnPromiseFailed, base::Unretained(this), |
| promise_id))); |
| cdm_->LoadSession(ConvertSessionType(session_type), web_session_str, |
| std::move(promise)); |
| } |
| |
| void ClearKeyCdm::UpdateSession(uint32_t promise_id, |
| const char* session_id, |
| uint32_t session_id_length, |
| const uint8_t* response, |
| uint32_t response_size) { |
| DVLOG(1) << __func__; |
| std::string web_session_str(session_id, session_id_length); |
| |
| std::unique_ptr<media::SimpleCdmPromise> promise( |
| new media::CdmCallbackPromise<>( |
| base::Bind(&ClearKeyCdm::OnUpdateSuccess, base::Unretained(this), |
| promise_id, web_session_str), |
| base::Bind(&ClearKeyCdm::OnPromiseFailed, base::Unretained(this), |
| promise_id))); |
| cdm_->UpdateSession(web_session_str, |
| std::vector<uint8_t>(response, response + response_size), |
| std::move(promise)); |
| } |
| |
| void ClearKeyCdm::OnUpdateSuccess(uint32_t promise_id, |
| const std::string& session_id) { |
| // Now create the expiration changed event. |
| cdm::Time expiration = 0.0; // Never expires. |
| |
| if (key_system_ == kExternalClearKeyMessageTypeTestKeySystem) { |
| // For renewal key system, set a non-zero expiration that is approximately |
| // 100 years after 01 January 1970 UTC. |
| expiration = 3153600000.0; // 100 * 365 * 24 * 60 * 60; |
| |
| if (!has_set_renewal_timer_) { |
| // Make sure the CDM can get time and sleep if necessary. |
| constexpr auto kSleepDuration = base::TimeDelta::FromSeconds(1); |
| auto start_time = base::Time::Now(); |
| base::PlatformThread::Sleep(kSleepDuration); |
| auto time_elapsed = base::Time::Now() - start_time; |
| CHECK_GE(time_elapsed, kSleepDuration); |
| |
| ScheduleNextRenewal(); |
| has_set_renewal_timer_ = true; |
| } |
| |
| // Also send an individualization request if never sent before. Only |
| // supported on Host_10 and later. |
| if (host_interface_version_ >= cdm::Host_10::kVersion && |
| !has_sent_individualization_request_) { |
| has_sent_individualization_request_ = true; |
| const std::string request = kDummyIndividualizationRequest; |
| cdm_host_proxy_->OnSessionMessage(session_id.data(), session_id.length(), |
| cdm::kIndividualizationRequest, |
| request.data(), request.size()); |
| } |
| } |
| |
| cdm_host_proxy_->OnExpirationChange(session_id.data(), session_id.length(), |
| expiration); |
| |
| // Resolve the promise. |
| OnPromiseResolved(promise_id); |
| } |
| |
| void ClearKeyCdm::CloseSession(uint32_t promise_id, |
| const char* session_id, |
| uint32_t session_id_length) { |
| DVLOG(1) << __func__; |
| std::string web_session_str(session_id, session_id_length); |
| |
| std::unique_ptr<media::SimpleCdmPromise> promise( |
| new media::CdmCallbackPromise<>( |
| base::Bind(&ClearKeyCdm::OnPromiseResolved, base::Unretained(this), |
| promise_id), |
| base::Bind(&ClearKeyCdm::OnPromiseFailed, base::Unretained(this), |
| promise_id))); |
| cdm_->CloseSession(web_session_str, std::move(promise)); |
| } |
| |
| void ClearKeyCdm::RemoveSession(uint32_t promise_id, |
| const char* session_id, |
| uint32_t session_id_length) { |
| DVLOG(1) << __func__; |
| std::string web_session_str(session_id, session_id_length); |
| |
| std::unique_ptr<media::SimpleCdmPromise> promise( |
| new media::CdmCallbackPromise<>( |
| base::Bind(&ClearKeyCdm::OnPromiseResolved, base::Unretained(this), |
| promise_id), |
| base::Bind(&ClearKeyCdm::OnPromiseFailed, base::Unretained(this), |
| promise_id))); |
| cdm_->RemoveSession(web_session_str, std::move(promise)); |
| } |
| |
| void ClearKeyCdm::SetServerCertificate(uint32_t promise_id, |
| const uint8_t* server_certificate_data, |
| uint32_t server_certificate_data_size) { |
| DVLOG(1) << __func__; |
| std::unique_ptr<media::SimpleCdmPromise> promise( |
| new media::CdmCallbackPromise<>( |
| base::Bind(&ClearKeyCdm::OnPromiseResolved, base::Unretained(this), |
| promise_id), |
| base::Bind(&ClearKeyCdm::OnPromiseFailed, base::Unretained(this), |
| promise_id))); |
| cdm_->SetServerCertificate( |
| std::vector<uint8_t>( |
| server_certificate_data, |
| server_certificate_data + server_certificate_data_size), |
| std::move(promise)); |
| } |
| |
| void ClearKeyCdm::TimerExpired(void* context) { |
| DVLOG(1) << __func__; |
| DCHECK(has_set_renewal_timer_); |
| std::string renewal_message; |
| if (!next_renewal_message_.empty() && context == &next_renewal_message_[0]) { |
| renewal_message = next_renewal_message_; |
| } else { |
| renewal_message = "ERROR: Invalid timer context found!"; |
| } |
| |
| cdm_host_proxy_->OnSessionMessage( |
| last_session_id_.data(), last_session_id_.length(), cdm::kLicenseRenewal, |
| renewal_message.data(), renewal_message.length()); |
| |
| ScheduleNextRenewal(); |
| } |
| |
| static void CopyDecryptResults(media::Decryptor::Status* status_copy, |
| scoped_refptr<DecoderBuffer>* buffer_copy, |
| media::Decryptor::Status status, |
| scoped_refptr<DecoderBuffer> buffer) { |
| *status_copy = status; |
| *buffer_copy = std::move(buffer); |
| } |
| |
| cdm::Status ClearKeyCdm::Decrypt(const cdm::InputBuffer_1& encrypted_buffer, |
| cdm::DecryptedBlock* decrypted_block) { |
| return Decrypt(ToInputBuffer_2(encrypted_buffer), decrypted_block); |
| } |
| |
| cdm::Status ClearKeyCdm::Decrypt(const cdm::InputBuffer_2& encrypted_buffer, |
| cdm::DecryptedBlock* decrypted_block) { |
| DVLOG(1) << __func__; |
| DCHECK(encrypted_buffer.data); |
| |
| scoped_refptr<DecoderBuffer> buffer; |
| cdm::Status status = DecryptToMediaDecoderBuffer(encrypted_buffer, &buffer); |
| |
| if (status != cdm::kSuccess) |
| return status; |
| |
| DCHECK(buffer->data()); |
| decrypted_block->SetDecryptedBuffer( |
| cdm_host_proxy_->Allocate(buffer->data_size())); |
| memcpy(reinterpret_cast<void*>(decrypted_block->DecryptedBuffer()->Data()), |
| buffer->data(), buffer->data_size()); |
| decrypted_block->DecryptedBuffer()->SetSize(buffer->data_size()); |
| decrypted_block->SetTimestamp(buffer->timestamp().InMicroseconds()); |
| |
| return cdm::kSuccess; |
| } |
| |
| cdm::Status ClearKeyCdm::InitializeAudioDecoder( |
| const cdm::AudioDecoderConfig_1& audio_decoder_config) { |
| // AudioDecoderConfig_1 doesn't specify the encryption scheme, but only |
| // supports 'cenc' or unencrypted media, so expect encrypted audio. |
| cdm::AudioDecoderConfig_2 audio_config = { |
| audio_decoder_config.codec, |
| audio_decoder_config.channel_count, |
| audio_decoder_config.bits_per_channel, |
| audio_decoder_config.samples_per_second, |
| audio_decoder_config.extra_data, |
| audio_decoder_config.extra_data_size, |
| cdm::EncryptionScheme::kCenc}; |
| return InitializeAudioDecoder(audio_config); |
| } |
| |
| cdm::Status ClearKeyCdm::InitializeAudioDecoder( |
| const cdm::AudioDecoderConfig_2& audio_decoder_config) { |
| if (key_system_ == kExternalClearKeyDecryptOnlyKeySystem) |
| return cdm::kInitializationError; |
| |
| #if defined(CLEAR_KEY_CDM_USE_FFMPEG_DECODER) |
| if (!audio_decoder_) |
| audio_decoder_.reset( |
| new media::FFmpegCdmAudioDecoder(cdm_host_proxy_.get())); |
| |
| if (!audio_decoder_->Initialize(audio_decoder_config)) |
| return cdm::kInitializationError; |
| |
| return cdm::kSuccess; |
| #else |
| NOTIMPLEMENTED(); |
| return cdm::kSessionError; |
| #endif // CLEAR_KEY_CDM_USE_FFMPEG_DECODER |
| } |
| |
| cdm::Status ClearKeyCdm::InitializeVideoDecoder( |
| const cdm::VideoDecoderConfig_1& video_decoder_config) { |
| // VideoDecoderConfig_1 doesn't specify the encryption scheme, but only |
| // supports 'cenc' or unencrypted media, so expect encrypted video. |
| cdm::VideoDecoderConfig_2 video_config = { |
| video_decoder_config.codec, video_decoder_config.profile, |
| video_decoder_config.format, video_decoder_config.coded_size, |
| video_decoder_config.extra_data, video_decoder_config.extra_data_size, |
| cdm::EncryptionScheme::kCenc}; |
| return InitializeVideoDecoder(video_config); |
| } |
| |
| cdm::Status ClearKeyCdm::InitializeVideoDecoder( |
| const cdm::VideoDecoderConfig_2& video_decoder_config) { |
| if (key_system_ == kExternalClearKeyDecryptOnlyKeySystem) |
| return cdm::kInitializationError; |
| |
| if (video_decoder_ && video_decoder_->is_initialized()) { |
| DCHECK(!video_decoder_->is_initialized()); |
| return cdm::kInitializationError; |
| } |
| |
| // Any uninitialized decoder will be replaced. |
| video_decoder_ = |
| CreateVideoDecoder(cdm_host_proxy_.get(), video_decoder_config); |
| if (!video_decoder_) |
| return cdm::kInitializationError; |
| |
| return cdm::kSuccess; |
| } |
| |
| void ClearKeyCdm::ResetDecoder(cdm::StreamType decoder_type) { |
| DVLOG(1) << __func__; |
| #if defined(CLEAR_KEY_CDM_USE_FFMPEG_DECODER) |
| switch (decoder_type) { |
| case cdm::kStreamTypeVideo: |
| video_decoder_->Reset(); |
| break; |
| case cdm::kStreamTypeAudio: |
| audio_decoder_->Reset(); |
| break; |
| default: |
| NOTREACHED() << "ResetDecoder(): invalid cdm::StreamType"; |
| } |
| #endif // CLEAR_KEY_CDM_USE_FFMPEG_DECODER |
| } |
| |
| void ClearKeyCdm::DeinitializeDecoder(cdm::StreamType decoder_type) { |
| DVLOG(1) << __func__; |
| switch (decoder_type) { |
| case cdm::kStreamTypeVideo: |
| video_decoder_->Deinitialize(); |
| break; |
| case cdm::kStreamTypeAudio: |
| #if defined(CLEAR_KEY_CDM_USE_FFMPEG_DECODER) |
| audio_decoder_->Deinitialize(); |
| #endif |
| break; |
| default: |
| NOTREACHED() << "DeinitializeDecoder(): invalid cdm::StreamType"; |
| } |
| } |
| |
| cdm::Status ClearKeyCdm::DecryptAndDecodeFrame( |
| const cdm::InputBuffer_1& encrypted_buffer, |
| cdm::VideoFrame* decoded_frame) { |
| return DecryptAndDecodeFrame(ToInputBuffer_2(encrypted_buffer), |
| decoded_frame); |
| } |
| |
| cdm::Status ClearKeyCdm::DecryptAndDecodeFrame( |
| const cdm::InputBuffer_2& encrypted_buffer, |
| cdm::VideoFrame* decoded_frame) { |
| DVLOG(1) << __func__; |
| TRACE_EVENT0("media", "ClearKeyCdm::DecryptAndDecodeFrame"); |
| |
| scoped_refptr<DecoderBuffer> buffer; |
| cdm::Status status = DecryptToMediaDecoderBuffer(encrypted_buffer, &buffer); |
| |
| if (status != cdm::kSuccess) |
| return status; |
| |
| const uint8_t* data = NULL; |
| int32_t size = 0; |
| int64_t timestamp = 0; |
| if (!buffer->end_of_stream()) { |
| data = buffer->data(); |
| size = buffer->data_size(); |
| timestamp = encrypted_buffer.timestamp; |
| } |
| |
| return video_decoder_->DecodeFrame(data, size, timestamp, decoded_frame); |
| } |
| |
| cdm::Status ClearKeyCdm::DecryptAndDecodeSamples( |
| const cdm::InputBuffer_1& encrypted_buffer, |
| cdm::AudioFrames* audio_frames) { |
| return DecryptAndDecodeSamples(ToInputBuffer_2(encrypted_buffer), |
| audio_frames); |
| } |
| |
| cdm::Status ClearKeyCdm::DecryptAndDecodeSamples( |
| const cdm::InputBuffer_2& encrypted_buffer, |
| cdm::AudioFrames* audio_frames) { |
| DVLOG(1) << __func__; |
| |
| // Trigger a crash on purpose for testing purpose. |
| // Only do this after a session has been created since the test also checks |
| // that the session is properly closed. |
| if (!last_session_id_.empty() && |
| key_system_ == kExternalClearKeyCrashKeySystem) { |
| CHECK(false) << "Crash in decrypt-and-decode with crash key system."; |
| } |
| |
| scoped_refptr<DecoderBuffer> buffer; |
| cdm::Status status = DecryptToMediaDecoderBuffer(encrypted_buffer, &buffer); |
| |
| if (status != cdm::kSuccess) |
| return status; |
| |
| #if defined(CLEAR_KEY_CDM_USE_FFMPEG_DECODER) |
| const uint8_t* data = NULL; |
| int32_t size = 0; |
| int64_t timestamp = 0; |
| if (!buffer->end_of_stream()) { |
| data = buffer->data(); |
| size = buffer->data_size(); |
| timestamp = encrypted_buffer.timestamp; |
| } |
| |
| return audio_decoder_->DecodeBuffer(data, size, timestamp, audio_frames); |
| #else |
| return cdm::kSuccess; |
| #endif // CLEAR_KEY_CDM_USE_FFMPEG_DECODER |
| } |
| |
| void ClearKeyCdm::Destroy() { |
| DVLOG(1) << __func__; |
| delete this; |
| } |
| |
| void ClearKeyCdm::ScheduleNextRenewal() { |
| // Prepare the next renewal message and set timer. |
| std::ostringstream msg_stream; |
| msg_stream << "Renewal from ClearKey CDM set at time " |
| << base::Time::FromDoubleT(cdm_host_proxy_->GetCurrentWallTime()) |
| << "."; |
| next_renewal_message_ = msg_stream.str(); |
| |
| cdm_host_proxy_->SetTimer(timer_delay_ms_, &next_renewal_message_[0]); |
| |
| // Use a smaller timer delay at start-up to facilitate testing. Increase the |
| // timer delay up to a limit to avoid message spam. |
| if (timer_delay_ms_ < kMaxTimerDelayMs) |
| timer_delay_ms_ = std::min(2 * timer_delay_ms_, kMaxTimerDelayMs); |
| } |
| |
| cdm::Status ClearKeyCdm::DecryptToMediaDecoderBuffer( |
| const cdm::InputBuffer_2& encrypted_buffer, |
| scoped_refptr<DecoderBuffer>* decrypted_buffer) { |
| DCHECK(decrypted_buffer); |
| |
| scoped_refptr<DecoderBuffer> buffer = CopyDecoderBufferFrom(encrypted_buffer); |
| |
| // EOS and unencrypted streams can be returned as-is. |
| if (buffer->end_of_stream() || !buffer->decrypt_config()) { |
| *decrypted_buffer = std::move(buffer); |
| return cdm::kSuccess; |
| } |
| |
| // Callback is called synchronously, so we can use variables on the stack. |
| media::Decryptor::Status status = media::Decryptor::kError; |
| // The CDM does not care what the stream type is. Pass kVideo |
| // for both audio and video decryption. |
| cdm_->GetCdmContext()->GetDecryptor()->Decrypt( |
| media::Decryptor::kVideo, std::move(buffer), |
| base::Bind(&CopyDecryptResults, &status, decrypted_buffer)); |
| |
| if (status == media::Decryptor::kError) |
| return cdm::kDecryptError; |
| |
| if (status == media::Decryptor::kNoKey) |
| return cdm::kNoKey; |
| |
| DCHECK_EQ(status, media::Decryptor::kSuccess); |
| return cdm::kSuccess; |
| } |
| |
| void ClearKeyCdm::OnPlatformChallengeResponse( |
| const cdm::PlatformChallengeResponse& response) { |
| DVLOG(1) << __func__; |
| |
| if (!is_running_platform_verification_test_) { |
| NOTREACHED() << "OnPlatformChallengeResponse() called unexpectedly."; |
| return; |
| } |
| |
| is_running_platform_verification_test_ = false; |
| |
| // We are good as long as we get some response back. Ignore the challenge |
| // response for now. |
| // TODO(xhwang): Also test host challenge here. |
| OnUnitTestComplete(true); |
| } |
| |
| void ClearKeyCdm::OnQueryOutputProtectionStatus( |
| cdm::QueryResult result, |
| uint32_t link_mask, |
| uint32_t output_protection_mask) { |
| DVLOG(1) << __func__; |
| |
| if (!is_running_output_protection_test_) { |
| NOTREACHED() << "OnQueryOutputProtectionStatus() called unexpectedly."; |
| return; |
| } |
| |
| is_running_output_protection_test_ = false; |
| |
| // On Chrome OS, status query will fail on Linux Chrome OS build. So we ignore |
| // the query result. On all other platforms, status query should succeed. |
| // TODO(xhwang): Improve the check on Chrome OS builds. For example, use |
| // base::SysInfo::IsRunningOnChromeOS() to differentiate between real Chrome OS |
| // build and Linux Chrome OS build. |
| #if !defined(OS_CHROMEOS) |
| if (result != cdm::kQuerySucceeded || link_mask != 0) { |
| OnUnitTestComplete(false); |
| return; |
| } |
| #endif |
| OnUnitTestComplete(true); |
| }; |
| |
| void ClearKeyCdm::OnStorageId(uint32_t version, |
| const uint8_t* storage_id, |
| uint32_t storage_id_size) { |
| if (!is_running_storage_id_test_) { |
| NOTREACHED() << "OnStorageId() called unexpectedly."; |
| return; |
| } |
| |
| is_running_storage_id_test_ = false; |
| DVLOG(1) << __func__ << ": storage_id (hex encoded) = " |
| << (storage_id_size ? base::HexEncode(storage_id, storage_id_size) |
| : "<empty>"); |
| |
| #if BUILDFLAG(ENABLE_CDM_STORAGE_ID) |
| // Storage Id is enabled, so something should be returned. It should be the |
| // length of a SHA-256 hash (256 bits). |
| constexpr uint32_t kExpectedStorageIdSizeInBytes = 256 / 8; |
| OnUnitTestComplete(storage_id_size == kExpectedStorageIdSizeInBytes); |
| #else |
| // Storage Id not available, so an empty vector should be returned. |
| OnUnitTestComplete(storage_id_size == 0); |
| #endif |
| } |
| |
| void ClearKeyCdm::OnSessionMessage(const std::string& session_id, |
| CdmMessageType message_type, |
| const std::vector<uint8_t>& message) { |
| DVLOG(1) << __func__ << ": size = " << message.size(); |
| |
| cdm_host_proxy_->OnSessionMessage( |
| session_id.data(), session_id.length(), ConvertMessageType(message_type), |
| reinterpret_cast<const char*>(message.data()), message.size()); |
| } |
| |
| void ClearKeyCdm::OnSessionKeysChange(const std::string& session_id, |
| bool has_additional_usable_key, |
| CdmKeysInfo keys_info) { |
| DVLOG(1) << __func__ << ": size = " << keys_info.size(); |
| |
| // Crash if the special key ID "crash" is present. |
| const std::vector<uint8_t> kCrashKeyId{'c', 'r', 'a', 's', 'h'}; |
| for (const auto& key_info : keys_info) { |
| if (key_info->key_id == kCrashKeyId) |
| CHECK(false) << "Crash on special crash key ID."; |
| } |
| |
| std::vector<cdm::KeyInformation> keys_vector; |
| ConvertCdmKeysInfo(keys_info, &keys_vector); |
| cdm_host_proxy_->OnSessionKeysChange(session_id.data(), session_id.length(), |
| has_additional_usable_key, |
| keys_vector.data(), keys_vector.size()); |
| } |
| |
| void ClearKeyCdm::OnSessionClosed(const std::string& session_id) { |
| cdm_host_proxy_->OnSessionClosed(session_id.data(), session_id.length()); |
| } |
| |
| void ClearKeyCdm::OnSessionExpirationUpdate(const std::string& session_id, |
| base::Time new_expiry_time) { |
| DVLOG(1) << __func__ << ": expiry_time = " << new_expiry_time; |
| cdm_host_proxy_->OnExpirationChange(session_id.data(), session_id.length(), |
| new_expiry_time.ToDoubleT()); |
| } |
| |
| void ClearKeyCdm::OnSessionCreated(uint32_t promise_id, |
| const std::string& session_id) { |
| // Save the latest session ID for renewal and file IO test messages. |
| last_session_id_ = session_id; |
| |
| cdm_host_proxy_->OnResolveNewSessionPromise(promise_id, session_id.data(), |
| session_id.length()); |
| } |
| |
| void ClearKeyCdm::OnPromiseResolved(uint32_t promise_id) { |
| cdm_host_proxy_->OnResolvePromise(promise_id); |
| } |
| |
| void ClearKeyCdm::OnPromiseFailed(uint32_t promise_id, |
| CdmPromise::Exception exception_code, |
| uint32_t system_code, |
| const std::string& error_message) { |
| DVLOG(1) << __func__ << ": error = " << error_message; |
| cdm_host_proxy_->OnRejectPromise(promise_id, ConvertException(exception_code), |
| system_code, error_message.data(), |
| error_message.length()); |
| } |
| |
| void ClearKeyCdm::OnUnitTestComplete(bool success) { |
| std::string message = GetUnitTestResultMessage(success); |
| cdm_host_proxy_->OnSessionMessage( |
| last_session_id_.data(), last_session_id_.length(), cdm::kLicenseRequest, |
| message.data(), message.length()); |
| } |
| |
| void ClearKeyCdm::StartFileIOTest() { |
| file_io_test_runner_.reset(new FileIOTestRunner(base::Bind( |
| &CdmHostProxy::CreateFileIO, base::Unretained(cdm_host_proxy_.get())))); |
| file_io_test_runner_->RunAllTests( |
| base::Bind(&ClearKeyCdm::OnFileIOTestComplete, base::Unretained(this))); |
| } |
| |
| void ClearKeyCdm::OnFileIOTestComplete(bool success) { |
| DVLOG(1) << __func__ << ": " << success; |
| OnUnitTestComplete(success); |
| file_io_test_runner_.reset(); |
| } |
| |
| void ClearKeyCdm::StartOutputProtectionTest() { |
| DVLOG(1) << __func__; |
| is_running_output_protection_test_ = true; |
| cdm_host_proxy_->QueryOutputProtectionStatus(); |
| } |
| |
| void ClearKeyCdm::StartPlatformVerificationTest() { |
| DVLOG(1) << __func__; |
| is_running_platform_verification_test_ = true; |
| |
| std::string service_id = "test_service_id"; |
| std::string challenge = "test_challenge"; |
| |
| cdm_host_proxy_->SendPlatformChallenge(service_id.data(), service_id.size(), |
| challenge.data(), challenge.size()); |
| } |
| |
| void ClearKeyCdm::ReportVerifyCdmHostTestResult() { |
| // VerifyCdmHost() should have already been called and test result stored |
| // in |g_verify_host_files_result|. |
| OnUnitTestComplete(g_verify_host_files_result); |
| } |
| |
| void ClearKeyCdm::StartStorageIdTest() { |
| DVLOG(1) << __func__; |
| is_running_storage_id_test_ = true; |
| |
| // Request the latest available version. |
| cdm_host_proxy_->RequestStorageId(0); |
| } |
| |
| void ClearKeyCdm::StartCdmProxyTest() { |
| DVLOG(1) << __func__; |
| DCHECK(!cdm_proxy_test_); |
| |
| cdm_proxy_test_.reset(new CdmProxyTest(cdm_host_proxy_.get())); |
| cdm_proxy_test_->Run(base::BindOnce(&ClearKeyCdm::OnCdmProxyTestComplete, |
| base::Unretained(this))); |
| } |
| |
| void ClearKeyCdm::OnCdmProxyTestComplete(bool success) { |
| DVLOG(1) << __func__; |
| DCHECK(cdm_proxy_test_); |
| |
| cdm_proxy_test_.reset(); |
| has_cdm_proxy_test_passed_ = success; |
| |
| // Ignore test result here. It will be reported in ReportCdmProxyTestResult(). |
| cdm_host_proxy_->OnInitialized(true); |
| } |
| |
| void ClearKeyCdm::ReportCdmProxyTestResult() { |
| // StartCdmProxyTest() should have already been called and finished. |
| OnUnitTestComplete(has_cdm_proxy_test_passed_); |
| } |
| |
| } // namespace media |