blob: bc1bb19af21ae8d3d2c9b54d782bb0bb0704f54e [file] [log] [blame]
// 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 <memory>
#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<uint8_t>* 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<uint8_t>* 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<CdmSessionAdapter>& 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<IgnoreResponsePromise>());
}
}
}
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<uint8_t> 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<NewSessionCdmPromise>(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<NewSessionCdmPromise>(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<uint8_t> 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<SimpleCdmPromise>(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<SimpleCdmPromise>(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<SimpleCdmPromise>(new CdmResultPromise<>(
result, adapter_->GetKeySystemUMAPrefix() + kRemoveSessionUMAName)));
}
void WebContentDecryptionModuleSessionImpl::OnSessionMessage(
CdmMessageType message_type,
const std::vector<uint8_t>& 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<blink::WebEncryptedMediaKeyInformation> 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<char*>(&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<double>::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