Add EMMG to carry fingerprinting and service blocking info

This commit is contained in:
Lu Chen
2020-09-15 09:16:59 -07:00
parent 3d8f585313
commit 1ce468e5ba
143 changed files with 2316 additions and 17450 deletions

View File

@@ -177,6 +177,18 @@ cc_library(
alwayslink = 1,
)
cc_library(
name = "wv_cas_emm",
srcs = ["wv_cas_emm.cc"],
hdrs = ["wv_cas_emm.h"],
deps = [
"//base",
"@abseil_repo//absl/memory",
"//common:status",
"//media_cas_packager_sdk/internal:emm",
],
)
cc_test(
name = "wv_cas_types_test",
size = "small",
@@ -187,6 +199,17 @@ cc_test(
],
)
cc_test(
name = "wv_cas_emm_test",
srcs = ["wv_cas_emm_test.cc"],
deps = [
":wv_cas_emm",
"//testing:gunit_main",
"//media_cas_packager_sdk/internal:emm",
"//media_cas_packager_sdk/internal:util",
],
)
cc_binary(
name = "wv_ecmg",
srcs = ["wv_ecmg.cc"],
@@ -206,6 +229,7 @@ cc_binary(
"//base",
"@abseil_repo//absl/flags:flag",
"@abseil_repo//absl/flags:parse",
"@abseil_repo//absl/strings",
"//media_cas_packager_sdk/internal:emmg",
],
)

View File

@@ -0,0 +1,101 @@
////////////////////////////////////////////////////////////////////////////////
// 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/public/wv_cas_emm.h"
#include "glog/logging.h"
#include "absl/memory/memory.h"
#include "media_cas_packager_sdk/internal/emm.h"
namespace widevine {
namespace cas {
namespace {
std::vector<FingerprintingInitParameters> ConvertFingerprintingInitVector(
const std::vector<WvCasFingerprintingInitParameters>& params) {
std::vector<FingerprintingInitParameters> converted_vector;
converted_vector.reserve(params.size());
for (const auto& param : params) {
FingerprintingInitParameters converted;
converted.channels.assign(param.channels.begin(), param.channels.end());
converted.control = param.control;
converted_vector.push_back(converted);
}
return converted_vector;
}
std::vector<ServiceBlockingInitParameters> ConvertServiceBlockingInitVector(
const std::vector<WvCasServiceBlockingInitParameters>& params) {
std::vector<ServiceBlockingInitParameters> converted_vector;
converted_vector.reserve(params.size());
for (const auto& param : params) {
ServiceBlockingInitParameters converted;
converted.channels.assign(param.channels.begin(), param.channels.end());
converted.device_groups.assign(param.device_groups.begin(),
param.device_groups.end());
converted.start_time = param.start_time;
converted.end_time = param.end_time;
converted_vector.push_back(converted);
}
return converted_vector;
}
} // namespace
WvCasEmm::WvCasEmm(std::unique_ptr<Emm> emm) : emm_(std::move(emm)) {}
WvCasEmm::~WvCasEmm() = default;
std::unique_ptr<WvCasEmm> WvCasEmm::Create(
const std::string& private_ecc_signing_key) {
if (private_ecc_signing_key.empty()) {
LOG(ERROR) << "private_ecc_signing_key must not be empty.";
return nullptr;
}
auto emm = absl::make_unique<Emm>();
Status status = emm->SetPrivateSigningKey(private_ecc_signing_key);
if (!status.ok()) {
LOG(ERROR) << "Failed to set private signing key: " << status.ToString();
return nullptr;
}
return absl::WrapUnique(new WvCasEmm(std::move(emm)));
}
Status WvCasEmm::SetFingerprinting(
const std::vector<WvCasFingerprintingInitParameters>& fingerprintings) {
return emm_->SetFingerprinting(
ConvertFingerprintingInitVector(fingerprintings));
}
Status WvCasEmm::SetServiceBlocking(
const std::vector<WvCasServiceBlockingInitParameters>& service_blockings) {
return emm_->SetServiceBlocking(
ConvertServiceBlockingInitVector(service_blockings));
}
Status WvCasEmm::GenerateEmm(std::string* serialized_emm) const {
if (serialized_emm == nullptr) {
return {error::INVALID_ARGUMENT, "serialized_emm must not be null"};
}
return emm_->GenerateEmm(serialized_emm);
}
Status WvCasEmm::GenerateEmmTsPackets(uint16_t pid, uint8_t* continuity_counter,
uint8_t* packet,
ssize_t* packet_size) const {
if (continuity_counter == nullptr || packet == nullptr ||
packet_size == nullptr) {
return {error::INVALID_ARGUMENT,
"continuity_counter, packet and packet_size must not be null"};
}
return emm_->GenerateEmmTsPackets(pid, continuity_counter, packet,
packet_size);
}
} // namespace cas
} // namespace widevine

View File

@@ -0,0 +1,91 @@
////////////////////////////////////////////////////////////////////////////////
// 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_PUBLIC_WV_CAS_EMM_H_
#define MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_EMM_H_
#include <memory>
#include <string>
#include <vector>
#include <cstdint>
#include "common/status.h"
namespace widevine {
namespace cas {
class Emm;
struct WvCasFingerprintingInitParameters {
std::vector<std::string> channels;
std::string control;
};
struct WvCasServiceBlockingInitParameters {
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;
};
// Class for generating Widevine CAS EMMs. Widevine CAS EMMs are not used for
// carrying entitlement information. Instead, it is used as an information
// channel carrying information for fingerprinting, service blocking, etc.
// Class WvCasEmm is NOT thread-safe.
class WvCasEmm {
public:
WvCasEmm(const WvCasEmm&) = delete;
WvCasEmm& operator=(const WvCasEmm&) = delete;
virtual ~WvCasEmm();
// Creates and returns a WvCasEmm with the given private ECC signing key.
// Returns a null value if the creation is unsuccessful.
static std::unique_ptr<WvCasEmm> Create(
const std::string& private_ecc_signing_key);
// Replaces current fingerprinting info with |fingerprintings|.
virtual Status SetFingerprinting(
const std::vector<WvCasFingerprintingInitParameters>& fingerprintings);
// Replaces current service blocking info with |service_blockings|.
virtual Status SetServiceBlocking(
const std::vector<WvCasServiceBlockingInitParameters>& service_blockings);
// Constructs an EMM payload with fingerprinting info set by calling
// SetFingerprinting(), and service blocking info set by calling
// SetServiceBlocking().
// Args (all pointer parameters must not be nullptr):
// - |serialized_emm| caller-supplied std::string pointer to receive the EMM.
virtual Status GenerateEmm(std::string* serialized_emm) const;
// Generates EMM that are wrapped in TS packets.
// Args (all pointer parameters must not be nullptr):
// - |pid| program ID for the EMM stream.
// - |continuity_counter| continuity_counter for the TS packet. It will be
// automatically incremented. Only the last 4 bits are used.
// - |packet| a buffer to be used to return the generated TS packet. Must be
// able to hold the maximum possible size of genereated Ts packets
// (kMaxPossibleTsPacketsSizeBytes = 1128 bytes).
// - |packet_size| is the size of the allocated |packet|. It will be updated
// as the number of bytes actually used.
virtual Status GenerateEmmTsPackets(uint16_t pid, uint8_t* continuity_counter,
uint8_t* packet,
ssize_t* packet_size) const;
protected:
explicit WvCasEmm(std::unique_ptr<Emm> emm);
private:
std::unique_ptr<Emm> emm_;
};
} // namespace cas
} // namespace widevine
#endif // MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_EMM_H_

View File

@@ -0,0 +1,260 @@
////////////////////////////////////////////////////////////////////////////////
// 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/public/wv_cas_emm.h"
#include <memory>
#include <vector>
#include "testing/gmock.h"
#include "testing/gunit.h"
#include "media_cas_packager_sdk/internal/emm.h"
#include "media_cas_packager_sdk/internal/mpeg2ts.h"
using ::testing::DoAll;
using ::testing::ElementsAreArray;
using ::testing::IsEmpty;
using ::testing::IsNull;
using ::testing::NotNull;
using ::testing::Pointwise;
using ::testing::Return;
using ::testing::SetArgPointee;
using ::testing::SetArrayArgument;
namespace widevine {
namespace cas {
namespace {
constexpr int kTestPid = 1;
constexpr char kChannelOne[] = "CH1";
constexpr char kChannelTwo[] = "CH2";
constexpr char kChannelThree[] = "CH3";
constexpr char kFingerprintingControl[] = "controls";
constexpr char kFingerprintingControlTwo[] = "another control";
constexpr char kDeviceGroupOne[] = "Group1";
constexpr char kDeviceGroupTwo[] = "Group2";
constexpr int64_t kServiceBockingStartTime = 100;
constexpr int64_t kServiceBockingEndTime = 1000;
TEST(WvCasEmmFactoryCeateTest, FactoryCreateSuccess) {
EXPECT_THAT(WvCasEmm::Create("Signing key"), NotNull());
}
TEST(WvCasEmmFactoryCeateTest, EmptySigningKeyFail) {
EXPECT_THAT(WvCasEmm::Create(/*private_ecc_signing_key=*/""), IsNull());
}
class MockEmm : public Emm {
public:
MOCK_METHOD(
Status, SetFingerprinting,
(const std::vector<FingerprintingInitParameters>& fingerprintings),
(override));
MOCK_METHOD(
Status, SetServiceBlocking,
(const std::vector<ServiceBlockingInitParameters>& service_blockings),
(override));
MOCK_METHOD(Status, GenerateEmm, (std::string * serialized_emm),
(const, override));
MOCK_METHOD(Status, GenerateEmmTsPackets,
(uint16_t pid, uint8_t* continuity_counter, uint8_t* packet,
ssize_t* packet_size),
(const, override));
};
class MockWvCasEmm : public WvCasEmm {
public:
explicit MockWvCasEmm(Emm* emm) : WvCasEmm(std::unique_ptr<Emm>(emm)) {}
};
class WvCasEmmTest : public ::testing::Test {
protected:
WvCasEmmTest()
: mock_internal_emm_(new MockEmm()),
cas_emm_(new MockWvCasEmm(mock_internal_emm_)) {}
MockEmm* const mock_internal_emm_;
std::unique_ptr<MockWvCasEmm> cas_emm_;
};
MATCHER(FingerprintingEq, "") {
const auto& actual = std::get<0>(arg);
const auto& expected = std::get<1>(arg);
return (actual.channels == expected.channels) &&
(actual.control == expected.control);
}
MATCHER(ServiceBlockingEq, "") {
const auto& actual = std::get<0>(arg);
const auto& expected = std::get<1>(arg);
return (actual.channels == expected.channels) &&
(actual.device_groups == expected.device_groups) &&
(actual.start_time == expected.start_time) &&
(actual.end_time == expected.end_time);
}
TEST_F(WvCasEmmTest, SetFingerprintingSuccess) {
EXPECT_CALL(*mock_internal_emm_, SetFingerprinting(IsEmpty()))
.WillOnce(Return(OkStatus()));
EXPECT_OK(cas_emm_->SetFingerprinting({}));
}
TEST_F(WvCasEmmTest, SetFingerprintingConvertSuccess) {
std::vector<WvCasFingerprintingInitParameters> param_vector;
param_vector.reserve(2);
param_vector.emplace_back();
param_vector.back().channels = {kChannelOne, kChannelTwo};
param_vector.back().control = kFingerprintingControl;
param_vector.emplace_back();
param_vector.back().channels = {kChannelThree};
param_vector.back().control = kFingerprintingControlTwo;
std::vector<FingerprintingInitParameters> expected_vector;
expected_vector.reserve(2);
expected_vector.emplace_back();
expected_vector.back().channels = {kChannelOne, kChannelTwo};
expected_vector.back().control = kFingerprintingControl;
expected_vector.emplace_back();
expected_vector.back().channels = {kChannelThree};
expected_vector.back().control = kFingerprintingControlTwo;
EXPECT_CALL(*mock_internal_emm_,
SetFingerprinting(Pointwise(FingerprintingEq(), expected_vector)))
.WillOnce(Return(OkStatus()));
EXPECT_OK(cas_emm_->SetFingerprinting(param_vector));
}
TEST_F(WvCasEmmTest, SetFingerprintingFail) {
EXPECT_CALL(*mock_internal_emm_, SetFingerprinting(IsEmpty()))
.WillOnce(Return(Status(error::INVALID_ARGUMENT)));
EXPECT_EQ(cas_emm_->SetFingerprinting({}).error_code(),
error::INVALID_ARGUMENT);
}
TEST_F(WvCasEmmTest, SetServiceBlockingSuccess) {
EXPECT_CALL(*mock_internal_emm_, SetServiceBlocking(IsEmpty()))
.WillOnce(Return(OkStatus()));
EXPECT_OK(cas_emm_->SetServiceBlocking({}));
}
TEST_F(WvCasEmmTest, SetServiceBlockingConvertSuccess) {
std::vector<WvCasServiceBlockingInitParameters> param_vector;
param_vector.reserve(2);
param_vector.emplace_back();
param_vector.back().channels = {kChannelOne, kChannelTwo};
param_vector.back().device_groups = {kDeviceGroupOne, kDeviceGroupTwo};
param_vector.back().start_time = kServiceBockingStartTime;
param_vector.back().end_time = kServiceBockingEndTime;
param_vector.emplace_back();
param_vector.back().channels = {kChannelThree};
std::vector<ServiceBlockingInitParameters> expected_vector;
expected_vector.reserve(2);
expected_vector.emplace_back();
expected_vector.back().channels = {kChannelOne, kChannelTwo};
expected_vector.back().device_groups = {kDeviceGroupOne, kDeviceGroupTwo};
expected_vector.back().start_time = kServiceBockingStartTime;
expected_vector.back().end_time = kServiceBockingEndTime;
expected_vector.emplace_back();
expected_vector.back().channels = {kChannelThree};
EXPECT_CALL(*mock_internal_emm_, SetServiceBlocking(Pointwise(
ServiceBlockingEq(), expected_vector)))
.WillOnce(Return(OkStatus()));
EXPECT_OK(cas_emm_->SetServiceBlocking(param_vector));
}
TEST_F(WvCasEmmTest, SetServiceBlockingFail) {
EXPECT_CALL(*mock_internal_emm_, SetServiceBlocking(IsEmpty()))
.WillOnce(Return(Status(error::INVALID_ARGUMENT)));
EXPECT_EQ(cas_emm_->SetServiceBlocking({}).error_code(),
error::INVALID_ARGUMENT);
}
TEST_F(WvCasEmmTest, GenerateEmmSuccess) {
std::string expected_emm = "emm";
EXPECT_CALL(*mock_internal_emm_, GenerateEmm(NotNull()))
.WillOnce(DoAll(SetArgPointee<0>(expected_emm), Return(OkStatus())));
std::string generated_emm;
EXPECT_OK(cas_emm_->GenerateEmm(&generated_emm));
EXPECT_EQ(generated_emm, expected_emm);
}
TEST_F(WvCasEmmTest, GenerateEmmFail) {
EXPECT_CALL(*mock_internal_emm_, GenerateEmm(NotNull()))
.WillOnce(Return(Status(error::INVALID_ARGUMENT)));
std::string generated_emm;
EXPECT_EQ(cas_emm_->GenerateEmm(&generated_emm).error_code(),
error::INVALID_ARGUMENT);
}
TEST_F(WvCasEmmTest, GenerateEmmNullBuffer) {
EXPECT_EQ(cas_emm_->GenerateEmm(nullptr).error_code(),
error::INVALID_ARGUMENT);
}
TEST_F(WvCasEmmTest, GenerateEmmTsPacketsSuccess) {
uint8_t expected_continuity_counter = 2;
uint8_t expected_packet[] = {1, 2, 3, 4, 5};
EXPECT_CALL(*mock_internal_emm_,
GenerateEmmTsPackets(kTestPid, NotNull(), NotNull(), NotNull()))
.WillOnce(
DoAll(SetArgPointee<1>(expected_continuity_counter),
SetArrayArgument<2>(expected_packet,
expected_packet + sizeof(expected_packet)),
SetArgPointee<3>(sizeof(expected_packet)), Return(OkStatus())));
uint8_t continuity_counter = 1;
uint8_t packet[sizeof(expected_packet)];
ssize_t packet_size = kTsPacketSize;
EXPECT_OK(cas_emm_->GenerateEmmTsPackets(kTestPid, &continuity_counter,
packet, &packet_size));
EXPECT_EQ(continuity_counter, expected_continuity_counter);
EXPECT_THAT(packet, ElementsAreArray(expected_packet));
}
TEST_F(WvCasEmmTest, GenerateEmmTsPacketsFail) {
EXPECT_CALL(*mock_internal_emm_,
GenerateEmmTsPackets(kTestPid, NotNull(), NotNull(), NotNull()))
.WillOnce(Return(Status(error::INVALID_ARGUMENT)));
uint8_t continuity_counter = 1;
uint8_t packet[kTsPacketSize];
ssize_t packet_size = kTsPacketSize;
EXPECT_EQ(cas_emm_
->GenerateEmmTsPackets(kTestPid, &continuity_counter, packet,
&packet_size)
.error_code(),
error::INVALID_ARGUMENT);
}
TEST_F(WvCasEmmTest, GenerateEmmTsPacketsNullPointer) {
uint8_t continuity_counter = 1;
uint8_t packet[kTsPacketSize];
ssize_t packet_size = kTsPacketSize;
EXPECT_EQ(
cas_emm_->GenerateEmmTsPackets(kTestPid, nullptr, packet, &packet_size)
.error_code(),
error::INVALID_ARGUMENT);
EXPECT_EQ(cas_emm_
->GenerateEmmTsPackets(kTestPid, &continuity_counter, nullptr,
&packet_size)
.error_code(),
error::INVALID_ARGUMENT);
EXPECT_EQ(
cas_emm_
->GenerateEmmTsPackets(kTestPid, &continuity_counter, packet, nullptr)
.error_code(),
error::INVALID_ARGUMENT);
}
} // namespace
} // namespace cas
} // namespace widevine

View File

@@ -61,6 +61,9 @@ Status WvCasKeyFetcher::CreateEntitlementRequest(
request.set_content_id(request_params.content_id);
request.set_provider(request_params.content_provider);
request.set_key_rotation(request_params.key_rotation);
if (!request_params.group_id.empty()) {
request.set_group_id(request_params.group_id);
}
// Add labels for tracks.
for (const auto& track_type : request_params.track_types) {
request.add_track_types(track_type);

View File

@@ -26,11 +26,15 @@ namespace cas {
// server.
// |key_rotation| the encryption uses two keys if set. Two entitlement keys
// are requested for each track type.
// |group_id| optional field indicates if this is a key used for a group of
// contents. If this field is not empty, entitlement key would be generated
// for the group instead of the single content.
struct EntitlementRequestParams {
std::string content_id;
std::string content_provider;
std::vector<std::string> track_types;
bool key_rotation;
std::string group_id;
};
// WV CAS KeyFetcher. Performs the communication with the Widevine License

View File

@@ -13,6 +13,7 @@
#include "testing/gmock.h"
#include "testing/gunit.h"
#include "absl/strings/escaping.h"
#include "absl/strings/string_view.h"
#include "protos/public/media_cas_encryption.pb.h"
using testing::_;
@@ -37,6 +38,12 @@ const char kSignedCasEncryptionRequest[] =
"RyYWNrX3R5cGVzIjpbIlNEIl0sImtleV9yb3RhdGlvbiI6ZmFsc2V9\",\"signature\":"
"\"JyTnKEy1w98HRP1lL78+OIEiqtIXyfCN8iudvNXIWhw=\",\"signer\":\"widevine_"
"test\"}";
absl::string_view kSignedCasEncryptionRequestForGroupKey =
"{\"request\":"
"\"eyJjb250ZW50X2lkIjoiTWpFeE5EQTRORFE9IiwicHJvdmlkZXIiOiJ3aWRldmluZSIsInRy"
"YWNrX3R5cGVzIjpbIlNEIl0sImtleV9yb3RhdGlvbiI6ZmFsc2UsImdyb3VwX2lkIjoiWjNKdm"
"RYQXgifQ==\",\"signature\":\"iDaAr74ldEV6X1X9kwyLoZ/"
"hfP5RJOkXEzucq6IXyfQ=\",\"signer\":\"widevine_test\"}";
const char kHttpResponse[] =
"{\"response\":"
"\"eyJzdGF0dXMiOiJPSyIsImNvbnRlbnRfaWQiOiJNakV4TkRBNE5EUT0iLCJlbnRpdGxlbWVu"
@@ -47,6 +54,9 @@ const char kTrackTypeSD[] = "SD";
const char kTrackTypeHD[] = "HD";
const char kProvider[] = "widevine";
const char kContentId[] = "21140844";
const char kGroupId[] = "group1";
const char kEntitlementKeyId[] = "fake_key_id.....";
const char kEntitlementKey[] = "fakefakefakefakefakefakefakefake";
} // namespace
namespace widevine {
@@ -89,6 +99,12 @@ class MockWvCasKeyFetcher : public WvCasKeyFetcher {
LOG(ERROR) << "Unable to understand signed_request_json.";
return Status(error::INTERNAL);
}
// Validate the parameters shown in the request.
EXPECT_EQ(request.content_id(), kContentId);
EXPECT_EQ(request.provider(), kProvider);
if (request.has_group_id()) {
EXPECT_EQ(request.group_id(), kGroupId);
}
CasEncryptionResponse response;
if (!report_status_ok_) {
@@ -96,24 +112,27 @@ class MockWvCasKeyFetcher : public WvCasKeyFetcher {
} else {
response.set_status(CasEncryptionResponse::OK);
response.set_content_id(request.content_id());
if (request.has_group_id()) {
response.set_group_id(request.group_id());
}
for (const auto& track_type : request.track_types()) {
if (request.key_rotation()) {
// Add the Even key.
auto key = response.add_entitlement_keys();
key->set_key_id("fake_key_id.....");
key->set_key("fakefakefakefakefakefakefakefake");
key->set_key_id(kEntitlementKeyId);
key->set_key(kEntitlementKey);
key->set_track_type(track_type);
key->set_key_slot(CasEncryptionResponse::KeyInfo::EVEN);
// Add the Odd key.
key = response.add_entitlement_keys();
key->set_key_id("fake_key_id.....");
key->set_key("fakefakefakefakefakefakefakefake");
key->set_key_id(kEntitlementKeyId);
key->set_key(kEntitlementKey);
key->set_track_type(track_type);
key->set_key_slot(CasEncryptionResponse::KeyInfo::ODD);
} else {
auto key = response.add_entitlement_keys();
key->set_key_id("fake_key_id.....");
key->set_key("fakefakefakefakefakefakefakefake");
key->set_key_id(kEntitlementKeyId);
key->set_key(kEntitlementKey);
key->set_track_type(track_type);
key->set_key_slot(CasEncryptionResponse::KeyInfo::SINGLE);
}
@@ -148,10 +167,12 @@ class WvCasKeyFetcherTest : public ::testing::Test {
protected:
EntitlementRequestParams CreateRequestParamsStruct(
const std::string& content_id, const std::string& content_provider,
const std::vector<std::string>& track_types, bool key_rotation) {
const std::string& group_id, const std::vector<std::string>& track_types,
bool key_rotation) {
EntitlementRequestParams request_params;
request_params.content_id = content_id;
request_params.content_provider = content_provider;
request_params.group_id = group_id;
request_params.track_types.assign(track_types.begin(), track_types.end());
request_params.key_rotation = key_rotation;
return request_params;
@@ -161,8 +182,9 @@ class WvCasKeyFetcherTest : public ::testing::Test {
TEST_F(WvCasKeyFetcherTest, TestRequestEntitlementKey) {
HardcodedWvCasKeyFetcher mock_key_fetcher(kSigningProvider, kSingingKey,
kSingingIv);
EntitlementRequestParams request_params = CreateRequestParamsStruct(
kContentId, kProvider, {kTrackTypeSD}, /*key_rotation=*/false);
EntitlementRequestParams request_params =
CreateRequestParamsStruct(kContentId, kProvider, /*group_id=*/"",
{kTrackTypeSD}, /*key_rotation=*/false);
std::string signed_request_json;
mock_key_fetcher.CreateEntitlementRequest(request_params,
&signed_request_json);
@@ -180,7 +202,7 @@ TEST_F(WvCasKeyFetcherTest, TestRequestEntitlementKey) {
&entitlements));
ASSERT_EQ(entitlements.size(), 1);
const EntitlementKeyInfo& entitlement = entitlements.at(0);
const EntitlementKeyInfo& entitlement = entitlements[0];
EXPECT_EQ(entitlement.track_type, "SD");
EXPECT_EQ(entitlement.is_even_key, true);
std::string expected_key_id;
@@ -192,12 +214,62 @@ TEST_F(WvCasKeyFetcherTest, TestRequestEntitlementKey) {
EXPECT_EQ(entitlement.key_value, expected_key_value);
}
TEST_F(WvCasKeyFetcherTest, TestRequestWithGroupId) {
MockWvCasKeyFetcher mock_key_fetcher(kSigningProvider, kSingingKey,
kSingingIv);
EntitlementRequestParams request_params = CreateRequestParamsStruct(
kContentId, kProvider, kGroupId, {kTrackTypeSD}, /*key_rotation=*/false);
std::string signed_request_json;
ASSERT_OK(mock_key_fetcher.CreateEntitlementRequest(request_params,
&signed_request_json));
EXPECT_EQ(signed_request_json, kSignedCasEncryptionRequestForGroupKey);
// Parse the signed request json std::string and validate the request proto.
SignedCasEncryptionRequest signed_request;
ASSERT_OK(
google::protobuf::util::JsonStringToMessage(signed_request_json, &signed_request));
CasEncryptionRequest request;
ASSERT_OK(
google::protobuf::util::JsonStringToMessage(signed_request.request(), &request));
EXPECT_EQ(request.content_id(), kContentId);
EXPECT_EQ(request.provider(), kProvider);
EXPECT_EQ(request.group_id(), kGroupId);
EXPECT_EQ(request.track_types_size(), 1);
EXPECT_EQ(request.track_types(0), kTrackTypeSD);
EXPECT_FALSE(request.key_rotation());
std::string actual_signed_response;
EXPECT_OK(mock_key_fetcher.MakeHttpRequest(signed_request_json,
&actual_signed_response));
// Parse the signed response json std::string and validate the response proto.
SignedCasEncryptionResponse signed_response;
ASSERT_OK(google::protobuf::util::JsonStringToMessage(actual_signed_response,
&signed_response));
CasEncryptionResponse response;
ASSERT_OK(
google::protobuf::util::JsonStringToMessage(signed_response.response(), &response));
EXPECT_EQ(response.content_id(), kContentId);
EXPECT_EQ(response.status(), CasEncryptionResponse::OK);
EXPECT_EQ(response.group_id(), kGroupId);
std::vector<EntitlementKeyInfo> entitlements;
EXPECT_OK(mock_key_fetcher.ParseEntitlementResponse(actual_signed_response,
&entitlements));
// Validate the entitlement key info in the response.
ASSERT_EQ(entitlements.size(), 1);
const EntitlementKeyInfo& entitlement = entitlements[0];
EXPECT_EQ(entitlement.track_type, "SD");
EXPECT_EQ(entitlement.is_even_key, true);
EXPECT_EQ(entitlement.key_id, kEntitlementKeyId);
EXPECT_EQ(entitlement.key_value, kEntitlementKey);
}
TEST_F(WvCasKeyFetcherTest, OneKeyOK) {
MockWvCasKeyFetcher mock_key_fetcher(kSigningProvider, kSingingKey,
kSingingIv);
std::string request;
EntitlementRequestParams request_params = CreateRequestParamsStruct(
kContentId, kProvider, {kTrackTypeSD}, /*key_rotation*/ false);
EntitlementRequestParams request_params =
CreateRequestParamsStruct(kContentId, kProvider, /*group_id=*/"",
{kTrackTypeSD}, /*key_rotation=*/false);
ASSERT_OK(
mock_key_fetcher.CreateEntitlementRequest(request_params, &request));
@@ -217,8 +289,9 @@ TEST_F(WvCasKeyFetcherTest, OneKeyOK) {
TEST_F(WvCasKeyFetcherTest, OneKeyConvenientOK) {
MockWvCasKeyFetcher mock_key_fetcher(kSigningProvider, kSingingKey,
kSingingIv);
EntitlementRequestParams request_params = CreateRequestParamsStruct(
kContentId, kProvider, {kTrackTypeSD}, /*key_rotation*/ false);
EntitlementRequestParams request_params =
CreateRequestParamsStruct(kContentId, kProvider, /*group_id=*/"",
{kTrackTypeSD}, /*key_rotation=*/false);
std::vector<EntitlementKeyInfo> entitlements;
ASSERT_OK(mock_key_fetcher.FetchEntitlements(request_params, &entitlements));
@@ -233,8 +306,9 @@ TEST_F(WvCasKeyFetcherTest, OneKeyConvenientOK) {
TEST_F(WvCasKeyFetcherTest, TwoKeysOK) {
MockWvCasKeyFetcher mock_key_fetcher(kSigningProvider, kSingingKey,
kSingingIv);
EntitlementRequestParams request_params = CreateRequestParamsStruct(
kContentId, kProvider, {kTrackTypeSD}, /*key_rotation*/ true);
EntitlementRequestParams request_params =
CreateRequestParamsStruct(kContentId, kProvider, /*group_id=*/"",
{kTrackTypeSD}, /*key_rotation=*/true);
std::vector<EntitlementKeyInfo> entitlements;
ASSERT_OK(mock_key_fetcher.FetchEntitlements(request_params, &entitlements));
@@ -247,8 +321,8 @@ TEST_F(WvCasKeyFetcherTest, TwoTracksOK) {
MockWvCasKeyFetcher mock_key_fetcher(kSigningProvider, kSingingKey,
kSingingIv);
EntitlementRequestParams request_params = CreateRequestParamsStruct(
kContentId, kProvider, {kTrackTypeSD, kTrackTypeHD},
/*key_rotation*/ true);
kContentId, kProvider, /*group_id=*/"", {kTrackTypeSD, kTrackTypeHD},
/*key_rotation=*/true);
std::vector<EntitlementKeyInfo> entitlements;
ASSERT_OK(mock_key_fetcher.FetchEntitlements(request_params, &entitlements));
ASSERT_EQ(entitlements.size(), 4);
@@ -258,8 +332,9 @@ TEST_F(WvCasKeyFetcherTest, BadResponseFail) {
MockWvCasKeyFetcher mock_key_fetcher(kSigningProvider, kSingingKey,
kSingingIv);
std::string request;
EntitlementRequestParams request_params = CreateRequestParamsStruct(
kContentId, kProvider, {kTrackTypeSD}, /*key_rotation*/ true);
EntitlementRequestParams request_params =
CreateRequestParamsStruct(kContentId, kProvider, /*group_id=*/"",
{kTrackTypeSD}, /*key_rotation=*/true);
ASSERT_OK(
mock_key_fetcher.CreateEntitlementRequest(request_params, &request));
std::string response;

View File

@@ -92,6 +92,9 @@ Status CreateWvCasEncryptionRequestJson(const WvCasEncryptionRequest& request,
CasEncryptionRequest request_proto;
request_proto.set_content_id(request.content_id);
request_proto.set_provider(request.provider);
if (!request.group_id.empty()) {
request_proto.set_group_id(request.group_id);
}
for (const std::string& track_type : request.track_types) {
request_proto.add_track_types(track_type);
}
@@ -129,6 +132,7 @@ Status ParseWvCasEncryptionResponseJson(const std::string& response_json,
static_cast<WvCasEncryptionResponse::Status>(response_proto.status());
response->status_message = response_proto.status_message();
response->content_id = response_proto.content_id();
response->group_id = response_proto.group_id();
for (const auto& key_info_proto : response_proto.entitlement_keys()) {
WvCasEncryptionResponse::KeyInfo key_info;
key_info.key_id = key_info_proto.key_id();

View File

@@ -68,6 +68,7 @@ struct EntitlementKeyInfo {
struct WvCasEncryptionRequest {
std::string content_id;
std::string provider;
std::string group_id;
// Track types such as "AUDIO", SD" or "HD".
std::vector<std::string> track_types;
// Indicates if the client is using key rotation. If true, the server will
@@ -104,6 +105,7 @@ struct WvCasEncryptionResponse {
Status status;
std::string status_message;
std::string content_id;
std::string group_id;
std::vector<KeyInfo> entitlement_keys;
};

View File

@@ -83,6 +83,17 @@ TEST(WvCasTypesTest, CreateWvCasEncryptionRequestJson) {
"{\"content_id\":\"dGVzdF9jb250ZW50X2lk\",\"provider\":\"test_provider\","
"\"track_types\":[\"SD\",\"AUDIO\"],\"key_rotation\":true}";
EXPECT_EQ(expected_request_json, actual_request_json);
// Set group_id in the request.
request.group_id = "test_group_id";
actual_request_json.clear();
EXPECT_OK(CreateWvCasEncryptionRequestJson(request, &actual_request_json));
// Content_id and group_id have been base64 encoded.
std::string expected_request_json_with_group_id =
"{\"content_id\":\"dGVzdF9jb250ZW50X2lk\",\"provider\":\"test_provider\","
"\"track_types\":[\"SD\",\"AUDIO\"],\"key_rotation\":true,\"group_id\":"
"\"dGVzdF9ncm91cF9pZA==\"}";
EXPECT_EQ(actual_request_json, expected_request_json_with_group_id);
}
TEST(WvCasTypesTest, ParseWvCasEncryptionResponseJson) {
@@ -93,7 +104,7 @@ TEST(WvCasTypesTest, ParseWvCasEncryptionResponseJson) {
"\"track_type\":\"SD\",\"key_slot\":\"EVEN\"},{\"key_id\":"
"\"ZmFrZV9rZXlfaWQyLi4uLg==\",\"key\":"
"\"ZmFrZWZha2VmYWtlZmFrZWZha2VmYWtlZmFrZTIuLi4=\",\"track_type\":\"SD\","
"\"key_slot\":\"ODD\"}]}";
"\"key_slot\":\"ODD\"}],\"group_id\":\"Z3JvdXAx\"}";
WvCasEncryptionResponse actual_response;
EXPECT_OK(ParseWvCasEncryptionResponseJson(response_json, &actual_response));
@@ -116,6 +127,8 @@ TEST(WvCasTypesTest, ParseWvCasEncryptionResponseJson) {
EXPECT_EQ("SD", actual_response.entitlement_keys.at(1).track_type);
EXPECT_EQ(WvCasEncryptionResponse::KeyInfo::KeySlot::ODD,
actual_response.entitlement_keys.at(1).key_slot);
// "group1" is base64 decode of "Z3JvdXAx".
EXPECT_EQ(actual_response.group_id, "group1");
}
} // namespace cas

View File

@@ -20,8 +20,19 @@
#include "glog/logging.h"
#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_split.h"
#include "media_cas_packager_sdk/internal/emmg.h"
namespace {
// Maximum number of entitlement key ids shown in private data.
constexpr uint16_t kMaxNumOfEntitlementKeyIds = 2;
// Entitlement key id length is fixed to 16 bytes.
constexpr uint16_t kEntitlementKeyIdLengthBytes = 16;
constexpr int32_t kEmmDataType = 0;
constexpr int32_t kPrivateDataType = 1;
} // namespace
ABSL_FLAG(std::string, mux_address, "", "Mux server address.");
ABSL_FLAG(int32_t, mux_port, 0, "Mux server port number.");
// EMMG specific configs.
@@ -40,43 +51,167 @@ ABSL_FLAG(int32_t, data_id, 0, "EMMG data_id.");
// 0x01: private data;
// 0x02: DVB reserved (ECM);
// other values: DVB reserved.
ABSL_FLAG(int32_t, data_type, 1, "EMMG data_type");
ABSL_FLAG(std::string, content_provider, "", "Content provider");
ABSL_FLAG(std::string, content_id, "", "Content id");
ABSL_FLAG(int32_t, data_type, kPrivateDataType,
"EMMG data_type, must be either 0 (EMM) or 1 (Private data).");
ABSL_FLAG(std::string, content_provider, "",
"Content provider to put into priavte data.");
ABSL_FLAG(std::string, content_id, "", "Content id to put into priavte data");
ABSL_FLAG(
std::vector<std::string>, entitlement_key_ids, {},
"Comma-separated list of entitlement_key_ids to put into private data");
ABSL_FLAG(int32_t, bandwidth, 100, "Requested bandwidth in kbps");
ABSL_FLAG(int32_t, max_num_message, 100,
"Maximum number of messages that can be sent");
ABSL_FLAG(std::string, ecc_signing_key, "",
"Signing key used to sign EMM data. Must be elliptic-curve "
"cryptography key.");
ABSL_FLAG(std::string, fingerprinting, "",
"Fingerprinting info to put into EMM data. Must follow this format:\n"
"1. Multiple fingerprintings are separated by \'||\';\n"
"2. Different fields (e.g., channels) within a fingerprinting are "
"separated by \';\'. All fields must present (channels;control);\n"
"3. Value list within a field is separated by \',\'.\n"
"Example: channel1,channel2;control||channel3;control2");
ABSL_FLAG(
std::string, service_blocking, "",
"Service blocking info to put into EMM data. Must follow this format:\n"
"1. Multiple service_blockings are separated by \'||\';\n"
"2. Different fields (e.g., channels) within a service_blocking are "
"separated by \';\'. All fields must present (channels;device_groups;"
"start_time;end_time). The start/end time are Epoch time in seconds with "
"start_time = 0 means immediate;\n"
"3. Value list within a field is separated by \',\'.\n"
"Example: channel1,channel2;device_group1,device_group2;0;1597186636"
"||channel3;device_group3;1597186636,1597186637");
#define BUFFER_SIZE (1024)
void CheckEmmgUsage(const widevine::cas::EmmgConfig& config) {
// Emmg is configured as EMM generator.
if (config.data_type == kEmmDataType) {
LOG_IF(WARNING, !config.content_provider.empty())
<< "content_provider is set but ignored as data_type is set to 0 (EMM "
"data).";
LOG_IF(WARNING, !config.content_id.empty())
<< "content_id is set but ignored as data_type is set to 0 (EMM data).";
LOG_IF(WARNING, !config.entitlement_key_ids.empty())
<< "entitlement_key_ids is set but ignored as data_type is set to 0 "
"(EMM data).";
}
void BuildEmmgConfig(widevine::cas::EmmgConfig *config) {
// Emmg is configured as private data generator (PDG).
if (config.data_type == kPrivateDataType) {
LOG_IF(WARNING, !config.ecc_signing_key.empty())
<< "ecc_signing_key is set but ignored as data_type is set to 1 "
"(Private data).";
LOG_IF(WARNING, !config.fingerprinting.empty())
<< "fingerprinting is set but ignored as data_type is set to 1 "
"(Private data).";
LOG_IF(WARNING, !config.service_blocking.empty())
<< "service_blocking is set but ignored as data_type is set to 1 "
"(Private data).";
}
}
void BuildEmmgConfig(widevine::cas::EmmgConfig* config) {
CHECK(config);
config->client_id = absl::GetFlag(FLAGS_client_id);
config->section_tspkt_flag = absl::GetFlag(FLAGS_section_tspkt_flag);
config->data_channel_id = absl::GetFlag(FLAGS_data_channel_id);
config->data_stream_id = absl::GetFlag(FLAGS_data_stream_id);
config->data_id = absl::GetFlag(FLAGS_data_id);
config->data_type = absl::GetFlag(FLAGS_data_type);
config->content_provider = absl::GetFlag(FLAGS_content_provider);
config->content_id = absl::GetFlag(FLAGS_content_id);
config->entitlement_key_ids = absl::GetFlag(FLAGS_entitlement_key_ids);
config->bandwidth = absl::GetFlag(FLAGS_bandwidth);
config->max_num_message = absl::GetFlag(FLAGS_max_num_message);
// Check and set data_type.
config->data_type = absl::GetFlag(FLAGS_data_type);
CHECK(config->data_type == kEmmDataType ||
config->data_type == kPrivateDataType)
<< "Data type must be either 0 (EMM) or 1 (Private data).";
// Below are private data specific configurations.
config->content_provider = absl::GetFlag(FLAGS_content_provider);
config->content_id = absl::GetFlag(FLAGS_content_id);
// Check and set entitlement_key_ids.
config->entitlement_key_ids = absl::GetFlag(FLAGS_entitlement_key_ids);
CHECK_LE(config->entitlement_key_ids.size(), kMaxNumOfEntitlementKeyIds)
<< absl::StrCat("Number of entitlement key ids shouldn't exceed ",
kMaxNumOfEntitlementKeyIds);
for (const auto& entitlement_key_id : config->entitlement_key_ids) {
CHECK_EQ(entitlement_key_id.size(), kEntitlementKeyIdLengthBytes)
<< absl::StrCat("Entitlement key id length must be ",
kEntitlementKeyIdLengthBytes,
". The offending key id is ", entitlement_key_id);
}
// Below are EMM data specific configurations.
config->ecc_signing_key = absl::GetFlag(FLAGS_ecc_signing_key);
if (config->data_type == kEmmDataType) {
CHECK(!config->ecc_signing_key.empty())
<< "ECC signing key is required to generate EMM data.";
}
// Check and set fingerprinting.
std::vector<std::string> fingerprintings = absl::StrSplit(
absl::GetFlag(FLAGS_fingerprinting), "||", absl::SkipWhitespace());
config->fingerprinting.reserve(fingerprintings.size());
for (const auto& fingerprinting : fingerprintings) {
config->fingerprinting.emplace_back();
widevine::cas::FingerprintingInitParameters& fingerprinting_inserted =
config->fingerprinting.back();
std::vector<std::string> fields = absl::StrSplit(fingerprinting, ';');
CHECK_EQ(fields.size(), 2)
<< "Each fingerprinting must contain exactly 2 fields (channels;"
"control) separated by \';\'.";
fingerprinting_inserted.channels = absl::StrSplit(fields[0], ',');
CHECK(!fingerprinting_inserted.channels.empty())
<< "Channels in fingerprinting must be specified.";
fingerprinting_inserted.control = fields[1];
CHECK(!fingerprinting_inserted.control.empty())
<< "Conrtol in fingerprinting must be specified.";
}
// Check and set service_blocking.
std::vector<std::string> service_blockings = absl::StrSplit(
absl::GetFlag(FLAGS_service_blocking), "||", absl::SkipWhitespace());
config->service_blocking.reserve(service_blockings.size());
for (const auto& service_blocking : service_blockings) {
config->service_blocking.emplace_back();
widevine::cas::ServiceBlockingInitParameters&
service_blocking_inserted = config->service_blocking.back();
std::vector<std::string> fields = absl::StrSplit(service_blocking, ';');
CHECK_EQ(fields.size(), 4)
<< "Each service blocking must contain exactly 4 fields (channels;"
"device_groups;start_time;end_time) separated by \';\'.";
service_blocking_inserted.channels = absl::StrSplit(fields[0], ',');
CHECK(!service_blocking_inserted.channels.empty())
<< "Channels in service blocking must be specified.";
service_blocking_inserted.device_groups = absl::StrSplit(fields[1], ',');
CHECK(!service_blocking_inserted.device_groups.empty())
<< "Device groups in service blocking must be specified.";
service_blocking_inserted.start_time = std::stoll(fields[2]);
service_blocking_inserted.end_time = std::stoll(fields[3]);
CHECK_NE(service_blocking_inserted.end_time, 0)
<< "End time in service blocking must be non zero.";
}
}
int main(int argc, char **argv) {
int main(int argc, char** argv) {
absl::ParseCommandLine(argc, argv);
CHECK(!absl::GetFlag(FLAGS_mux_address).empty())
<< "flag --mux_address is required";
CHECK(absl::GetFlag(FLAGS_mux_port) != 0) << "flag --mux_port is required";
widevine::cas::EmmgConfig emmg_config;
BuildEmmgConfig(&emmg_config);
CheckEmmgUsage(emmg_config);
// Create server address.
struct hostent ret;
struct hostent *server;
char buffer[BUFFER_SIZE];
struct hostent* server;
char buffer[1024];
int h_errnop = 0;
int return_val =
gethostbyname_r(absl::GetFlag(FLAGS_mux_address).c_str(), &ret, buffer,
@@ -86,10 +221,10 @@ int main(int argc, char **argv) {
<< absl::GetFlag(FLAGS_mux_address);
}
struct sockaddr_in server_address;
bzero(reinterpret_cast<char *>(&server_address), sizeof(server_address));
bzero(reinterpret_cast<char*>(&server_address), sizeof(server_address));
server_address.sin_family = AF_INET;
bcopy(server->h_addr,
reinterpret_cast<char *>(&server_address.sin_addr.s_addr),
reinterpret_cast<char*>(&server_address.sin_addr.s_addr),
server->h_length);
server_address.sin_port = htons(absl::GetFlag(FLAGS_mux_port));
@@ -99,14 +234,12 @@ int main(int argc, char **argv) {
LOG(FATAL) << "Failed to create socket.";
}
if (connect(server_socket_fd,
reinterpret_cast<struct sockaddr *>(&server_address),
reinterpret_cast<struct sockaddr*>(&server_address),
sizeof(server_address)) < 0) {
LOG(FATAL) << "Failed to connect to server.";
}
// Send EMMG messages.
widevine::cas::EmmgConfig emmg_config;
BuildEmmgConfig(&emmg_config);
widevine::cas::Emmg emmg(&emmg_config, server_socket_fd);
emmg.Start();