| // 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 "webcontentdecryptionmodulesession_impl.h" |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "media/base/cdm_key_information.h" |
| #include "media/base/cdm_promise.h" |
| #include "media/base/key_systems.h" |
| #include "media/base/limits.h" |
| #include "media/base/media_keys.h" |
| #include "media/blink/cdm_result_promise.h" |
| #include "media/blink/cdm_session_adapter.h" |
| #include "media/blink/new_session_cdm_result_promise.h" |
| #include "media/blink/webmediaplayer_util.h" |
| #include "media/cdm/cenc_utils.h" |
| #include "media/cdm/json_web_key.h" |
| #include "third_party/WebKit/public/platform/WebData.h" |
| #include "third_party/WebKit/public/platform/WebEncryptedMediaKeyInformation.h" |
| #include "third_party/WebKit/public/platform/WebString.h" |
| #include "third_party/WebKit/public/platform/WebURL.h" |
| #include "third_party/WebKit/public/platform/WebVector.h" |
| |
| namespace media { |
| |
| const char kCloseSessionUMAName[] = "CloseSession"; |
| const char kGenerateRequestUMAName[] = "GenerateRequest"; |
| const char kLoadSessionUMAName[] = "LoadSession"; |
| const char kRemoveSessionUMAName[] = "RemoveSession"; |
| const char kUpdateSessionUMAName[] = "UpdateSession"; |
| |
| static blink::WebContentDecryptionModuleSession::Client::MessageType |
| convertMessageType(MediaKeys::MessageType message_type) { |
| switch (message_type) { |
| case media::MediaKeys::LICENSE_REQUEST: |
| return blink::WebContentDecryptionModuleSession::Client::MessageType:: |
| LicenseRequest; |
| case media::MediaKeys::LICENSE_RENEWAL: |
| return blink::WebContentDecryptionModuleSession::Client::MessageType:: |
| LicenseRenewal; |
| case media::MediaKeys::LICENSE_RELEASE: |
| return blink::WebContentDecryptionModuleSession::Client::MessageType:: |
| LicenseRelease; |
| } |
| |
| NOTREACHED(); |
| return blink::WebContentDecryptionModuleSession::Client::MessageType:: |
| LicenseRequest; |
| } |
| |
| static blink::WebEncryptedMediaKeyInformation::KeyStatus convertStatus( |
| media::CdmKeyInformation::KeyStatus status) { |
| switch (status) { |
| case media::CdmKeyInformation::USABLE: |
| return blink::WebEncryptedMediaKeyInformation::KeyStatus::Usable; |
| case media::CdmKeyInformation::INTERNAL_ERROR: |
| return blink::WebEncryptedMediaKeyInformation::KeyStatus::InternalError; |
| case media::CdmKeyInformation::EXPIRED: |
| return blink::WebEncryptedMediaKeyInformation::KeyStatus::Expired; |
| case media::CdmKeyInformation::OUTPUT_NOT_ALLOWED: |
| return blink::WebEncryptedMediaKeyInformation::KeyStatus:: |
| OutputNotAllowed; |
| case media::CdmKeyInformation::OUTPUT_DOWNSCALED: |
| return blink::WebEncryptedMediaKeyInformation::KeyStatus:: |
| OutputDownscaled; |
| case media::CdmKeyInformation::KEY_STATUS_PENDING: |
| return blink::WebEncryptedMediaKeyInformation::KeyStatus::StatusPending; |
| } |
| |
| NOTREACHED(); |
| return blink::WebEncryptedMediaKeyInformation::KeyStatus::InternalError; |
| } |
| |
| static MediaKeys::SessionType convertSessionType( |
| blink::WebEncryptedMediaSessionType session_type) { |
| switch (session_type) { |
| case blink::WebEncryptedMediaSessionType::Temporary: |
| return MediaKeys::TEMPORARY_SESSION; |
| case blink::WebEncryptedMediaSessionType::PersistentLicense: |
| return MediaKeys::PERSISTENT_LICENSE_SESSION; |
| case blink::WebEncryptedMediaSessionType::PersistentReleaseMessage: |
| return MediaKeys::PERSISTENT_RELEASE_MESSAGE_SESSION; |
| case blink::WebEncryptedMediaSessionType::Unknown: |
| break; |
| } |
| |
| NOTREACHED(); |
| return MediaKeys::TEMPORARY_SESSION; |
| } |
| |
| static bool SanitizeInitData(EmeInitDataType init_data_type, |
| const unsigned char* init_data, |
| size_t init_data_length, |
| std::vector<uint8>* sanitized_init_data, |
| std::string* error_message) { |
| if (init_data_length > limits::kMaxInitDataLength) { |
| error_message->assign("Initialization data too long."); |
| return false; |
| } |
| |
| switch (init_data_type) { |
| case EmeInitDataType::WEBM: |
| sanitized_init_data->assign(init_data, init_data + init_data_length); |
| return true; |
| |
| case EmeInitDataType::CENC: |
| sanitized_init_data->assign(init_data, init_data + init_data_length); |
| if (!ValidatePsshInput(*sanitized_init_data)) { |
| error_message->assign("Initialization data for CENC is incorrect."); |
| return false; |
| } |
| return true; |
| |
| case EmeInitDataType::KEYIDS: { |
| // Extract the keys and then rebuild the message. This ensures that any |
| // extra data in the provided JSON is dropped. |
| std::string init_data_string(init_data, init_data + init_data_length); |
| KeyIdList key_ids; |
| if (!ExtractKeyIdsFromKeyIdsInitData(init_data_string, &key_ids, |
| error_message)) |
| return false; |
| |
| for (const auto& key_id : key_ids) { |
| if (key_id.size() < limits::kMinKeyIdLength || |
| key_id.size() > limits::kMaxKeyIdLength) { |
| error_message->assign("Incorrect key size."); |
| return false; |
| } |
| } |
| |
| CreateKeyIdsInitData(key_ids, sanitized_init_data); |
| return true; |
| } |
| |
| case EmeInitDataType::UNKNOWN: |
| break; |
| } |
| |
| NOTREACHED(); |
| error_message->assign("Initialization data type is not supported."); |
| return false; |
| } |
| |
| WebContentDecryptionModuleSessionImpl::WebContentDecryptionModuleSessionImpl( |
| const scoped_refptr<CdmSessionAdapter>& adapter) |
| : adapter_(adapter), is_closed_(false), weak_ptr_factory_(this) { |
| } |
| |
| WebContentDecryptionModuleSessionImpl:: |
| ~WebContentDecryptionModuleSessionImpl() { |
| if (!session_id_.empty()) |
| adapter_->UnregisterSession(session_id_); |
| } |
| |
| void WebContentDecryptionModuleSessionImpl::setClientInterface(Client* client) { |
| client_ = client; |
| } |
| |
| blink::WebString WebContentDecryptionModuleSessionImpl::sessionId() const { |
| return blink::WebString::fromUTF8(session_id_); |
| } |
| |
| void WebContentDecryptionModuleSessionImpl::initializeNewSession( |
| blink::WebEncryptedMediaInitDataType init_data_type, |
| const unsigned char* init_data, |
| size_t init_data_length, |
| blink::WebEncryptedMediaSessionType session_type, |
| blink::WebContentDecryptionModuleResult result) { |
| DCHECK(init_data); |
| DCHECK(session_id_.empty()); |
| |
| // From https://w3c.github.io/encrypted-media/#generateRequest. |
| // 5. If the Key System implementation represented by this object's cdm |
| // implementation value does not support initDataType as an Initialization |
| // Data Type, return a promise rejected with a new DOMException whose name |
| // is NotSupportedError. String comparison is case-sensitive. |
| EmeInitDataType eme_init_data_type = ConvertToEmeInitDataType(init_data_type); |
| if (!IsSupportedKeySystemWithInitDataType(adapter_->GetKeySystem(), |
| eme_init_data_type)) { |
| std::string message = |
| "The initialization data type is not supported by the key system."; |
| result.completeWithError( |
| blink::WebContentDecryptionModuleExceptionNotSupportedError, 0, |
| blink::WebString::fromUTF8(message)); |
| return; |
| } |
| |
| // 9.1 If the init data is not valid for initDataType, reject promise with a |
| // new DOMException whose name is InvalidAccessError. |
| // 9.2 Let sanitized init data be a validated and sanitized version of init |
| // data. The user agent must thoroughly validate the Initialization Data |
| // before passing it to the CDM. This includes verifying that the length |
| // and values of fields are reasonable, verifying that values are within |
| // reasonable limits, and stripping irrelevant, unsupported, or unknown |
| // data or fields. It is recommended that user agents pre-parse, sanitize, |
| // and/or generate a fully sanitized version of the Initialization Data. |
| // If the Initialization Data format specified by initDataType support |
| // multiple entries, the user agent should remove entries that are not |
| // needed by the CDM. |
| // 9.3 If the previous step failed, reject promise with a new DOMException |
| // whose name is InvalidAccessError. |
| std::vector<uint8> sanitized_init_data; |
| std::string message; |
| if (!SanitizeInitData(eme_init_data_type, init_data, init_data_length, |
| &sanitized_init_data, &message)) { |
| result.completeWithError( |
| blink::WebContentDecryptionModuleExceptionInvalidAccessError, 0, |
| blink::WebString::fromUTF8(message)); |
| return; |
| } |
| |
| // 9.4 Let session id be the empty string. |
| // (Done in constructor.) |
| |
| // 9.5 Let message be null. |
| // (Done by CDM.) |
| |
| // 9.6 Let cdm be the CDM instance represented by this object's cdm |
| // instance value. |
| // 9.7 Use the cdm to execute the following steps: |
| adapter_->InitializeNewSession( |
| eme_init_data_type, sanitized_init_data, convertSessionType(session_type), |
| scoped_ptr<NewSessionCdmPromise>(new NewSessionCdmResultPromise( |
| result, adapter_->GetKeySystemUMAPrefix() + kGenerateRequestUMAName, |
| base::Bind( |
| &WebContentDecryptionModuleSessionImpl::OnSessionInitialized, |
| base::Unretained(this))))); |
| } |
| |
| void WebContentDecryptionModuleSessionImpl::load( |
| const blink::WebString& session_id, |
| blink::WebContentDecryptionModuleResult result) { |
| DCHECK(!session_id.isEmpty()); |
| DCHECK(session_id_.empty()); |
| |
| // TODO(jrummell): Now that there are 2 types of persistent sessions, the |
| // session type should be passed from blink. Type should also be passed in the |
| // constructor (and removed from initializeNewSession()). |
| adapter_->LoadSession( |
| MediaKeys::PERSISTENT_LICENSE_SESSION, base::UTF16ToASCII(session_id), |
| scoped_ptr<NewSessionCdmPromise>(new NewSessionCdmResultPromise( |
| result, adapter_->GetKeySystemUMAPrefix() + kLoadSessionUMAName, |
| base::Bind( |
| &WebContentDecryptionModuleSessionImpl::OnSessionInitialized, |
| base::Unretained(this))))); |
| } |
| |
| void WebContentDecryptionModuleSessionImpl::update( |
| const uint8* response, |
| size_t response_length, |
| blink::WebContentDecryptionModuleResult result) { |
| DCHECK(response); |
| DCHECK(!session_id_.empty()); |
| adapter_->UpdateSession( |
| session_id_, std::vector<uint8>(response, response + response_length), |
| scoped_ptr<SimpleCdmPromise>(new CdmResultPromise<>( |
| result, adapter_->GetKeySystemUMAPrefix() + kUpdateSessionUMAName))); |
| } |
| |
| void WebContentDecryptionModuleSessionImpl::close( |
| blink::WebContentDecryptionModuleResult result) { |
| DCHECK(!session_id_.empty()); |
| adapter_->CloseSession( |
| session_id_, |
| scoped_ptr<SimpleCdmPromise>(new CdmResultPromise<>( |
| result, adapter_->GetKeySystemUMAPrefix() + kCloseSessionUMAName))); |
| } |
| |
| void WebContentDecryptionModuleSessionImpl::remove( |
| blink::WebContentDecryptionModuleResult result) { |
| DCHECK(!session_id_.empty()); |
| adapter_->RemoveSession( |
| session_id_, |
| scoped_ptr<SimpleCdmPromise>(new CdmResultPromise<>( |
| result, adapter_->GetKeySystemUMAPrefix() + kRemoveSessionUMAName))); |
| } |
| |
| void WebContentDecryptionModuleSessionImpl::OnSessionMessage( |
| MediaKeys::MessageType message_type, |
| const std::vector<uint8>& message) { |
| DCHECK(client_) << "Client not set before message event"; |
| client_->message(convertMessageType(message_type), vector_as_array(&message), |
| message.size()); |
| } |
| |
| void WebContentDecryptionModuleSessionImpl::OnSessionKeysChange( |
| bool has_additional_usable_key, |
| CdmKeysInfo keys_info) { |
| blink::WebVector<blink::WebEncryptedMediaKeyInformation> keys( |
| keys_info.size()); |
| for (size_t i = 0; i < keys_info.size(); ++i) { |
| const auto& key_info = keys_info[i]; |
| keys[i].setId(blink::WebData(reinterpret_cast<char*>(&key_info->key_id[0]), |
| key_info->key_id.size())); |
| keys[i].setStatus(convertStatus(key_info->status)); |
| keys[i].setSystemCode(key_info->system_code); |
| } |
| |
| // Now send the event to blink. |
| client_->keysStatusesChange(keys, has_additional_usable_key); |
| } |
| |
| void WebContentDecryptionModuleSessionImpl::OnSessionExpirationUpdate( |
| const base::Time& new_expiry_time) { |
| client_->expirationChanged(new_expiry_time.ToJsTime()); |
| } |
| |
| void WebContentDecryptionModuleSessionImpl::OnSessionClosed() { |
| if (is_closed_) |
| return; |
| |
| is_closed_ = true; |
| client_->close(); |
| } |
| |
| blink::WebContentDecryptionModuleResult::SessionStatus |
| WebContentDecryptionModuleSessionImpl::OnSessionInitialized( |
| const std::string& session_id) { |
| // CDM will return NULL if the session to be loaded can't be found. |
| if (session_id.empty()) |
| return blink::WebContentDecryptionModuleResult::SessionNotFound; |
| |
| DCHECK(session_id_.empty()) << "Session ID may not be changed once set."; |
| session_id_ = session_id; |
| return adapter_->RegisterSession(session_id_, weak_ptr_factory_.GetWeakPtr()) |
| ? blink::WebContentDecryptionModuleResult::NewSession |
| : blink::WebContentDecryptionModuleResult::SessionAlreadyExists; |
| } |
| |
| } // namespace media |