blob: 8cdd99404f765d31c5faa398610e6c00388b615d [file] [log] [blame]
/*
* Copyright (C) 2013 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/modules/encryptedmedia/media_keys.h"
#include <memory>
#include "base/memory/scoped_refptr.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/platform/web_content_decryption_module.h"
#include "third_party/blink/public/platform/web_encrypted_media_key_information.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/modules/encryptedmedia/content_decryption_module_result_promise.h"
#include "third_party/blink/renderer/modules/encryptedmedia/encrypted_media_utils.h"
#include "third_party/blink/renderer/modules/encryptedmedia/media_key_session.h"
#include "third_party/blink/renderer/modules/encryptedmedia/media_keys_policy.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h"
#include "third_party/blink/renderer/platform/instrumentation/instance_counters.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/timer.h"
#define MEDIA_KEYS_LOG_LEVEL 3
namespace blink {
// A class holding a pending action.
class MediaKeys::PendingAction final
: public GarbageCollected<MediaKeys::PendingAction> {
public:
enum class Type { kSetServerCertificate, kGetStatusForPolicy };
Type GetType() const { return type_; }
const Persistent<ContentDecryptionModuleResult> Result() const {
return result_;
}
DOMArrayBuffer* Data() const {
DCHECK_EQ(Type::kSetServerCertificate, type_);
return data_;
}
const String& StringData() const {
DCHECK_EQ(Type::kGetStatusForPolicy, type_);
return string_data_;
}
static PendingAction* CreatePendingSetServerCertificate(
ContentDecryptionModuleResult* result,
DOMArrayBuffer* server_certificate) {
DCHECK(result);
DCHECK(server_certificate);
return MakeGarbageCollected<PendingAction>(
Type::kSetServerCertificate, result, server_certificate, String());
}
static PendingAction* CreatePendingGetStatusForPolicy(
ContentDecryptionModuleResult* result,
const String& min_hdcp_version) {
DCHECK(result);
return MakeGarbageCollected<PendingAction>(
Type::kGetStatusForPolicy, result, nullptr, min_hdcp_version);
}
PendingAction(Type type,
ContentDecryptionModuleResult* result,
DOMArrayBuffer* data,
const String& string_data)
: type_(type), result_(result), data_(data), string_data_(string_data) {}
void Trace(blink::Visitor* visitor) {
visitor->Trace(result_);
visitor->Trace(data_);
}
private:
const Type type_;
const Member<ContentDecryptionModuleResult> result_;
const Member<DOMArrayBuffer> data_;
const String string_data_;
};
// This class wraps the promise resolver used when setting the certificate
// and is passed to Chromium to fullfill the promise. This implementation of
// complete() will resolve the promise with true, while completeWithError()
// will reject the promise with an exception. completeWithSession()
// is not expected to be called, and will reject the promise.
class SetCertificateResultPromise
: public ContentDecryptionModuleResultPromise {
public:
SetCertificateResultPromise(ScriptState* script_state,
MediaKeys* media_keys,
const char* interface_name,
const char* property_name)
: ContentDecryptionModuleResultPromise(script_state,
interface_name,
property_name),
media_keys_(media_keys) {}
~SetCertificateResultPromise() override = default;
// ContentDecryptionModuleResult implementation.
void Complete() override {
if (!IsValidToFulfillPromise())
return;
Resolve(true);
}
void CompleteWithError(WebContentDecryptionModuleException exception_code,
uint32_t system_code,
const WebString& error_message) override {
if (!IsValidToFulfillPromise())
return;
// The EME spec specifies that "If the Key System implementation does
// not support server certificates, return a promise resolved with
// false." So convert any NOTSUPPORTEDERROR into resolving with false.
if (exception_code ==
kWebContentDecryptionModuleExceptionNotSupportedError) {
Resolve(false);
return;
}
ContentDecryptionModuleResultPromise::CompleteWithError(
exception_code, system_code, error_message);
}
void Trace(blink::Visitor* visitor) override {
visitor->Trace(media_keys_);
ContentDecryptionModuleResultPromise::Trace(visitor);
}
private:
// Keeping a reference to MediaKeys to prevent GC from collecting it while
// the promise is pending.
Member<MediaKeys> media_keys_;
};
// This class wraps the promise resolver used when getting the key status for
// policy and is passed to Chromium to fullfill the promise.
class GetStatusForPolicyResultPromise
: public ContentDecryptionModuleResultPromise {
public:
GetStatusForPolicyResultPromise(ScriptState* script_state,
MediaKeys* media_keys,
const char* interface_name,
const char* property_name)
: ContentDecryptionModuleResultPromise(script_state,
interface_name,
property_name),
media_keys_(media_keys) {}
~GetStatusForPolicyResultPromise() override = default;
// ContentDecryptionModuleResult implementation.
void CompleteWithKeyStatus(
WebEncryptedMediaKeyInformation::KeyStatus key_status) override {
if (!IsValidToFulfillPromise())
return;
Resolve(EncryptedMediaUtils::ConvertKeyStatusToString(key_status));
}
void Trace(blink::Visitor* visitor) override {
visitor->Trace(media_keys_);
ContentDecryptionModuleResultPromise::Trace(visitor);
}
private:
// Keeping a reference to MediaKeys to prevent GC from collecting it while
// the promise is pending.
Member<MediaKeys> media_keys_;
};
MediaKeys::MediaKeys(
ExecutionContext* context,
const WebVector<WebEncryptedMediaSessionType>& supported_session_types,
std::unique_ptr<WebContentDecryptionModule> cdm)
: ContextLifecycleObserver(context),
supported_session_types_(supported_session_types),
cdm_(std::move(cdm)),
media_element_(nullptr),
reserved_for_media_element_(false),
timer_(context->GetTaskRunner(TaskType::kMiscPlatformAPI),
this,
&MediaKeys::TimerFired) {
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << "(" << this << ")";
InstanceCounters::IncrementCounter(InstanceCounters::kMediaKeysCounter);
}
MediaKeys::~MediaKeys() {
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << "(" << this << ")";
InstanceCounters::DecrementCounter(InstanceCounters::kMediaKeysCounter);
}
MediaKeySession* MediaKeys::createSession(ScriptState* script_state,
const String& session_type_string,
ExceptionState& exception_state) {
DVLOG(MEDIA_KEYS_LOG_LEVEL)
<< __func__ << "(" << this << ") " << session_type_string;
// [RuntimeEnabled] does not work with enum values. So we have to check it
// here. See https://crbug.com/871867 for details.
if (!RuntimeEnabledFeatures::
EncryptedMediaPersistentUsageRecordSessionEnabled() &&
session_type_string == "persistent-usage-record") {
DVLOG(MEDIA_KEYS_LOG_LEVEL)
<< __func__ << ": 'persistent-usage-record' support not enabled.";
// The message here is carefully chosen to be exactly the same as what the
// generated bindings would generate for invalid enum values.
exception_state.ThrowTypeError(
"The provided value 'persistent-usage-record' is not a valid enum "
"value of type MediaKeySessionType.");
return nullptr;
}
// From http://w3c.github.io/encrypted-media/#createSession
// When this method is invoked, the user agent must run the following steps:
// 1. If this object's persistent state allowed value is false and
// sessionType is not "temporary", throw a new DOMException whose name is
// NotSupportedError.
// (Chromium ensures that only session types supported by the
// configuration are listed in supportedSessionTypes.)
// 2. If the Key System implementation represented by this object's cdm
// implementation value does not support sessionType, throw a new
// DOMException whose name is NotSupportedError.
WebEncryptedMediaSessionType session_type =
EncryptedMediaUtils::ConvertToSessionType(session_type_string);
if (!SessionTypeSupported(session_type)) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Unsupported session type.");
return nullptr;
}
// 3. Let session be a new MediaKeySession object, and initialize it as
// follows:
// (Initialization is performed in the constructor.)
// 4. Return session.
return MakeGarbageCollected<MediaKeySession>(script_state, this,
session_type);
}
ScriptPromise MediaKeys::setServerCertificate(
ScriptState* script_state,
const DOMArrayPiece& server_certificate,
ExceptionState& exception_state) {
// From https://w3c.github.io/encrypted-media/#setServerCertificate
// The setServerCertificate(serverCertificate) method provides a server
// certificate to be used to encrypt messages to the license server.
// It must run the following steps:
// 1. If the Key System implementation represented by this object's cdm
// implementation value does not support server certificates, return
// a promise resolved with false.
// TODO(jrummell): Provide a way to determine if the CDM supports this.
// http://crbug.com/647816.
//
// 2. If serverCertificate is an empty array, return a promise rejected
// with a new a newly created TypeError.
if (!server_certificate.ByteLengthAsSizeT()) {
exception_state.ThrowTypeError("The serverCertificate parameter is empty.");
return ScriptPromise();
}
// 3. Let certificate be a copy of the contents of the serverCertificate
// parameter.
DOMArrayBuffer* server_certificate_buffer = DOMArrayBuffer::Create(
server_certificate.Data(), server_certificate.ByteLengthAsSizeT());
// 4. Let promise be a new promise.
SetCertificateResultPromise* result =
MakeGarbageCollected<SetCertificateResultPromise>(
script_state, this, "MediaKeys", "setServerCertificate");
ScriptPromise promise = result->Promise();
// 5. Run the following steps asynchronously. See SetServerCertificateTask().
pending_actions_.push_back(PendingAction::CreatePendingSetServerCertificate(
result, server_certificate_buffer));
if (!timer_.IsActive())
timer_.StartOneShot(base::TimeDelta(), FROM_HERE);
// 6. Return promise.
return promise;
}
void MediaKeys::SetServerCertificateTask(
DOMArrayBuffer* server_certificate,
ContentDecryptionModuleResult* result) {
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << "(" << this << ")";
// 5.1 Let cdm be the cdm during the initialization of this object.
WebContentDecryptionModule* cdm = ContentDecryptionModule();
// 5.2 Use the cdm to process certificate.
cdm->SetServerCertificate(
static_cast<unsigned char*>(server_certificate->Data()),
server_certificate->ByteLengthAsSizeT(), result->Result());
// 5.3 If any of the preceding steps failed, reject promise with a
// new DOMException whose name is the appropriate error name.
// 5.4 Resolve promise.
// (These are handled by Chromium and the CDM.)
}
ScriptPromise MediaKeys::getStatusForPolicy(
ScriptState* script_state,
const MediaKeysPolicy* media_keys_policy) {
// TODO(xhwang): Pass MediaKeysPolicy classes all the way to Chromium when
// we have more than one policy to check.
String min_hdcp_version = media_keys_policy->minHdcpVersion();
// Let promise be a new promise.
GetStatusForPolicyResultPromise* result =
MakeGarbageCollected<GetStatusForPolicyResultPromise>(
script_state, this, "MediaKeys", "getStatusForPolicy");
ScriptPromise promise = result->Promise();
// Run the following steps asynchronously. See GetStatusForPolicyTask().
pending_actions_.push_back(
PendingAction::CreatePendingGetStatusForPolicy(result, min_hdcp_version));
if (!timer_.IsActive())
timer_.StartOneShot(base::TimeDelta(), FROM_HERE);
// Return promise.
return promise;
}
void MediaKeys::GetStatusForPolicyTask(const String& min_hdcp_version,
ContentDecryptionModuleResult* result) {
DVLOG(MEDIA_KEYS_LOG_LEVEL) << __func__ << ": " << min_hdcp_version;
WebContentDecryptionModule* cdm = ContentDecryptionModule();
cdm->GetStatusForPolicy(min_hdcp_version, result->Result());
}
bool MediaKeys::ReserveForMediaElement(HTMLMediaElement* media_element) {
// If some other HtmlMediaElement already has a reference to us, fail.
if (media_element_)
return false;
media_element_ = media_element;
reserved_for_media_element_ = true;
return true;
}
void MediaKeys::AcceptReservation() {
reserved_for_media_element_ = false;
}
void MediaKeys::CancelReservation() {
reserved_for_media_element_ = false;
media_element_.Clear();
}
void MediaKeys::ClearMediaElement() {
DCHECK(media_element_);
media_element_.Clear();
}
bool MediaKeys::SessionTypeSupported(
WebEncryptedMediaSessionType session_type) {
for (size_t i = 0; i < supported_session_types_.size(); i++) {
if (supported_session_types_[i] == session_type)
return true;
}
return false;
}
void MediaKeys::TimerFired(TimerBase*) {
DCHECK(pending_actions_.size());
// Swap the queue to a local copy to avoid problems if resolving promises
// run synchronously.
HeapDeque<Member<PendingAction>> pending_actions;
pending_actions.Swap(pending_actions_);
while (!pending_actions.IsEmpty()) {
PendingAction* action = pending_actions.TakeFirst();
switch (action->GetType()) {
case PendingAction::Type::kSetServerCertificate:
SetServerCertificateTask(action->Data(), action->Result());
break;
case PendingAction::Type::kGetStatusForPolicy:
GetStatusForPolicyTask(action->StringData(), action->Result());
break;
}
}
}
WebContentDecryptionModule* MediaKeys::ContentDecryptionModule() {
return cdm_.get();
}
void MediaKeys::Trace(blink::Visitor* visitor) {
visitor->Trace(pending_actions_);
visitor->Trace(media_element_);
ScriptWrappable::Trace(visitor);
ContextLifecycleObserver::Trace(visitor);
}
void MediaKeys::ContextDestroyed(ExecutionContext*) {
timer_.Stop();
pending_actions_.clear();
// We don't need the CDM anymore. Only destroyed after all related
// ContextLifecycleObservers have been stopped.
cdm_.reset();
}
bool MediaKeys::HasPendingActivity() const {
// Remain around if there are pending events.
DVLOG(MEDIA_KEYS_LOG_LEVEL)
<< __func__ << "(" << this << ")"
<< (!pending_actions_.IsEmpty() ? " !pending_actions_.isEmpty()" : "")
<< (reserved_for_media_element_ ? " reserved_for_media_element_" : "");
return !pending_actions_.IsEmpty() || reserved_for_media_element_;
}
} // namespace blink