Add EMMG to carry fingerprinting and service blocking info
This commit is contained in:
@@ -68,6 +68,7 @@ cc_test(
|
||||
"//external:protobuf",
|
||||
"//testing:gunit_main",
|
||||
"@abseil_repo//absl/memory",
|
||||
"@abseil_repo//absl/strings",
|
||||
"//common:aes_cbc_util",
|
||||
"//protos/public:media_cas_encryption_cc_proto",
|
||||
],
|
||||
@@ -87,7 +88,7 @@ cc_library(
|
||||
":simulcrypt_util",
|
||||
":util",
|
||||
"//base",
|
||||
"@abseil_repo//absl/container:node_hash_map",
|
||||
"@abseil_repo//absl/container:flat_hash_map",
|
||||
"@abseil_repo//absl/memory",
|
||||
"@abseil_repo//absl/strings",
|
||||
"@abseil_repo//absl/types:optional",
|
||||
@@ -123,10 +124,12 @@ cc_library(
|
||||
"simulcrypt_constants.h",
|
||||
],
|
||||
deps = [
|
||||
":emm",
|
||||
":simulcrypt_util",
|
||||
":ts_packet",
|
||||
":util",
|
||||
"//base",
|
||||
"@abseil_repo//absl/memory",
|
||||
"@abseil_repo//absl/strings",
|
||||
"@abseil_repo//absl/strings:str_format",
|
||||
"@abseil_repo//absl/time",
|
||||
@@ -226,6 +229,7 @@ cc_library(
|
||||
":mpeg2ts",
|
||||
":ts_packet",
|
||||
"//base",
|
||||
"@abseil_repo//absl/strings",
|
||||
"//common:status",
|
||||
],
|
||||
)
|
||||
@@ -249,8 +253,12 @@ cc_library(
|
||||
srcs = ["emm.cc"],
|
||||
hdrs = ["emm.h"],
|
||||
deps = [
|
||||
":util",
|
||||
"//base",
|
||||
"@abseil_repo//absl/strings",
|
||||
"@abseil_repo//absl/time",
|
||||
"//common:ec_key",
|
||||
"//common:hash_algorithm",
|
||||
"//common:status",
|
||||
"//common:string_util",
|
||||
"//protos/public:media_cas_cc_proto",
|
||||
@@ -262,6 +270,14 @@ cc_test(
|
||||
srcs = ["emm_test.cc"],
|
||||
deps = [
|
||||
":emm",
|
||||
":util",
|
||||
"//testing:gunit_main",
|
||||
"@abseil_repo//absl/memory",
|
||||
"@abseil_repo//absl/strings",
|
||||
"@abseil_repo//absl/time",
|
||||
"//common:ec_key",
|
||||
"//common:ec_test_keys",
|
||||
"//common:hash_algorithm",
|
||||
"//common:status",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "testing/gmock.h"
|
||||
#include "testing/gunit.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "common/aes_cbc_util.h"
|
||||
#include "media_cas_packager_sdk/internal/fixed_key_fetcher.h"
|
||||
#include "protos/public/media_cas_encryption.pb.h"
|
||||
@@ -50,9 +51,24 @@ constexpr char kEcmContentIvOdd[] = "AaCbEcGd";
|
||||
constexpr char kFakeCasEncryptionResponseKeyId[] = "fake_key_id.....";
|
||||
constexpr char kFakeCasEncryptionResponseKeyData[] =
|
||||
"fakefakefakefakefakefakefakefake";
|
||||
constexpr char kFakeCasEncryptionResponseGroupKeyId[] = "fake_key_id12345";
|
||||
constexpr char kFakeCasEncryptionResponseGroupKeyData[] =
|
||||
"fakefakefakefakefakefake12345678";
|
||||
|
||||
constexpr char kTrackType[] = "SD";
|
||||
|
||||
void SetKeyInfoInTestResponse(
|
||||
widevine::CasEncryptionResponse_KeyInfo* key, bool group_id_exists) {
|
||||
ASSERT_TRUE(key != nullptr);
|
||||
if (!group_id_exists) {
|
||||
key->set_key_id(kFakeCasEncryptionResponseKeyId);
|
||||
key->set_key(kFakeCasEncryptionResponseKeyData);
|
||||
} else {
|
||||
key->set_key_id(kFakeCasEncryptionResponseGroupKeyId);
|
||||
key->set_key(kFakeCasEncryptionResponseGroupKeyData);
|
||||
}
|
||||
}
|
||||
|
||||
Status HandleCasEncryptionRequest(const std::string& signed_request_json,
|
||||
std::string* http_response_json) {
|
||||
SignedCasEncryptionRequest signed_request;
|
||||
@@ -72,24 +88,22 @@ Status HandleCasEncryptionRequest(const std::string& signed_request_json,
|
||||
CasEncryptionResponse response;
|
||||
response.set_status(CasEncryptionResponse::OK);
|
||||
response.set_content_id(request.content_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(kFakeCasEncryptionResponseKeyId);
|
||||
key->set_key(kFakeCasEncryptionResponseKeyData);
|
||||
SetKeyInfoInTestResponse(key, request.has_group_id());
|
||||
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(kFakeCasEncryptionResponseKeyId);
|
||||
key->set_key(kFakeCasEncryptionResponseKeyData);
|
||||
SetKeyInfoInTestResponse(key, request.has_group_id());
|
||||
key->set_track_type(track_type);
|
||||
key->set_key_slot(CasEncryptionResponse::KeyInfo::ODD);
|
||||
} else {
|
||||
auto key = response.add_entitlement_keys();
|
||||
key->set_key_id(kFakeCasEncryptionResponseKeyId);
|
||||
key->set_key(kFakeCasEncryptionResponseKeyData);
|
||||
SetKeyInfoInTestResponse(key, request.has_group_id());
|
||||
key->set_track_type(track_type);
|
||||
key->set_key_slot(CasEncryptionResponse::KeyInfo::SINGLE);
|
||||
}
|
||||
@@ -146,7 +160,8 @@ class EcmGeneratorTest : public testing::Test {
|
||||
}
|
||||
|
||||
// Call this to setup the guts (Ecm) of the ECM Generator.
|
||||
void PrepareEcmGenerator(bool key_rotation_enabled) {
|
||||
void PrepareEcmGenerator(bool key_rotation_enabled,
|
||||
const std::string& group_id) {
|
||||
std::string entitlement_request;
|
||||
std::string entitlement_response;
|
||||
ecm_init_params_.key_rotation_enabled = key_rotation_enabled;
|
||||
@@ -154,6 +169,7 @@ class EcmGeneratorTest : public testing::Test {
|
||||
EntitlementRequestParams request_params;
|
||||
request_params.content_id = kContentId;
|
||||
request_params.content_provider = kProvider;
|
||||
request_params.group_id = group_id;
|
||||
request_params.track_types = {kTrackType};
|
||||
request_params.key_rotation = key_rotation_enabled;
|
||||
ASSERT_OK(key_fetcher.CreateEntitlementRequest(request_params,
|
||||
@@ -169,6 +185,78 @@ class EcmGeneratorTest : public testing::Test {
|
||||
ecm_gen_.set_ecm(std::move(ecm_));
|
||||
}
|
||||
|
||||
void CheckECMOutput(absl::string_view ecm_string,
|
||||
absl::string_view response_entitlement_key_id,
|
||||
absl::string_view response_entitltement_key_data,
|
||||
bool key_rotation_enabled) {
|
||||
// Expected size (bytes):
|
||||
// CA system ID: 2 bytes
|
||||
// version: 1 byte
|
||||
// flags: 1 byte
|
||||
// flags: 1 byte
|
||||
// entitlement key ID: 16 bytes
|
||||
// Single key: ID (16), Data (16), IV (16), IV (8) = 56
|
||||
// total = 77 (ECM message complete if no key retotation)
|
||||
// second entitlement key ID: 16 bytes
|
||||
// Second key: ID (16), Data (16), IV (16), IV (8) = 56
|
||||
// total = 149
|
||||
uint32_t ecm_string_size = key_rotation_enabled ? 149 : 77;
|
||||
ASSERT_EQ(ecm_string_size, ecm_string.size());
|
||||
EXPECT_EQ('\x4A', ecm_string[0]); // CA System ID first byte.
|
||||
EXPECT_EQ('\xD4', ecm_string[1]); // CA System ID second byte.
|
||||
EXPECT_EQ('\x02', ecm_string[2]); // ECM version
|
||||
if (key_rotation_enabled) {
|
||||
EXPECT_EQ('\x03', ecm_string[3]); // flags
|
||||
} else {
|
||||
EXPECT_EQ('\x02', ecm_string[3]); // flags
|
||||
}
|
||||
EXPECT_EQ('\x80', ecm_string[4]); // flags
|
||||
EXPECT_EQ(response_entitlement_key_id, ecm_string.substr(5, 16));
|
||||
if (!key_rotation_enabled) {
|
||||
EXPECT_EQ(kEcmKeyIdSingle, ecm_string.substr(21, 16));
|
||||
// Key data has been wrapped (encrypted).
|
||||
EXPECT_NE(kEcmKeyDataSingle, ecm_string.substr(37, 16));
|
||||
EXPECT_EQ(kEcmWrappedKeyIvSingle, ecm_string.substr(53, 16));
|
||||
// Unwrap key and compare with original.
|
||||
absl::string_view wrapping_key = response_entitltement_key_data;
|
||||
absl::string_view wrapping_iv = ecm_string.substr(53, 16);
|
||||
absl::string_view wrapped_key = ecm_string.substr(37, 16);
|
||||
std::string unwrapped_key = crypto_util::DecryptAesCbcNoPad(
|
||||
std::string(wrapping_key), std::string(wrapping_iv),
|
||||
std::string(wrapped_key));
|
||||
EXPECT_EQ(kEcmKeyDataSingle, unwrapped_key);
|
||||
EXPECT_EQ(kEcmContentIvSingle, ecm_string.substr(69, 8));
|
||||
} else {
|
||||
EXPECT_EQ(kEcmKeyIdEven, ecm_string.substr(21, 16));
|
||||
// Key data has been wrapped (encrypted).
|
||||
EXPECT_NE(kEcmKeyDataEven, ecm_string.substr(37, 16));
|
||||
EXPECT_EQ(kEcmWrappedKeyIvEven, ecm_string.substr(53, 16));
|
||||
EXPECT_EQ(kEcmContentIvEven, ecm_string.substr(69, 8));
|
||||
EXPECT_EQ(response_entitlement_key_id, ecm_string.substr(77, 16));
|
||||
EXPECT_EQ(kEcmKeyIdOdd, ecm_string.substr(93, 16));
|
||||
// Key data has been wrapped (encrypted).
|
||||
EXPECT_NE(kEcmKeyDataOdd, ecm_string.substr(109, 16));
|
||||
EXPECT_EQ(kEcmWrappedKeyIvOdd, ecm_string.substr(125, 16));
|
||||
EXPECT_EQ(kEcmContentIvOdd, ecm_string.substr(141, 8));
|
||||
// Unwrap even key and compare with original.
|
||||
absl::string_view wrapping_key_even = response_entitltement_key_data;
|
||||
absl::string_view wrapping_iv_even = ecm_string.substr(53, 16);
|
||||
absl::string_view wrapped_key_even = ecm_string.substr(37, 16);
|
||||
std::string unwrapped_key_even = crypto_util::DecryptAesCbcNoPad(
|
||||
std::string(wrapping_key_even), std::string(wrapping_iv_even),
|
||||
std::string(wrapped_key_even));
|
||||
EXPECT_EQ(kEcmKeyDataEven, unwrapped_key_even);
|
||||
// Unwrap odd key and compare with original.
|
||||
absl::string_view wrapping_key_odd = response_entitltement_key_data;
|
||||
absl::string_view wrapping_iv_odd = ecm_string.substr(125, 16);
|
||||
absl::string_view wrapped_key_odd = ecm_string.substr(109, 16);
|
||||
std::string unwrapped_key_odd = crypto_util::DecryptAesCbcNoPad(
|
||||
std::string(wrapping_key_odd), std::string(wrapping_iv_odd),
|
||||
std::string(wrapped_key_odd));
|
||||
EXPECT_EQ(kEcmKeyDataOdd, unwrapped_key_odd);
|
||||
}
|
||||
}
|
||||
|
||||
const KeyParameters kKeyParamsSingle{kEcmKeyIdSingle,
|
||||
kEcmKeyDataSingle,
|
||||
kEcmWrappedKeyIvSingle,
|
||||
@@ -187,101 +275,55 @@ class EcmGeneratorTest : public testing::Test {
|
||||
|
||||
TEST_F(EcmGeneratorTest, InitializeNoRotation) {
|
||||
EXPECT_FALSE(ecm_gen_.initialized());
|
||||
|
||||
PrepareEcmGenerator(false);
|
||||
|
||||
PrepareEcmGenerator(false, /*group_id=*/"");
|
||||
EcmParameters ecm_params;
|
||||
std::vector<EntitledKeyInfo> keys;
|
||||
|
||||
SetTestConfig1(&ecm_params);
|
||||
|
||||
Status status = ProcessEcmParameters(ecm_params, &keys);
|
||||
ASSERT_OK(status);
|
||||
|
||||
ASSERT_EQ(EntitlementKeySize(ecm_params), 16);
|
||||
ASSERT_TRUE(ecm_gen_.initialized());
|
||||
EXPECT_FALSE(ecm_gen_.rotation_enabled());
|
||||
}
|
||||
|
||||
TEST_F(EcmGeneratorTest, GenerateNoRotation) {
|
||||
PrepareEcmGenerator(false);
|
||||
PrepareEcmGenerator(false, /*group_id=*/"");
|
||||
EcmParameters ecm_params;
|
||||
|
||||
SetTestConfig1(&ecm_params);
|
||||
|
||||
std::string ecm_string = ecm_gen_.GenerateEcm(ecm_params);
|
||||
|
||||
// Expected size (bytes):
|
||||
// CA system ID: 2 bytes
|
||||
// version: 1 byte
|
||||
// flags: 1 byte
|
||||
// flags: 1 byte
|
||||
// entitlement key ID: 16 bytes
|
||||
// Single key: ID (16), Data (16), IV (16), IV (8) = 56
|
||||
// total = 77
|
||||
ASSERT_EQ(77, ecm_string.size());
|
||||
EXPECT_EQ('\x4A', ecm_string[0]); // CA System ID first byte.
|
||||
EXPECT_EQ('\xD4', ecm_string[1]); // CA System ID second byte.
|
||||
EXPECT_EQ('\x02', ecm_string[2]); // ECM version
|
||||
EXPECT_EQ('\x02', ecm_string[3]); // flags
|
||||
EXPECT_EQ('\x80', ecm_string[4]); // flags
|
||||
EXPECT_EQ(kFakeCasEncryptionResponseKeyId, ecm_string.substr(5, 16));
|
||||
EXPECT_EQ(kEcmKeyIdSingle, ecm_string.substr(21, 16));
|
||||
// Key data has been wrapped (encrypted).
|
||||
EXPECT_NE(kEcmKeyDataSingle, ecm_string.substr(37, 16));
|
||||
EXPECT_EQ(kEcmWrappedKeyIvSingle, ecm_string.substr(53, 16));
|
||||
// Unwrap key and compare with original.
|
||||
std::string wrapping_key = kFakeCasEncryptionResponseKeyData;
|
||||
std::string wrapping_iv = ecm_string.substr(53, 16);
|
||||
std::string wrapped_key = ecm_string.substr(37, 16);
|
||||
std::string unwrapped_key =
|
||||
crypto_util::DecryptAesCbcNoPad(wrapping_key, wrapping_iv, wrapped_key);
|
||||
EXPECT_EQ(kEcmKeyDataSingle, unwrapped_key);
|
||||
EXPECT_EQ(kEcmContentIvSingle, ecm_string.substr(69, 8));
|
||||
CheckECMOutput(ecm_string, kFakeCasEncryptionResponseKeyId,
|
||||
kFakeCasEncryptionResponseKeyData, false);
|
||||
}
|
||||
|
||||
TEST_F(EcmGeneratorTest, GenerateNoRotationWithGroupId) {
|
||||
PrepareEcmGenerator(false, "groupId_1");
|
||||
EcmParameters ecm_params;
|
||||
SetTestConfig1(&ecm_params);
|
||||
std::string ecm_string = ecm_gen_.GenerateEcm(ecm_params);
|
||||
CheckECMOutput(ecm_string, kFakeCasEncryptionResponseGroupKeyId,
|
||||
kFakeCasEncryptionResponseGroupKeyData, false);
|
||||
}
|
||||
|
||||
TEST_F(EcmGeneratorTest, Generate2NoRotation) {
|
||||
PrepareEcmGenerator(false);
|
||||
PrepareEcmGenerator(false, /*group_id=*/"");
|
||||
EcmParameters ecm_params;
|
||||
|
||||
SetTestConfig1(&ecm_params);
|
||||
|
||||
std::string ecm_string = ecm_gen_.GenerateEcm(ecm_params);
|
||||
ecm_string = ecm_gen_.GenerateEcm(ecm_params);
|
||||
|
||||
ASSERT_EQ(77, ecm_string.size());
|
||||
EXPECT_EQ('\x4A', ecm_string[0]); // CA System ID first byte.
|
||||
EXPECT_EQ('\xD4', ecm_string[1]); // CA System ID second byte.
|
||||
EXPECT_EQ('\x02', ecm_string[2]); // ECM version
|
||||
EXPECT_EQ('\x02', ecm_string[3]); // flags
|
||||
EXPECT_EQ('\x80', ecm_string[4]); // flags
|
||||
EXPECT_EQ(kFakeCasEncryptionResponseKeyId, ecm_string.substr(5, 16));
|
||||
EXPECT_EQ(kEcmKeyIdSingle, ecm_string.substr(21, 16));
|
||||
// Key data has been wrapped (encrypted).
|
||||
EXPECT_NE(kEcmKeyDataSingle, ecm_string.substr(37, 16));
|
||||
EXPECT_EQ(kEcmWrappedKeyIvSingle, ecm_string.substr(53, 16));
|
||||
// Unwrap key and compare with original.
|
||||
std::string wrapping_key = kFakeCasEncryptionResponseKeyData;
|
||||
std::string wrapping_iv = ecm_string.substr(53, 16);
|
||||
std::string wrapped_key = ecm_string.substr(37, 16);
|
||||
std::string unwrapped_key =
|
||||
crypto_util::DecryptAesCbcNoPad(wrapping_key, wrapping_iv, wrapped_key);
|
||||
EXPECT_EQ(kEcmKeyDataSingle, unwrapped_key);
|
||||
EXPECT_EQ(kEcmContentIvSingle, ecm_string.substr(69, 8));
|
||||
CheckECMOutput(ecm_string, kFakeCasEncryptionResponseKeyId,
|
||||
kFakeCasEncryptionResponseKeyData, false);
|
||||
}
|
||||
|
||||
TEST_F(EcmGeneratorTest, InitializeSimpleRotation) {
|
||||
EXPECT_FALSE(ecm_gen_.initialized());
|
||||
EcmParameters ecm_params;
|
||||
std::vector<EntitledKeyInfo> keys;
|
||||
|
||||
PrepareEcmGenerator(true);
|
||||
|
||||
PrepareEcmGenerator(true, /*group_id=*/"");
|
||||
SetTestConfig2(&ecm_params);
|
||||
ecm_init_params_.key_rotation_enabled = true;
|
||||
|
||||
Status status = ProcessEcmParameters(ecm_params, &keys);
|
||||
|
||||
EXPECT_TRUE(status.ok());
|
||||
EXPECT_TRUE(ecm_gen_.initialized());
|
||||
EXPECT_TRUE(ecm_gen_.rotation_enabled());
|
||||
@@ -289,50 +331,20 @@ TEST_F(EcmGeneratorTest, InitializeSimpleRotation) {
|
||||
|
||||
TEST_F(EcmGeneratorTest, GenerateSimpleRotation) {
|
||||
EcmParameters ecm_params;
|
||||
|
||||
PrepareEcmGenerator(true);
|
||||
|
||||
PrepareEcmGenerator(true, /*group_id=*/"");
|
||||
SetTestConfig2(&ecm_params);
|
||||
|
||||
std::string ecm_string = ecm_gen_.GenerateEcm(ecm_params);
|
||||
CheckECMOutput(ecm_string, kFakeCasEncryptionResponseKeyId,
|
||||
kFakeCasEncryptionResponseKeyData, true);
|
||||
}
|
||||
|
||||
// Expected size (bytes):
|
||||
// same as no rotation case = 77
|
||||
// second entitlement key ID: 16 bytes
|
||||
// Second key: ID (16), Data (16), IV (16), IV (8) = 56
|
||||
// total = 149
|
||||
ASSERT_EQ(149, ecm_string.size());
|
||||
EXPECT_EQ('\x4A', ecm_string[0]); // CA System ID first byte.
|
||||
EXPECT_EQ('\xD4', ecm_string[1]); // CA System ID second byte.
|
||||
EXPECT_EQ('\x02', ecm_string[2]); // ECM version
|
||||
EXPECT_EQ('\x03', ecm_string[3]); // flags
|
||||
EXPECT_EQ('\x80', ecm_string[4]); // flags
|
||||
EXPECT_EQ(kFakeCasEncryptionResponseKeyId, ecm_string.substr(5, 16));
|
||||
EXPECT_EQ(kEcmKeyIdEven, ecm_string.substr(21, 16));
|
||||
// Key data has been wrapped (encrypted).
|
||||
EXPECT_NE(kEcmKeyDataEven, ecm_string.substr(37, 16));
|
||||
EXPECT_EQ(kEcmWrappedKeyIvEven, ecm_string.substr(53, 16));
|
||||
EXPECT_EQ(kEcmContentIvEven, ecm_string.substr(69, 8));
|
||||
EXPECT_EQ(kFakeCasEncryptionResponseKeyId, ecm_string.substr(77, 16));
|
||||
EXPECT_EQ(kEcmKeyIdOdd, ecm_string.substr(93, 16));
|
||||
// Key data has been wrapped (encrypted).
|
||||
EXPECT_NE(kEcmKeyDataOdd, ecm_string.substr(109, 16));
|
||||
EXPECT_EQ(kEcmWrappedKeyIvOdd, ecm_string.substr(125, 16));
|
||||
EXPECT_EQ(kEcmContentIvOdd, ecm_string.substr(141, 8));
|
||||
// Unwrap even key and compare with original.
|
||||
std::string wrapping_key_even = kFakeCasEncryptionResponseKeyData;
|
||||
std::string wrapping_iv_even = ecm_string.substr(53, 16);
|
||||
std::string wrapped_key_even = ecm_string.substr(37, 16);
|
||||
std::string unwrapped_key_even = crypto_util::DecryptAesCbcNoPad(
|
||||
wrapping_key_even, wrapping_iv_even, wrapped_key_even);
|
||||
EXPECT_EQ(kEcmKeyDataEven, unwrapped_key_even);
|
||||
// Unwrap odd key and compare with original.
|
||||
std::string wrapping_key_odd = kFakeCasEncryptionResponseKeyData;
|
||||
std::string wrapping_iv_odd = ecm_string.substr(125, 16);
|
||||
std::string wrapped_key_odd = ecm_string.substr(109, 16);
|
||||
std::string unwrapped_key_odd = crypto_util::DecryptAesCbcNoPad(
|
||||
wrapping_key_odd, wrapping_iv_odd, wrapped_key_odd);
|
||||
EXPECT_EQ(kEcmKeyDataOdd, unwrapped_key_odd);
|
||||
TEST_F(EcmGeneratorTest, GenerateSimpleRotationWithGroupId) {
|
||||
EcmParameters ecm_params;
|
||||
PrepareEcmGenerator(true, "groupId_1");
|
||||
SetTestConfig2(&ecm_params);
|
||||
std::string ecm_string = ecm_gen_.GenerateEcm(ecm_params);
|
||||
CheckECMOutput(ecm_string, kFakeCasEncryptionResponseGroupKeyId,
|
||||
kFakeCasEncryptionResponseGroupKeyData, true);
|
||||
}
|
||||
|
||||
} // namespace cas
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
|
||||
// CA System ID for Widevine.
|
||||
static constexpr uint16_t kWidevineSystemId = 0x4AD4;
|
||||
// New CA System ID range for Widevine, all inclusive.
|
||||
static constexpr uint16_t kWidevineNewSystemIdLowerBound = 0x56C0;
|
||||
static constexpr uint16_t kWidevineNewSystemIdUpperBound = 0x56C9;
|
||||
// 'section_TSpkt_flag' defines the format of the ECM.
|
||||
// We only support MPEG-2 transport stream packet format for now.
|
||||
// We do NOT support MPEG-2 section format yet.
|
||||
@@ -500,7 +503,10 @@ void EcmgClientHandler::HandleChannelSetup(const EcmgParameters& params,
|
||||
}
|
||||
// The super_cas_id is a 32-bit identifier formed by the concatenation of the
|
||||
// CA_system_id (16 bit) and the CA_subsystem_id (16 bit).
|
||||
if ((params.super_cas_id >> 16) != kWidevineSystemId) {
|
||||
uint16_t cas_id = params.super_cas_id >> 16;
|
||||
if (cas_id != kWidevineSystemId &&
|
||||
(cas_id < kWidevineNewSystemIdLowerBound ||
|
||||
cas_id > kWidevineNewSystemIdUpperBound)) {
|
||||
BuildChannelError(params.ecm_channel_id, UNKNOWN_SUPER_CAS_ID_VALUE, "",
|
||||
response, response_length);
|
||||
return;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <cstdint>
|
||||
#include "absl/container/node_hash_map.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/types/optional.h"
|
||||
#include "common/status.h"
|
||||
#include "media_cas_packager_sdk/internal/ecm.h"
|
||||
@@ -141,7 +141,7 @@ class EcmgClientHandler {
|
||||
std::vector<std::string> content_ivs_;
|
||||
|
||||
// Map from ECM_stream_id to EcmgStreamInfo.
|
||||
absl::node_hash_map<uint16_t, std::unique_ptr<EcmgStreamInfo>> streams_info_;
|
||||
absl::flat_hash_map<uint16_t, std::unique_ptr<EcmgStreamInfo>> streams_info_;
|
||||
};
|
||||
|
||||
} // namespace cas
|
||||
|
||||
@@ -34,7 +34,10 @@ using simulcrypt_util::AddUint8Param;
|
||||
using simulcrypt_util::BuildMessageHeader;
|
||||
|
||||
constexpr size_t kBufferSize = 1024;
|
||||
constexpr size_t kSuperCasId = 0x4AD40000;
|
||||
constexpr uint16_t kWidevineSystemId = 0x4AD4;
|
||||
constexpr uint32_t kSuperCasId = kWidevineSystemId << 16;
|
||||
constexpr uint16_t kWidevineNewSystemIdLowerBound = 0x56C0;
|
||||
constexpr uint16_t kWidevineNewSystemIdUpperBound = 0x56C9;
|
||||
constexpr size_t kChannelId = 1;
|
||||
constexpr size_t kStreamId = 1;
|
||||
constexpr size_t kEcmId = 2;
|
||||
@@ -605,6 +608,38 @@ TEST_F(EcmgClientHandlerTest, BuildEcmDatagramSequenceOfEvenOdd) {
|
||||
EXPECT_NE(std::string(response_, response_len_), first_response);
|
||||
}
|
||||
|
||||
class CasIdTest
|
||||
: public EcmgClientHandlerTest,
|
||||
public ::testing::WithParamInterface<::testing::tuple<int, bool>> {};
|
||||
|
||||
TEST_P(CasIdTest, ValidateCasIds) {
|
||||
uint32_t super_cas_id = ::testing::get<0>(GetParam()) << 16;
|
||||
bool is_valid_cas_id = ::testing::get<1>(GetParam());
|
||||
BuildChannelSetupRequest(kChannelId, super_cas_id, kAgeRestriction,
|
||||
kCryptoMode, request_, &request_len_);
|
||||
handler_->HandleRequest(request_, response_, &response_len_);
|
||||
if (is_valid_cas_id) {
|
||||
EXPECT_EQ(response_len_, sizeof(kTestEcmgChannelStatus));
|
||||
} else {
|
||||
EXPECT_EQ(response_len_, sizeof(kTestEcmgChannelError));
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
ChannelSetupWithNewCasId, CasIdTest,
|
||||
::testing::Combine(
|
||||
::testing::Range(static_cast<int>(kWidevineNewSystemIdLowerBound),
|
||||
static_cast<int>(kWidevineNewSystemIdUpperBound) + 1),
|
||||
::testing::Values(true)));
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
ChannelSetupWithInvalidCasId, CasIdTest,
|
||||
::testing::Combine(::testing::Values(0, kWidevineSystemId - 1,
|
||||
kWidevineSystemId + 1,
|
||||
kWidevineNewSystemIdLowerBound - 1,
|
||||
kWidevineNewSystemIdUpperBound + 1),
|
||||
::testing::Values(false)));
|
||||
|
||||
} // namespace
|
||||
} // namespace cas
|
||||
} // namespace widevine
|
||||
|
||||
@@ -12,14 +12,18 @@
|
||||
|
||||
#include "glog/logging.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "common/ec_key.h"
|
||||
#include "common/hash_algorithm.h"
|
||||
#include "common/status.h"
|
||||
#include "common/string_util.h"
|
||||
#include "media_cas_packager_sdk/internal/util.h"
|
||||
|
||||
namespace widevine {
|
||||
namespace cas {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kNumBitsVersionField = 8;
|
||||
constexpr int kNumBitsHeaderLengthField = 8;
|
||||
constexpr int kNumBitsTimestampLengthField = 64;
|
||||
@@ -29,13 +33,27 @@ constexpr int kNumBitsPayloadLengthField = 16;
|
||||
constexpr uint8_t kEmmVersion = 1;
|
||||
} // namespace
|
||||
|
||||
Status Emm::SetPrivateSigningKey(const std::string& private_signing_key) {
|
||||
if (private_signing_key.empty()) {
|
||||
return {error::INVALID_ARGUMENT, "private_signing_key must not be empty."};
|
||||
}
|
||||
private_signing_key_ = private_signing_key;
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
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 : fingerprintings) {
|
||||
status = ValidateFingerprintingInitParameters(fingerprinting);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& fingerprinting_param : fingerprintings) {
|
||||
Fingerprinting* fingerprinting_payload = emm_payload_.add_fingerprinting();
|
||||
for (const auto& channel : fingerprinting_param.channels) {
|
||||
@@ -51,8 +69,14 @@ Status Emm::SetServiceBlocking(
|
||||
// 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 : service_blockings) {
|
||||
status = ValidateServiceBlockingInitParameters(service_blocking);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& service_blocking_param : service_blockings) {
|
||||
ServiceBlocking* service_blocking_payload =
|
||||
emm_payload_.add_service_blocking();
|
||||
@@ -78,7 +102,7 @@ Status Emm::GenerateEmm(std::string* serialized_emm) const {
|
||||
|
||||
EmmSerializingParameters serializing_params;
|
||||
serializing_params.payload = emm_payload_.SerializeAsString();
|
||||
serializing_params.timestamp = GenerateTimestamp();
|
||||
serializing_params.timestamp = GenerateTimestampEpochSeconds();
|
||||
|
||||
// Generate serialized emm (without signature yet).
|
||||
Status status =
|
||||
@@ -88,7 +112,47 @@ Status Emm::GenerateEmm(std::string* serialized_emm) const {
|
||||
}
|
||||
|
||||
// Calculate and append signature.
|
||||
absl::StrAppend(serialized_emm, GenerateSignature(*serialized_emm));
|
||||
std::string signature;
|
||||
status = GenerateSignature(*serialized_emm, &signature);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
absl::StrAppend(serialized_emm, signature);
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
Status Emm::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"};
|
||||
}
|
||||
|
||||
std::string serialized_emm;
|
||||
Status status = GenerateEmm(&serialized_emm);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (*packet_size < static_cast<ssize_t>(CalculateTsBufferSizeForEcmEmm(
|
||||
serialized_emm.size()))) {
|
||||
return {error::INVALID_ARGUMENT, "packet buffer is too small"};
|
||||
}
|
||||
|
||||
ssize_t bytes_modified = 0;
|
||||
status = InsertEmmAsTsPacket(serialized_emm, pid, continuity_counter, packet,
|
||||
&bytes_modified);
|
||||
if (!status.ok() || bytes_modified <= 0 ||
|
||||
bytes_modified % kTsPacketSize != 0) {
|
||||
LOG(ERROR) << "Failed to generate TS packet: "
|
||||
<< (status.ok()
|
||||
? absl::StrCat("Unexpected bytes_modified value ",
|
||||
bytes_modified)
|
||||
: status.error_message());
|
||||
return {error::INTERNAL, "Failed to generate TS packet"};
|
||||
}
|
||||
|
||||
*packet_size = bytes_modified;
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
@@ -121,15 +185,76 @@ Status Emm::GenerateSerializedEmmNoSignature(
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
int64_t Emm::GenerateTimestamp() const {
|
||||
// TODO(b/161252065): Generate timestamp.
|
||||
return 0;
|
||||
int64_t Emm::GenerateTimestampEpochSeconds() const {
|
||||
return absl::ToUnixSeconds(absl::Now());
|
||||
}
|
||||
|
||||
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;
|
||||
Status Emm::GenerateSignature(const std::string& message,
|
||||
std::string* signature) const {
|
||||
DCHECK(signature);
|
||||
if (message.empty()) {
|
||||
return {error::INVALID_ARGUMENT, "Input message must not be empty."};
|
||||
}
|
||||
if (private_signing_key_.empty()) {
|
||||
return {error::NOT_FOUND, "Private signing key is not set."};
|
||||
}
|
||||
|
||||
std::unique_ptr<ECPrivateKey> ec_private_key(
|
||||
ECPrivateKey::Create(private_signing_key_));
|
||||
if (ec_private_key == nullptr) {
|
||||
return Status(error::INTERNAL, "Failed to construct a ECPrivateKey.");
|
||||
}
|
||||
if (!ec_private_key->GenerateSignature(message, HashAlgorithm::kSha256,
|
||||
signature)) {
|
||||
return Status(error::INTERNAL, "Failed to generate signature.");
|
||||
}
|
||||
if (signature->empty()) {
|
||||
return Status(error::INTERNAL, "Computed signature is empty.");
|
||||
}
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
Status Emm::ValidateFingerprintingInitParameters(
|
||||
const FingerprintingInitParameters& fingerprinting) const {
|
||||
if (fingerprinting.channels.empty()) {
|
||||
return {error::INVALID_ARGUMENT, "channels are missing."};
|
||||
}
|
||||
if (fingerprinting.control.empty()) {
|
||||
return {error::INVALID_ARGUMENT, "control is missing."};
|
||||
}
|
||||
for (const auto& channel : fingerprinting.channels) {
|
||||
if (channel.empty()) {
|
||||
return {error::INVALID_ARGUMENT, "empty channel is observed."};
|
||||
}
|
||||
}
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
Status Emm::ValidateServiceBlockingInitParameters(
|
||||
const ServiceBlockingInitParameters& service_blocking) const {
|
||||
if (service_blocking.channels.empty()) {
|
||||
return {error::INVALID_ARGUMENT, "channels are missing."};
|
||||
}
|
||||
if (service_blocking.device_groups.empty()) {
|
||||
return {error::INVALID_ARGUMENT, "device_groups are missing."};
|
||||
}
|
||||
for (const auto& channel : service_blocking.channels) {
|
||||
if (channel.empty()) {
|
||||
return {error::INVALID_ARGUMENT, "empty channel is observed."};
|
||||
}
|
||||
}
|
||||
for (const auto& device_group : service_blocking.device_groups) {
|
||||
if (device_group.empty()) {
|
||||
return {error::INVALID_ARGUMENT, "empty device_group is observed."};
|
||||
}
|
||||
}
|
||||
if (service_blocking.end_time == 0) {
|
||||
return {error::INVALID_ARGUMENT, "end_time must be specified"};
|
||||
}
|
||||
if (service_blocking.end_time <= service_blocking.start_time) {
|
||||
return {error::INVALID_ARGUMENT, "end_time must be after start_time"};
|
||||
}
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
} // namespace cas
|
||||
|
||||
@@ -27,14 +27,15 @@ struct FingerprintingInitParameters {
|
||||
struct ServiceBlockingInitParameters {
|
||||
std::vector<std::string> channels;
|
||||
std::vector<std::string> device_groups;
|
||||
// Value 0 in start_time means immediate.
|
||||
// Epoch time in seconds. Value 0 in start_time means immediate.
|
||||
int64_t start_time = 0;
|
||||
int64_t end_time;
|
||||
// Epoch time in seconds.
|
||||
int64_t end_time = 0;
|
||||
};
|
||||
|
||||
// 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.
|
||||
// Generator for producing Widevine CAS-compliant EMMs (Entitlement Management
|
||||
// Messages). Used to construct the TS (Transport Stream) packet payload of an
|
||||
// EMM containing messages including fingerprinting and service blocking.
|
||||
// Class Emm is not thread safe.
|
||||
class Emm {
|
||||
public:
|
||||
@@ -43,16 +44,34 @@ class Emm {
|
||||
Emm& operator=(const Emm&) = delete;
|
||||
virtual ~Emm() = default;
|
||||
|
||||
// Sets the signing key used to generate signature field in EMM.
|
||||
// Must be called before calling GenerateEmm() or GenerateEmmTsPackets().
|
||||
virtual Status SetPrivateSigningKey(const std::string& private_signing_key);
|
||||
|
||||
// Replaces current fingerprinting info with |fingerprintings|.
|
||||
Status SetFingerprinting(
|
||||
virtual Status SetFingerprinting(
|
||||
const std::vector<FingerprintingInitParameters>& fingerprintings);
|
||||
|
||||
// Replaces current service blocking info with |service_blockings|.
|
||||
Status SetServiceBlocking(
|
||||
virtual Status SetServiceBlocking(
|
||||
const std::vector<ServiceBlockingInitParameters>& service_blockings);
|
||||
|
||||
// Generates serialized EMM to |serialized_emm|.
|
||||
Status GenerateEmm(std::string* serialized_emm) const;
|
||||
virtual Status GenerateEmm(std::string* serialized_emm) const;
|
||||
|
||||
// Generates serialized EMM and wraps it in TS (transport stream) packets.
|
||||
// Args (all pointer parameters must be not nullptr):
|
||||
// - |pid| program ID for the EMM stream
|
||||
// - |continuity_counter| continuity_counter for the EMM packet,
|
||||
// it will be incremented, only 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;
|
||||
|
||||
private:
|
||||
struct EmmSerializingParameters {
|
||||
@@ -63,10 +82,17 @@ class Emm {
|
||||
Status GenerateSerializedEmmNoSignature(
|
||||
const EmmSerializingParameters& params,
|
||||
std::string* serialized_emm) const;
|
||||
int64_t GenerateTimestamp() const;
|
||||
std::string GenerateSignature(const std::string& content) const;
|
||||
virtual int64_t GenerateTimestampEpochSeconds() const;
|
||||
virtual Status GenerateSignature(const std::string& message,
|
||||
std::string* signature) const;
|
||||
|
||||
Status ValidateFingerprintingInitParameters(
|
||||
const FingerprintingInitParameters& fingerprinting) const;
|
||||
Status ValidateServiceBlockingInitParameters(
|
||||
const ServiceBlockingInitParameters& service_blocking) const;
|
||||
|
||||
EmmPayload emm_payload_;
|
||||
std::string private_signing_key_;
|
||||
};
|
||||
|
||||
} // namespace cas
|
||||
|
||||
@@ -8,9 +8,26 @@
|
||||
|
||||
#include "media_cas_packager_sdk/internal/emm.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "testing/gmock.h"
|
||||
#include "testing/gunit.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/strings/escaping.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "common/ec_key.h"
|
||||
#include "common/ec_test_keys.h"
|
||||
#include "common/hash_algorithm.h"
|
||||
#include "common/status.h"
|
||||
#include "media_cas_packager_sdk/internal/mpeg2ts.h"
|
||||
|
||||
using ::testing::DoAll;
|
||||
using ::testing::ElementsAreArray;
|
||||
using ::testing::HasSubstr;
|
||||
using ::testing::NotNull;
|
||||
using ::testing::Return;
|
||||
using ::testing::SetArgPointee;
|
||||
|
||||
namespace widevine {
|
||||
namespace cas {
|
||||
@@ -22,19 +39,91 @@ 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;
|
||||
constexpr int64_t kServiceBockingStartTime = 1597342393;
|
||||
constexpr int64_t kServiceBockingEndTime = 1597342394;
|
||||
constexpr uint16_t kTestPid = 1;
|
||||
constexpr int64_t kTestTimestamp = 1597882875;
|
||||
// Hex std::string of kTestTimestamp.
|
||||
constexpr char kTestTimestampHexString[] = "000000005f3dc1fb";
|
||||
|
||||
// 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;
|
||||
constexpr int kVersionStartIndex = 0;
|
||||
constexpr int kHeaderLengthStartIndex = 1;
|
||||
constexpr int kTimestampStartIndex = 2;
|
||||
constexpr int kTimestampLengthBytes = 8;
|
||||
constexpr int kPayloadLengthStartIndex = 10;
|
||||
constexpr int kPayloadStartIndex = 12;
|
||||
constexpr int kSignatureLength = 71;
|
||||
// Length in bytes when there is no payload.
|
||||
constexpr int kExpectedNoPayloadLengthBytes =
|
||||
kPayloadStartIndex + kSignatureLength;
|
||||
|
||||
constexpr char kFakeSignatureFiller = 'x'; // 0x78
|
||||
|
||||
constexpr char kExpectedEmptyEmmPacket[] = {
|
||||
// TS header.
|
||||
'\x47', '\x40', '\x01', '\x10',
|
||||
// Section header.
|
||||
'\x00', '\x82', '\x70', '\x53',
|
||||
// EMM version.
|
||||
'\x01',
|
||||
// EMM header size.
|
||||
'\x08',
|
||||
// Timestamp (8 bytes)
|
||||
'\x00', '\x00', '\x00', '\x00', '\x5f', '\x3d', '\xc1', '\xfb',
|
||||
// Payload length (2 bytes)
|
||||
'\x00', '\x00',
|
||||
// Signature (71 bytes)
|
||||
'\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78',
|
||||
'\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78',
|
||||
'\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78',
|
||||
'\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78',
|
||||
'\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78',
|
||||
'\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78',
|
||||
'\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78',
|
||||
'\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78', '\x78',
|
||||
// Padding
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff'};
|
||||
constexpr char kExpectedMockEmmPacket[] = {
|
||||
// TS header.
|
||||
'\x47', '\x40', '\x01', '\x10',
|
||||
// Section header.
|
||||
'\x00', '\x82', '\x70', '\x07',
|
||||
// Mocked EMM payload ("abcdefg").
|
||||
'\x61', '\x62', '\x63', '\x64', '\x65', '\x66', '\x67',
|
||||
// Padding
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff'};
|
||||
|
||||
FingerprintingInitParameters GetValidFingerprintingParams() {
|
||||
FingerprintingInitParameters fingerprinting_params;
|
||||
@@ -68,25 +157,86 @@ void LoadExpectedServiceBlockingProto(
|
||||
service_blocking_payload->set_end_time_sec(kServiceBockingEndTime);
|
||||
}
|
||||
|
||||
TEST(EmmTest, GenerateEmmSinglePayloadSuccess) {
|
||||
Emm emm_gen;
|
||||
class FakeEmmWithFakeSignature : public Emm {
|
||||
private:
|
||||
// Generates a fake signature.
|
||||
Status GenerateSignature(const std::string& message,
|
||||
std::string* signature) const override {
|
||||
signature->assign(kSignatureLength, kFakeSignatureFiller);
|
||||
return OkStatus();
|
||||
}
|
||||
};
|
||||
|
||||
// Verifies a timestamp can be generated and inserted into EMM.
|
||||
TEST(GenerateTimestampEpochSecondsTest, TimestampGenerated) {
|
||||
const absl::Time test_start_time = absl::Now();
|
||||
FakeEmmWithFakeSignature emm_gen;
|
||||
std::string serialized_emm;
|
||||
EXPECT_OK(emm_gen.GenerateEmm(&serialized_emm));
|
||||
uint32_t timestamp = 0;
|
||||
// Extract timestamp value from serialized EMM.
|
||||
for (const auto& timestamp_byte :
|
||||
serialized_emm.substr(kTimestampStartIndex, kTimestampLengthBytes)) {
|
||||
timestamp = (timestamp << 8) | static_cast<uint8_t>(timestamp_byte);
|
||||
}
|
||||
EXPECT_NE(timestamp, 0);
|
||||
EXPECT_GE(timestamp, absl::ToUnixSeconds(test_start_time));
|
||||
}
|
||||
|
||||
class FakeEmm : public Emm {
|
||||
private:
|
||||
// Generates a fake timestamp.
|
||||
int64_t GenerateTimestampEpochSeconds() const override {
|
||||
return kTestTimestamp;
|
||||
}
|
||||
// Generates a fake signature.
|
||||
Status GenerateSignature(const std::string& message,
|
||||
std::string* signature) const override {
|
||||
signature->assign(kSignatureLength, kFakeSignatureFiller);
|
||||
return OkStatus();
|
||||
}
|
||||
};
|
||||
|
||||
class EmmTest : public ::testing::Test {
|
||||
protected:
|
||||
EmmTest() : emm_(absl::make_unique<FakeEmm>()) {}
|
||||
|
||||
void VerifiyEmmHeader(const std::string& serialized_emm) {
|
||||
EXPECT_EQ(static_cast<uint8_t>(serialized_emm[kVersionStartIndex]),
|
||||
kExpectedEmmVersion);
|
||||
EXPECT_EQ(static_cast<uint8_t>(serialized_emm[kHeaderLengthStartIndex]),
|
||||
kExpectedHeaderLength);
|
||||
EXPECT_EQ(absl::BytesToHexString(serialized_emm.substr(
|
||||
kTimestampStartIndex, kTimestampLengthBytes)),
|
||||
kTestTimestampHexString);
|
||||
}
|
||||
void VerifiySignature(const std::string& signature) {
|
||||
std::string expected_signature(kSignatureLength, kFakeSignatureFiller);
|
||||
EXPECT_EQ(signature, expected_signature);
|
||||
}
|
||||
int GetPayloadLength(const std::string& serialized_emm) {
|
||||
return static_cast<uint16_t>(serialized_emm[kPayloadLengthStartIndex]) << 8 |
|
||||
static_cast<uint8_t>(serialized_emm[kPayloadLengthStartIndex + 1]);
|
||||
}
|
||||
|
||||
std::unique_ptr<Emm> emm_;
|
||||
};
|
||||
|
||||
// Verifies GenerateEmm is successful with single fingerprinting and
|
||||
// service_blocking information in the payload.
|
||||
TEST_F(EmmTest, GenerateEmmSinglePayloadSuccess) {
|
||||
FingerprintingInitParameters fingerprinting = GetValidFingerprintingParams();
|
||||
ServiceBlockingInitParameters service_blocking =
|
||||
GetValidServiceBlockingParams();
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting({fingerprinting}), OkStatus());
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking({service_blocking}), OkStatus());
|
||||
EXPECT_OK(emm_->SetFingerprinting({fingerprinting}));
|
||||
EXPECT_OK(emm_->SetServiceBlocking({service_blocking}));
|
||||
|
||||
std::string result;
|
||||
EXPECT_EQ(emm_gen.GenerateEmm(&result), OkStatus());
|
||||
EXPECT_OK(emm_->GenerateEmm(&result));
|
||||
EXPECT_GT(result.length(), kExpectedNoPayloadLengthBytes);
|
||||
VerifiyEmmHeader(result);
|
||||
|
||||
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]);
|
||||
int payload_lengh = GetPayloadLength(result);
|
||||
ASSERT_GT(payload_lengh, 0);
|
||||
ASSERT_EQ(result.length(),
|
||||
kPayloadStartIndex + payload_lengh + kSignatureLength);
|
||||
@@ -103,37 +253,33 @@ TEST(EmmTest, GenerateEmmSinglePayloadSuccess) {
|
||||
std::string serialized_expected_payload;
|
||||
expected_payload.SerializeToString(&serialized_expected_payload);
|
||||
EXPECT_EQ(payload_section, serialized_expected_payload);
|
||||
|
||||
VerifiySignature(result.substr(kPayloadStartIndex + payload_lengh));
|
||||
}
|
||||
|
||||
TEST(EmmTest, GenerateEmmMultiplePayloadSuccess) {
|
||||
Emm emm_gen;
|
||||
// Verifies GenerateEmm is successful with multiple fingerprinting and
|
||||
// service_blocking information in the payload.
|
||||
TEST_F(EmmTest, GenerateEmmMultiplePayloadSuccess) {
|
||||
FingerprintingInitParameters fingerprinting_params;
|
||||
fingerprinting_params.channels = {kChannelThree};
|
||||
fingerprinting_params.control = kFingerprintingControl;
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting(
|
||||
{GetValidFingerprintingParams(), fingerprinting_params}),
|
||||
OkStatus());
|
||||
EXPECT_OK(emm_->SetFingerprinting(
|
||||
{GetValidFingerprintingParams(), fingerprinting_params}));
|
||||
|
||||
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());
|
||||
EXPECT_OK(emm_->SetServiceBlocking(
|
||||
{GetValidServiceBlockingParams(), service_blocking_params}));
|
||||
|
||||
std::string result;
|
||||
EXPECT_EQ(emm_gen.GenerateEmm(&result), OkStatus());
|
||||
EXPECT_OK(emm_->GenerateEmm(&result));
|
||||
EXPECT_GT(result.length(), kExpectedNoPayloadLengthBytes);
|
||||
VerifiyEmmHeader(result);
|
||||
|
||||
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]);
|
||||
int payload_lengh = GetPayloadLength(result);
|
||||
ASSERT_GT(payload_lengh, 0);
|
||||
ASSERT_EQ(result.length(),
|
||||
kPayloadStartIndex + payload_lengh + kSignatureLength);
|
||||
@@ -145,26 +291,24 @@ TEST(EmmTest, GenerateEmmMultiplePayloadSuccess) {
|
||||
ASSERT_TRUE(emm_payload.ParseFromString(payload_section));
|
||||
EXPECT_EQ(emm_payload.fingerprinting_size(), 2);
|
||||
EXPECT_EQ(emm_payload.service_blocking_size(), 2);
|
||||
|
||||
VerifiySignature(result.substr(kPayloadStartIndex + payload_lengh));
|
||||
}
|
||||
|
||||
TEST(EmmTest, GenerateEmmFingerprintingOnlySuccess) {
|
||||
Emm emm_gen;
|
||||
// Verifies GenerateEmm is successful with only fingerprinting information in
|
||||
// the payload.
|
||||
TEST_F(EmmTest, GenerateEmmFingerprintingOnlySuccess) {
|
||||
FingerprintingInitParameters fingerprinting = GetValidFingerprintingParams();
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting({fingerprinting}), OkStatus());
|
||||
EXPECT_OK(emm_->SetFingerprinting({fingerprinting}));
|
||||
// OK to be called again.
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting({fingerprinting}), OkStatus());
|
||||
EXPECT_OK(emm_->SetFingerprinting({fingerprinting}));
|
||||
|
||||
std::string result;
|
||||
EXPECT_EQ(emm_gen.GenerateEmm(&result), OkStatus());
|
||||
EXPECT_OK(emm_->GenerateEmm(&result));
|
||||
EXPECT_GT(result.length(), kExpectedNoPayloadLengthBytes);
|
||||
VerifiyEmmHeader(result);
|
||||
|
||||
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]);
|
||||
int payload_lengh = GetPayloadLength(result);
|
||||
ASSERT_GT(payload_lengh, 0);
|
||||
ASSERT_EQ(result.length(),
|
||||
kPayloadStartIndex + payload_lengh + kSignatureLength);
|
||||
@@ -179,25 +323,23 @@ TEST(EmmTest, GenerateEmmFingerprintingOnlySuccess) {
|
||||
std::string serialized_expected_payload;
|
||||
expected_payload.SerializeToString(&serialized_expected_payload);
|
||||
EXPECT_EQ(payload_section, serialized_expected_payload);
|
||||
|
||||
VerifiySignature(result.substr(kPayloadStartIndex + payload_lengh));
|
||||
}
|
||||
|
||||
TEST(EmmTest, GenerateEmmServiceBlockingOnlySuccess) {
|
||||
Emm emm_gen;
|
||||
// Verifies GenerateEmm is successful with only service_blocking information in
|
||||
// the payload.
|
||||
TEST_F(EmmTest, GenerateEmmServiceBlockingOnlySuccess) {
|
||||
ServiceBlockingInitParameters service_blocking =
|
||||
GetValidServiceBlockingParams();
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking({service_blocking}), OkStatus());
|
||||
EXPECT_OK(emm_->SetServiceBlocking({service_blocking}));
|
||||
|
||||
std::string result;
|
||||
EXPECT_EQ(emm_gen.GenerateEmm(&result), OkStatus());
|
||||
EXPECT_OK(emm_->GenerateEmm(&result));
|
||||
EXPECT_GT(result.length(), kExpectedNoPayloadLengthBytes);
|
||||
VerifiyEmmHeader(result);
|
||||
|
||||
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]);
|
||||
int payload_lengh = GetPayloadLength(result);
|
||||
ASSERT_GT(payload_lengh, 0);
|
||||
ASSERT_EQ(result.length(),
|
||||
kPayloadStartIndex + payload_lengh + kSignatureLength);
|
||||
@@ -212,23 +354,246 @@ TEST(EmmTest, GenerateEmmServiceBlockingOnlySuccess) {
|
||||
std::string serialized_expected_payload;
|
||||
expected_payload.SerializeToString(&serialized_expected_payload);
|
||||
EXPECT_EQ(payload_section, serialized_expected_payload);
|
||||
|
||||
VerifiySignature(result.substr(kPayloadStartIndex + payload_lengh));
|
||||
}
|
||||
|
||||
TEST(EmmTest, GenerateEmmNoPayloadSuccess) {
|
||||
Emm emm_gen;
|
||||
// Verifies GenerateEmm is successful with empty payload.
|
||||
TEST_F(EmmTest, GenerateEmmNoPayloadSuccess) {
|
||||
std::string result;
|
||||
EXPECT_EQ(emm_gen.GenerateEmm(&result), OkStatus());
|
||||
EXPECT_OK(emm_->GenerateEmm(&result));
|
||||
EXPECT_EQ(result.length(), kExpectedNoPayloadLengthBytes);
|
||||
VerifiyEmmHeader(result);
|
||||
|
||||
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]);
|
||||
int payload_lengh = GetPayloadLength(result);
|
||||
EXPECT_EQ(payload_lengh, 0);
|
||||
EXPECT_EQ(result.length(), kPayloadStartIndex + kSignatureLength);
|
||||
|
||||
VerifiySignature(result.substr(kPayloadStartIndex + payload_lengh));
|
||||
}
|
||||
|
||||
// Verifies GenerateEmmTsPackets is successful with empty payload.
|
||||
TEST_F(EmmTest, GenerateEmmTsPacketsNoPayloadSuccess) {
|
||||
uint8_t counter = 0;
|
||||
uint8_t packet[kTsPacketSize];
|
||||
ssize_t packet_size = kTsPacketSize;
|
||||
EXPECT_OK(
|
||||
emm_->GenerateEmmTsPackets(kTestPid, &counter, packet, &packet_size));
|
||||
EXPECT_THAT(packet, ElementsAreArray(kExpectedEmptyEmmPacket));
|
||||
EXPECT_EQ(packet_size, kTsPacketSize);
|
||||
}
|
||||
|
||||
// Verifies GenerateSignature is successful with empty payload.
|
||||
TEST(GenerateSignatureTest, GenerateSignatureSuccess) {
|
||||
Emm emm;
|
||||
std::string emm_generated;
|
||||
ECTestKeys test_keys;
|
||||
|
||||
// Generates signature for empty EMM payload.
|
||||
EXPECT_OK(emm.SetPrivateSigningKey(test_keys.private_test_key_1_secp256r1()));
|
||||
EXPECT_OK(emm.GenerateEmm(&emm_generated));
|
||||
|
||||
// Empty payload, so payload start is signature start.
|
||||
std::string message = emm_generated.substr(0, kPayloadStartIndex);
|
||||
std::string signature = emm_generated.substr(kPayloadStartIndex);
|
||||
EXPECT_FALSE(signature.empty());
|
||||
|
||||
// Check the signaure can be verified.
|
||||
std::unique_ptr<ECPublicKey> public_key(
|
||||
ECPublicKey::Create(test_keys.public_test_key_1_secp256r1()));
|
||||
ASSERT_TRUE(public_key != nullptr);
|
||||
EXPECT_TRUE(
|
||||
public_key->VerifySignature(message, HashAlgorithm::kSha256, signature));
|
||||
}
|
||||
|
||||
// Verifies GenerateEmm fails with no singing key set.
|
||||
TEST(GenerateSignatureTest, SignningKeyNotSetFail) {
|
||||
Emm emm;
|
||||
std::string emm_generated;
|
||||
Status status = emm.GenerateEmm(&emm_generated);
|
||||
EXPECT_EQ(status.error_code(), error::NOT_FOUND);
|
||||
EXPECT_THAT(status.error_message(), HasSubstr("signing key is not set"));
|
||||
}
|
||||
|
||||
// Verifies GenerateEmm fails with empty singing key set.
|
||||
TEST(GenerateSignatureTest, SetPrivateSigningKeyEmptyKeyFail) {
|
||||
Emm emm;
|
||||
EXPECT_EQ(emm.SetPrivateSigningKey("").error_code(), error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// Verifies GenerateEmm fails with invalid singing key set.
|
||||
TEST(GenerateSignatureTest, InvalidSigningKeyFail) {
|
||||
Emm emm;
|
||||
EXPECT_OK(emm.SetPrivateSigningKey("signing key"));
|
||||
|
||||
std::string emm_generated;
|
||||
Status status = emm.GenerateEmm(&emm_generated);
|
||||
EXPECT_EQ(status.error_code(), error::INTERNAL);
|
||||
EXPECT_THAT(status.error_message(),
|
||||
HasSubstr("Failed to construct a ECPrivateKey"));
|
||||
}
|
||||
|
||||
// Overrides GenerateEmm() with mocked method for easier test of
|
||||
// GenerateEmmTsPacketsTest.
|
||||
class MockEmmGenerate : public Emm {
|
||||
public:
|
||||
MOCK_METHOD(Status, GenerateEmm, (std::string * serialized_emm),
|
||||
(const, override));
|
||||
};
|
||||
|
||||
// Verifies GenerateEmmTsPackets is successful with mocked EMM.
|
||||
TEST(GenerateEmmTsPacketsTest, MockPayloadSuccess) {
|
||||
MockEmmGenerate emm_gen;
|
||||
std::string serialized_emm = "abcdefg";
|
||||
EXPECT_CALL(emm_gen, GenerateEmm(NotNull()))
|
||||
.WillOnce(DoAll(SetArgPointee<0>(serialized_emm), Return(OkStatus())));
|
||||
|
||||
uint8_t counter = 0;
|
||||
uint8_t packet[kTsPacketSize];
|
||||
ssize_t packet_size = kTsPacketSize;
|
||||
EXPECT_OK(
|
||||
emm_gen.GenerateEmmTsPackets(kTestPid, &counter, packet, &packet_size));
|
||||
EXPECT_THAT(packet, ElementsAreArray(kExpectedMockEmmPacket));
|
||||
EXPECT_EQ(packet_size, kTsPacketSize);
|
||||
}
|
||||
|
||||
// Verifies GenerateEmmTsPackets fails if the given continuity counter is null.
|
||||
TEST(GenerateEmmTsPacketsTest, NullContinuityCounterPointerFail) {
|
||||
Emm emm_gen;
|
||||
uint8_t packet[kTsPacketSize];
|
||||
ssize_t packet_size = kTsPacketSize;
|
||||
EXPECT_EQ(
|
||||
emm_gen.GenerateEmmTsPackets(kTestPid, nullptr, packet, &packet_size)
|
||||
.error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// Verifies GenerateEmmTsPackets fails if the given packet buffer is null.
|
||||
TEST(GenerateEmmTsPacketsTest, NullPacketBufferFail) {
|
||||
Emm emm_gen;
|
||||
uint8_t counter = 0;
|
||||
uint8_t packet[kTsPacketSize];
|
||||
ssize_t packet_size = kTsPacketSize;
|
||||
EXPECT_EQ(
|
||||
emm_gen.GenerateEmmTsPackets(kTestPid, &counter, nullptr, &packet_size)
|
||||
.error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
EXPECT_EQ(emm_gen.GenerateEmmTsPackets(kTestPid, &counter, packet, nullptr)
|
||||
.error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// Verifies GenerateEmmTsPackets fails if the given buffer is too small.
|
||||
TEST(GenerateEmmTsPacketsTest, PacketBufferTooSmallFail) {
|
||||
MockEmmGenerate emm_gen;
|
||||
std::string serialized_emm = "abcdefg";
|
||||
EXPECT_CALL(emm_gen, GenerateEmm(NotNull()))
|
||||
.WillOnce(DoAll(SetArgPointee<0>(serialized_emm), Return(OkStatus())));
|
||||
|
||||
uint8_t counter = 0;
|
||||
uint8_t packet[kTsPacketSize];
|
||||
ssize_t packet_size = 1;
|
||||
Status status =
|
||||
emm_gen.GenerateEmmTsPackets(kTestPid, &counter, packet, &packet_size);
|
||||
EXPECT_EQ(status.error_code(), error::INVALID_ARGUMENT);
|
||||
EXPECT_THAT(status.error_message(), HasSubstr("buffer is too small"));
|
||||
}
|
||||
|
||||
TEST(SetFingerprinting, ValidParametersSuccess) {
|
||||
Emm emm_gen;
|
||||
EXPECT_OK(emm_gen.SetFingerprinting({GetValidFingerprintingParams()}));
|
||||
}
|
||||
|
||||
TEST(SetFingerprinting, EmptyParamsFail) {
|
||||
Emm emm_gen;
|
||||
FingerprintingInitParameters params;
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting({params}).error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST(SetFingerprinting, EmptyChannelsFail) {
|
||||
Emm emm_gen;
|
||||
FingerprintingInitParameters params = GetValidFingerprintingParams();
|
||||
params.channels = {};
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting({params}).error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST(SetFingerprinting, EmptyChannelValueFail) {
|
||||
Emm emm_gen;
|
||||
FingerprintingInitParameters params = GetValidFingerprintingParams();
|
||||
params.channels = {kChannelOne, ""};
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting({params}).error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST(SetFingerprinting, EmptyControlsFail) {
|
||||
Emm emm_gen;
|
||||
FingerprintingInitParameters params = GetValidFingerprintingParams();
|
||||
params.control = "";
|
||||
EXPECT_EQ(emm_gen.SetFingerprinting({params}).error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST(SetServiceBlocking, ValidParametersSuccess) {
|
||||
Emm emm_gen;
|
||||
EXPECT_OK(emm_gen.SetServiceBlocking({GetValidServiceBlockingParams()}));
|
||||
}
|
||||
|
||||
TEST(SetServiceBlocking, EmptyParamsFail) {
|
||||
Emm emm_gen;
|
||||
ServiceBlockingInitParameters params;
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking({params}).error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST(SetServiceBlocking, EmptyChannelsFail) {
|
||||
Emm emm_gen;
|
||||
ServiceBlockingInitParameters params = GetValidServiceBlockingParams();
|
||||
params.channels = {};
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking({params}).error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST(SetServiceBlocking, EmptyChannelValueFail) {
|
||||
Emm emm_gen;
|
||||
ServiceBlockingInitParameters params = GetValidServiceBlockingParams();
|
||||
params.channels = {kChannelOne, ""};
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking({params}).error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST(SetServiceBlocking, EmptyDeviceGroupsFail) {
|
||||
Emm emm_gen;
|
||||
ServiceBlockingInitParameters params = GetValidServiceBlockingParams();
|
||||
params.device_groups = {};
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking({params}).error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST(SetServiceBlocking, EmptyDeviceGroupValueFail) {
|
||||
Emm emm_gen;
|
||||
ServiceBlockingInitParameters params = GetValidServiceBlockingParams();
|
||||
params.device_groups = {"", kDeviceGroupTwo};
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking({params}).error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST(SetServiceBlocking, InvalidEndTimeFail) {
|
||||
Emm emm_gen;
|
||||
ServiceBlockingInitParameters params = GetValidServiceBlockingParams();
|
||||
params.end_time = 0;
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking({params}).error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST(SetServiceBlocking, EndTimeBeforeStartTimeFail) {
|
||||
Emm emm_gen;
|
||||
ServiceBlockingInitParameters params = GetValidServiceBlockingParams();
|
||||
params.start_time = 10;
|
||||
params.end_time = 9;
|
||||
EXPECT_EQ(emm_gen.SetServiceBlocking({params}).error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "glog/logging.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/strings/escaping.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/time/clock.h"
|
||||
@@ -26,17 +28,16 @@
|
||||
#include "media_cas_packager_sdk/internal/util.h"
|
||||
#include "protos/public/media_cas.pb.h"
|
||||
|
||||
// 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 {
|
||||
|
||||
namespace {
|
||||
// Minimum sending interval in milliseconds.
|
||||
constexpr uint16_t KMinSendIntervalMillis = 2;
|
||||
// Data type is Emm.
|
||||
constexpr uint8_t kEmmDataType = 0;
|
||||
// Data type is private data.
|
||||
constexpr uint8_t kPrivateDataDataType = 1;
|
||||
|
||||
void Print(const char* const msg, size_t msg_size) {
|
||||
for (size_t i = 0; i < msg_size; i++) {
|
||||
@@ -142,7 +143,8 @@ Status HandleParameters(const char* const response, size_t response_length,
|
||||
|
||||
} // namespace
|
||||
|
||||
Emmg::Emmg(EmmgConfig* emmg_config, int server_socket_fd) {
|
||||
Emmg::Emmg(EmmgConfig* emmg_config, int server_socket_fd)
|
||||
: emm_impl_(absl::make_unique<Emm>()) {
|
||||
CHECK(emmg_config);
|
||||
emmg_config_ = emmg_config;
|
||||
server_socket_fd_ = server_socket_fd;
|
||||
@@ -157,7 +159,7 @@ void Emmg::Start() {
|
||||
|
||||
for (size_t i = 0; i < emmg_config_->max_num_message; i++) {
|
||||
SendDataProvision();
|
||||
absl::SleepFor(std::max(absl::Milliseconds(KMinSendIntervalMs),
|
||||
absl::SleepFor(std::max(absl::Milliseconds(KMinSendIntervalMillis),
|
||||
absl::Milliseconds(send_interval_ms_)));
|
||||
}
|
||||
|
||||
@@ -226,28 +228,11 @@ void Emmg::BuildStreamBwRequest() {
|
||||
|
||||
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);
|
||||
const std::vector<std::string>& entitlement_key_ids) {
|
||||
// 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);
|
||||
}
|
||||
@@ -270,7 +255,45 @@ Status Emmg::GeneratePrivateData(
|
||||
LOG(ERROR) << status.ToString();
|
||||
return status;
|
||||
}
|
||||
memcpy(buffer, ecm_ts_packet.data(), ecm_ts_packet.size());
|
||||
|
||||
uint8_t datagram[kTsPacketSize];
|
||||
memcpy(datagram, ecm_ts_packet.data(), ecm_ts_packet.size());
|
||||
simulcrypt_util::AddParam(EMMG_DATAGRAM, datagram, kTsPacketSize, request_,
|
||||
&request_length_);
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
Status Emmg::GenerateEmmData() {
|
||||
Status status;
|
||||
if (!has_configured_emm_impl_) {
|
||||
status = emm_impl_->SetPrivateSigningKey(
|
||||
absl::HexStringToBytes(emmg_config_->ecc_signing_key));
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = emm_impl_->SetFingerprinting(emmg_config_->fingerprinting);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = emm_impl_->SetServiceBlocking(emmg_config_->service_blocking);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
has_configured_emm_impl_ = true;
|
||||
}
|
||||
|
||||
uint8_t datagram[kMaxPossibleTsPacketsSizeBytes];
|
||||
ssize_t datagram_size = kMaxPossibleTsPacketsSizeBytes;
|
||||
status = emm_impl_->GenerateEmmTsPackets(
|
||||
/*pid=*/0, &continuity_counter_, datagram, &datagram_size);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (datagram_size <= 0 || datagram_size % kTsPacketSize != 0) {
|
||||
return {error::INTERNAL, "Failed to generate EMM TS packet"};
|
||||
}
|
||||
simulcrypt_util::AddParam(EMMG_DATAGRAM, datagram, datagram_size, request_,
|
||||
&request_length_);
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
@@ -291,16 +314,26 @@ void Emmg::BuildDataProvision() {
|
||||
simulcrypt_util::AddUint16Param(EMMG_DATA_ID, emmg_config_->data_id, request_,
|
||||
&request_length_);
|
||||
|
||||
uint8_t datagram[kTsPacketSize];
|
||||
Status status = GeneratePrivateData(
|
||||
emmg_config_->content_provider, emmg_config_->content_id,
|
||||
emmg_config_->entitlement_key_ids, datagram);
|
||||
Status status;
|
||||
// Generate and load datagram to |request_| message based on specified
|
||||
// data_type.
|
||||
switch (emmg_config_->data_type) {
|
||||
case kEmmDataType:
|
||||
status = GenerateEmmData();
|
||||
break;
|
||||
case kPrivateDataDataType:
|
||||
status = GeneratePrivateData(emmg_config_->content_provider,
|
||||
emmg_config_->content_id,
|
||||
emmg_config_->entitlement_key_ids);
|
||||
break;
|
||||
default:
|
||||
LOG(ERROR) << "Unexpected data type: " << emmg_config_->data_type;
|
||||
return;
|
||||
}
|
||||
if (!status.ok()) {
|
||||
LOG(ERROR) << "Fail to generate private data. " << status.ToString();
|
||||
LOG(ERROR) << "Fail to generate datagram. " << status.ToString();
|
||||
return;
|
||||
}
|
||||
simulcrypt_util::AddParam(EMMG_DATAGRAM, datagram, kTsPacketSize, request_,
|
||||
&request_length_);
|
||||
|
||||
uint16_t total_param_length = request_length_ - 5;
|
||||
Host16ToBigEndian(request_ + 3, &total_param_length);
|
||||
@@ -413,7 +446,7 @@ void Emmg::UpdateSendInterval(uint16_t bandwidth_kbps) {
|
||||
return;
|
||||
}
|
||||
send_interval_ms_ =
|
||||
std::max(KMinSendIntervalMs,
|
||||
std::max(KMinSendIntervalMillis,
|
||||
static_cast<uint16_t>((kTsPacketSize * 8) / bandwidth_kbps));
|
||||
LOG(INFO) << "Send interval set to " << send_interval_ms_ << " ms.";
|
||||
}
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include "common/status.h"
|
||||
#include "media_cas_packager_sdk/internal/emm.h"
|
||||
|
||||
#define BUFFER_SIZE (1024)
|
||||
#define BUFFER_SIZE (2048)
|
||||
|
||||
namespace widevine {
|
||||
namespace cas {
|
||||
@@ -34,6 +35,9 @@ struct EmmgConfig {
|
||||
std::vector<std::string> entitlement_key_ids;
|
||||
uint16_t bandwidth;
|
||||
uint32_t max_num_message;
|
||||
std::string ecc_signing_key;
|
||||
std::vector<FingerprintingInitParameters> fingerprinting;
|
||||
std::vector<ServiceBlockingInitParameters> service_blocking;
|
||||
};
|
||||
|
||||
// A struct that is used to hold all possible params for a EMMG request.
|
||||
@@ -67,10 +71,11 @@ class Emmg {
|
||||
|
||||
size_t request_length_ = 0;
|
||||
char request_[BUFFER_SIZE];
|
||||
|
||||
// Sending interval in milliseconds. Stream specific parameter but currently
|
||||
// only one stream will be setup.
|
||||
uint16_t send_interval_ms_ = 0;
|
||||
// Internal implementation of Emm.
|
||||
std::unique_ptr<Emm> emm_impl_;
|
||||
|
||||
private:
|
||||
void SendChannelSetup();
|
||||
@@ -86,7 +91,9 @@ class Emmg {
|
||||
|
||||
Status GeneratePrivateData(
|
||||
const std::string& content_provider, const std::string& content_id,
|
||||
const std::vector<std::string>& entitlement_key_ids, uint8_t* buffer);
|
||||
const std::vector<std::string>& entitlement_key_ids);
|
||||
Status GenerateEmmData();
|
||||
|
||||
void ReceiveResponseAndVerify(uint16_t expected_type);
|
||||
void Send(uint16_t message_type);
|
||||
|
||||
@@ -98,7 +105,9 @@ class Emmg {
|
||||
// with the MUX server.
|
||||
int server_socket_fd_;
|
||||
// |continuity_counter| is incremented each time private data is generated.
|
||||
int continuity_counter_ = 0;
|
||||
// Only the lower 4 bits are used.
|
||||
uint8_t continuity_counter_ = 0;
|
||||
bool has_configured_emm_impl_ = false;
|
||||
};
|
||||
|
||||
} // namespace cas
|
||||
|
||||
@@ -23,10 +23,30 @@ namespace widevine {
|
||||
namespace cas {
|
||||
namespace {
|
||||
|
||||
constexpr int64_t kTestTimestamp = 1597882875;
|
||||
constexpr int kSignatureLength = 71;
|
||||
constexpr char kFakeSignatureFiller = 'x'; // 0x78
|
||||
|
||||
class FakeEmm : public Emm {
|
||||
private:
|
||||
// Generates a fake timestamp.
|
||||
int64_t GenerateTimestampEpochSeconds() const override {
|
||||
return kTestTimestamp;
|
||||
}
|
||||
// Generates a fake signature.
|
||||
Status GenerateSignature(const std::string& message,
|
||||
std::string* signature) const override {
|
||||
signature->assign(kSignatureLength, kFakeSignatureFiller);
|
||||
return OkStatus();
|
||||
}
|
||||
};
|
||||
|
||||
class TestableEmmg : public Emmg {
|
||||
public:
|
||||
explicit TestableEmmg(EmmgConfig* emmg_config)
|
||||
: Emmg(emmg_config, /* server_socket_fd= */ 1) {}
|
||||
: Emmg(emmg_config, /* server_socket_fd= */ 1) {
|
||||
emm_impl_ = absl::make_unique<FakeEmm>();
|
||||
}
|
||||
void PublicBuildChannelSetup() { BuildChannelSetup(); }
|
||||
void PublicBuildStreamSetup() { BuildStreamSetup(); }
|
||||
void PublicBuildStreamBwRequest() { BuildStreamBwRequest(); }
|
||||
@@ -48,13 +68,46 @@ class EmmgTest : public ::testing::Test {
|
||||
config_.data_channel_id = 0x0001;
|
||||
config_.data_stream_id = 0x0001;
|
||||
config_.data_id = 0x0001;
|
||||
config_.bandwidth = 100;
|
||||
config_.max_num_message = 100;
|
||||
config_.data_type = 0x01;
|
||||
}
|
||||
|
||||
void InitEmmg() { emmg_ = absl::make_unique<TestableEmmg>(&config_); }
|
||||
|
||||
void LoadPrivateDataConfigs() {
|
||||
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_);
|
||||
}
|
||||
|
||||
void LoadEmmDataConfigs() {
|
||||
config_.data_type = 0x00;
|
||||
config_.ecc_signing_key = "signing_key";
|
||||
FingerprintingInitParameters fingerprinting_1;
|
||||
fingerprinting_1.channels = {"CH1", "CH2"};
|
||||
fingerprinting_1.control = "controls";
|
||||
config_.fingerprinting.push_back(std::move(fingerprinting_1));
|
||||
|
||||
FingerprintingInitParameters fingerprinting_2;
|
||||
fingerprinting_2.channels = {"CH3"};
|
||||
fingerprinting_2.control = "another control";
|
||||
config_.fingerprinting.push_back(std::move(fingerprinting_2));
|
||||
|
||||
ServiceBlockingInitParameters service_blocking_1;
|
||||
service_blocking_1.channels = {"CH1", "CH2"};
|
||||
service_blocking_1.device_groups = {"Group1"};
|
||||
service_blocking_1.start_time = 0;
|
||||
service_blocking_1.end_time = 1234567890;
|
||||
config_.service_blocking.push_back(std::move(service_blocking_1));
|
||||
|
||||
ServiceBlockingInitParameters service_blocking_2;
|
||||
service_blocking_2.channels = {"CH3"};
|
||||
service_blocking_2.device_groups = {"Group2", "Group3"};
|
||||
service_blocking_2.start_time = 1234567890;
|
||||
service_blocking_2.end_time = 1234567891;
|
||||
config_.service_blocking.push_back(std::move(service_blocking_2));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -76,63 +129,76 @@ class EmmgTest : public ::testing::Test {
|
||||
};
|
||||
|
||||
TEST_F(EmmgTest, BuildChannelSetup) {
|
||||
InitEmmg();
|
||||
emmg_->PublicBuildChannelSetup();
|
||||
EXPECT_EQ(0, memcmp(kTestEmmgChannelSetup, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgChannelSetup)));
|
||||
EXPECT_EQ(memcmp(kTestEmmgChannelSetup, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgChannelSetup)),
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(EmmgTest, BuildStreamSetup) {
|
||||
TEST_F(EmmgTest, BuildStreamSetupPrivateData) {
|
||||
InitEmmg();
|
||||
emmg_->PublicBuildStreamSetup();
|
||||
EXPECT_EQ(0, memcmp(kTestEmmgStreamSetup, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgStreamSetup)));
|
||||
EXPECT_EQ(memcmp(kTestEmmgStreamSetupForPrivateData, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgStreamSetupForPrivateData)),
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(EmmgTest, BuildStreamSetupEmm) {
|
||||
config_.data_type = 0x00; // EMM data type.
|
||||
InitEmmg();
|
||||
emmg_->PublicBuildStreamSetup();
|
||||
EXPECT_EQ(memcmp(kTestEmmgStreamSetupForEmm, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgStreamSetupForEmm)),
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(EmmgTest, BuildStreamBwRequest) {
|
||||
InitEmmg();
|
||||
emmg_->PublicBuildStreamBwRequest();
|
||||
EXPECT_EQ(0, memcmp(kTestEmmgStreamBwRequest, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgStreamBwRequest)));
|
||||
EXPECT_EQ(memcmp(kTestEmmgStreamBwRequest, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgStreamBwRequest)),
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(EmmgTest, HandleStreamBwAllocation) {
|
||||
InitEmmg();
|
||||
emmg_->PublicHandleResponse(kTestEmmgStreamBwAllocation);
|
||||
EXPECT_EQ(30, emmg_->GetSendInterval());
|
||||
EXPECT_EQ(emmg_->GetSendInterval(), 30);
|
||||
}
|
||||
|
||||
TEST_F(EmmgTest, BuildDataProvision) {
|
||||
TEST_F(EmmgTest, BuildDataProvisionPrivateData) {
|
||||
LoadPrivateDataConfigs();
|
||||
InitEmmg();
|
||||
emmg_->PublicBuildDataProvision();
|
||||
EXPECT_EQ(0, memcmp(kTestEmmgDataProvision, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgDataProvision)));
|
||||
EXPECT_EQ(memcmp(kTestEmmgPrivateDataProvision, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgPrivateDataProvision)),
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(EmmgTest,
|
||||
BuildDataProvisionFailedWhenNumOfEntitlementKeyIdsExceedLimit) {
|
||||
config_.entitlement_key_ids = {"fakeKeyId1KeyId1", "fakeKeyId2KeyId2",
|
||||
"fakeKeyId3KeyId3"};
|
||||
emmg_ = absl::make_unique<TestableEmmg>(&config_);
|
||||
TEST_F(EmmgTest, BuildDataProvisionEmmData) {
|
||||
LoadEmmDataConfigs();
|
||||
InitEmmg();
|
||||
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)));
|
||||
EXPECT_EQ(memcmp(kTestEmmgEmmDataProvision, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgEmmDataProvision)),
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(EmmgTest, BuildStreamCloseRequest) {
|
||||
InitEmmg();
|
||||
emmg_->PublicBuildStreamCloseRequest();
|
||||
EXPECT_EQ(0, memcmp(kTestEmmgStreamCloseRequest, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgStreamCloseRequest)));
|
||||
EXPECT_EQ(memcmp(kTestEmmgStreamCloseRequest, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgStreamCloseRequest)),
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(EmmgTest, BuildChannelClose) {
|
||||
InitEmmg();
|
||||
emmg_->PublicBuildChannelClose();
|
||||
EXPECT_EQ(0, memcmp(kTestEmmgChannelClose, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgChannelClose)));
|
||||
EXPECT_EQ(memcmp(kTestEmmgChannelClose, emmg_->GetRequest(),
|
||||
sizeof(kTestEmmgChannelClose)),
|
||||
0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -37,6 +37,9 @@ Status FixedKeyFetcher::MakeHttpRequest(const std::string& signed_request_json,
|
||||
CasEncryptionResponse response;
|
||||
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.
|
||||
|
||||
@@ -27,6 +27,10 @@ constexpr ProgramId kInvalidPid = 0x2000;
|
||||
constexpr size_t kTsPacketSize = 188;
|
||||
constexpr size_t kTsPacketHeaderSize = 4;
|
||||
constexpr size_t kMaxTsPayloadSize = kTsPacketSize - kTsPacketHeaderSize;
|
||||
constexpr size_t kMaxSectionSize = 1021;
|
||||
constexpr size_t kMaxPossibleTsPacketsCount = 6;
|
||||
constexpr size_t kMaxPossibleTsPacketsSizeBytes =
|
||||
kMaxPossibleTsPacketsCount * kTsPacketSize;
|
||||
|
||||
constexpr uint8_t kTsPacketSyncByte = 0x47;
|
||||
|
||||
|
||||
@@ -10,22 +10,113 @@
|
||||
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
#include "glog/logging.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "common/status.h"
|
||||
#include "media_cas_packager_sdk/internal/ts_packet.h"
|
||||
|
||||
namespace widevine {
|
||||
namespace cas {
|
||||
namespace {
|
||||
|
||||
// Reference https://en.wikipedia.org/wiki/Program-specific_information
|
||||
constexpr size_t kSectionHeaderSize = 4;
|
||||
constexpr size_t kTsFillValue = 0xFF;
|
||||
// ETSI ETR 289 specifies table ids 0x82 to 0x8F are for CA System private
|
||||
// usage, which are typically used by EMM, with one table id for each EMM type.
|
||||
constexpr uint8_t kEmmTableId = 0x82;
|
||||
// Present at the start of the TS packet payload signaled by the
|
||||
// payload_unit_start_indicator bit in the TS header. Used to set packet
|
||||
// alignment bytes or content before the start of tabled payload data.
|
||||
constexpr uint8_t kPointerFieldZero = '\x00';
|
||||
// Here are the 8 bits covers multiple fields from
|
||||
// "section syntax indicator" to the first two bits of "section length".
|
||||
// Section syntax indicator (1 bit): Set to 0 as this is not PAT, PMT or CAT.
|
||||
// Private bit (1 bit): Set to 1 as this is not PAT, PMT or CAT.
|
||||
// Reserved bits (2 bits): Set to 0x03 (all bits on).
|
||||
// Section length unused bits (2 bits): Set to 0 (all bits off).
|
||||
// Section length higher 2 bits (2 bits): 0.
|
||||
constexpr uint8_t kSyntaxIndicatorToLengthByte = '\x70';
|
||||
|
||||
ContinuityCounter Increment(ContinuityCounter continuity_counter) {
|
||||
return ++continuity_counter & 0xf;
|
||||
}
|
||||
|
||||
Status InsertDataAsTsPacket(const std::string& insert_data, ProgramId pid,
|
||||
uint8_t table_id, ContinuityCounter* cc,
|
||||
uint8_t* buffer, ssize_t* bytes_modified) {
|
||||
if (insert_data.empty()) {
|
||||
return {error::INVALID_ARGUMENT, "insert_data must not be empty"};
|
||||
}
|
||||
if (cc == nullptr || buffer == nullptr || bytes_modified == nullptr) {
|
||||
return {error::INVALID_ARGUMENT,
|
||||
"cc, buffer and bytes_modified must not be null"};
|
||||
}
|
||||
if (*bytes_modified % kTsPacketSize != 0) {
|
||||
return {error::INVALID_ARGUMENT,
|
||||
"bytes_modified must be a multiple of TS packet size (188 bytes)"};
|
||||
}
|
||||
if (insert_data.size() > kMaxSectionSize) {
|
||||
return {error::INVALID_ARGUMENT,
|
||||
absl::StrCat("Data is too large. Max: ", kMaxSectionSize)};
|
||||
}
|
||||
|
||||
int reading_index = 0;
|
||||
while (reading_index < insert_data.size()) {
|
||||
bool is_first_packet = reading_index == 0;
|
||||
// Create a TS payload of 184 bytes (Max TS size - ts header length).
|
||||
std::string payload(kMaxTsPayloadSize, kTsFillValue);
|
||||
|
||||
// Section header if this is the first TS packet.
|
||||
if (is_first_packet) {
|
||||
uint8_t syntax_indicator_to_length_byte =
|
||||
kSyntaxIndicatorToLengthByte | ((insert_data.size() >> 8) & 0x3);
|
||||
uint8_t emm_size_lower_bits = insert_data.size() & 0xFF;
|
||||
|
||||
memcpy(&payload.at(0), &kPointerFieldZero, 1);
|
||||
memcpy(&payload.at(1), &table_id, 1);
|
||||
memcpy(&payload.at(2), &syntax_indicator_to_length_byte, 1);
|
||||
memcpy(&payload.at(3), &emm_size_lower_bits, 1);
|
||||
}
|
||||
|
||||
// Payload data.
|
||||
int payload_index = is_first_packet ? kSectionHeaderSize : 0;
|
||||
// Min of {remaining bytes in TS payload, remaining bytes in insert data}.
|
||||
int used_bytes = std::min(kMaxTsPayloadSize - payload_index,
|
||||
insert_data.size() - reading_index);
|
||||
memcpy(&payload.at(payload_index), insert_data.substr(reading_index).data(),
|
||||
used_bytes);
|
||||
reading_index += used_bytes;
|
||||
|
||||
// Wrap the data with a TS header.
|
||||
TsPacket ts_packet;
|
||||
ts_packet.set_payload_unit_start_indicator(is_first_packet);
|
||||
ts_packet.set_pid(pid);
|
||||
ts_packet.set_payload(payload);
|
||||
ts_packet.set_adaptation_field_control(TsPacket::kPayloadOnly);
|
||||
ts_packet.set_continuity_counter(*cc);
|
||||
*cc = Increment(*cc);
|
||||
|
||||
// And write the packet to the buffer.
|
||||
std::string serialized_ts_packet;
|
||||
Status status = ts_packet.Write(&serialized_ts_packet);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (serialized_ts_packet.size() != kTsPacketSize) {
|
||||
return {error::INTERNAL, "Unexpected TS packet size generated."};
|
||||
}
|
||||
memcpy(buffer + *bytes_modified, serialized_ts_packet.data(),
|
||||
serialized_ts_packet.size());
|
||||
*bytes_modified += serialized_ts_packet.size();
|
||||
}
|
||||
|
||||
return OkStatus();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void BigEndianToHost16(uint16_t* destination, const void* source) {
|
||||
@@ -68,52 +159,20 @@ void Host32ToBigEndian(void* destination, const uint32_t* source) {
|
||||
Status InsertEcmAsTsPacket(const std::string& ecm, ProgramId pid,
|
||||
uint8_t table_id, ContinuityCounter* cc, uint8_t* buffer,
|
||||
ssize_t* bytes_modified) {
|
||||
DCHECK(cc);
|
||||
DCHECK(buffer);
|
||||
DCHECK(bytes_modified);
|
||||
DCHECK_EQ(*bytes_modified % kTsPacketSize, 0);
|
||||
return InsertDataAsTsPacket(ecm, pid, table_id, cc, buffer, bytes_modified);
|
||||
}
|
||||
|
||||
// Create a TS payload of 184 bytes (Max TS size - ts header length).
|
||||
CHECK_LE(ecm.size(), kMaxTsPayloadSize);
|
||||
std::string payload(kMaxTsPayloadSize, kTsFillValue);
|
||||
// Reference https://en.wikipedia.org/wiki/Program-specific_information
|
||||
static constexpr uint8_t kPointerField = '\x00';
|
||||
// Here are the 8 bits covers multiple fields from
|
||||
// "section syntax indicator" to the first two bits of "section length".
|
||||
// We always set the first two bits of "section length" to 0 because
|
||||
// the data follows (i.e. the ECM) cannot be more than 180 bytes, so
|
||||
// the remaining 8 bits in "section length" is sufficient to store its
|
||||
// length.
|
||||
static constexpr uint8_t kSyntaxtIndicatorToLength = '\x70';
|
||||
uint8_t ecm_length = ecm.size();
|
||||
// Section header.
|
||||
memcpy(&payload.at(0), &kPointerField, 1);
|
||||
memcpy(&payload.at(1), &table_id, 1);
|
||||
memcpy(&payload.at(2), &kSyntaxtIndicatorToLength, 1);
|
||||
memcpy(&payload.at(3), &ecm_length, 1);
|
||||
// ECM follows the header.
|
||||
memcpy(&payload.at(kSectionHeaderSize), ecm.data(), ecm.size());
|
||||
Status InsertEmmAsTsPacket(const std::string& emm, ProgramId pid,
|
||||
ContinuityCounter* cc, uint8_t* buffer,
|
||||
ssize_t* bytes_modified) {
|
||||
return InsertDataAsTsPacket(emm, pid, kEmmTableId, cc, buffer,
|
||||
bytes_modified);
|
||||
}
|
||||
|
||||
// Wrap the data with a TS header.
|
||||
TsPacket ecm_packet;
|
||||
ecm_packet.set_payload_unit_start_indicator(true);
|
||||
ecm_packet.set_pid(pid);
|
||||
ecm_packet.set_payload(payload);
|
||||
ecm_packet.set_adaptation_field_control(0x1);
|
||||
ecm_packet.set_continuity_counter(*cc);
|
||||
*cc = Increment(*cc);
|
||||
|
||||
// And write the packet.
|
||||
std::string ecm_ts_packet;
|
||||
Status status = ecm_packet.Write(&ecm_ts_packet);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
memcpy(buffer + *bytes_modified, ecm_ts_packet.data(), ecm_ts_packet.size());
|
||||
*bytes_modified += ecm_ts_packet.size();
|
||||
|
||||
return OkStatus();
|
||||
uint16_t CalculateTsBufferSizeForEcmEmm(uint16_t ecm_emm_size) {
|
||||
return kTsPacketSize *
|
||||
std::ceil(static_cast<float>(kSectionHeaderSize + ecm_emm_size) /
|
||||
kMaxTsPayloadSize);
|
||||
}
|
||||
|
||||
} // namespace cas
|
||||
|
||||
@@ -49,15 +49,39 @@ void Host32ToBigEndian(void* destination, const uint32_t* source);
|
||||
// - |cc| is the continuity counter to use in the header, which will also be
|
||||
// incremented by this function.
|
||||
// - |buffer| is the output buffer. Must be big enough to hold an additional
|
||||
// 188 bytes.
|
||||
// 188 bytes * number of TS packets needed to put |ecm|.
|
||||
// - |bytes_modified| the number of bytes which have already been modified in
|
||||
// the |buffer| and is used as an offset.
|
||||
// |bytes_modified| will be incremented by 188 if insertion of ECM into
|
||||
// |buffer| is successful.
|
||||
// |bytes_modified| will be incremented by a multiple of 188 if insertion of
|
||||
// ECM into |buffer| is successful.
|
||||
Status InsertEcmAsTsPacket(const std::string& ecm, ProgramId pid,
|
||||
uint8_t table_id, ContinuityCounter* cc, uint8_t* buffer,
|
||||
ssize_t* bytes_modified);
|
||||
|
||||
// Packages an EMM as a TS packet and inserts it into a buffer.
|
||||
// Args:
|
||||
// - |emm| is the serialized EMM.
|
||||
// - |pid| is the PID used for ECMs in the TS header.
|
||||
// - |cc| is the continuity counter to use in the header, which will also be
|
||||
// incremented by this function.
|
||||
// - |buffer| is the output buffer. Must be big enough to hold an additional
|
||||
// 188 bytes * number of TS packets needed to put |emm|.
|
||||
// - |bytes_modified| the number of bytes which have already been modified in
|
||||
// the |buffer| and is used as an offset.
|
||||
// |bytes_modified| will be incremented by a multiple of 188 if insertion of
|
||||
// ECM into |buffer| is successful.
|
||||
Status InsertEmmAsTsPacket(const std::string& emm, ProgramId pid,
|
||||
ContinuityCounter* cc, uint8_t* buffer,
|
||||
ssize_t* bytes_modified);
|
||||
|
||||
// Calculates the buffer size needed to hold TS packets generated by inserting
|
||||
// ECM/EMM calls InsertEcmAsTsPacket() and InsertEmmAsTsPacket(). The returned
|
||||
// buffer size will always be a multiple of kTsPacketSize (188 bytes).
|
||||
// Args:
|
||||
// - |ecm_emm_size| is the size of the serialized ECM or EMM that will be
|
||||
// inserted to TS packets.
|
||||
uint16_t CalculateTsBufferSizeForEcmEmm(uint16_t ecm_emm_size);
|
||||
|
||||
} // namespace cas
|
||||
} // namespace widevine
|
||||
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
#include "testing/gmock.h"
|
||||
#include "testing/gunit.h"
|
||||
|
||||
namespace widevine {
|
||||
namespace cas {
|
||||
namespace {
|
||||
|
||||
constexpr ProgramId kTestPid = 0x1FFD;
|
||||
// ECM payload data taken from a CETS encrypted file at Google Fiber
|
||||
// default_key_id = 0000000000000002, random_iv = 0x30EB, 0xDAF4, 0xC66D, 0x57DC
|
||||
constexpr char kEcmPayload[] = {'\x5F', '\x08', '\x30', '\x30', '\x30', '\x30',
|
||||
@@ -53,28 +56,209 @@ constexpr char kExpectedEcmPacket[] = {
|
||||
'\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF',
|
||||
'\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF',
|
||||
'\xFF', '\xFF', '\xFF'};
|
||||
} // namespace
|
||||
|
||||
namespace widevine {
|
||||
namespace cas {
|
||||
// 300 bytes payload.
|
||||
constexpr char kLongPayload[] = {
|
||||
'\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09',
|
||||
'\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', '\x12',
|
||||
'\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1a', '\x1b',
|
||||
'\x1c', '\x1d', '\x1e', '\x1f', '\x20', '\x21', '\x22', '\x23', '\x24',
|
||||
'\x25', '\x26', '\x27', '\x28', '\x29', '\x2a', '\x2b', '\x2c', '\x2d',
|
||||
'\x2e', '\x2f', '\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36',
|
||||
'\x37', '\x38', '\x39', '\x3a', '\x3b', '\x3c', '\x3d', '\x3e', '\x3f',
|
||||
'\x40', '\x41', '\x42', '\x43', '\x44', '\x45', '\x46', '\x47', '\x48',
|
||||
'\x49', '\x4a', '\x4b', '\x4c', '\x4d', '\x4e', '\x4f', '\x50', '\x51',
|
||||
'\x52', '\x53', '\x54', '\x55', '\x56', '\x57', '\x58', '\x59', '\x5a',
|
||||
'\x5b', '\x5c', '\x5d', '\x5e', '\x5f', '\x60', '\x61', '\x62', '\x63',
|
||||
'\x64', '\x65', '\x66', '\x67', '\x68', '\x69', '\x6a', '\x6b', '\x6c',
|
||||
'\x6d', '\x6e', '\x6f', '\x70', '\x71', '\x72', '\x73', '\x74', '\x75',
|
||||
'\x76', '\x77', '\x78', '\x79', '\x7a', '\x7b', '\x7c', '\x7d', '\x7e',
|
||||
'\x7f', '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87',
|
||||
'\x88', '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', '\x90',
|
||||
'\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x98', '\x99',
|
||||
'\x9a', '\x9b', '\x9c', '\x9d', '\x9e', '\x9f', '\xa0', '\xa1', '\xa2',
|
||||
'\xa3', '\xa4', '\xa5', '\xa6', '\xa7', '\xa8', '\xa9', '\xaa', '\xab',
|
||||
'\xac', '\xad', '\xae', '\xaf', '\xb0', '\xb1', '\xb2', '\xb3', '\xb4',
|
||||
'\xb5', '\xb6', '\xb7', '\xb8', '\xb9', '\xba', '\xbb', '\xbc', '\xbd',
|
||||
'\xbe', '\xbf', '\xc0', '\xc1', '\xc2', '\xc3', '\xc4', '\xc5', '\xc6',
|
||||
'\xc7', '\xc8', '\xc9', '\xca', '\xcb', '\xcc', '\xcd', '\xce', '\xcf',
|
||||
'\xd0', '\xd1', '\xd2', '\xd3', '\xd4', '\xd5', '\xd6', '\xd7', '\xd8',
|
||||
'\xd9', '\xda', '\xdb', '\xdc', '\xdd', '\xde', '\xdf', '\xe0', '\xe1',
|
||||
'\xe2', '\xe3', '\xe4', '\xe5', '\xe6', '\xe7', '\xe8', '\xe9', '\xea',
|
||||
'\xeb', '\xec', '\xed', '\xee', '\xef', '\xf0', '\xf1', '\xf2', '\xf3',
|
||||
'\xf4', '\xf5', '\xf6', '\xf7', '\xf8', '\xf9', '\xfa', '\xfb', '\xfc',
|
||||
'\xfd', '\xfe', '\xff', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06',
|
||||
'\x07', '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f',
|
||||
'\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18',
|
||||
'\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', '\x20', '\x21',
|
||||
'\x22', '\x23', '\x24', '\x25', '\x26', '\x27', '\x28', '\x29', '\x2a',
|
||||
'\x2b', '\x2c', '\x2d'};
|
||||
constexpr char kExpectedLongTs[] = {
|
||||
// The 1st TS header.
|
||||
'\x47', '\x5f', '\xfd', '\x10',
|
||||
// Section header.
|
||||
'\x00', '\x80', '\x71', '\x2c',
|
||||
// Payload.
|
||||
'\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09',
|
||||
'\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', '\x12',
|
||||
'\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1a', '\x1b',
|
||||
'\x1c', '\x1d', '\x1e', '\x1f', '\x20', '\x21', '\x22', '\x23', '\x24',
|
||||
'\x25', '\x26', '\x27', '\x28', '\x29', '\x2a', '\x2b', '\x2c', '\x2d',
|
||||
'\x2e', '\x2f', '\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36',
|
||||
'\x37', '\x38', '\x39', '\x3a', '\x3b', '\x3c', '\x3d', '\x3e', '\x3f',
|
||||
'\x40', '\x41', '\x42', '\x43', '\x44', '\x45', '\x46', '\x47', '\x48',
|
||||
'\x49', '\x4a', '\x4b', '\x4c', '\x4d', '\x4e', '\x4f', '\x50', '\x51',
|
||||
'\x52', '\x53', '\x54', '\x55', '\x56', '\x57', '\x58', '\x59', '\x5a',
|
||||
'\x5b', '\x5c', '\x5d', '\x5e', '\x5f', '\x60', '\x61', '\x62', '\x63',
|
||||
'\x64', '\x65', '\x66', '\x67', '\x68', '\x69', '\x6a', '\x6b', '\x6c',
|
||||
'\x6d', '\x6e', '\x6f', '\x70', '\x71', '\x72', '\x73', '\x74', '\x75',
|
||||
'\x76', '\x77', '\x78', '\x79', '\x7a', '\x7b', '\x7c', '\x7d', '\x7e',
|
||||
'\x7f', '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87',
|
||||
'\x88', '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', '\x90',
|
||||
'\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x98', '\x99',
|
||||
'\x9a', '\x9b', '\x9c', '\x9d', '\x9e', '\x9f', '\xa0', '\xa1', '\xa2',
|
||||
'\xa3', '\xa4', '\xa5', '\xa6', '\xa7', '\xa8', '\xa9', '\xaa', '\xab',
|
||||
'\xac', '\xad', '\xae', '\xaf', '\xb0', '\xb1', '\xb2', '\xb3', '\xb4',
|
||||
// The 2nd TS header.
|
||||
'\x47', '\x1f', '\xfd', '\x11',
|
||||
// Payload (continued).
|
||||
'\xb5', '\xb6', '\xb7', '\xb8', '\xb9', '\xba', '\xbb', '\xbc', '\xbd',
|
||||
'\xbe', '\xbf', '\xc0', '\xc1', '\xc2', '\xc3', '\xc4', '\xc5', '\xc6',
|
||||
'\xc7', '\xc8', '\xc9', '\xca', '\xcb', '\xcc', '\xcd', '\xce', '\xcf',
|
||||
'\xd0', '\xd1', '\xd2', '\xd3', '\xd4', '\xd5', '\xd6', '\xd7', '\xd8',
|
||||
'\xd9', '\xda', '\xdb', '\xdc', '\xdd', '\xde', '\xdf', '\xe0', '\xe1',
|
||||
'\xe2', '\xe3', '\xe4', '\xe5', '\xe6', '\xe7', '\xe8', '\xe9', '\xea',
|
||||
'\xeb', '\xec', '\xed', '\xee', '\xef', '\xf0', '\xf1', '\xf2', '\xf3',
|
||||
'\xf4', '\xf5', '\xf6', '\xf7', '\xf8', '\xf9', '\xfa', '\xfb', '\xfc',
|
||||
'\xfd', '\xfe', '\xff', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06',
|
||||
'\x07', '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f',
|
||||
'\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18',
|
||||
'\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', '\x20', '\x21',
|
||||
'\x22', '\x23', '\x24', '\x25', '\x26', '\x27', '\x28', '\x29', '\x2a',
|
||||
'\x2b', '\x2c', '\x2d',
|
||||
// Padding.
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff',
|
||||
'\xff'};
|
||||
|
||||
// TODO(user): Add unit tests for BigEndianToHost16, BigEndianToHost32 and
|
||||
// Host16ToBigEndian.
|
||||
|
||||
TEST(InsertEcmAsTsPacketTest, BasicHappyPath) {
|
||||
TEST(InsertEcmAsTsPacketTest, SingleTsPacket) {
|
||||
// declare variables used by InsertEcmAsTsPacket()
|
||||
ContinuityCounter ecm_cc_ = 0;
|
||||
ProgramId video_ecm_pid_ = 0x1FFD;
|
||||
uint8_t buffer[188]; // output ts packet size
|
||||
ContinuityCounter ecm_cc = 0;
|
||||
uint8_t buffer[kTsPacketSize]; // output ts packet size
|
||||
ssize_t output_bytes_modified = 0;
|
||||
// convert kEcmPayload[] into a std::string to be accepted by the method
|
||||
std::string ecm_data(kEcmPayload);
|
||||
EXPECT_OK(InsertEcmAsTsPacket(ecm_data, video_ecm_pid_, kTsPacketTableId80,
|
||||
&ecm_cc_, buffer, &output_bytes_modified));
|
||||
EXPECT_EQ(0, memcmp(kExpectedEcmPacket, buffer, sizeof(buffer)));
|
||||
EXPECT_EQ(1, ecm_cc_);
|
||||
EXPECT_EQ(188, output_bytes_modified);
|
||||
|
||||
EXPECT_OK(InsertEcmAsTsPacket(ecm_data, kTestPid, kTsPacketTableId80, &ecm_cc,
|
||||
buffer, &output_bytes_modified));
|
||||
|
||||
EXPECT_EQ(memcmp(kExpectedEcmPacket, buffer, sizeof(buffer)), 0);
|
||||
EXPECT_EQ(ecm_cc, 1);
|
||||
EXPECT_EQ(output_bytes_modified, kTsPacketSize);
|
||||
}
|
||||
|
||||
TEST(InsertEcmAsTsPacketTest, MultipleTsPackets) {
|
||||
ContinuityCounter ecm_cc = 0;
|
||||
uint8_t buffer[kTsPacketSize * 2];
|
||||
ssize_t output_bytes_modified = 0;
|
||||
std::string long_payload(kLongPayload);
|
||||
|
||||
EXPECT_OK(InsertEcmAsTsPacket(long_payload, kTestPid, kTsPacketTableId80,
|
||||
&ecm_cc, buffer, &output_bytes_modified));
|
||||
|
||||
EXPECT_EQ(memcmp(kExpectedLongTs, buffer, sizeof(buffer)), 0);
|
||||
EXPECT_EQ(ecm_cc, 2);
|
||||
EXPECT_EQ(output_bytes_modified, kTsPacketSize * 2);
|
||||
}
|
||||
|
||||
TEST(InsertEmmAsTsPacketTest, SingleTsPacket) {
|
||||
ContinuityCounter counter = 0;
|
||||
uint8_t buffer[kTsPacketSize];
|
||||
ssize_t output_bytes_modified = 0;
|
||||
std::string payload(kEcmPayload);
|
||||
|
||||
EXPECT_OK(InsertEmmAsTsPacket(payload, kTestPid, &counter, buffer,
|
||||
&output_bytes_modified));
|
||||
|
||||
char expectedEmmPacket[sizeof(kExpectedEcmPacket)];
|
||||
memcpy(expectedEmmPacket, kExpectedEcmPacket, sizeof(kExpectedEcmPacket));
|
||||
expectedEmmPacket[5] = 0x82; // Expected table id for emm
|
||||
EXPECT_EQ(memcmp(expectedEmmPacket, buffer, sizeof(buffer)), 0);
|
||||
EXPECT_EQ(counter, 1);
|
||||
EXPECT_EQ(output_bytes_modified, kTsPacketSize);
|
||||
}
|
||||
|
||||
TEST(InsertEmmAsTsPacketTest, MultipleTsPackets) {
|
||||
ContinuityCounter counter = 0;
|
||||
uint8_t buffer[kTsPacketSize * 2];
|
||||
ssize_t output_bytes_modified = 0;
|
||||
std::string long_payload(kLongPayload);
|
||||
|
||||
EXPECT_OK(InsertEmmAsTsPacket(long_payload, kTestPid, &counter, buffer,
|
||||
&output_bytes_modified));
|
||||
|
||||
char expectedEmmPacket[sizeof(kExpectedLongTs)];
|
||||
memcpy(expectedEmmPacket, kExpectedLongTs, sizeof(kExpectedLongTs));
|
||||
expectedEmmPacket[5] = 0x82; // Expected table id for emm
|
||||
EXPECT_EQ(memcmp(expectedEmmPacket, buffer, sizeof(buffer)), 0);
|
||||
EXPECT_EQ(counter, 2);
|
||||
EXPECT_EQ(output_bytes_modified, kTsPacketSize * 2);
|
||||
}
|
||||
|
||||
TEST(InsertEcmAsTsPacketTest, InvalidArgument) {
|
||||
ContinuityCounter counter = 0;
|
||||
uint8_t buffer[kTsPacketSize];
|
||||
ssize_t output_bytes_modified = 0;
|
||||
std::string payload(kEcmPayload);
|
||||
|
||||
// Continuity counter is null.
|
||||
EXPECT_EQ(InsertEcmAsTsPacket(payload, kTestPid, kTsPacketTableId80, nullptr,
|
||||
buffer, &output_bytes_modified)
|
||||
.error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
// Buffer is null.
|
||||
EXPECT_EQ(InsertEcmAsTsPacket(payload, kTestPid, kTsPacketTableId80, &counter,
|
||||
nullptr, &output_bytes_modified)
|
||||
.error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
// Output bytes modified is null.
|
||||
EXPECT_EQ(InsertEcmAsTsPacket(payload, kTestPid, kTsPacketTableId80, &counter,
|
||||
buffer, nullptr)
|
||||
.error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
|
||||
// Payload is empty.
|
||||
EXPECT_EQ(InsertEcmAsTsPacket("", kTestPid, kTsPacketTableId80, &counter,
|
||||
buffer, &output_bytes_modified)
|
||||
.error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
|
||||
// Payload is too large.
|
||||
std::string too_large_payload(kMaxSectionSize + 1, '\x11');
|
||||
EXPECT_EQ(InsertEcmAsTsPacket(too_large_payload, kTestPid, kTsPacketTableId80,
|
||||
&counter, buffer, &output_bytes_modified)
|
||||
.error_code(),
|
||||
error::INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST(CalculateTsBufferSizeForEcmEmm, VariousSize) {
|
||||
EXPECT_EQ(CalculateTsBufferSizeForEcmEmm(1), kTsPacketSize);
|
||||
EXPECT_EQ(CalculateTsBufferSizeForEcmEmm(180), kTsPacketSize);
|
||||
EXPECT_EQ(CalculateTsBufferSizeForEcmEmm(181), 2 * kTsPacketSize);
|
||||
EXPECT_EQ(CalculateTsBufferSizeForEcmEmm(300), 2 * kTsPacketSize);
|
||||
EXPECT_EQ(CalculateTsBufferSizeForEcmEmm(364), 2 * kTsPacketSize);
|
||||
EXPECT_EQ(CalculateTsBufferSizeForEcmEmm(365), 3 * kTsPacketSize);
|
||||
EXPECT_EQ(CalculateTsBufferSizeForEcmEmm(500), 3 * kTsPacketSize);
|
||||
EXPECT_EQ(CalculateTsBufferSizeForEcmEmm(1021), 6 * kTsPacketSize);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace cas
|
||||
} // namespace widevine
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
101
media_cas_packager_sdk/public/wv_cas_emm.cc
Normal file
101
media_cas_packager_sdk/public/wv_cas_emm.cc
Normal 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
|
||||
91
media_cas_packager_sdk/public/wv_cas_emm.h
Normal file
91
media_cas_packager_sdk/public/wv_cas_emm.h
Normal 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_
|
||||
260
media_cas_packager_sdk/public/wv_cas_emm_test.cc
Normal file
260
media_cas_packager_sdk/public/wv_cas_emm_test.cc
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user