// 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 #include "base/bind.h" #include "base/callback_helpers.h" #include "base/logging.h" #include "base/metrics/histogram_functions.h" #include "base/numerics/safe_conversions.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/content_decryption_module.h" #include "media/base/key_system_names.h" #include "media/base/key_systems.h" #include "media/base/limits.h" #include "media/blink/cdm_result_promise.h" #include "media/blink/cdm_result_promise_helper.h" #include "media/blink/cdm_session_adapter.h" #include "media/blink/webmediaplayer_util.h" #include "media/cdm/cenc_utils.h" #include "media/cdm/json_web_key.h" #include "third_party/blink/public/platform/web_data.h" #include "third_party/blink/public/platform/web_encrypted_media_key_information.h" #include "third_party/blink/public/platform/web_string.h" #include "third_party/blink/public/platform/web_url.h" #include "third_party/blink/public/platform/web_vector.h" namespace media { namespace { const char kCloseSessionUMAName[] = "CloseSession"; const char kGenerateRequestUMAName[] = "GenerateRequest"; const char kLoadSessionUMAName[] = "LoadSession"; const char kRemoveSessionUMAName[] = "RemoveSession"; const char kUpdateSessionUMAName[] = "UpdateSession"; const char kKeyStatusSystemCodeUMAName[] = "KeyStatusSystemCode"; blink::WebContentDecryptionModuleSession::Client::MessageType convertMessageType(CdmMessageType message_type) { switch (message_type) { case CdmMessageType::LICENSE_REQUEST: return blink::WebContentDecryptionModuleSession::Client::MessageType:: kLicenseRequest; case CdmMessageType::LICENSE_RENEWAL: return blink::WebContentDecryptionModuleSession::Client::MessageType:: kLicenseRenewal; case CdmMessageType::LICENSE_RELEASE: return blink::WebContentDecryptionModuleSession::Client::MessageType:: kLicenseRelease; case CdmMessageType::INDIVIDUALIZATION_REQUEST: return blink::WebContentDecryptionModuleSession::Client::MessageType:: kIndividualizationRequest; } NOTREACHED(); return blink::WebContentDecryptionModuleSession::Client::MessageType:: kLicenseRequest; } CdmSessionType convertSessionType( blink::WebEncryptedMediaSessionType session_type) { switch (session_type) { case blink::WebEncryptedMediaSessionType::kTemporary: return CdmSessionType::kTemporary; case blink::WebEncryptedMediaSessionType::kPersistentLicense: return CdmSessionType::kPersistentLicense; case blink::WebEncryptedMediaSessionType::kPersistentUsageRecord: return CdmSessionType::kPersistentUsageRecord; case blink::WebEncryptedMediaSessionType::kUnknown: break; } NOTREACHED(); return CdmSessionType::kTemporary; } bool SanitizeInitData(EmeInitDataType init_data_type, const unsigned char* init_data, size_t init_data_length, std::vector* sanitized_init_data, std::string* error_message) { DCHECK_GT(init_data_length, 0u); if (init_data_length > limits::kMaxInitDataLength) { error_message->assign("Initialization data too long."); return false; } switch (init_data_type) { case EmeInitDataType::WEBM: // |init_data| for WebM is a single key. if (init_data_length > limits::kMaxKeyIdLength) { error_message->assign("Initialization data for WebM is too long."); return false; } 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; } bool SanitizeSessionId(const blink::WebString& session_id, std::string* sanitized_session_id) { // The user agent should thoroughly validate the sessionId value before // passing it to the CDM. At a minimum, this should include checking that // the length and value (e.g. alphanumeric) are reasonable. if (!session_id.ContainsOnlyASCII()) return false; sanitized_session_id->assign(session_id.Ascii()); if (sanitized_session_id->length() > limits::kMaxSessionIdLength) return false; for (const char c : *sanitized_session_id) { if (!base::IsAsciiAlpha(c) && !base::IsAsciiDigit(c)) return false; } return true; } bool SanitizeResponse(const std::string& key_system, const uint8_t* response, size_t response_length, std::vector* sanitized_response) { // The user agent should thoroughly validate the response before passing it // to the CDM. This may include verifying values are within reasonable limits, // stripping irrelevant data or fields, pre-parsing it, sanitizing it, // and/or generating a fully sanitized version. The user agent should check // that the length and values of fields are reasonable. Unknown fields should // be rejected or removed. if (response_length > limits::kMaxSessionResponseLength) return false; if (IsClearKey(key_system) || IsExternalClearKey(key_system)) { std::string key_string(response, response + response_length); KeyIdAndKeyPairs keys; CdmSessionType session_type = CdmSessionType::kTemporary; if (!ExtractKeysFromJWKSet(key_string, &keys, &session_type)) return false; // Must contain at least one key. if (keys.empty()) return false; for (const auto key_pair : keys) { if (key_pair.first.size() < limits::kMinKeyIdLength || key_pair.first.size() > limits::kMaxKeyIdLength) { return false; } } std::string sanitized_data = GenerateJWKSet(keys, session_type); sanitized_response->assign(sanitized_data.begin(), sanitized_data.end()); return true; } // TODO(jrummell): Verify responses for Widevine. sanitized_response->assign(response, response + response_length); return true; } // If we need to close the session while destroying this object, we need a // dummy promise that won't call back into this object (or try to pass // something back to blink). class IgnoreResponsePromise : public SimpleCdmPromise { public: IgnoreResponsePromise() = default; ~IgnoreResponsePromise() override = default; // SimpleCdmPromise implementation. void resolve() final { MarkPromiseSettled(); } void reject(CdmPromise::Exception exception_code, uint32_t system_code, const std::string& error_message) final { MarkPromiseSettled(); } }; } // namespace WebContentDecryptionModuleSessionImpl::WebContentDecryptionModuleSessionImpl( const scoped_refptr& adapter) : adapter_(adapter), has_close_been_called_(false), is_closed_(false), is_persistent_session_(false), weak_ptr_factory_(this) {} WebContentDecryptionModuleSessionImpl:: ~WebContentDecryptionModuleSessionImpl() { DCHECK(thread_checker_.CalledOnValidThread()); if (!session_id_.empty()) { adapter_->UnregisterSession(session_id_); // From http://w3c.github.io/encrypted-media/#mediakeysession-interface // "If a MediaKeySession object is not closed when it becomes inaccessible // to the page, the CDM shall close the key session associated with the // object." // // This object is destroyed when the corresponding blink object is no // longer needed (which may be due to it becoming inaccessible to the // page), so if the session is not closed and CloseSession() has not yet // been called, call CloseSession() now. Since this object is being // destroyed, there is no need for the promise to do anything as this // session will be gone. if (!is_closed_ && !has_close_been_called_) { adapter_->CloseSession(session_id_, std::make_unique()); } } } 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()); DCHECK(thread_checker_.CalledOnValidThread()); // From https://w3c.github.io/encrypted-media/#generateRequest. // 6. 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 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::kWebContentDecryptionModuleExceptionNotSupportedError, 0, blink::WebString::FromUTF8(message)); return; } // 10.1 If the init data is not valid for initDataType, reject promise with // a newly created TypeError. // 10.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 supports multiple entries, the user agent should remove // entries that are not needed by the CDM. The user agent must not // re-order entries within the Initialization Data. // 10.3 If the preceding step failed, reject promise with a newly created // TypeError. std::vector sanitized_init_data; std::string message; if (!SanitizeInitData(eme_init_data_type, init_data, init_data_length, &sanitized_init_data, &message)) { result.CompleteWithError( blink::kWebContentDecryptionModuleExceptionTypeError, 0, blink::WebString::FromUTF8(message)); return; } // 10.4 If sanitized init data is empty, reject promise with a // NotSupportedError. if (sanitized_init_data.empty()) { result.CompleteWithError( blink::kWebContentDecryptionModuleExceptionNotSupportedError, 0, "No initialization data provided."); return; } // 10.5 Let session id be the empty string. // (Done in constructor.) // 10.6 Let message be null. // 10.7 Let message type be null. // (Done by CDM.) // 10.8 Let cdm be the CDM instance represented by this object's cdm // instance value. // 10.9 Use the cdm to execute the following steps: CdmSessionType cdm_session_type = convertSessionType(session_type); is_persistent_session_ = cdm_session_type != CdmSessionType::kTemporary; adapter_->InitializeNewSession( eme_init_data_type, sanitized_init_data, cdm_session_type, std::unique_ptr(new NewSessionCdmResultPromise( result, adapter_->GetKeySystemUMAPrefix(), kGenerateRequestUMAName, base::Bind( &WebContentDecryptionModuleSessionImpl::OnSessionInitialized, weak_ptr_factory_.GetWeakPtr()), {SessionInitStatus::NEW_SESSION}))); } void WebContentDecryptionModuleSessionImpl::Load( const blink::WebString& session_id, blink::WebContentDecryptionModuleResult result) { DCHECK(!session_id.IsEmpty()); DCHECK(session_id_.empty()); DCHECK(thread_checker_.CalledOnValidThread()); // From https://w3c.github.io/encrypted-media/#load. // 8.1 Let sanitized session ID be a validated and/or sanitized version of // sessionId. The user agent should thoroughly validate the sessionId // value before passing it to the CDM. At a minimum, this should include // checking that the length and value (e.g. alphanumeric) are reasonable. // 8.2 If the preceding step failed, or if sanitized session ID is empty, // reject promise with a newly created TypeError. std::string sanitized_session_id; if (!SanitizeSessionId(session_id, &sanitized_session_id)) { result.CompleteWithError( blink::kWebContentDecryptionModuleExceptionTypeError, 0, "Invalid session ID."); return; } // 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()). is_persistent_session_ = true; adapter_->LoadSession( CdmSessionType::kPersistentLicense, sanitized_session_id, std::unique_ptr(new NewSessionCdmResultPromise( result, adapter_->GetKeySystemUMAPrefix(), kLoadSessionUMAName, base::Bind( &WebContentDecryptionModuleSessionImpl::OnSessionInitialized, weak_ptr_factory_.GetWeakPtr()), {SessionInitStatus::NEW_SESSION, SessionInitStatus::SESSION_NOT_FOUND}))); } void WebContentDecryptionModuleSessionImpl::Update( const uint8_t* response, size_t response_length, blink::WebContentDecryptionModuleResult result) { DCHECK(response); DCHECK(!session_id_.empty()); DCHECK(thread_checker_.CalledOnValidThread()); // From https://w3c.github.io/encrypted-media/#update. // 6.1 Let sanitized response be a validated and/or sanitized version of // response copy. The user agent should thoroughly validate the response // before passing it to the CDM. This may include verifying values are // within reasonable limits, stripping irrelevant data or fields, // pre-parsing it, sanitizing it, and/or generating a fully sanitized // version. The user agent should check that the length and values of // fields are reasonable. Unknown fields should be rejected or removed. // 6.2 If the preceding step failed, or if sanitized response is empty, // reject promise with a newly created TypeError. std::vector sanitized_response; if (!SanitizeResponse(adapter_->GetKeySystem(), response, response_length, &sanitized_response)) { result.CompleteWithError( blink::kWebContentDecryptionModuleExceptionTypeError, 0, "Invalid response."); return; } adapter_->UpdateSession( session_id_, sanitized_response, std::unique_ptr(new CdmResultPromise<>( result, adapter_->GetKeySystemUMAPrefix() + kUpdateSessionUMAName))); } void WebContentDecryptionModuleSessionImpl::Close( blink::WebContentDecryptionModuleResult result) { DCHECK(!session_id_.empty()); DCHECK(thread_checker_.CalledOnValidThread()); // close() shouldn't be called if the session is already closed. Since the // operation is asynchronous, there is a window where close() was called // just before the closed event arrives. The CDM should handle the case where // close() is called after it has already closed the session. However, if // we can tell the session is now closed, simply resolve the promise. if (is_closed_) { result.Complete(); return; } has_close_been_called_ = true; adapter_->CloseSession( session_id_, std::unique_ptr(new CdmResultPromise<>( result, adapter_->GetKeySystemUMAPrefix() + kCloseSessionUMAName))); } void WebContentDecryptionModuleSessionImpl::Remove( blink::WebContentDecryptionModuleResult result) { DCHECK(!session_id_.empty()); DCHECK(thread_checker_.CalledOnValidThread()); adapter_->RemoveSession( session_id_, std::unique_ptr(new CdmResultPromise<>( result, adapter_->GetKeySystemUMAPrefix() + kRemoveSessionUMAName))); } void WebContentDecryptionModuleSessionImpl::OnSessionMessage( CdmMessageType message_type, const std::vector& message) { DCHECK(client_) << "Client not set before message event"; DCHECK(thread_checker_.CalledOnValidThread()); client_->Message(convertMessageType(message_type), message.data(), message.size()); } void WebContentDecryptionModuleSessionImpl::OnSessionKeysChange( bool has_additional_usable_key, CdmKeysInfo keys_info) { DCHECK(thread_checker_.CalledOnValidThread()); blink::WebVector keys( keys_info.size()); for (size_t i = 0; i < keys_info.size(); ++i) { auto& key_info = keys_info[i]; keys[i].SetId(blink::WebData(reinterpret_cast(&key_info->key_id[0]), key_info->key_id.size())); keys[i].SetStatus(ConvertCdmKeyStatus(key_info->status)); keys[i].SetSystemCode(key_info->system_code); base::UmaHistogramSparse( adapter_->GetKeySystemUMAPrefix() + kKeyStatusSystemCodeUMAName, key_info->system_code); } // Now send the event to blink. client_->KeysStatusesChange(keys, has_additional_usable_key); } void WebContentDecryptionModuleSessionImpl::OnSessionExpirationUpdate( base::Time new_expiry_time) { DCHECK(thread_checker_.CalledOnValidThread()); // The check works around an issue in base::Time that converts null base::Time // to |1601-01-01 00:00:00 UTC| in ToJsTime(). See http://crbug.com/679079 client_->ExpirationChanged(new_expiry_time.is_null() ? std::numeric_limits::quiet_NaN() : new_expiry_time.ToJsTime()); } void WebContentDecryptionModuleSessionImpl::OnSessionClosed() { DCHECK(thread_checker_.CalledOnValidThread()); // Only send one closed event to blink. if (is_closed_) return; is_closed_ = true; client_->Close(); } void WebContentDecryptionModuleSessionImpl::OnSessionInitialized( const std::string& session_id, SessionInitStatus* status) { DCHECK(thread_checker_.CalledOnValidThread()); // CDM will return NULL if the session to be loaded can't be found. if (session_id.empty()) { *status = SessionInitStatus::SESSION_NOT_FOUND; return; } DCHECK(session_id_.empty()) << "Session ID may not be changed once set."; session_id_ = session_id; *status = adapter_->RegisterSession(session_id_, weak_ptr_factory_.GetWeakPtr()) ? SessionInitStatus::NEW_SESSION : SessionInitStatus::SESSION_ALREADY_EXISTS; } } // namespace media