blob: 0b4df5886cf9d6e103d98c249ddc7a77ece3dc34 [file] [log] [blame]
/*
* Copyright (C) 2017 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 "config.h"
#include "CDMFairPlayStreaming.h"
#if ENABLE(ENCRYPTED_MEDIA)
#include "CDMClearKey.h"
#include "CDMKeySystemConfiguration.h"
#include "CDMRestrictions.h"
#include "CDMSessionType.h"
#include "ISOFairPlayStreamingPsshBox.h"
#include "ISOSchemeInformationBox.h"
#include "ISOSchemeTypeBox.h"
#include "ISOTrackEncryptionBox.h"
#include "InitDataRegistry.h"
#include "NotImplemented.h"
#include <JavaScriptCore/ArrayBuffer.h>
#include <JavaScriptCore/DataView.h>
#include <wtf/Algorithms.h>
#include <wtf/JSONValues.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/text/Base64.h>
#if HAVE(AVCONTENTKEYSESSION)
#include "CDMInstanceFairPlayStreamingAVFObjC.h"
#endif
namespace WebCore {
static const Vector<FourCC>& validFairPlayStreamingSchemes()
{
static NeverDestroyed<Vector<FourCC>> validSchemes = Vector<FourCC>({
"cbcs",
"cbc2",
"cbc1",
"cenc",
});
return validSchemes;
}
const AtomicString& CDMPrivateFairPlayStreaming::sinfName()
{
static NeverDestroyed<AtomicString> sinf { MAKE_STATIC_STRING_IMPL("sinf") };
return sinf;
}
const AtomicString& CDMPrivateFairPlayStreaming::skdName()
{
static NeverDestroyed<AtomicString> skd { MAKE_STATIC_STRING_IMPL("skd") };
return skd;
}
const AtomicString& CDMPrivateFairPlayStreaming::cencName()
{
static NeverDestroyed<AtomicString> cenc { MAKE_STATIC_STRING_IMPL("cenc") };
return cenc;
}
static const Vector<uint8_t>& fairPlaySystemID()
{
static NeverDestroyed<Vector<uint8_t>> systemID = Vector<uint8_t>({ 0x94, 0xCE, 0x86, 0xFB, 0x07, 0xFF, 0x4F, 0x43, 0xAD, 0xB8, 0x93, 0xD2, 0xFA, 0x96, 0x8C, 0xA2 });
return systemID;
}
static Vector<Ref<SharedBuffer>> extractSinfData(const SharedBuffer& buffer)
{
// JSON of the format: "{ sinf: [ <base64-encoded-string> ] }"
if (buffer.size() > std::numeric_limits<unsigned>::max())
return { };
String json { buffer.data(), static_cast<unsigned>(buffer.size()) };
RefPtr<JSON::Value> value;
if (!JSON::Value::parseJSON(json, value))
return { };
RefPtr<JSON::Object> object;
if (!value->asObject(object))
return { };
RefPtr<JSON::Array> sinfArray;
if (!object->getArray(CDMPrivateFairPlayStreaming::sinfName(), sinfArray))
return { };
Vector<Ref<SharedBuffer>> sinfs;
sinfs.reserveInitialCapacity(sinfArray->length());
for (auto& value : *sinfArray) {
String keyID;
if (!value->asString(keyID))
continue;
Vector<char> sinfData;
if (!WTF::base64Decode(keyID, { sinfData }, WTF::Base64IgnoreSpacesAndNewLines))
continue;
sinfs.uncheckedAppend(SharedBuffer::create(WTFMove(sinfData)));
}
return sinfs;
}
using SchemeAndKeyResult = Vector<std::pair<FourCC, Vector<uint8_t>>>;
static SchemeAndKeyResult extractSchemeAndKeyIdFromSinf(const SharedBuffer& buffer)
{
auto buffers = extractSinfData(buffer);
if (!buffers.size())
return { };
SchemeAndKeyResult result;
for (auto& buffer : buffers) {
unsigned offset = 0;
Optional<FourCC> scheme;
Optional<Vector<uint8_t>> keyID;
auto view = JSC::DataView::create(buffer->tryCreateArrayBuffer(), offset, buffer->size());
while (auto optionalBoxType = ISOBox::peekBox(view, offset)) {
auto& boxTypeName = optionalBoxType.value().first;
auto& boxSize = optionalBoxType.value().second;
if (boxTypeName == ISOSchemeTypeBox::boxTypeName()) {
ISOSchemeTypeBox schemeTypeBox;
if (!schemeTypeBox.read(view, offset))
break;
scheme = schemeTypeBox.schemeType();
continue;
}
if (boxTypeName == ISOSchemeInformationBox::boxTypeName()) {
ISOSchemeInformationBox schemeInfoBox;
if (!schemeInfoBox.read(view, offset))
break;
auto trackEncryptionBox = downcast<ISOTrackEncryptionBox>(schemeInfoBox.schemeSpecificData());
if (trackEncryptionBox)
keyID = trackEncryptionBox->defaultKID();
continue;
}
offset += boxSize;
}
if (scheme && keyID)
result.append(std::make_pair(scheme.value(), WTFMove(keyID.value())));
}
return result;
}
Optional<Vector<Ref<SharedBuffer>>> CDMPrivateFairPlayStreaming::extractKeyIDsSinf(const SharedBuffer& buffer)
{
Vector<Ref<SharedBuffer>> keyIDs;
auto results = extractSchemeAndKeyIdFromSinf(buffer);
for (auto& result : results) {
if (validFairPlayStreamingSchemes().contains(result.first))
keyIDs.append(SharedBuffer::create(result.second.data(), result.second.size()));
}
return keyIDs;
}
static SchemeAndKeyResult extractSchemeAndKeyIdFromCenc(const SharedBuffer& buffer)
{
auto arrayBuffer = buffer.tryCreateArrayBuffer();
if (!arrayBuffer)
return { };
auto view = JSC::DataView::create(WTFMove(arrayBuffer), 0, buffer.size());
unsigned offset { 0 };
SchemeAndKeyResult result;
while (offset < buffer.size()) {
auto peekResult = ISOBox::peekBox(view, offset);
if (!peekResult || peekResult.value().first != ISOProtectionSystemSpecificHeaderBox::boxTypeName())
return { };
ISOProtectionSystemSpecificHeaderBox psshBox;
if (!psshBox.read(view, offset))
return { };
if (psshBox.systemID() != fairPlaySystemID())
continue;
ISOFairPlayStreamingPsshBox fpsPssh;
offset -= psshBox.size();
if (!fpsPssh.read(view, offset))
return { };
FourCC scheme = fpsPssh.initDataBox().info().scheme();
for (auto request : fpsPssh.initDataBox().requests())
result.append(std::make_pair(scheme, request.requestInfo().keyID()));
}
return result;
}
Optional<Vector<Ref<SharedBuffer>>> CDMPrivateFairPlayStreaming::extractKeyIDsCenc(const SharedBuffer& buffer)
{
Vector<Ref<SharedBuffer>> keyIDs;
auto results = extractSchemeAndKeyIdFromCenc(buffer);
for (auto& result : results) {
if (validFairPlayStreamingSchemes().contains(result.first))
keyIDs.append(SharedBuffer::create(result.second.data(), result.second.size()));
}
return keyIDs;
}
RefPtr<SharedBuffer> CDMPrivateFairPlayStreaming::sanitizeSinf(const SharedBuffer& buffer)
{
// Common SINF Box Format
UNUSED_PARAM(buffer);
notImplemented();
return buffer.copy();
}
RefPtr<SharedBuffer> CDMPrivateFairPlayStreaming::sanitizeSkd(const SharedBuffer& buffer)
{
UNUSED_PARAM(buffer);
notImplemented();
return buffer.copy();
}
RefPtr<SharedBuffer> CDMPrivateFairPlayStreaming::sanitizeCenc(const SharedBuffer& buffer)
{
UNUSED_PARAM(buffer);
notImplemented();
return buffer.copy();
}
Optional<Vector<Ref<SharedBuffer>>> CDMPrivateFairPlayStreaming::extractKeyIDsSkd(const SharedBuffer& buffer)
{
// In the 'skd' scheme, the init data is the key ID.
Vector<Ref<SharedBuffer>> keyIDs;
keyIDs.append(buffer.copy());
return keyIDs;
}
static const HashSet<AtomicString>& validInitDataTypes()
{
static NeverDestroyed<HashSet<AtomicString>> validTypes = HashSet<AtomicString>({
CDMPrivateFairPlayStreaming::sinfName(),
CDMPrivateFairPlayStreaming::skdName(),
#if HAVE(FAIRPLAYSTREAMING_CENC_INITDATA)
CDMPrivateFairPlayStreaming::cencName(),
#endif
});
return validTypes;
}
void CDMFactory::platformRegisterFactories(Vector<CDMFactory*>& factories)
{
factories.append(&CDMFactoryClearKey::singleton());
factories.append(&CDMFactoryFairPlayStreaming::singleton());
InitDataRegistry::shared().registerInitDataType(CDMPrivateFairPlayStreaming::sinfName(), { CDMPrivateFairPlayStreaming::sanitizeSinf, CDMPrivateFairPlayStreaming::extractKeyIDsSinf });
InitDataRegistry::shared().registerInitDataType(CDMPrivateFairPlayStreaming::skdName(), { CDMPrivateFairPlayStreaming::sanitizeSkd, CDMPrivateFairPlayStreaming::extractKeyIDsSkd });
#if HAVE(FAIRPLAYSTREAMING_CENC_INITDATA)
InitDataRegistry::shared().registerInitDataType(CDMPrivateFairPlayStreaming::cencName(), { CDMPrivateFairPlayStreaming::sanitizeCenc, CDMPrivateFairPlayStreaming::extractKeyIDsCenc });
#endif
}
CDMFactoryFairPlayStreaming& CDMFactoryFairPlayStreaming::singleton()
{
static NeverDestroyed<CDMFactoryFairPlayStreaming> s_factory;
return s_factory;
}
CDMFactoryFairPlayStreaming::CDMFactoryFairPlayStreaming() = default;
CDMFactoryFairPlayStreaming::~CDMFactoryFairPlayStreaming() = default;
std::unique_ptr<CDMPrivate> CDMFactoryFairPlayStreaming::createCDM(const String& keySystem)
{
if (!supportsKeySystem(keySystem))
return nullptr;
return std::make_unique<CDMPrivateFairPlayStreaming>();
}
bool CDMFactoryFairPlayStreaming::supportsKeySystem(const String& keySystem)
{
// https://w3c.github.io/encrypted-media/#key-system
// "Key System strings are compared using case-sensitive matching."
return keySystem == "com.apple.fps" || keySystem.startsWith("com.apple.fps."_s);
}
CDMPrivateFairPlayStreaming::CDMPrivateFairPlayStreaming() = default;
CDMPrivateFairPlayStreaming::~CDMPrivateFairPlayStreaming() = default;
bool CDMPrivateFairPlayStreaming::supportsInitDataType(const AtomicString& initDataType) const
{
return validInitDataTypes().contains(initDataType);
}
bool CDMPrivateFairPlayStreaming::supportsConfiguration(const CDMKeySystemConfiguration& configuration) const
{
if (!WTF::anyOf(configuration.initDataTypes, [] (auto& initDataType) { return validInitDataTypes().contains(initDataType); }))
return false;
#if HAVE(AVCONTENTKEYSESSION)
// FIXME: verify that FairPlayStreaming does not (and cannot) expose a distinctive identifier to the client
if (configuration.distinctiveIdentifier == CDMRequirement::Required)
return false;
if (configuration.persistentState == CDMRequirement::Required && !CDMInstanceFairPlayStreamingAVFObjC::supportsPersistableState())
return false;
if (configuration.sessionTypes.contains(CDMSessionType::PersistentLicense)
&& !configuration.sessionTypes.contains(CDMSessionType::Temporary)
&& !CDMInstanceFairPlayStreamingAVFObjC::supportsPersistentKeys())
return false;
if (!configuration.audioCapabilities.isEmpty()
&& !WTF::anyOf(configuration.audioCapabilities, [](auto& capability) {
return CDMInstanceFairPlayStreamingAVFObjC::supportsMediaCapability(capability);
}))
return false;
if (!configuration.videoCapabilities.isEmpty()
&& !WTF::anyOf(configuration.videoCapabilities, [](auto& capability) {
return CDMInstanceFairPlayStreamingAVFObjC::supportsMediaCapability(capability);
}))
return false;
return true;
#else
return false;
#endif
}
bool CDMPrivateFairPlayStreaming::supportsConfigurationWithRestrictions(const CDMKeySystemConfiguration& configuration, const CDMRestrictions& restrictions) const
{
if (restrictions.persistentStateDenied
&& !configuration.sessionTypes.isEmpty()
&& !configuration.sessionTypes.contains(CDMSessionType::Temporary))
return false;
if (restrictions.persistentStateDenied && configuration.persistentState == CDMRequirement::Required)
return false;
if (WTF::allOf(configuration.sessionTypes, [restrictions](auto& sessionType) {
return restrictions.deniedSessionTypes.contains(sessionType);
}))
return false;
return supportsConfiguration(configuration);
}
bool CDMPrivateFairPlayStreaming::supportsSessionTypeWithConfiguration(CDMSessionType& sessionType, const CDMKeySystemConfiguration& configuration) const
{
if (sessionType == CDMSessionType::Temporary) {
if (configuration.persistentState == CDMRequirement::Required)
return false;
} else if (configuration.persistentState == CDMRequirement::NotAllowed)
return false;
return supportsConfiguration(configuration);
}
bool CDMPrivateFairPlayStreaming::supportsRobustness(const String& robustness) const
{
if (robustness.isEmpty())
return true;
// FIXME: Determine an enumerated list of robustness values supported by FPS.
return false;
}
CDMRequirement CDMPrivateFairPlayStreaming::distinctiveIdentifiersRequirement(const CDMKeySystemConfiguration&, const CDMRestrictions&) const
{
// FIXME: verify that FairPlayStreaming does not (and cannot) expose a distinctive identifier to the client
return CDMRequirement::NotAllowed;
}
CDMRequirement CDMPrivateFairPlayStreaming::persistentStateRequirement(const CDMKeySystemConfiguration&, const CDMRestrictions&) const
{
return CDMRequirement::Optional;
}
bool CDMPrivateFairPlayStreaming::distinctiveIdentifiersAreUniquePerOriginAndClearable(const CDMKeySystemConfiguration&) const
{
return true;
}
RefPtr<CDMInstance> CDMPrivateFairPlayStreaming::createInstance()
{
#if HAVE(AVCONTENTKEYSESSION)
return adoptRef(new CDMInstanceFairPlayStreamingAVFObjC());
#else
return nullptr;
#endif
}
void CDMPrivateFairPlayStreaming::loadAndInitialize()
{
}
bool CDMPrivateFairPlayStreaming::supportsServerCertificates() const
{
return true;
}
bool CDMPrivateFairPlayStreaming::supportsSessions() const
{
return true;
}
bool CDMPrivateFairPlayStreaming::supportsInitData(const AtomicString& initDataType, const SharedBuffer& initData) const
{
if (!supportsInitDataType(initDataType))
return false;
if (initDataType == sinfName()) {
return WTF::anyOf(extractSchemeAndKeyIdFromSinf(initData), [](auto& result) {
return validFairPlayStreamingSchemes().contains(result.first);
});
}
if (initDataType == skdName())
return true;
ASSERT_NOT_REACHED();
return false;
}
RefPtr<SharedBuffer> CDMPrivateFairPlayStreaming::sanitizeResponse(const SharedBuffer& response) const
{
return response.copy();
}
Optional<String> CDMPrivateFairPlayStreaming::sanitizeSessionId(const String& sessionId) const
{
return sessionId;
}
} // namespace WebCore
#endif // ENABLE(ENCRYPTED_MEDIA)