diff --git a/plugin/include/emm_parser.h b/plugin/include/emm_parser.h new file mode 100644 index 0000000..05f0362 --- /dev/null +++ b/plugin/include/emm_parser.h @@ -0,0 +1,49 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#ifndef EMM_PARSER_H +#define EMM_PARSER_H + +#include +#include + +#include "cas_types.h" +#include "media_cas.pb.h" + +namespace wvcas { + +using video_widevine::EmmPayload; + +class EmmParser { + public: + EmmParser(const EmmParser&) = delete; + EmmParser& operator=(const EmmParser&) = delete; + virtual ~EmmParser() = default; + + // The EmmParser factory method. + // The methods validates the passed in |emm|. If validation is successful, it + // constructs and returns an EmmParser. Otherwise, nullptr is returned. + static std::unique_ptr Create(const CasEmm& emm); + + // Accessor methods. + virtual uint64_t timestamp() const { return timestamp_; } + virtual std::string signature() const { return signature_; } + virtual EmmPayload emm_payload() const { return emm_payload_; }; + + protected: + // Called by the factory create and unit test. + EmmParser() = default; + + private: + bool Parse(int start_index, const CasEmm& emm); + + uint8_t version_; + uint64_t timestamp_; + EmmPayload emm_payload_; + std::string signature_; +}; + +} // namespace wvcas + +#endif // EMM_PARSER_H \ No newline at end of file diff --git a/plugin/src/emm_parser.cpp b/plugin/src/emm_parser.cpp new file mode 100644 index 0000000..ec5ce38 --- /dev/null +++ b/plugin/src/emm_parser.cpp @@ -0,0 +1,75 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "emm_parser.h" + +#include "log.h" + +namespace wvcas { +namespace { +// 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 uint16_t kSectionHeader = 0x82; +constexpr size_t kSectionHeaderSize = 3; +constexpr size_t kSectionHeaderWithPointerSize = 4; +constexpr uint8_t kPointerFieldZero = 0x00; + +// Returns the possible starting index of EMM. -1 will be returned in case of +// error. It assumes the pointer field will always set to 0, if present. +int find_emm_start_index(const CasEmm& cas_emm) { + if (cas_emm.empty()) { + return -1; + } + // Case 1: Pointer field (always set to 0); section header; EMM. + if (cas_emm[0] == kPointerFieldZero) { + return kSectionHeaderWithPointerSize < cas_emm.size() + ? kSectionHeaderWithPointerSize + : -1; + } + // Case 2: Section header (3 bytes), EMM. + if (cas_emm[0] == kSectionHeader) { + return kSectionHeaderSize < cas_emm.size() ? kSectionHeaderSize : -1; + } + // Case 3: EMM. + return 0; +} + +} // namespace + +std::unique_ptr EmmParser::Create(const CasEmm& emm) { + auto parser = std::unique_ptr(new EmmParser()); + if (!parser->Parse(find_emm_start_index(emm), emm)) { + return nullptr; + } + return parser; +} + +bool EmmParser::Parse(int start_index, const CasEmm& emm) { + if (start_index < 0) { + return false; + } + + video_widevine::SignedEmmPayload signed_emm; + if (!signed_emm.ParseFromArray(emm.data() + start_index, + emm.size() - start_index)) { + LOGE("Failed to parse signed EMM."); + return false; + } + + signature_ = signed_emm.signature(); + if (signature_.empty()) { + LOGE("No signature in the EMM."); + return false; + } + + if (!emm_payload_.ParseFromString(signed_emm.serialized_payload())) { + LOGE("Failed to parse EMM payload."); + return false; + } + timestamp_ = emm_payload_.timestamp_secs(); + + return true; +} + +} // namespace wvcas diff --git a/tests/src/emm_parser_test.cpp b/tests/src/emm_parser_test.cpp new file mode 100644 index 0000000..89304b7 --- /dev/null +++ b/tests/src/emm_parser_test.cpp @@ -0,0 +1,133 @@ +// Copyright 2020 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "emm_parser.h" + +#include +#include + +#include +#include + +#include "cas_types.h" + +namespace wvcas { +namespace { + +using video_widevine::EmmPayload; +using video_widevine::SignedEmmPayload; + +constexpr uint8_t kSectionHeader = 0x82; +constexpr int64_t kDefaultTimestamp = 1598905921; +constexpr char kDefaultSignature[] = "signature"; + +class EmmParserTest : public testing::Test { + protected: + EmmParserTest() + : timestamp_(kDefaultTimestamp), signature_(kDefaultSignature) {} + void SetSectionHeader(const std::vector section_header) { + section_header_.assign(section_header.begin(), section_header.end()); + } + void SetTimestamp(uint64_t timestamp) { timestamp_ = timestamp; } + void SetSignedEmm(const std::string& signed_emm) { + serialized_signed_emm_ = signed_emm; + } + void SetEmmPayload(const std::string& serialized_payload) { + serialized_emm_payload_ = serialized_payload; + } + void SetSignature(const std::string& signature) { signature_ = signature; } + + std::vector BuildEmm() const { + std::vector emm_data(section_header_.begin(), + section_header_.end()); + if (!serialized_signed_emm_.empty()) { + emm_data.insert(emm_data.end(), serialized_signed_emm_.begin(), + serialized_signed_emm_.end()); + return emm_data; + } + + SignedEmmPayload signed_emm; + if (serialized_emm_payload_.empty()) { + EmmPayload emm_payload; + emm_payload.set_timestamp_secs(timestamp_); + emm_payload.SerializeToString(signed_emm.mutable_serialized_payload()); + } else { + signed_emm.set_serialized_payload(serialized_emm_payload_); + } + signed_emm.set_signature(signature_); + + emm_data.resize(emm_data.size() + signed_emm.ByteSizeLong()); + signed_emm.SerializeToArray(&emm_data[section_header_.size()], + emm_data.size()); + return emm_data; + } + + void ValidateParserAgainstDefault(const EmmParser* const parser) { + ASSERT_NE(parser, nullptr); + EXPECT_EQ(parser->timestamp(), kDefaultTimestamp); + EmmPayload expected_emm_payload; + expected_emm_payload.set_timestamp_secs(timestamp_); + EXPECT_EQ(parser->emm_payload().SerializeAsString(), + expected_emm_payload.SerializeAsString()); + EXPECT_EQ(parser->signature(), kDefaultSignature); + } + + private: + std::vector section_header_; + uint64_t timestamp_; + std::string signature_; + std::string serialized_signed_emm_; + std::string serialized_emm_payload_; +}; + +TEST_F(EmmParserTest, ParseDefaultSuccess) { + auto parser = EmmParser::Create(BuildEmm()); + ValidateParserAgainstDefault(parser.get()); +} + +TEST_F(EmmParserTest, EmmWithSectionHeaderSuccess) { + SetSectionHeader({kSectionHeader, 0, 0}); + auto parser = EmmParser::Create(BuildEmm()); + ValidateParserAgainstDefault(parser.get()); +} + +TEST_F(EmmParserTest, EmmWithSectionHeaderAndPointerFieldSuccess) { + SetSectionHeader({0, kSectionHeader, 0, 0}); + auto parser = EmmParser::Create(BuildEmm()); + ValidateParserAgainstDefault(parser.get()); +} + +TEST_F(EmmParserTest, EmmWithMalformedEmmCreateFail) { + SetSignedEmm("some emm"); + EXPECT_THAT(EmmParser::Create(BuildEmm()), ::testing::IsNull()); +} + +TEST_F(EmmParserTest, EmmWithMalformedPayloadCreateFail) { + SetEmmPayload("some payload"); + EXPECT_THAT(EmmParser::Create(BuildEmm()), ::testing::IsNull()); +} + +TEST_F(EmmParserTest, EmmWithNoSignatureCreateFail) { + SetSignature(""); + EXPECT_THAT(EmmParser::Create(BuildEmm()), ::testing::IsNull()); +} + +class EmmParserWrongPrefixTest + : public EmmParserTest, + public ::testing::WithParamInterface> {}; + +TEST_P(EmmParserWrongPrefixTest, EmmWithWrongPrefixCreateFail) { + SetSectionHeader(GetParam()); + EXPECT_THAT(EmmParser::Create(BuildEmm()), ::testing::IsNull()); +} + +INSTANTIATE_TEST_SUITE_P( + EmmParserWrongPrefixes, EmmParserWrongPrefixTest, + ::testing::Values(std::vector({0}), + std::vector({1, 0, 0}), + std::vector({kSectionHeader, 0}), + std::vector({1, kSectionHeader, 0, 0}))); + +} // namespace +} // namespace wvcas diff --git a/tests/src/mock_ecm_parser.h b/tests/src/mock_ecm_parser.h new file mode 100644 index 0000000..a4c5b87 --- /dev/null +++ b/tests/src/mock_ecm_parser.h @@ -0,0 +1,45 @@ +// Copyright 2021 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#ifndef MOCK_ECM_PARSER_H +#define MOCK_ECM_PARSER_H + +#include +#include + +#include "ecm_parser.h" + +class MockEcmParser : public wvcas::EcmParser { + public: + MOCK_METHOD(uint8_t, version, (), (const, override)); + MOCK_METHOD(uint8_t, age_restriction, (), (const, override)); + MOCK_METHOD(wvcas::CryptoMode, crypto_mode, (), (const, override)); + MOCK_METHOD(bool, rotation_enabled, (), (const, override)); + MOCK_METHOD(size_t, content_iv_size, (), (const, override)); + MOCK_METHOD(std::vector, entitlement_key_id, (wvcas::KeySlotId id), + (const, override)); + MOCK_METHOD(std::vector, content_key_id, (wvcas::KeySlotId id), + (const, override)); + MOCK_METHOD(std::vector, wrapped_key_data, (wvcas::KeySlotId id), + (const, override)); + MOCK_METHOD(std::vector, wrapped_key_iv, (wvcas::KeySlotId id), + (const, override)); + MOCK_METHOD(std::vector, content_iv, (wvcas::KeySlotId id), + (const, override)); + MOCK_METHOD(bool, set_group_id, (const std::string& group_id), (override)); + MOCK_METHOD(bool, has_fingerprinting, (), (const, override)); + MOCK_METHOD(video_widevine::Fingerprinting, fingerprinting, (), + (const, override)); + MOCK_METHOD(bool, has_service_blocking, (), (const, override)); + MOCK_METHOD(video_widevine::ServiceBlocking, service_blocking, (), + (const, override)); + MOCK_METHOD(std::string, ecm_serialized_payload, (), (const, override)); + MOCK_METHOD(std::string, signature, (), (const, override)); + MOCK_METHOD(bool, is_entitlement_rotation_enabled, (), (const, override)); + MOCK_METHOD(uint32_t, entitlement_period_index, (), (const, override)); + MOCK_METHOD(uint32_t, entitlement_rotation_window_left, (), + (const, override)); +}; + +#endif // MOCK_ECM_PARSER_H \ No newline at end of file diff --git a/tests/src/mock_event_listener.h b/tests/src/mock_event_listener.h new file mode 100644 index 0000000..6c215b6 --- /dev/null +++ b/tests/src/mock_event_listener.h @@ -0,0 +1,47 @@ +// Copyright 2021 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#ifndef MOCK_EVENT_LISTENER_H +#define MOCK_EVENT_LISTENER_H + +#include +#include + +#include "cas_types.h" + +class MockEventListener : public wvcas::CasEventListener { + public: + MockEventListener() {} + virtual ~MockEventListener() {} + + MOCK_METHOD(void, OnSessionRenewalNeeded, (), (override)); + MOCK_METHOD(void, OnSessionKeysChange, + (const wvcas::KeyStatusMap& keys_status, bool has_new_usable_key), + (override)); + MOCK_METHOD(void, OnExpirationUpdate, (int64_t new_expiry_time_seconds), + (override)); + MOCK_METHOD(void, OnNewRenewalServerUrl, + (const std::string& renewal_server_url), (override)); + MOCK_METHOD(void, OnLicenseExpiration, (), (override)); + MOCK_METHOD(void, OnAgeRestrictionUpdated, + (const wvcas::WvCasSessionId& sessionId, + uint8_t ecm_age_restriction), + (override)); + MOCK_METHOD(void, OnSessionFingerprintingUpdated, + (const int32_t& sessionId, + const std::vector& fingerprinting), + (override)); + MOCK_METHOD(void, OnSessionServiceBlockingUpdated, + (const int32_t& sessionId, + const std::vector& service_blocking), + (override)); + MOCK_METHOD(void, OnFingerprintingUpdated, + (const std::vector& fingerprinting), (override)); + MOCK_METHOD(void, OnServiceBlockingUpdated, + (const std::vector& service_blocking), (override)); + MOCK_METHOD(void, OnEntitlementPeriodUpdateNeeded, + (const std::string& signed_license_request), (override)); +}; + +#endif // MOCK_EVENT_LISTENER_H \ No newline at end of file