Update Simulcrypt ECMg
This commit is contained in:
@@ -90,6 +90,7 @@ cc_library(
|
||||
"@abseil_repo//absl/container:node_hash_map",
|
||||
"@abseil_repo//absl/memory",
|
||||
"@abseil_repo//absl/strings",
|
||||
"@abseil_repo//absl/types:optional",
|
||||
"//common:crypto_util",
|
||||
"//common:random_util",
|
||||
"//common:status",
|
||||
@@ -242,3 +243,25 @@ cc_test(
|
||||
"//common:status",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "emm",
|
||||
srcs = ["emm.cc"],
|
||||
hdrs = ["emm.h"],
|
||||
deps = [
|
||||
"//base",
|
||||
"@abseil_repo//absl/strings",
|
||||
"//common:status",
|
||||
"//common:string_util",
|
||||
"//protos/public:media_cas_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "emm_test",
|
||||
srcs = ["emm_test.cc"],
|
||||
deps = [
|
||||
":emm",
|
||||
"//testing:gunit_main",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -95,10 +95,10 @@ class MockEcm : public Ecm {
|
||||
MockEcm() = default;
|
||||
~MockEcm() override = default;
|
||||
|
||||
MOCK_CONST_METHOD0(age_restriction, uint8_t());
|
||||
MOCK_CONST_METHOD0(crypto_mode, CryptoMode());
|
||||
MOCK_CONST_METHOD0(paired_keys_required, bool());
|
||||
MOCK_CONST_METHOD0(content_iv_size, size_t());
|
||||
MOCK_METHOD(uint8_t, age_restriction, (), (const, override));
|
||||
MOCK_METHOD(CryptoMode, crypto_mode, (), (const, override));
|
||||
MOCK_METHOD(bool, paired_keys_required, (), (const, override));
|
||||
MOCK_METHOD(size_t, content_iv_size, (), (const, override));
|
||||
|
||||
std::string CallSerializeEcm(const std::vector<EntitledKeyInfo*>& keys) {
|
||||
return SerializeEcm(keys);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "media_cas_packager_sdk/internal/ecmg_client_handler.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
#include "glog/logging.h"
|
||||
@@ -873,32 +874,55 @@ Status EcmgClientHandler::BuildEcmDatagram(const EcmgParameters& params,
|
||||
EcmgStreamInfo* stream_info = streams_info_.at(params.ecm_stream_id).get();
|
||||
DCHECK(stream_info->ecm);
|
||||
|
||||
size_t key_count = params.cp_cw_combinations.size();
|
||||
// Number of keys can only be 1 or 2.
|
||||
if (key_count != ecmg_config_->number_of_content_keys || key_count < 1 ||
|
||||
key_count > 2) {
|
||||
return {error::INVALID_ARGUMENT, "Unexpected cp_cw_combinations size."};
|
||||
}
|
||||
|
||||
// Generate serialized ECM.
|
||||
CryptoMode crypto_mode = stream_info->crypto_mode == CryptoMode::kInvalid
|
||||
? ecmg_config_->crypto_mode
|
||||
: stream_info->crypto_mode;
|
||||
|
||||
// If two keys are present, the even key should be put first, followed by the
|
||||
// odd key.
|
||||
std::vector<EntitledKeyInfo> keys;
|
||||
keys.reserve(ecmg_config_->number_of_content_keys);
|
||||
for (size_t i = 0; i < ecmg_config_->number_of_content_keys; i++) {
|
||||
DCHECK(params.cp_cw_combinations[i].cp == params.cp_number + i);
|
||||
keys.emplace_back();
|
||||
keys[i].key_value = params.cp_cw_combinations[i].cw;
|
||||
keys.reserve(key_count);
|
||||
for (const auto& cp_cw : params.cp_cw_combinations) {
|
||||
auto key_info = cp_cw.cp % 2 == 0 ? keys.emplace(keys.begin())
|
||||
: keys.emplace(keys.end());
|
||||
key_info->key_value = cp_cw.cw;
|
||||
// Make content key to 16 bytes if crypto mode is Csa2.
|
||||
if (crypto_mode == CryptoMode::kDvbCsa2 && keys[i].key_value.size() == 8) {
|
||||
keys[i].key_value = keys[i].key_value + keys[i].key_value;
|
||||
if (crypto_mode == CryptoMode::kDvbCsa2 &&
|
||||
key_info->key_value.size() == 8) {
|
||||
key_info->key_value =
|
||||
absl::StrCat(key_info->key_value, key_info->key_value);
|
||||
}
|
||||
keys[i].key_id = crypto_util::DeriveKeyId(keys[i].key_value);
|
||||
keys[i].content_iv = stream_info->content_ivs.empty()
|
||||
? content_ivs_[i]
|
||||
: stream_info->content_ivs[i];
|
||||
if (!RandomBytes(kWrappedKeyIvSizeBytes, &keys[i].wrapped_key_iv)) {
|
||||
key_info->key_id = crypto_util::DeriveKeyId(key_info->key_value);
|
||||
auto generated_key_iv = GenerateRandomWrappedKeyIv();
|
||||
if (!generated_key_iv.has_value() ||
|
||||
generated_key_iv->size() != kWrappedKeyIvSizeBytes) {
|
||||
return {error::INTERNAL, "Unable to generate random wrapped key iv."};
|
||||
}
|
||||
key_info->wrapped_key_iv = generated_key_iv.value();
|
||||
}
|
||||
|
||||
if (content_ivs_.size() < key_count &&
|
||||
stream_info->content_ivs.size() < key_count) {
|
||||
return {error::INVALID_ARGUMENT, "Not enough content iv."};
|
||||
}
|
||||
// The first iv received is for even key and second is for odd key.
|
||||
for (size_t i = 0; i < key_count; ++i) {
|
||||
keys[i].content_iv = stream_info->content_ivs.size() >= key_count
|
||||
? stream_info->content_ivs[i]
|
||||
: content_ivs_[i];
|
||||
}
|
||||
|
||||
Status status;
|
||||
std::string serialized_ecm;
|
||||
if (ecmg_config_->number_of_content_keys > 1) {
|
||||
if (key_count > 1) {
|
||||
status = stream_info->ecm->GenerateEcm(
|
||||
&keys[0], &keys[1], stream_info->track_type, &serialized_ecm);
|
||||
} else {
|
||||
@@ -926,5 +950,14 @@ Status EcmgClientHandler::BuildEcmDatagram(const EcmgParameters& params,
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
absl::optional<std::string> EcmgClientHandler::GenerateRandomWrappedKeyIv()
|
||||
const {
|
||||
std::string output_iv;
|
||||
if (RandomBytes(kWrappedKeyIvSizeBytes, &output_iv)) {
|
||||
return output_iv;
|
||||
}
|
||||
return absl::nullopt;
|
||||
}
|
||||
|
||||
} // namespace cas
|
||||
} // namespace widevine
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include "absl/container/node_hash_map.h"
|
||||
#include "absl/types/optional.h"
|
||||
#include "common/status.h"
|
||||
#include "media_cas_packager_sdk/internal/ecm.h"
|
||||
#include "media_cas_packager_sdk/public/wv_cas_types.h"
|
||||
@@ -126,6 +127,10 @@ class EcmgClientHandler {
|
||||
Status BuildEcmDatagram(const EcmgParameters& params,
|
||||
uint8_t* ecm_datagram) const;
|
||||
|
||||
// Generates a random wrapped key iv string. Returns true on success, false
|
||||
// otherwise. The main purpose for this function is easier testing.
|
||||
virtual absl::optional<std::string> GenerateRandomWrappedKeyIv() const;
|
||||
|
||||
EcmgConfig* ecmg_config_;
|
||||
// Per spec, "There is always one (and only one) channel per TCP connection".
|
||||
bool channel_id_set_;
|
||||
|
||||
@@ -12,12 +12,14 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "testing/gmock.h"
|
||||
#include "testing/gunit.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "example/test_ecmg_messages.h"
|
||||
#include "media_cas_packager_sdk/internal/ecmg_constants.h"
|
||||
#include "media_cas_packager_sdk/internal/mpeg2ts.h"
|
||||
#include "media_cas_packager_sdk/internal/simulcrypt_util.h"
|
||||
#include "media_cas_packager_sdk/internal/util.h"
|
||||
|
||||
@@ -31,28 +33,36 @@ using simulcrypt_util::AddUint32Param;
|
||||
using simulcrypt_util::AddUint8Param;
|
||||
using simulcrypt_util::BuildMessageHeader;
|
||||
|
||||
static constexpr size_t kBufferSize = 1024;
|
||||
static constexpr size_t kSuperCasId = 0x4AD40000;
|
||||
static constexpr size_t kChannelId = 1;
|
||||
static constexpr size_t kStreamId = 1;
|
||||
static constexpr size_t kEcmId = 2;
|
||||
static constexpr size_t kNominalCpDuration = 0x64;
|
||||
static constexpr size_t kCpNumber = 0;
|
||||
static constexpr char kContentKeyEven[] = "0123456701234567";
|
||||
static constexpr char kContentKeyEven8Bytes[] = "01234567";
|
||||
static constexpr char kContentKeyOdd[] = "abcdefghabcdefgh";
|
||||
static constexpr char kContentKeyOdd8Bytes[] = "abcdefgh";
|
||||
static constexpr char kEntitlementKeyIdEven[] = "0123456701234567";
|
||||
static constexpr char kEntitlementKeyValueEven[] =
|
||||
"01234567012345670123456701234567";
|
||||
static constexpr char kEntitlementKeyIdOdd[] = "abcdefghabcdefgh";
|
||||
static constexpr char kEntitlementKeyValueOdd[] =
|
||||
"abcdefghabcdefghabcdefghabcdefgh";
|
||||
static constexpr size_t kAgeRestriction = 3;
|
||||
static constexpr char kCryptoMode[] = "AesScte";
|
||||
static constexpr char kCryptoModeCsa2[] = "DvbCsa2";
|
||||
static constexpr char kTrackTypesSD[] = "SD";
|
||||
static constexpr char kTrackTypesHD[] = "HD";
|
||||
constexpr size_t kBufferSize = 1024;
|
||||
constexpr size_t kSuperCasId = 0x4AD40000;
|
||||
constexpr size_t kChannelId = 1;
|
||||
constexpr size_t kStreamId = 1;
|
||||
constexpr size_t kEcmId = 2;
|
||||
constexpr size_t kNominalCpDuration = 0x64;
|
||||
constexpr size_t kCpNumber = 0;
|
||||
constexpr char kContentKeyEven[] = "0123456701234567";
|
||||
constexpr char kContentKeyEven8Bytes[] = "01234567";
|
||||
constexpr char kContentKeyOdd[] = "abcdefghabcdefgh";
|
||||
constexpr char kContentKeyOdd8Bytes[] = "abcdefgh";
|
||||
constexpr char kEntitlementKeyIdEven[] = "0123456701234567";
|
||||
constexpr char kEntitlementKeyValueEven[] = "01234567012345670123456701234567";
|
||||
constexpr char kEntitlementKeyIdOdd[] = "abcdefghabcdefgh";
|
||||
constexpr char kEntitlementKeyValueOdd[] = "abcdefghabcdefghabcdefghabcdefgh";
|
||||
constexpr size_t kAgeRestriction = 3;
|
||||
constexpr char kCryptoMode[] = "AesScte";
|
||||
constexpr char kCryptoModeCsa2[] = "DvbCsa2";
|
||||
constexpr char kTrackTypesSD[] = "SD";
|
||||
constexpr char kTrackTypesHD[] = "HD";
|
||||
constexpr absl::string_view kWrappedKeyIv = "0123456701234567";
|
||||
|
||||
class MockEcmgClientHandler : public EcmgClientHandler {
|
||||
public:
|
||||
explicit MockEcmgClientHandler(EcmgConfig* ecmg_config)
|
||||
: EcmgClientHandler(ecmg_config) {}
|
||||
absl::optional<std::string> GenerateRandomWrappedKeyIv() const override {
|
||||
return std::string(kWrappedKeyIv);
|
||||
}
|
||||
};
|
||||
|
||||
class EcmgClientHandlerTest : public ::testing::Test {
|
||||
protected:
|
||||
@@ -63,7 +73,7 @@ class EcmgClientHandlerTest : public ::testing::Test {
|
||||
config_.max_comp_time = 100;
|
||||
config_.access_criteria_transfer_mode = 1;
|
||||
config_.number_of_content_keys = 2;
|
||||
handler_ = absl::make_unique<EcmgClientHandler>(&config_);
|
||||
handler_ = absl::make_unique<MockEcmgClientHandler>(&config_);
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -564,6 +574,37 @@ TEST_F(EcmgClientHandlerTest, WrongMessageLength) {
|
||||
CheckChannelError(UNKNOWN_PARAMETER_TYPE_VALUE, response_, response_len_);
|
||||
}
|
||||
|
||||
TEST_F(EcmgClientHandlerTest, BuildEcmDatagramSequenceOfEvenOdd) {
|
||||
SetupValidChannelStream();
|
||||
|
||||
std::vector<EcmgCpCwCombination> cp_cw_combination = {
|
||||
{kCpNumber, kContentKeyEven}, {kCpNumber + 1, kContentKeyOdd}};
|
||||
BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination,
|
||||
request_, &request_len_);
|
||||
handler_->HandleRequest(request_, response_, &response_len_);
|
||||
EXPECT_EQ(response_len_, sizeof(kTestEcmgEcmResponse));
|
||||
std::string first_response(response_, response_len_);
|
||||
|
||||
// Change the sequence of cp_cw_combination.
|
||||
cp_cw_combination = {{kCpNumber + 1, kContentKeyOdd},
|
||||
{kCpNumber, kContentKeyEven}};
|
||||
BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination,
|
||||
request_, &request_len_);
|
||||
handler_->HandleRequest(request_, response_, &response_len_);
|
||||
// Sequence of cp_cw_combination does not matter as even/odd is based on cp
|
||||
// number.
|
||||
EXPECT_EQ(std::string(response_, response_len_), first_response);
|
||||
|
||||
// Swap the key value in cp_cw_combination.
|
||||
cp_cw_combination = {{kCpNumber, kContentKeyOdd},
|
||||
{kCpNumber + 1, kContentKeyEven}};
|
||||
BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination,
|
||||
request_, &request_len_);
|
||||
handler_->HandleRequest(request_, response_, &response_len_);
|
||||
// Swapping key value changes generated ecm.
|
||||
EXPECT_NE(std::string(response_, response_len_), first_response);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace cas
|
||||
} // namespace widevine
|
||||
|
||||
136
media_cas_packager_sdk/internal/emm.cc
Normal file
136
media_cas_packager_sdk/internal/emm.cc
Normal file
@@ -0,0 +1,136 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright 2020 Google LLC.
|
||||
//
|
||||
// This software is licensed under the terms defined in the Widevine Master
|
||||
// License Agreement. For a copy of this agreement, please contact
|
||||
// widevine-licensing@google.com.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "media_cas_packager_sdk/internal/emm.h"
|
||||
|
||||
#include <bitset>
|
||||
|
||||
#include "glog/logging.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "common/status.h"
|
||||
#include "common/string_util.h"
|
||||
|
||||
namespace widevine {
|
||||
namespace cas {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kNumBitsVersionField = 8;
|
||||
constexpr int kNumBitsHeaderLengthField = 8;
|
||||
constexpr int kNumBitsTimestampLengthField = 64;
|
||||
constexpr int kNumBitsPayloadLengthField = 16;
|
||||
|
||||
// Version - this should be incremented if there are changes to the EMM.
|
||||
constexpr uint8_t kEmmVersion = 1;
|
||||
} // namespace
|
||||
|
||||
Status Emm::SetFingerprinting(
|
||||
const std::vector<FingerprintingInitParameters>& fingerprintings) {
|
||||
// First clear all current fingerprinting payload.
|
||||
emm_payload_.clear_fingerprinting();
|
||||
|
||||
// TODO(b/161149665): validate passed in data.
|
||||
Status status;
|
||||
for (const auto& fingerprinting_param : fingerprintings) {
|
||||
Fingerprinting* fingerprinting_payload = emm_payload_.add_fingerprinting();
|
||||
for (const auto& channel : fingerprinting_param.channels) {
|
||||
fingerprinting_payload->add_channels(channel);
|
||||
}
|
||||
fingerprinting_payload->set_control(fingerprinting_param.control);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
Status Emm::SetServiceBlocking(
|
||||
const std::vector<ServiceBlockingInitParameters>& service_blockings) {
|
||||
// First clear all current service blocking payload.
|
||||
emm_payload_.clear_service_blocking();
|
||||
|
||||
// TODO(b/161149665): validate passed in data.
|
||||
Status status;
|
||||
for (const auto& service_blocking_param : service_blockings) {
|
||||
ServiceBlocking* service_blocking_payload =
|
||||
emm_payload_.add_service_blocking();
|
||||
for (const auto& channel : service_blocking_param.channels) {
|
||||
service_blocking_payload->add_channels(channel);
|
||||
}
|
||||
for (const auto& device_group : service_blocking_param.device_groups) {
|
||||
service_blocking_payload->add_device_groups(device_group);
|
||||
}
|
||||
if (service_blocking_param.start_time != 0) {
|
||||
service_blocking_payload->set_start_time_sec(
|
||||
service_blocking_param.start_time);
|
||||
}
|
||||
service_blocking_payload->set_end_time_sec(service_blocking_param.end_time);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
Status Emm::GenerateEmm(std::string* serialized_emm) const {
|
||||
if (serialized_emm == nullptr) {
|
||||
return {error::INVALID_ARGUMENT, "No return emm std::string pointer."};
|
||||
}
|
||||
|
||||
EmmSerializingParameters serializing_params;
|
||||
serializing_params.payload = emm_payload_.SerializeAsString();
|
||||
serializing_params.timestamp = GenerateTimestamp();
|
||||
|
||||
// Generate serialized emm (without signature yet).
|
||||
Status status =
|
||||
GenerateSerializedEmmNoSignature(serializing_params, serialized_emm);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Calculate and append signature.
|
||||
absl::StrAppend(serialized_emm, GenerateSignature(*serialized_emm));
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
Status Emm::GenerateSerializedEmmNoSignature(
|
||||
const EmmSerializingParameters& params, std::string* serialized_emm) const {
|
||||
if (serialized_emm == nullptr) {
|
||||
return {error::INVALID_ARGUMENT, "No return emm std::string pointer."};
|
||||
}
|
||||
|
||||
std::bitset<kNumBitsVersionField> version(kEmmVersion);
|
||||
std::bitset<kNumBitsHeaderLengthField> header_length(
|
||||
sizeof(params.timestamp));
|
||||
std::bitset<kNumBitsTimestampLengthField> timestamp(params.timestamp);
|
||||
std::bitset<kNumBitsPayloadLengthField> payload_length(
|
||||
params.payload.length());
|
||||
|
||||
std::string emm_bitset =
|
||||
absl::StrCat(version.to_string(), header_length.to_string(),
|
||||
timestamp.to_string(), payload_length.to_string());
|
||||
|
||||
Status status =
|
||||
string_util::BitsetStringToBinaryString(emm_bitset, serialized_emm);
|
||||
if (!status.ok() || serialized_emm->empty()) {
|
||||
LOG(ERROR) << "Failed to convert EMM bitset to std::string";
|
||||
return {error::INTERNAL, "Failed to convert EMM bitset to std::string"};
|
||||
}
|
||||
|
||||
// Appends payload.
|
||||
absl::StrAppend(serialized_emm, params.payload);
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
int64_t Emm::GenerateTimestamp() const {
|
||||
// TODO(b/161252065): Generate timestamp.
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string Emm::GenerateSignature(const std::string& content) const {
|
||||
// TODO(b/161252442): Calculate signature.
|
||||
std::string signature(32, 'x'); // A fake 32 bytes signature.
|
||||
return signature;
|
||||
}
|
||||
|
||||
} // namespace cas
|
||||
} // namespace widevine
|
||||
74
media_cas_packager_sdk/internal/emm.h
Normal file
74
media_cas_packager_sdk/internal/emm.h
Normal file
@@ -0,0 +1,74 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright 2020 Google LLC.
|
||||
//
|
||||
// This software is licensed under the terms defined in the Widevine Master
|
||||
// License Agreement. For a copy of this agreement, please contact
|
||||
// widevine-licensing@google.com.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef MEDIA_CAS_PACKAGER_SDK_INTERNAL_EMM_H_
|
||||
#define MEDIA_CAS_PACKAGER_SDK_INTERNAL_EMM_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <cstdint>
|
||||
#include "common/status.h"
|
||||
#include "protos/public/media_cas.pb.h"
|
||||
|
||||
namespace widevine {
|
||||
namespace cas {
|
||||
|
||||
struct FingerprintingInitParameters {
|
||||
std::vector<std::string> channels;
|
||||
std::string control;
|
||||
};
|
||||
|
||||
struct ServiceBlockingInitParameters {
|
||||
std::vector<std::string> channels;
|
||||
std::vector<std::string> device_groups;
|
||||
// Value 0 in start_time means immediate.
|
||||
int64_t start_time = 0;
|
||||
int64_t end_time;
|
||||
};
|
||||
|
||||
// Generator for producing Widevine CAS-compliant EMMs. Used to construct the
|
||||
// Transport Stream packet payload of an EMM containing messages including
|
||||
// fingerprinting and service blocking.
|
||||
// Class Emm is not thread safe.
|
||||
class Emm {
|
||||
public:
|
||||
Emm() = default;
|
||||
Emm(const Emm&) = delete;
|
||||
Emm& operator=(const Emm&) = delete;
|
||||
virtual ~Emm() = default;
|
||||
|
||||
// Replaces current fingerprinting info with |fingerprintings|.
|
||||
Status SetFingerprinting(
|
||||
const std::vector<FingerprintingInitParameters>& fingerprintings);
|
||||
|
||||
// Replaces current service blocking info with |service_blockings|.
|
||||
Status SetServiceBlocking(
|
||||
const std::vector<ServiceBlockingInitParameters>& service_blockings);
|
||||
|
||||
// Generates serialized EMM to |serialized_emm|.
|
||||
Status GenerateEmm(std::string* serialized_emm) const;
|
||||
|
||||
private:
|
||||
struct EmmSerializingParameters {
|
||||
int64_t timestamp;
|
||||
std::string payload;
|
||||
};
|
||||
|
||||
Status GenerateSerializedEmmNoSignature(
|
||||
const EmmSerializingParameters& params,
|
||||
std::string* serialized_emm) const;
|
||||
int64_t GenerateTimestamp() const;
|
||||
std::string GenerateSignature(const std::string& content) const;
|
||||
|
||||
EmmPayload emm_payload_;
|
||||
};
|
||||
|
||||
} // namespace cas
|
||||
} // namespace widevine
|
||||
#endif // MEDIA_CAS_PACKAGER_SDK_INTERNAL_EMM_H_
|
||||
236
media_cas_packager_sdk/internal/emm_test.cc
Normal file
236
media_cas_packager_sdk/internal/emm_test.cc
Normal file
@@ -0,0 +1,236 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright 2020 Google LLC.
|
||||
//
|
||||
// This software is licensed under the terms defined in the Widevine Master
|
||||
// License Agreement. For a copy of this agreement, please contact
|
||||
// widevine-licensing@google.com.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "media_cas_packager_sdk/internal/emm.h"
|
||||
|
||||
#include "testing/gmock.h"
|
||||
#include "testing/gunit.h"
|
||||
|
||||
|
||||
namespace widevine {
|
||||
namespace cas {
|
||||
namespace {
|
||||
|
||||
constexpr char kChannelOne[] = "CH1";
|
||||
constexpr char kChannelTwo[] = "CH2";
|
||||
constexpr char kChannelThree[] = "CH3";
|
||||
constexpr char kFingerprintingControl[] = "controls";
|
||||
constexpr char kDeviceGroupOne[] = "Group1";
|
||||
constexpr char kDeviceGroupTwo[] = "Group2";
|
||||
constexpr int64_t kServiceBockingStartTime = 100;
|
||||
constexpr int64_t kServiceBockingEndTime = 1000;
|
||||
|
||||
// Length in bytes when there is no payload.
|
||||
constexpr uint8_t kExpectedNoPayloadLengthBytes = 44;
|
||||
constexpr uint8_t kExpectedEmmVersion = 1;
|
||||
constexpr uint8_t kExpectedHeaderLength = 8;
|
||||
|
||||
constexpr uint8_t kVersionStartIndex = 0;
|
||||
constexpr uint8_t kHeaderLengthStartIndex = 1;
|
||||
constexpr uint8_t kPayloadLengthStartIndex = 10;
|
||||
constexpr uint8_t kPayloadStartIndex = 12;
|
||||
constexpr uint8_t kSignatureLength = 32;
|
||||
|
||||
FingerprintingInitParameters GetValidFingerprintingParams() {
|
||||
FingerprintingInitParameters fingerprinting_params;
|
||||
fingerprinting_params.channels = {kChannelOne, kChannelTwo};
|
||||
fingerprinting_params.control = kFingerprintingControl;
|
||||
return fingerprinting_params;
|
||||
}
|
||||
|
||||
void LoadExpectedFingerprintingProto(Fingerprinting* fingerprinting_payload) {
|
||||
fingerprinting_payload->add_channels(kChannelOne);
|
||||
fingerprinting_payload->add_channels(kChannelTwo);
|
||||
fingerprinting_payload->set_control(kFingerprintingControl);
|
||||
}
|
||||
|
||||
ServiceBlockingInitParameters GetValidServiceBlockingParams() {
|
||||
ServiceBlockingInitParameters service_blocking_params;
|
||||
service_blocking_params.channels = {kChannelOne, kChannelTwo};
|
||||
service_blocking_params.device_groups = {kDeviceGroupOne, kDeviceGroupTwo};
|
||||
service_blocking_params.start_time = kServiceBockingStartTime;
|
||||
service_blocking_params.end_time = kServiceBockingEndTime;
|
||||
return service_blocking_params;
|
||||
}
|
||||
|
||||
void LoadExpectedServiceBlockingProto(
|
||||
ServiceBlocking* service_blocking_payload) {
|
||||
service_blocking_payload->add_channels(kChannelOne);
|
||||
service_blocking_payload->add_channels(kChannelTwo);
|
||||
service_blocking_payload->add_device_groups(kDeviceGroupOne);
|
||||
service_blocking_payload->add_device_groups(kDeviceGroupTwo);
|
||||
service_blocking_payload->set_start_time_sec(kServiceBockingStartTime);
|
||||
service_blocking_payload->set_end_time_sec(kServiceBockingEndTime);
|
||||
}
|
||||
|
||||
TEST(EmmTest, GenerateEmmSinglePayloadSuccess) {
|
||||
Emm emm_gen;
|
||||
FingerprintingInitParameters fingerprinting = GetValidFingerprintingParams();
|
||||
ServiceBlockingInitParameters service_blocking =
|
||||
GetValidServiceBlockingParams();
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting({fingerprinting}), OkStatus());
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking({service_blocking}), OkStatus());
|
||||
|
||||
std::string result;
|
||||
EXPECT_EQ(emm_gen.GenerateEmm(&result), OkStatus());
|
||||
EXPECT_GT(result.length(), kExpectedNoPayloadLengthBytes);
|
||||
|
||||
EXPECT_EQ(static_cast<uint8_t>(result[kVersionStartIndex]),
|
||||
kExpectedEmmVersion);
|
||||
EXPECT_EQ(static_cast<uint8_t>(result[kHeaderLengthStartIndex]),
|
||||
kExpectedHeaderLength);
|
||||
int payload_lengh = static_cast<uint16_t>(result[kPayloadLengthStartIndex])
|
||||
<< 8 |
|
||||
static_cast<uint8_t>(result[kPayloadLengthStartIndex + 1]);
|
||||
ASSERT_GT(payload_lengh, 0);
|
||||
ASSERT_EQ(result.length(),
|
||||
kPayloadStartIndex + payload_lengh + kSignatureLength);
|
||||
|
||||
std::string payload_section =
|
||||
result.substr(kPayloadStartIndex, payload_lengh);
|
||||
// Parse the payload and validate fields.
|
||||
EmmPayload emm_payload;
|
||||
ASSERT_TRUE(emm_payload.ParseFromString(payload_section));
|
||||
|
||||
EmmPayload expected_payload;
|
||||
LoadExpectedFingerprintingProto(expected_payload.add_fingerprinting());
|
||||
LoadExpectedServiceBlockingProto(expected_payload.add_service_blocking());
|
||||
std::string serialized_expected_payload;
|
||||
expected_payload.SerializeToString(&serialized_expected_payload);
|
||||
EXPECT_EQ(payload_section, serialized_expected_payload);
|
||||
}
|
||||
|
||||
TEST(EmmTest, GenerateEmmMultiplePayloadSuccess) {
|
||||
Emm emm_gen;
|
||||
FingerprintingInitParameters fingerprinting_params;
|
||||
fingerprinting_params.channels = {kChannelThree};
|
||||
fingerprinting_params.control = kFingerprintingControl;
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting(
|
||||
{GetValidFingerprintingParams(), fingerprinting_params}),
|
||||
OkStatus());
|
||||
|
||||
ServiceBlockingInitParameters service_blocking_params;
|
||||
service_blocking_params.channels = {kChannelThree};
|
||||
service_blocking_params.device_groups = {kDeviceGroupOne, kDeviceGroupTwo};
|
||||
service_blocking_params.start_time = kServiceBockingStartTime;
|
||||
service_blocking_params.end_time = kServiceBockingEndTime;
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking(
|
||||
{GetValidServiceBlockingParams(), service_blocking_params}),
|
||||
OkStatus());
|
||||
|
||||
std::string result;
|
||||
EXPECT_EQ(emm_gen.GenerateEmm(&result), OkStatus());
|
||||
EXPECT_GT(result.length(), kExpectedNoPayloadLengthBytes);
|
||||
|
||||
EXPECT_EQ(static_cast<uint8_t>(result[kVersionStartIndex]),
|
||||
kExpectedEmmVersion);
|
||||
EXPECT_EQ(static_cast<uint8_t>(result[kHeaderLengthStartIndex]),
|
||||
kExpectedHeaderLength);
|
||||
int payload_lengh = static_cast<uint16_t>(result[kPayloadLengthStartIndex])
|
||||
<< 8 |
|
||||
static_cast<uint8_t>(result[kPayloadLengthStartIndex + 1]);
|
||||
ASSERT_GT(payload_lengh, 0);
|
||||
ASSERT_EQ(result.length(),
|
||||
kPayloadStartIndex + payload_lengh + kSignatureLength);
|
||||
|
||||
std::string payload_section =
|
||||
result.substr(kPayloadStartIndex, payload_lengh);
|
||||
// Parse the payload and validate fields.
|
||||
EmmPayload emm_payload;
|
||||
ASSERT_TRUE(emm_payload.ParseFromString(payload_section));
|
||||
EXPECT_EQ(emm_payload.fingerprinting_size(), 2);
|
||||
EXPECT_EQ(emm_payload.service_blocking_size(), 2);
|
||||
}
|
||||
|
||||
TEST(EmmTest, GenerateEmmFingerprintingOnlySuccess) {
|
||||
Emm emm_gen;
|
||||
FingerprintingInitParameters fingerprinting = GetValidFingerprintingParams();
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting({fingerprinting}), OkStatus());
|
||||
// OK to be called again.
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting({fingerprinting}), OkStatus());
|
||||
|
||||
std::string result;
|
||||
EXPECT_EQ(emm_gen.GenerateEmm(&result), OkStatus());
|
||||
EXPECT_GT(result.length(), kExpectedNoPayloadLengthBytes);
|
||||
|
||||
EXPECT_EQ(static_cast<uint8_t>(result[kVersionStartIndex]),
|
||||
kExpectedEmmVersion);
|
||||
EXPECT_EQ(static_cast<uint8_t>(result[kHeaderLengthStartIndex]),
|
||||
kExpectedHeaderLength);
|
||||
int payload_lengh = static_cast<uint16_t>(result[kPayloadLengthStartIndex])
|
||||
<< 8 |
|
||||
static_cast<uint8_t>(result[kPayloadLengthStartIndex + 1]);
|
||||
ASSERT_GT(payload_lengh, 0);
|
||||
ASSERT_EQ(result.length(),
|
||||
kPayloadStartIndex + payload_lengh + kSignatureLength);
|
||||
|
||||
std::string payload_section =
|
||||
result.substr(kPayloadStartIndex, payload_lengh);
|
||||
// Parse the payload and validate fields.
|
||||
EmmPayload emm_payload;
|
||||
ASSERT_TRUE(emm_payload.ParseFromString(payload_section));
|
||||
EmmPayload expected_payload;
|
||||
LoadExpectedFingerprintingProto(expected_payload.add_fingerprinting());
|
||||
std::string serialized_expected_payload;
|
||||
expected_payload.SerializeToString(&serialized_expected_payload);
|
||||
EXPECT_EQ(payload_section, serialized_expected_payload);
|
||||
}
|
||||
|
||||
TEST(EmmTest, GenerateEmmServiceBlockingOnlySuccess) {
|
||||
Emm emm_gen;
|
||||
ServiceBlockingInitParameters service_blocking =
|
||||
GetValidServiceBlockingParams();
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking({service_blocking}), OkStatus());
|
||||
|
||||
std::string result;
|
||||
EXPECT_EQ(emm_gen.GenerateEmm(&result), OkStatus());
|
||||
EXPECT_GT(result.length(), kExpectedNoPayloadLengthBytes);
|
||||
|
||||
EXPECT_EQ(static_cast<uint8_t>(result[kVersionStartIndex]),
|
||||
kExpectedEmmVersion);
|
||||
EXPECT_EQ(static_cast<uint8_t>(result[kHeaderLengthStartIndex]),
|
||||
kExpectedHeaderLength);
|
||||
int payload_lengh = static_cast<uint16_t>(result[kPayloadLengthStartIndex])
|
||||
<< 8 |
|
||||
static_cast<uint8_t>(result[kPayloadLengthStartIndex + 1]);
|
||||
ASSERT_GT(payload_lengh, 0);
|
||||
ASSERT_EQ(result.length(),
|
||||
kPayloadStartIndex + payload_lengh + kSignatureLength);
|
||||
|
||||
std::string payload_section =
|
||||
result.substr(kPayloadStartIndex, payload_lengh);
|
||||
// Parse the payload and validate fields.
|
||||
EmmPayload emm_payload;
|
||||
ASSERT_TRUE(emm_payload.ParseFromString(payload_section));
|
||||
EmmPayload expected_payload;
|
||||
LoadExpectedServiceBlockingProto(expected_payload.add_service_blocking());
|
||||
std::string serialized_expected_payload;
|
||||
expected_payload.SerializeToString(&serialized_expected_payload);
|
||||
EXPECT_EQ(payload_section, serialized_expected_payload);
|
||||
}
|
||||
|
||||
TEST(EmmTest, GenerateEmmNoPayloadSuccess) {
|
||||
Emm emm_gen;
|
||||
std::string result;
|
||||
EXPECT_EQ(emm_gen.GenerateEmm(&result), OkStatus());
|
||||
EXPECT_EQ(result.length(), kExpectedNoPayloadLengthBytes);
|
||||
|
||||
EXPECT_EQ(static_cast<uint8_t>(result[kVersionStartIndex]),
|
||||
kExpectedEmmVersion);
|
||||
EXPECT_EQ(static_cast<uint8_t>(result[kHeaderLengthStartIndex]),
|
||||
kExpectedHeaderLength);
|
||||
int payload_lengh = static_cast<uint16_t>(result[kPayloadLengthStartIndex])
|
||||
<< 8 |
|
||||
static_cast<uint8_t>(result[kPayloadLengthStartIndex + 1]);
|
||||
EXPECT_EQ(payload_lengh, 0);
|
||||
EXPECT_EQ(result.length(), kPayloadStartIndex + kSignatureLength);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace cas
|
||||
} // namespace widevine
|
||||
@@ -28,6 +28,10 @@
|
||||
|
||||
// Minimum sending interval in milliseconds.
|
||||
static constexpr uint16_t KMinSendIntervalMs = 2;
|
||||
// Maximum number of entitlement key ids shown in private data.
|
||||
static constexpr uint16_t kMaxNumOfEntitlementKeyIds = 2;
|
||||
// Entitlement key id length is fixed to 16 bytes.
|
||||
static constexpr uint16_t kEntitlementKeyIdLength = 16;
|
||||
|
||||
namespace widevine {
|
||||
namespace cas {
|
||||
@@ -153,8 +157,8 @@ void Emmg::Start() {
|
||||
|
||||
for (size_t i = 0; i < emmg_config_->max_num_message; i++) {
|
||||
SendDataProvision();
|
||||
absl::SleepFor(
|
||||
absl::Milliseconds(std::max(KMinSendIntervalMs, send_interval_ms_)));
|
||||
absl::SleepFor(std::max(absl::Milliseconds(KMinSendIntervalMs),
|
||||
absl::Milliseconds(send_interval_ms_)));
|
||||
}
|
||||
|
||||
SendStreamCloseRequest();
|
||||
@@ -220,13 +224,33 @@ void Emmg::BuildStreamBwRequest() {
|
||||
Host16ToBigEndian(request_ + 3, &total_param_length);
|
||||
}
|
||||
|
||||
Status Emmg::GeneratePrivateData(const std::string& content_provider,
|
||||
const std::string& content_id, uint8_t* buffer) {
|
||||
Status Emmg::GeneratePrivateData(
|
||||
const std::string& content_provider, const std::string& content_id,
|
||||
const std::vector<std::string>& entitlement_key_ids, uint8_t* buffer) {
|
||||
DCHECK(buffer);
|
||||
// Generate payload.
|
||||
CaDescriptorPrivateData private_data;
|
||||
private_data.set_provider(content_provider);
|
||||
private_data.set_content_id(content_id);
|
||||
|
||||
if (entitlement_key_ids.size() > kMaxNumOfEntitlementKeyIds) {
|
||||
return Status(
|
||||
error::INVALID_ARGUMENT,
|
||||
absl::StrCat("Number of entitlement key ids shouldn't exceed ",
|
||||
kMaxNumOfEntitlementKeyIds));
|
||||
}
|
||||
for (const auto& entitlement_key_id : entitlement_key_ids) {
|
||||
if (entitlement_key_id.size() != kEntitlementKeyIdLength) {
|
||||
return Status(
|
||||
error::INVALID_ARGUMENT,
|
||||
absl::StrCat("Entitlement key id length must be ",
|
||||
kEntitlementKeyIdLength, ". The offending key id is ",
|
||||
entitlement_key_id));
|
||||
}
|
||||
}
|
||||
for (const auto& entitlement_key_id : entitlement_key_ids) {
|
||||
private_data.add_entitlement_key_ids(entitlement_key_id);
|
||||
}
|
||||
std::string private_data_str = private_data.SerializeAsString();
|
||||
std::string payload_filler(kMaxTsPayloadSize - private_data_str.size(), 0);
|
||||
|
||||
@@ -268,8 +292,13 @@ void Emmg::BuildDataProvision() {
|
||||
&request_length_);
|
||||
|
||||
uint8_t datagram[kTsPacketSize];
|
||||
GeneratePrivateData(emmg_config_->content_provider, emmg_config_->content_id,
|
||||
datagram);
|
||||
Status status = GeneratePrivateData(
|
||||
emmg_config_->content_provider, emmg_config_->content_id,
|
||||
emmg_config_->entitlement_key_ids, datagram);
|
||||
if (!status.ok()) {
|
||||
LOG(ERROR) << "Fail to generate private data. " << status.ToString();
|
||||
return;
|
||||
}
|
||||
simulcrypt_util::AddParam(EMMG_DATAGRAM, datagram, kTsPacketSize, request_,
|
||||
&request_length_);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <cstdint>
|
||||
#include "common/status.h"
|
||||
@@ -30,6 +31,7 @@ struct EmmgConfig {
|
||||
uint8_t data_type;
|
||||
std::string content_provider;
|
||||
std::string content_id;
|
||||
std::vector<std::string> entitlement_key_ids;
|
||||
uint16_t bandwidth;
|
||||
uint32_t max_num_message;
|
||||
};
|
||||
@@ -82,8 +84,9 @@ class Emmg {
|
||||
|
||||
void UpdateSendInterval(uint16_t bandwidth_kbps);
|
||||
|
||||
Status GeneratePrivateData(const std::string& content_provider,
|
||||
const std::string& content_id, uint8_t* buffer);
|
||||
Status GeneratePrivateData(
|
||||
const std::string& content_provider, const std::string& content_id,
|
||||
const std::vector<std::string>& entitlement_key_ids, uint8_t* buffer);
|
||||
void ReceiveResponseAndVerify(uint16_t expected_type);
|
||||
void Send(uint16_t message_type);
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class EmmgTest : public ::testing::Test {
|
||||
config_.data_type = 0x01;
|
||||
config_.content_provider = "widevine_test";
|
||||
config_.content_id = "CasTsFake";
|
||||
config_.entitlement_key_ids = {"fakeKeyId1KeyId1", "fakeKeyId2KeyId2"};
|
||||
config_.bandwidth = 100;
|
||||
config_.max_num_message = 100;
|
||||
emmg_ = absl::make_unique<TestableEmmg>(&config_);
|
||||
@@ -103,6 +104,25 @@ TEST_F(EmmgTest, BuildDataProvision) {
|
||||
sizeof(kTestEmmgDataProvision)));
|
||||
}
|
||||
|
||||
TEST_F(EmmgTest,
|
||||
BuildDataProvisionFailedWhenNumOfEntitlementKeyIdsExceedLimit) {
|
||||
config_.entitlement_key_ids = {"fakeKeyId1KeyId1", "fakeKeyId2KeyId2",
|
||||
"fakeKeyId3KeyId3"};
|
||||
emmg_ = absl::make_unique<TestableEmmg>(&config_);
|
||||
emmg_->PublicBuildDataProvision();
|
||||
EXPECT_EQ(0, memcmp(kTestEmptyEmmgDataProvision, emmg_->GetRequest(),
|
||||
sizeof(kTestEmptyEmmgDataProvision)));
|
||||
}
|
||||
|
||||
TEST_F(EmmgTest,
|
||||
BuildDataProvisionFailedWhenEntitlementKeyIdLengthExceedLimit) {
|
||||
config_.entitlement_key_ids = {"fakeKeyId1KeyId1", "fakeKeyId2KeyId2KeyId2"};
|
||||
emmg_ = absl::make_unique<TestableEmmg>(&config_);
|
||||
emmg_->PublicBuildDataProvision();
|
||||
EXPECT_EQ(0, memcmp(kTestEmptyEmmgDataProvision, emmg_->GetRequest(),
|
||||
sizeof(kTestEmptyEmmgDataProvision)));
|
||||
}
|
||||
|
||||
TEST_F(EmmgTest, BuildStreamCloseRequest) {
|
||||
emmg_->PublicBuildStreamCloseRequest();
|
||||
EXPECT_EQ(0, memcmp(kTestEmmgStreamCloseRequest, emmg_->GetRequest(),
|
||||
|
||||
Reference in New Issue
Block a user