diff --git a/oemcrypto/include/OEMCryptoCAS.h b/oemcrypto/include/OEMCryptoCAS.h index 8fc3220..f79c464 100644 --- a/oemcrypto/include/OEMCryptoCAS.h +++ b/oemcrypto/include/OEMCryptoCAS.h @@ -5063,6 +5063,29 @@ OEMCryptoResult OEMCrypto_CreateEntitledKeySession( OEMCryptoResult OEMCrypto_RemoveEntitledKeySession( OEMCrypto_SESSION key_session); +/* + * OEMCrypto_ReassociateEntitledKeySession + * + * Description: + * This method associates an existing entitled key session to the specified OEMCrypto session. + * + * Parameters: + * [in] key_session: id of the entitled key session. + * [in] oec_session: handle for the OEMCrypto session to be associated with + * the entitled key session. + * + * Returns: + * OEMCrypto_SUCCESS success + * OEMCrypto_ERROR_NOT_IMPLEMENTED + * OEMCrypto_ERROR_INVALID_ENTITLED_KEY_SESSION + * OEMCrypto_ERROR_INVALID_SESSION + * + * Version: + * This method is new in API version 17. + */ +OEMCryptoResult OEMCrypto_ReassociateEntitledKeySession( + OEMCrypto_SESSION key_session, OEMCrypto_SESSION oec_session); + /****************************************************************************/ /****************************************************************************/ /* The following functions are deprecated. They are not required for the diff --git a/oemcrypto/ref/src/oemcrypto_engine_ref.cpp b/oemcrypto/ref/src/oemcrypto_engine_ref.cpp index 2f48da3..590bcbc 100644 --- a/oemcrypto/ref/src/oemcrypto_engine_ref.cpp +++ b/oemcrypto/ref/src/oemcrypto_engine_ref.cpp @@ -166,6 +166,22 @@ OEMCryptoResult CryptoEngine::RemoveEntitledKeySession(SessionId key_sid) { return OEMCrypto_SUCCESS; } +OEMCryptoResult CryptoEngine::ReassociateEntitledKeySession(SessionId key_sid, + SessionId oec_sid) { + std::unique_lock lock(session_table_lock_); + if (entitled_key_session_table_ == nullptr) { + return OEMCrypto_ERROR_INVALID_ENTITLED_KEY_SESSION; + } + if (entitled_key_session_table_->FindEntitledKeySession(key_sid) == nullptr) { + return OEMCrypto_ERROR_INVALID_ENTITLED_KEY_SESSION; + } + if (!entitled_key_session_table_->ReassociateEntitledKeySession(key_sid, + oec_sid)) { + return OEMCrypto_ERROR_UNKNOWN_FAILURE; + } + return OEMCrypto_SUCCESS; +} + EntitledKeySession* CryptoEngine::FindEntitledKeySession(SessionId key_sid) { std::unique_lock lock(session_table_lock_); if (entitled_key_session_table_ == nullptr) { diff --git a/oemcrypto/ref/src/oemcrypto_engine_ref.h b/oemcrypto/ref/src/oemcrypto_engine_ref.h index 237f508..b4096a4 100644 --- a/oemcrypto/ref/src/oemcrypto_engine_ref.h +++ b/oemcrypto/ref/src/oemcrypto_engine_ref.h @@ -110,6 +110,8 @@ class CryptoEngine { virtual OEMCryptoResult RemoveEntitledKeySession(SessionId key_sid); + virtual OEMCryptoResult ReassociateEntitledKeySession(SessionId key_sid, SessionId oec_sid); + EntitledKeySession* FindEntitledKeySession(SessionId key_sid); size_t GetNumberOfOpenSessions() { return sessions_.size(); } diff --git a/oemcrypto/ref/src/oemcrypto_entitled_key_session.cpp b/oemcrypto/ref/src/oemcrypto_entitled_key_session.cpp index 8d307d8..779d631 100644 --- a/oemcrypto/ref/src/oemcrypto_entitled_key_session.cpp +++ b/oemcrypto/ref/src/oemcrypto_entitled_key_session.cpp @@ -279,6 +279,21 @@ void EntitledKeySessionTable::RemoveEntitledKeySession(SessionId key_sid) { entitled_key_sessions_.erase(key_sid); } +bool EntitledKeySessionTable::ReassociateEntitledKeySession(SessionId key_sid, + SessionId oec_sid) { + std::unique_lock lock(session_lock_); + // The given |key_sid| does not exist. + if (entitled_key_sessions_.find(key_sid) == entitled_key_sessions_.end()) { + return false; + } + + oec_session_to_key_sessions_[oec_sid].erase( + key_session_to_oec_session_[key_sid]); + oec_session_to_key_sessions_[oec_sid].insert(oec_sid); + key_session_to_oec_session_[key_sid] = oec_sid; + return true; +} + SessionId EntitledKeySessionTable::GetOEMCryptoSessionId(SessionId key_sid) { std::unique_lock lock(session_lock_); if (key_session_to_oec_session_.find(key_sid) == diff --git a/oemcrypto/ref/src/oemcrypto_entitled_key_session.h b/oemcrypto/ref/src/oemcrypto_entitled_key_session.h index fe1b2df..332a2b8 100644 --- a/oemcrypto/ref/src/oemcrypto_entitled_key_session.h +++ b/oemcrypto/ref/src/oemcrypto_entitled_key_session.h @@ -113,6 +113,9 @@ class EntitledKeySessionTable { EntitledKeySession* FindEntitledKeySession(SessionId key_sid); // Remove the entitled key session that has |key_sid| as the id. void RemoveEntitledKeySession(SessionId key_sid); + // Reassociates an existing entitled key session |key_sid| to an existing + // OEMCrypto session |oec_sid|. + bool ReassociateEntitledKeySession(SessionId key_sid, SessionId oec_sid); // Get the referenced OEMCrypto session id of entitled key session |key_sid|. SessionId GetOEMCryptoSessionId(SessionId key_sid); diff --git a/oemcrypto/ref/src/oemcrypto_ref.cpp b/oemcrypto/ref/src/oemcrypto_ref.cpp index f34b1e9..5bcfd1f 100644 --- a/oemcrypto/ref/src/oemcrypto_ref.cpp +++ b/oemcrypto/ref/src/oemcrypto_ref.cpp @@ -1989,4 +1989,38 @@ OEMCrypto_RemoveEntitledKeySession(OEMCrypto_SESSION key_session) { return crypto_engine->RemoveEntitledKeySession(key_session); } +OEMCRYPTO_API OEMCryptoResult OEMCrypto_ReassociateEntitledKeySession( + OEMCrypto_SESSION key_session, OEMCrypto_SESSION oec_session) { + if (crypto_engine == nullptr) { + LOGE( + "[OEMCrypto_ReassociateEntitledKeySession: OEMCrypto Not " + "Initialized.]"); + return OEMCrypto_ERROR_UNKNOWN_FAILURE; + } +#ifndef NDEBUG + if (!crypto_engine->ValidRootOfTrust()) { + LOGE("[OEMCrypto_ReassociateEntitledKeySession(): ERROR_KEYBOX_INVALID]"); + return OEMCrypto_ERROR_KEYBOX_INVALID; + } +#endif + if (crypto_engine->SessionTypeBits(key_session) != kSessionTypeEntitledKey) { + LOGE( + "[OEMCrypto_ReassociateEntitledKeySession: Unexpected key_session " + "type.]"); + return OEMCrypto_ERROR_INVALID_ENTITLED_KEY_SESSION; + } + if (crypto_engine->SessionTypeBits(oec_session) != kSessionTypeOEMCrypto) { + LOGE( + "[OEMCrypto_ReassociateEntitledKeySession: Unexpected oec_session " + "type.]"); + return OEMCrypto_ERROR_INVALID_SESSION; + } + SessionContext* session_ctx = crypto_engine->FindSession(oec_session); + if (!session_ctx || !session_ctx->isValid()) { + LOGE("[OEMCrypto_ReassociateEntitledKeySession(): ERROR_INVALID_SESSION]"); + return OEMCrypto_ERROR_INVALID_SESSION; + } + return crypto_engine->ReassociateEntitledKeySession(key_session, oec_session); +} + } // namespace wvoec_ref diff --git a/oemcrypto/test/oemcrypto_test.cpp b/oemcrypto/test/oemcrypto_test.cpp index 8641049..8d7bb71 100644 --- a/oemcrypto/test/oemcrypto_test.cpp +++ b/oemcrypto/test/oemcrypto_test.cpp @@ -1736,6 +1736,45 @@ TEST_P(OEMCryptoLicenseTest, strlen(content_key_id_1), OEMCrypto_CipherMode_CTR)); } +// This verifies that an entitled key session can be reassociated to an OEMCrypto session. +TEST_P(OEMCryptoLicenseTest, ReassociateEntitledKeySessionAPI16) { + ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); + license_messages_.set_license_type(OEMCrypto_EntitlementLicense); + ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); + ASSERT_NO_FATAL_FAILURE(license_messages_.CreateDefaultResponse()); + ASSERT_NO_FATAL_FAILURE(license_messages_.EncryptAndSignResponse()); + ASSERT_EQ(OEMCrypto_SUCCESS, license_messages_.LoadResponse()); + // Setup another session. + Session session2; + ASSERT_NO_FATAL_FAILURE(session2.open()); + ASSERT_NO_FATAL_FAILURE(InstallTestRSAKey(&session2)); + ASSERT_NO_FATAL_FAILURE(session2.GenerateDerivedKeysFromSessionKey()); + // Setup an entitled key session in the first OEMCrypto session. + uint32_t key_session_id; + OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( + session_.session_id(), &key_session_id); + ASSERT_EQ(OEMCrypto_SUCCESS, sts); + EntitledMessage entitled_message(&license_messages_); + entitled_message.FillKeyArray(); + entitled_message.SetEntitledKeySession(key_session_id); + ASSERT_NO_FATAL_FAILURE(entitled_message.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); + + // Now reassociate the entitled key session to the second OEMCrypto session. + ASSERT_EQ(OEMCrypto_SUCCESS, OEMCrypto_ReassociateEntitledKeySession( + key_session_id, session2.session_id())); + // session2 does not have entitlement keys. + ASSERT_NO_FATAL_FAILURE(entitled_message.LoadCasKeys( + OEMCrypto_ERROR_INVALID_CONTEXT, /*load_even=*/true, /*load_odd=*/true)); + + // Now reassociate the entitled key session back to the first OEMCrypto + // session. + ASSERT_EQ(OEMCrypto_SUCCESS, OEMCrypto_ReassociateEntitledKeySession( + key_session_id, session_.session_id())); + ASSERT_NO_FATAL_FAILURE(entitled_message.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); +} + // 'cens' mode is no longer supported in v16 TEST_P(OEMCryptoLicenseTest, RejectCensAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); diff --git a/plugin/Android.bp b/plugin/Android.bp index 3d8c016..f64f89c 100644 --- a/plugin/Android.bp +++ b/plugin/Android.bp @@ -12,6 +12,7 @@ cc_library_static { "src/ecm_parser.cpp", "src/ecm_parser_v2.cpp", "src/ecm_parser_v3.cpp", + "src/emm_parser.cpp", "src/license_key_status.cpp", "src/oemcrypto_interface.cpp", "src/policy_engine.cpp", diff --git a/plugin/include/cas_events.h b/plugin/include/cas_events.h index 2a6e401..af9955d 100644 --- a/plugin/include/cas_events.h +++ b/plugin/include/cas_events.h @@ -37,6 +37,9 @@ typedef enum { LICENSE_NEW_EXPIRY_TIME, MULTI_CONTENT_LICENSE_INFO, GROUP_LICENSE_INFO, + LICENSE_ENTITLEMENT_PERIOD_UPDATE_REQUEST, + LICENSE_ENTITLEMENT_PERIOD_UPDATE_RESPONSE, + LICENSE_ENTITLEMENT_PERIOD_UPDATED, // TODO(jfore): Evaluate removing this event in favor of return status codes // from @@ -49,6 +52,8 @@ typedef enum { UNIQUE_ID = CAS_QUERY_EVENT_START, QUERY_UNIQUE_ID, + WV_CAS_PLUGIN_VERSION, + QUERY_WV_CAS_PLUGIN_VERSION, SET_PARENTAL_CONTROL_AGE = CAS_PARENTAL_CONTROL_EVENT_START, DEPRECATED_PARENTAL_CONTROL_AGE_UPDATED, @@ -82,14 +87,30 @@ typedef enum { ECHO, // Respond to TEST_FOR_ECHO. } CasEventId; +// Types used inside an FINGERPRINTING_INFO event. +typedef enum { + FINGERPRINTING_CHANNEL = 0, + FINGERPRINTING_CONTROL, +} FingerprintingFieldType; + +// Types used inside an SERVICE_BLOCKING_INFO event. +typedef enum { + SERVICE_BLOCKING_CHANNEL = 0, + SERVICE_BLOCKING_DEVICE_GROUP, + // Epoch time in seconds. Missing of this field or a value of 0 means + // immediate start. + SERVICE_BLOCKING_START_TIME_SECONDS, + SERVICE_BLOCKING_END_TIME_SECONDS, // Epoch time in seconds. +} ServiceBlockingFieldType; + // Types used inside an SESSION_FINGERPRINTING_CONTROL event. typedef enum { - FINGERPRINTING_CONTROL = 0, + SESSION_FINGERPRINTING_CONTROL = 0, } SessionFingerprintingFieldType; // Types used inside an SESSION_SERVICE_BLOCKING_GROUPS event. typedef enum { - SERVICE_BLOCKING_DEVICE_GROUP = 0, + SESSION_SERVICE_BLOCKING_DEVICE_GROUP = 0, } SessionServiceBlockingFieldType; // Types used inside a MULTI_CONTENT_LICENSE_INFO event. diff --git a/plugin/include/cas_license.h b/plugin/include/cas_license.h index 26e179a..25c33fc 100644 --- a/plugin/include/cas_license.h +++ b/plugin/include/cas_license.h @@ -53,22 +53,16 @@ class CasLicense : public wvutil::TimerHandler, public wvcas::CasEventListener { std::string* signed_license_request); // Restores a stored license making the keys available for use. - // If |content_id_filter| is not null, only matching entitlement keys (as - // specified in KeyCategory) will be installed. virtual CasStatus HandleStoredLicense(const std::string& wrapped_rsa_key, - const std::string& license_file, - const std::string* content_id_filter); + const std::string& license_file); // Process a server response containing a EMM for use in the processing of // ECM(s). - // If |content_id_filter| is not null, only matching entitlement keys (as - // specified in KeyCategory) will be installed. // If |device_file| is not nullptr and the license policy allows a license to // be stored |device_file| is populated with the bytes of the license secured // for storage. virtual CasStatus HandleEntitlementResponse( - const std::string& entitlement_response, - const std::string* content_id_filter, std::string* device_file); + const std::string& entitlement_response, std::string* device_file); // Process a previously stored device |certificate| and make it available // for use in an EMM request. @@ -132,6 +126,8 @@ class CasLicense : public wvutil::TimerHandler, public wvcas::CasEventListener { void OnAgeRestrictionUpdated(const WvCasSessionId& sessionId, uint8_t ecm_age_restriction) override; + void OnFingerprintingUpdated(const CasData& fingerprinting) override; + void OnServiceBlockingUpdated(const CasData& service_blocking) override; void OnSessionFingerprintingUpdated(const WvCasSessionId& sessionId, const CasData& fingerprinting) override; @@ -140,12 +136,20 @@ class CasLicense : public wvutil::TimerHandler, public wvcas::CasEventListener { const WvCasSessionId& sessionId, const CasData& service_blocking) override; + void OnEntitlementPeriodUpdateNeeded( + const std::string& signed_license_request) override; + // Query to see if the license is expired. virtual bool IsExpired() const; // Notify the license that playback decryption has begun. virtual void BeginDecryption(); + // Returns NoError if a valid entitlement period index exists in + // |license_file|. The index will be assigned to |entitlement_period_index|. + static CasStatus GetEntitlementPeriodIndexFromStoredLicense( + const std::string& license_file, uint32_t& entitlement_period_index); + CasLicense(const CasLicense&) = delete; CasLicense& operator=(const CasLicense&) = delete; @@ -156,8 +160,7 @@ class CasLicense : public wvutil::TimerHandler, public wvcas::CasEventListener { CasStatus InstallLicense(const std::string& session_key, const std::string& serialized_license, const std::string& core_message, - const std::string& signature, - const std::string* content_id_filter); + const std::string& signature); CasStatus InstallLicenseRenewal(const std::string& serialized_license, const std::string& core_message, const std::string& signature); diff --git a/plugin/include/cas_media_id.h b/plugin/include/cas_media_id.h index c993d93..b20bc0d 100644 --- a/plugin/include/cas_media_id.h +++ b/plugin/include/cas_media_id.h @@ -15,6 +15,9 @@ class CasMediaId { virtual CasStatus initialize(const std::string& init_data) = 0; virtual const std::string content_id() = 0; virtual const std::string provider_id() = 0; + virtual bool is_entitlement_rotation_enabled() { return false; } + virtual uint32_t entitlement_period_index() = 0; + virtual std::string get_init_data() = 0; }; } // namespace wvcas diff --git a/plugin/include/cas_types.h b/plugin/include/cas_types.h index c688e7e..b1d1423 100644 --- a/plugin/include/cas_types.h +++ b/plugin/include/cas_types.h @@ -89,14 +89,23 @@ class CasEventListener { virtual void OnAgeRestrictionUpdated(const WvCasSessionId& sessionId, uint8_t ecm_age_restriction) = 0; - // Notifies listeners of new fingerprinting info. + // Notifies listeners of new session fingerprinting info. virtual void OnSessionFingerprintingUpdated( const WvCasSessionId& sessionId, const CasData& fingerprinting) = 0; - // Notifies listeners of new service blocking info. + // Notifies listeners of new session service blocking info. virtual void OnSessionServiceBlockingUpdated( const WvCasSessionId& sessionId, const CasData& service_blocking) = 0; + // Notifies listeners of new fingerprinting info. + virtual void OnFingerprintingUpdated(const CasData& fingerprinting) = 0; + + // Notifies listeners of new service blocking info. + virtual void OnServiceBlockingUpdated(const CasData& service_blocking) = 0; + + virtual void OnEntitlementPeriodUpdateNeeded( + const std::string& signed_license_request) = 0; + CasEventListener(const CasEventListener&) = delete; CasEventListener& operator=(const CasEventListener&) = delete; }; diff --git a/plugin/include/crypto_session.h b/plugin/include/crypto_session.h index 1410f9f..8621380 100644 --- a/plugin/include/crypto_session.h +++ b/plugin/include/crypto_session.h @@ -172,6 +172,8 @@ class CryptoInterface { OEMCrypto_SESSION session, OEMCrypto_SESSION* entitled_key_session_id); virtual OEMCryptoResult OEMCrypto_RemoveEntitledKeySession( OEMCrypto_SESSION entitled_key_session_id); + virtual OEMCryptoResult OEMCrypto_ReassociateEntitledKeySession( + OEMCrypto_SESSION key_sid, OEMCrypto_SESSION oec_sid); virtual uint32_t OEMCrypto_APIVersion(); // This is the factory method used to enable the oemcrypto interface. @@ -284,6 +286,8 @@ class CryptoSession { OEMCrypto_SESSION* entitled_key_session_id); virtual CasStatus RemoveEntitledKeySession( OEMCrypto_SESSION entitled_key_session_id); + virtual CasStatus ReassociateEntitledKeySession( + OEMCrypto_SESSION entitled_key_session_id); virtual CasStatus APIVersion(uint32_t* api_version); CryptoSession(const CryptoSession&) = delete; diff --git a/plugin/include/ecm_parser.h b/plugin/include/ecm_parser.h index e26480c..4ce731e 100644 --- a/plugin/include/ecm_parser.h +++ b/plugin/include/ecm_parser.h @@ -48,6 +48,10 @@ class EcmParser { // The serialized payload that the signature is calculated on. virtual std::string ecm_serialized_payload() const = 0; virtual std::string signature() const = 0; + + virtual bool is_entitlement_rotation_enabled() const = 0; + virtual uint32_t entitlement_period_index() const = 0; + virtual uint32_t entitlement_rotation_window_left() const = 0; }; } // namespace wvcas diff --git a/plugin/include/ecm_parser_v2.h b/plugin/include/ecm_parser_v2.h index 9037eb9..77e08e1 100644 --- a/plugin/include/ecm_parser_v2.h +++ b/plugin/include/ecm_parser_v2.h @@ -63,6 +63,10 @@ class EcmParserV2 : public EcmParser { std::string ecm_serialized_payload() const override { return ""; } std::string signature() const override { return ""; } + bool is_entitlement_rotation_enabled() const override { return false; } + uint32_t entitlement_period_index() const override { return 0; } + uint32_t entitlement_rotation_window_left() const override { return 0; } + private: // Constructs an EcmParserV2 using |ecm|. explicit EcmParserV2(const CasEcm& ecm); diff --git a/plugin/include/ecm_parser_v3.h b/plugin/include/ecm_parser_v3.h index f3a15cf..17433bb 100644 --- a/plugin/include/ecm_parser_v3.h +++ b/plugin/include/ecm_parser_v3.h @@ -49,6 +49,10 @@ class EcmParserV3 : public EcmParser { std::string ecm_serialized_payload() const override; std::string signature() const override; + bool is_entitlement_rotation_enabled() const override; + uint32_t entitlement_period_index() const override; + uint32_t entitlement_rotation_window_left() const override; + private: // Constructs an EcmParserV3 using |ecm|. EcmParserV3(video_widevine::SignedEcmPayload signed_ecm_payload, diff --git a/plugin/include/oemcrypto_interface.h b/plugin/include/oemcrypto_interface.h index c8148da..efa6b53 100644 --- a/plugin/include/oemcrypto_interface.h +++ b/plugin/include/oemcrypto_interface.h @@ -118,6 +118,8 @@ class OEMCryptoInterface { OEMCrypto_SESSION oec_session, OEMCrypto_SESSION* key_session); virtual OEMCryptoResult OEMCrypto_RemoveEntitledKeySession( OEMCrypto_SESSION key_session); + virtual OEMCryptoResult OEMCrypto_ReassociateEntitledKeySession( + OEMCrypto_SESSION key_session, OEMCrypto_SESSION oec_session); virtual uint32_t OEMCrypto_APIVersion() const; OEMCryptoInterface(const OEMCryptoInterface&) = delete; diff --git a/plugin/include/widevine_cas_api.h b/plugin/include/widevine_cas_api.h index f2edeb0..dec170f 100644 --- a/plugin/include/widevine_cas_api.h +++ b/plugin/include/widevine_cas_api.h @@ -7,13 +7,17 @@ #include #include +#include #include "cas_license.h" #include "cas_media_id.h" #include "cas_status.h" #include "cas_types.h" #include "crypto_session.h" +#include "ecm_parser.h" +#include "emm_parser.h" #include "file_store.h" +#include "media_cas.pb.h" #include "timer.h" #include "widevine_cas_session.h" @@ -36,6 +40,9 @@ class WidevineCas : public wvutil::TimerHandler { // Close a previously opened session. virtual CasStatus closeSession(WvCasSessionId sessionId); + // Process an EMM which may contain fingerprinting and service blocking info. + virtual CasStatus processEmm(const CasEmm& emm); + // Process an ECM from the ECM stream for this session’s elementary // stream. virtual CasStatus processEcm(WvCasSessionId sessionId, const CasEcm& ecm); @@ -74,6 +81,16 @@ class WidevineCas : public wvutil::TimerHandler { virtual CasStatus handleEntitlementRenewalResponse( const std::string& response, std::string& license_id); + // Generates an entitlement license request in a new crypto session, and send + // the license request as an event to the app. + virtual CasStatus generateEntitlementPeriodUpdateRequest( + const std::string& init_data); + + // Processes the license |response| to switch the current license to this + // new one. + virtual CasStatus handleEntitlementPeriodUpdateResponse( + const std::string& response, std::string& license_id); + // Returns true if the device has been provisioned with a device certificate. virtual bool is_provisioned() const; @@ -97,7 +114,8 @@ class WidevineCas : public wvutil::TimerHandler { // Remove the license file given the filename user provides. virtual CasStatus RemoveLicense(const std::string& file_name); - // Record the license id that user provides. + // Record the license id that user provides. This license id will be used to + // select license if multiple licenses exist. virtual CasStatus RecordLicenseId(const std::string& license_id); void OnTimerEvent() override; @@ -107,11 +125,30 @@ class WidevineCas : public wvutil::TimerHandler { virtual CasStatus HandleProcessEcm(const WvCasSessionId& sessionId, const CasEcm& ecm); virtual CasStatus HandleDeferredECMs(); + // Extracts the entitlement rotation period index from ECM if specified, and + // store it. The function should be called before any license request and the + // extracted index will be included in the license request. + virtual void TryExtractEntitlementPeriodIndex(const CasEcm& ecm); + // Returns true if an offline license with |filename| is successfully loaded. + virtual bool TryReuseStoredLicense(const std::string& filename); + // Check if a new license is needed due to entitlement period changes. If so, + // it will call generateEntitlementPeriodUpdateRequest(). + void CheckEntitlementPeriodUpdate(uint32_t period_index, + uint32_t window_left); virtual std::shared_ptr getCryptoSession(); virtual std::unique_ptr getCasLicense(); virtual std::unique_ptr getFileSystem(); virtual std::shared_ptr newCasSession(); + virtual std::unique_ptr getEcmParser(const CasEcm& ecm) const; + + // Creates an EmmParser. Marked as virtual for easier unit test. + virtual std::unique_ptr getEmmParser( + const CasEmm& emm) const; + std::vector GenerateFingerprintingEventMessage( + const video_widevine::Fingerprinting& fingerprinting) const; + std::vector GenerateServiceBlockingEventMessage( + const video_widevine::ServiceBlocking& service_blocking) const; // The CryptoSession will be shared by the all cas sessions. It is also needed // by the cas api to generate EMM requests. @@ -137,15 +174,31 @@ class WidevineCas : public wvutil::TimerHandler { // The age_restriction field in ECM must be greater or equal to // |parental_control_min_age|. Otherwise, ECM will stop being processed. uint parental_control_age_ = 0; - // The assigned_license_id helps to indicate which license file current + // The requested_license_id helps to indicate which license file current // content will use if multiple licenses exist. - std::string assigned_license_id_; + std::string requested_license_id_; // The current in use license_id. std::string license_id_; // The group id of a Group license. Empty if the license is not a Group // license (multi content license is not a group license). Used in processECM // to select group keys that can be decrypted by the license. std::string license_group_id_; + // Fingerprinting events sent in processing last ECM/EMM. Used to avoid + // sending a same event again. + std::set last_fingerprinting_events_; + // Service blocking events sent in processing last ECM/EMM. Used to avoid + // sending a same event again. + std::set last_service_blocking_events_; + // Indicates if |entitlement_period_index_| below is valid or not. + bool is_entitlement_rotation_enabled_ = false; + // The entitlement period index in the last received ECM. + uint32_t entitlement_period_index_; + + // |next_*| used to handle entitlement key rotation. They will be moved to + // normal ones once the license switch completed. + std::shared_ptr next_crypto_session_; + std::unique_ptr next_cas_license_; + std::unique_ptr next_media_id_; }; // namespace wvcas } // namespace wvcas diff --git a/plugin/include/widevine_cas_session.h b/plugin/include/widevine_cas_session.h index 314edb6..0900d12 100644 --- a/plugin/include/widevine_cas_session.h +++ b/plugin/include/widevine_cas_session.h @@ -49,6 +49,8 @@ class WidevineCasSession { CasStatus initialize(std::shared_ptr crypto_session, CasEventListener* event_listener, uint32_t* session_id); + CasStatus resetCryptoSession(std::shared_ptr crypto_session); + // Process an ecm and extract the key slot data. Extracted data will be used // to update |current_ecm_| and |entitlement_key_id_| and |keys_|. // |parental_control_age| (if non-zero) must be greater or equal to the @@ -63,7 +65,16 @@ class WidevineCasSession { const char* securityLevel(); // Returns current ecm age restriction value. - uint8_t GetEcmAgeRestriction() { return ecm_age_restriction_; } + uint8_t GetEcmAgeRestriction() const { return ecm_age_restriction_; } + // Returns the entitlement period index specified in the last received ECM. + uint32_t GetEntitlementPeriodIndex() const { + return entitlement_period_index_; + } + // Returns the entitlement rotation window left value specified in the last + // received ECM. + uint32_t GetEntitlementRotationWindowLeft() const { + return entitlement_rotation_window_left_; + } WidevineCasSession(const WidevineCasSession&) = delete; WidevineCasSession& operator=(const WidevineCasSession&) = delete; @@ -74,7 +85,7 @@ class WidevineCasSession { CasKeySlotData keys_; // Odd and even key slots. std::string entitlement_key_id_; - std::mutex lock_; + std::mutex crypto_lock_; CasEcm current_ecm_; uint8_t ecm_age_restriction_ = 0; std::shared_ptr crypto_session_; @@ -87,6 +98,10 @@ class WidevineCasSession { // Service blocking events sent in processing last ECM/EMM. Used to avoid // sending a same event again. std::vector last_service_blocking_message_; + // The entitlement period index in the last received ECM. + uint32_t entitlement_period_index_; + // The entitlement rotation window left in the last received ECM. + uint32_t entitlement_rotation_window_left_; }; } // namespace wvcas diff --git a/plugin/include/widevine_cas_session_map.h b/plugin/include/widevine_cas_session_map.h index 7abc68a..ed717b6 100644 --- a/plugin/include/widevine_cas_session_map.h +++ b/plugin/include/widevine_cas_session_map.h @@ -34,6 +34,8 @@ class WidevineCasSessionMap { CasSessionPtr GetSession(WvCasSessionId cas_session_id) const; // Remove an entry in the map. void RemoveSession(WvCasSessionId cas_session_id); + // Retrieves all the session ids. + std::vector GetAllSessions() const; // Returns a reference to the map. static WidevineCasSessionMap& instance(); diff --git a/plugin/include/widevine_media_cas_plugin.h b/plugin/include/widevine_media_cas_plugin.h index f9f7039..4b676ee 100644 --- a/plugin/include/widevine_media_cas_plugin.h +++ b/plugin/include/widevine_media_cas_plugin.h @@ -105,9 +105,11 @@ class WidevineCasPlugin : public CasPlugin, public CasEventListener { CasStatus HandleSetParentalControlAge(const CasData& data); CasStatus HandleLicenseRemoval(const CasData& license_id); CasStatus HandleAssignLicenseID(const CasData& license_id); + CasStatus HandlePluginVersionQuery(); + CasStatus HandleEntitlementPeriodUpdateResponse(const CasData& response); + // Returns true if the device has been provisioned with a device certificate. bool is_provisioned() const; - // Event listener implementation void OnSessionRenewalNeeded() override; void OnSessionKeysChange(const KeyStatusMap& keys_status, @@ -123,6 +125,10 @@ class WidevineCasPlugin : public CasPlugin, public CasEventListener { void OnSessionServiceBlockingUpdated( const WvCasSessionId& sessionId, const CasData& service_blocking) override; + void OnFingerprintingUpdated(const CasData& fingerprinting) override; + void OnServiceBlockingUpdated(const CasData& service_blocking) override; + void OnEntitlementPeriodUpdateNeeded( + const std::string& signed_license_request) override; // Choose to use |callback_| or |callback_ext_| to send back information. // |sessionId| is ignored if |callback_ext_| is null, @@ -138,7 +144,8 @@ class WidevineCasPlugin : public CasPlugin, public CasEventListener { // Otherwise, first CA descriptor available to the plugin // is used to build a PSSH, and others are discarded. bool is_emm_request_sent_ = false; - std::string provision_data_; + // This is always the serialized PSSH data. + std::string init_data_; std::unique_ptr widevine_cas_api_; }; diff --git a/plugin/src/cas_license.cpp b/plugin/src/cas_license.cpp index 8b88de3..cf9fe8f 100644 --- a/plugin/src/cas_license.cpp +++ b/plugin/src/cas_license.cpp @@ -60,6 +60,7 @@ constexpr char kKeyModelName[] = "model_name"; constexpr char kKeyArchitectureName[] = "architecture_name"; constexpr char kKeyDeviceName[] = "device_name"; constexpr char kKeyProductName[] = "product_name"; +constexpr char kKeyWvCasVersion[] = "widevine_cdm_version"; // TODO(jfore): These variables are currently unused and are flagged as build // errors in android. These values will be used in a future cl. @@ -166,8 +167,6 @@ bool Hash(const std::string& data, std::string* hash) { return true; } -} // namespace - CasStatus GenerateLicenseFile( const std::string& emm_request, const std::string& emm_response, const std::string& renewal_request, const std::string& renewal_response, @@ -199,6 +198,8 @@ CasStatus GenerateLicenseFile( return CasStatusCode::kNoError; } +} // namespace + CasStatus CasLicense::initialize(std::shared_ptr crypto_session, CasEventListener* listener) { if (!crypto_session) { @@ -360,6 +361,11 @@ CasStatus CasLicense::GenerateEntitlementRequest( client_info->set_name(kKeyDeviceName); client_info->set_value(value); } + if (Properties::GetWvCasPluginVersion(value)) { + client_info = client_id->add_client_info(); + client_info->set_name(kKeyWvCasVersion); + client_info->set_value(value); + } ClientIdentification_ClientCapabilities* client_capabilities = client_id->mutable_client_capabilities(); @@ -433,9 +439,8 @@ CasStatus CasLicense::GenerateEntitlementRequest( return CasStatusCode::kNoError; } -CasStatus CasLicense::HandleStoredLicense( - const std::string& wrapped_rsa_key, const std::string& license_file, - const std::string* content_id_filter) { +CasStatus CasLicense::HandleStoredLicense(const std::string& wrapped_rsa_key, + const std::string& license_file) { HashedFile hash_file; if (!hash_file.ParseFromString(license_file)) { return CasStatus(CasStatusCode::kLicenseFileParseError, @@ -502,7 +507,7 @@ CasStatus CasLicense::HandleStoredLicense( status = InstallLicense(signed_message.session_key(), signed_message.msg(), signed_message.oemcrypto_core_message(), - signed_message.signature(), content_id_filter); + signed_message.signature()); if (!status.ok()) { return status; } @@ -524,9 +529,57 @@ CasStatus CasLicense::HandleStoredLicense( return CasStatusCode::kNoError; } +CasStatus CasLicense::GetEntitlementPeriodIndexFromStoredLicense( + const std::string& license_file, uint32_t& entitlement_period_index) { + HashedFile hash_file; + if (!hash_file.ParseFromString(license_file)) { + return CasStatus(CasStatusCode::kLicenseFileParseError, + "unable to parse license file"); + } + std::string file_hash; + if (!Hash(hash_file.file(), &file_hash)) { + return CasStatus(CasStatusCode::kLicenseFileParseError, + "generating file hash fails"); + } + if (hash_file.hash() != file_hash) { + return CasStatus(CasStatusCode::kLicenseFileParseError, + "corrupt license file data"); + } + File file; + if (!file.ParseFromString(hash_file.file())) { + return CasStatus(CasStatusCode::kLicenseFileParseError, + "unable to parse the file data"); + } + if (file.type() != File::LICENSE) { + return CasStatus(CasStatusCode::kLicenseFileParseError, + "invalid file type"); + } + // Get PSSH from license request. + LicenseRequest license_request; + if (!license_request.ParseFromString(file.license().license_request())) { + return CasStatus(CasStatusCode::kLicenseFileParseError, + "invalid license request"); + } + if (license_request.content_id().cenc_id_deprecated().pssh_size() == 0) { + return CasStatus(CasStatusCode::kLicenseFileParseError, "no pssh"); + } + // Only one PSSH should exist in the request. + video_widevine::WidevinePsshData pssh; + if (!pssh.ParseFromString( + license_request.content_id().cenc_id_deprecated().pssh(0))) { + return CasStatus(CasStatusCode::kLicenseFileParseError, "invalid pssh"); + } + + if (!pssh.has_entitlement_period_index()) { + return CasStatus(CasStatusCode::kCasLicenseError, + "no entitlement period index"); + } + entitlement_period_index = pssh.entitlement_period_index(); + return CasStatusCode::kNoError; +} + CasStatus CasLicense::HandleEntitlementResponse( - const std::string& entitlement_response, - const std::string* content_id_filter, std::string* device_file) { + const std::string& entitlement_response, std::string* device_file) { video_widevine::SignedMessage signed_message; if (!signed_message.ParseFromString(entitlement_response)) { return CasStatus(CasStatusCode::kCasLicenseError, @@ -549,10 +602,9 @@ CasStatus CasLicense::HandleEntitlementResponse( "no oemcrypto core message present"); } - CasStatus status = - InstallLicense(signed_message.session_key(), signed_message.msg(), - signed_message.oemcrypto_core_message(), - signed_message.signature(), content_id_filter); + CasStatus status = InstallLicense( + signed_message.session_key(), signed_message.msg(), + signed_message.oemcrypto_core_message(), signed_message.signature()); if (!status.ok()) { return status; } @@ -680,8 +732,7 @@ CasStatus CasLicense::GenerateDeviceProvisioningRequestWithOEMCert() const { CasStatus CasLicense::InstallLicense(const std::string& session_key, const std::string& serialized_license, const std::string& core_message, - const std::string& signature, - const std::string* /*content_id_filter*/) { + const std::string& signature) { video_widevine::License license; if (!license.ParseFromString(serialized_license)) { return CasStatus(CasStatusCode::kCasLicenseError, @@ -728,7 +779,6 @@ CasStatus CasLicense::InstallLicense(const std::string& session_key, mac_key_str.resize(2 * kMacKeySizeBytes); } - // TODO: apply content_id_filter status = crypto_session_->LoadLicense(serialized_license, core_message, signature); if (!status.ok()) { @@ -1019,4 +1069,23 @@ void CasLicense::OnSessionServiceBlockingUpdated( } } +void CasLicense::OnFingerprintingUpdated(const CasData& fingerprinting) { + if (event_listener_ != nullptr) { + event_listener_->OnFingerprintingUpdated(fingerprinting); + } +} + +void CasLicense::OnServiceBlockingUpdated(const CasData& fingerprinting) { + if (event_listener_ != nullptr) { + event_listener_->OnServiceBlockingUpdated(fingerprinting); + } +} + +void CasLicense::OnEntitlementPeriodUpdateNeeded( + const std::string& signed_license_request) { + if (event_listener_ != nullptr) { + event_listener_->OnEntitlementPeriodUpdateNeeded(signed_license_request); + } +} + } // namespace wvcas diff --git a/plugin/src/crypto_session.cpp b/plugin/src/crypto_session.cpp index e337415..f83daa2 100644 --- a/plugin/src/crypto_session.cpp +++ b/plugin/src/crypto_session.cpp @@ -16,18 +16,6 @@ static const uint32_t kExpectedOEMCryptoVersion = 16; namespace wvcas { namespace { - -// Find the offset of the |field| within |message|. Notice that if |field| is -// empty, 0 will be returned as |pos|. -size_t GetOffset(const std::string& message, const std::string& field) { - size_t pos = message.find(field); - if (pos == std::string::npos) { - LOGE("GetOffset : Cannot find offset for %s", field.c_str()); - pos = 0; - } - return pos; -} - OEMCryptoCipherMode CipherModeFromKeyData(CryptoMode key_cipher) { switch (key_cipher) { case CryptoMode::kAesCBC: @@ -342,6 +330,14 @@ OEMCryptoResult CryptoInterface::OEMCrypto_RemoveEntitledKeySession( }); } +OEMCryptoResult CryptoInterface::OEMCrypto_ReassociateEntitledKeySession( + OEMCrypto_SESSION key_sid, OEMCrypto_SESSION oec_sid) { + return lock_->WithOecSessionLock("ReassociateEntitledKeySession", [&] { + return oemcrypto_interface_->OEMCrypto_ReassociateEntitledKeySession( + key_sid, oec_sid); + }); +} + uint32_t CryptoInterface::OEMCrypto_APIVersion() { return lock_->WithOecReadLock("APIVersion", [&] { return oemcrypto_interface_->OEMCrypto_APIVersion(); @@ -1043,6 +1039,24 @@ CasStatus CryptoSession::RemoveEntitledKeySession( return CasStatus::OkStatus(); } +CasStatus CryptoSession::ReassociateEntitledKeySession( + OEMCrypto_SESSION entitled_key_session_id) { + if (!crypto_interface_) { + return CasStatus(CasStatusCode::kCryptoSessionError, + "missing crypto interface"); + } + + OEMCryptoResult result = + crypto_interface_->OEMCrypto_ReassociateEntitledKeySession( + entitled_key_session_id, session_); + if (result != OEMCrypto_SUCCESS) { + std::ostringstream err_string; + err_string << "OEMCrypto_ReassociateEntitledKeySession returned " << result; + return CasStatus(CasStatusCode::kCryptoSessionError, err_string.str()); + } + return CasStatus::OkStatus(); +} + CasStatus CryptoSession::APIVersion(uint32_t* api_version) { if (!crypto_interface_) { return CasStatus(CasStatusCode::kCryptoSessionError, diff --git a/plugin/src/ecm_parser_v3.cpp b/plugin/src/ecm_parser_v3.cpp index 79f65cb..f016d4f 100644 --- a/plugin/src/ecm_parser_v3.cpp +++ b/plugin/src/ecm_parser_v3.cpp @@ -189,6 +189,7 @@ bool EcmParserV3::set_group_id(const std::string& group_id) { if (group_id.empty()) { even_key_data_ = ecm_payload_.even_key_data(); odd_key_data_ = ecm_payload_.odd_key_data(); + return true; } bool found = false; @@ -205,4 +206,16 @@ bool EcmParserV3::set_group_id(const std::string& group_id) { return found; } +bool EcmParserV3::is_entitlement_rotation_enabled() const { + return ecm_payload_.meta_data().has_entitlement_period_index(); +} + +uint32_t EcmParserV3::entitlement_period_index() const { + return ecm_payload_.meta_data().entitlement_period_index(); +} + +uint32_t EcmParserV3::entitlement_rotation_window_left() const { + return ecm_payload_.meta_data().entitlement_rotation_window_left(); +} + } // namespace wvcas diff --git a/plugin/src/oemcrypto_interface.cpp b/plugin/src/oemcrypto_interface.cpp index 01738b4..d7e444a 100644 --- a/plugin/src/oemcrypto_interface.cpp +++ b/plugin/src/oemcrypto_interface.cpp @@ -109,6 +109,8 @@ class OEMCryptoInterface::Impl { OEMCrypto_SESSION oec_session, OEMCrypto_SESSION* key_session); typedef OEMCryptoResult (*RemoveEntitledKeySession_t)( OEMCrypto_SESSION key_session); + typedef OEMCryptoResult (*ReassociateEntitledKeySession_t)( + OEMCrypto_SESSION key_session, OEMCrypto_SESSION oec_session); typedef uint32_t (*APIVersion_t)(); Initialize_t Initialize = nullptr; @@ -138,6 +140,7 @@ class OEMCryptoInterface::Impl { SecurityLevel_t SecurityLevel = nullptr; CreateEntitledKeySession_t CreateEntitledKeySession = nullptr; RemoveEntitledKeySession_t RemoveEntitledKeySession = nullptr; + ReassociateEntitledKeySession_t ReassociateEntitledKeySession = nullptr; APIVersion_t APIVersion = nullptr; private: @@ -181,6 +184,7 @@ class OEMCryptoInterface::Impl { LOAD_SYM(CreateEntitledKeySession); LOAD_SYM(RemoveEntitledKeySession); LOAD_SYM(APIVersion); + LOAD_SYM(ReassociateEntitledKeySession); // Optional methods that may be available. TRY_LOAD_SYM(LoadTestKeybox); @@ -379,6 +383,11 @@ OEMCryptoResult OEMCryptoInterface::OEMCrypto_RemoveEntitledKeySession( return impl_->RemoveEntitledKeySession(key_session); } +OEMCryptoResult OEMCryptoInterface::OEMCrypto_ReassociateEntitledKeySession( + OEMCrypto_SESSION key_session, OEMCrypto_SESSION oec_session) { + return impl_->ReassociateEntitledKeySession(key_session, oec_session); +} + uint32_t OEMCryptoInterface::OEMCrypto_APIVersion() const { return impl_->APIVersion(); } diff --git a/plugin/src/widevine_cas_api.cpp b/plugin/src/widevine_cas_api.cpp index eae4d75..321da4d 100644 --- a/plugin/src/widevine_cas_api.cpp +++ b/plugin/src/widevine_cas_api.cpp @@ -1,7 +1,10 @@ #include "widevine_cas_api.h" +#include #include +#include + #include "cas_events.h" #include "cas_util.h" #include "license_protocol.pb.h" @@ -116,6 +119,23 @@ std::string GenerateGroupLicenseInfo(const std::string& license_id, return message; } +// Generates a random number between 1 and |range_to|, all inclusive. +uint32_t GetRandom(uint32_t range_to) { + if (range_to <= 1) { + return 1; + } + constexpr uint32_t max_val = std::numeric_limits::max(); + + // Keep searching for a random value in a range divisible by |range_to|. + // Worst case we have 1/2 chance to end the loop on each roll. + uint32_t generated; + do { + RAND_bytes(reinterpret_cast(&generated), /*len=*/4); + } while (generated >= (max_val - (max_val % range_to))); + + return 1 + (generated % range_to); +} + } // namespace namespace wvcas { @@ -129,6 +149,13 @@ class MediaContext : public CasMediaId { const std::string content_id() override { return pssh_.content_id(); } const std::string provider_id() override { return pssh_.provider(); } + bool is_entitlement_rotation_enabled() override { + return pssh_.has_entitlement_period_index(); + } + uint32_t entitlement_period_index() override { + return pssh_.entitlement_period_index(); + } + std::string get_init_data() override { return pssh_.SerializeAsString(); } CasStatus initialize(const std::string& init_data) override { if (!pssh_.ParseFromString(init_data)) { @@ -162,6 +189,10 @@ std::shared_ptr WidevineCas::newCasSession() { return std::make_shared(); } +std::unique_ptr WidevineCas::getEcmParser(const CasEcm& ecm) const { + return EcmParser::Create(ecm); +} + void WidevineCas::OnTimerEvent() { std::unique_lock locker(lock_); if (cas_license_.get() != nullptr) { @@ -250,6 +281,60 @@ CasStatus WidevineCas::closeSession(WvCasSessionId sessionId) { return CasStatusCode::kNoError; } +CasStatus WidevineCas::processEmm(const CasEmm& emm) { + LOGI("WidevineCas::processEmm."); + std::unique_ptr emm_parser = getEmmParser(emm); + if (emm_parser == nullptr) { + return CasStatus(CasStatusCode::kInvalidParameter, "Unable to parse emm"); + } + if (event_listener_ == nullptr) { + LOGW("processEmm: Event listener is not initialized."); + return CasStatusCode::kNoError; + } + + // TODO(b/): Verify signature. + // TODO(b/): Update EMM timer. + + const video_widevine::EmmPayload& emm_payload = emm_parser->emm_payload(); + // Process fingerprinting info. + std::set current_fingerprinting_events_; + for (int i = 0; i < emm_payload.fingerprinting_size(); ++i) { + CasData message = + GenerateFingerprintingEventMessage(emm_payload.fingerprinting(i)); + if (message.empty()) { + continue; + } + if (last_fingerprinting_events_.find(message) == + last_fingerprinting_events_.end()) { + event_listener_->OnFingerprintingUpdated(message); + } + current_fingerprinting_events_.insert(message); + } + last_fingerprinting_events_.clear(); + last_fingerprinting_events_.insert(current_fingerprinting_events_.begin(), + current_fingerprinting_events_.end()); + + // Process service blocking info. + std::set current_service_blocking_events_; + for (int i = 0; i < emm_payload.service_blocking_size(); ++i) { + CasData message = + GenerateServiceBlockingEventMessage(emm_payload.service_blocking(i)); + if (message.empty()) { + continue; + } + if (last_service_blocking_events_.find(message) == + last_service_blocking_events_.end()) { + event_listener_->OnServiceBlockingUpdated(message); + } + current_service_blocking_events_.insert(message); + } + last_service_blocking_events_.clear(); + last_service_blocking_events_.insert(current_service_blocking_events_.begin(), + current_service_blocking_events_.end()); + + return CasStatusCode::kNoError; +} + // TODO(jfore): Add unit test to widevine_cas_api_test.cpp that is added in // another cl. CasStatus WidevineCas::processEcm(WvCasSessionId sessionId, const CasEcm& ecm) { @@ -257,6 +342,10 @@ CasStatus WidevineCas::processEcm(WvCasSessionId sessionId, const CasEcm& ecm) { std::unique_lock locker(lock_); // If we don't have a license yet, save the ecm and session id. if (!has_license_) { + // In the case of entitlement key rotation enabled, the caller is expected + // to call processEcm first (before processPrivateData), so we know which + // entitlement period index to request when requesting license. + TryExtractEntitlementPeriodIndex(ecm); deferred_ecms_.emplace(sessionId, ecm); return CasStatusCode::kDeferedEcmProcessing; } @@ -284,6 +373,11 @@ CasStatus WidevineCas::HandleProcessEcm(const WvCasSessionId& sessionId, event_listener_->OnAgeRestrictionUpdated(sessionId, ecm_age_current); } + if (media_id_ != nullptr && media_id_->is_entitlement_rotation_enabled()) { + CheckEntitlementPeriodUpdate(session->GetEntitlementPeriodIndex(), + session->GetEntitlementRotationWindowLeft()); + } + if (status.ok()) { cas_license_->BeginDecryption(); } @@ -344,49 +438,44 @@ CasStatus WidevineCas::generateEntitlementRequest( } std::string filename; - // Backward compatible. If the license_filename is unassigned by app, plugin + // Backward compatible. If the license_filename is unrequested by app, plugin // will directly use the single_content_license named "content_id + // provider_id" by default. - if (assigned_license_id_.empty()) { + if (requested_license_id_.empty()) { filename = GenerateLicenseFilename(media_id_->content_id(), media_id_->provider_id()); } else { - filename = assigned_license_id_ + kLicenseFileNameSuffix; + filename = requested_license_id_ + kLicenseFileNameSuffix; // Clean up the assigned_license_filename for next round use. - assigned_license_id_.clear(); + requested_license_id_.clear(); } - std::string license_file; - if (ReadFileFromStorage(*file_system_, filename, &license_file)) { - status = cas_license_->HandleStoredLicense(wrapped_rsa_key_, license_file, - /*content_id_filter=*/nullptr); - if (status.ok()) { - // If license file is expired, don't proceed the request. Also - // delete the stored license file. - std::unique_lock locker(lock_); - if (cas_license_->IsExpired()) { - if (!RemoveFile(*file_system_, filename)) { - return CasStatus(CasStatusCode::kInvalidLicenseFile, - "unable to remove expired license file from disk"); - } - LOGI("Remove expired license file from disk successfully."); - return CasStatus(CasStatusCode::kCasLicenseError, - "license is expired, unable to process emm"); + // An offline license file is successfully loaded. + if (TryReuseStoredLicense(filename)) { + // If license file is expired, don't proceed the request. Also + // delete the stored license file. + std::unique_lock locker(lock_); + if (cas_license_->IsExpired()) { + if (!RemoveFile(*file_system_, filename)) { + return CasStatus(CasStatusCode::kInvalidLicenseFile, + "unable to remove expired license file from disk"); } - license_id = - filename.substr(0, filename.size() - strlen(kLicenseFileNameSuffix)); - if (cas_license_->IsGroupLicense()) { - license_group_id_ = cas_license_->GetGroupId(); - } - - // Save current in use license_id. The purpose is to make the license_id - // available for license removal or license expiration. - license_id_ = license_id; - policy_timer_.Start(this, 1); - has_license_ = true; - return HandleDeferredECMs(); + LOGI("Remove expired license file from disk successfully."); + return CasStatus(CasStatusCode::kCasLicenseError, + "license is expired, unable to process emm"); } - LOGI("Fallthru"); + license_id = + filename.substr(0, filename.size() - strlen(kLicenseFileNameSuffix)); + if (cas_license_->IsGroupLicense()) { + license_group_id_ = cas_license_->GetGroupId(); + } + + // Save current in use license_id. The purpose is to make the license_id + // available for license removal or license expiration. + license_id_ = license_id; + policy_timer_.Start(this, 1); + has_license_ = true; + return HandleDeferredECMs(); } if (entitlement_request == nullptr) { @@ -412,8 +501,8 @@ CasStatus WidevineCas::handleEntitlementResponse( std::string device_file; std::unique_lock locker(lock_); - CasStatus status = cas_license_->HandleEntitlementResponse( - response, /*content_id_filter=*/nullptr, &device_file); + CasStatus status = + cas_license_->HandleEntitlementResponse(response, &device_file); if (status.ok()) { // A license has been successfully loaded. Load any ecms that may have been // deferred waiting for the license. @@ -493,6 +582,96 @@ CasStatus WidevineCas::handleEntitlementRenewalResponse( return CasStatusCode::kNoError; } +CasStatus WidevineCas::generateEntitlementPeriodUpdateRequest( + const std::string& init_data) { + std::unique_lock locker(lock_); + next_media_id_ = CasMediaId::create(); + CasStatus status = next_media_id_->initialize(init_data); + if (!status.ok()) { + return status; + } + + // Setup a new OEMCrypto session. + next_crypto_session_ = getCryptoSession(); + status = next_crypto_session_->initialize(); + if (!status.ok()) { + LOGE("WidevineCas new oemcrypto session failed: %d", status.status_code()); + return status; + } + // Setup a new CasLicense. + next_cas_license_ = getCasLicense(); + status = next_cas_license_->initialize(next_crypto_session_, event_listener_); + if (!status.ok()) { + LOGE("WidevineCas new license initialize failed: %d", status.status_code()); + return status; + } + + std::string entitlement_request; + status = next_cas_license_->GenerateEntitlementRequest( + init_data, device_certificate_, wrapped_rsa_key_, license_type_, + &entitlement_request); + if (!status.ok()) { + LOGE("WidevineCas generate entitlement request failed: %d", + status.status_code()); + return status; + } + + if (event_listener_ == nullptr) { + LOGE("No event listener"); + return CasStatus(CasStatusCode::kUnknownError, "No event listener"); + } + event_listener_->OnEntitlementPeriodUpdateNeeded(entitlement_request); + return CasStatusCode::kNoError; +} + +CasStatus WidevineCas::handleEntitlementPeriodUpdateResponse( + const std::string& response, std::string& license_id) { + std::unique_lock locker(lock_); + if (next_media_id_ == nullptr || next_crypto_session_ == nullptr || + next_cas_license_ == nullptr) { + return CasStatus(CasStatusCode::kInvalidParameter, + "Must generate entitlement switch request first."); + } + // Install the new license. + std::string device_file; + CasStatus status = + next_cas_license_->HandleEntitlementResponse(response, &device_file); + if (!status.ok()) { + LOGE("WidevineCas install new license failed: %d", status.status_code()); + return status; + } + // License has been successfully installed. Switch to use it across all + // sessions. + for (const auto& session : + WidevineCasSessionMap::instance().GetAllSessions()) { + status = session->resetCryptoSession(next_crypto_session_); + if (!status.ok()) { + // Some of the sessions may have already been reassociated (unlikely to + // happen). Here we continue process ignoring the errors. Some sessions + // will become unusable. + LOGE("resetCryptoSession failed, error %d: %s", status.status_code(), + status.error_string().c_str()); + } + } + // Close the current OEMCrypto session. + crypto_session_->close(); + // Apply the new crypto session and cas license. + crypto_session_ = std::move(next_crypto_session_); + cas_license_ = std::move(next_cas_license_); + media_id_ = std::move(next_media_id_); + + // Store offline license. + if (!device_file.empty()) { + std::string filename = GenerateLicenseFilename(media_id_->content_id(), + media_id_->provider_id()); + StoreFile(*file_system_, filename, device_file); + // license_id will be the filename without ".lic" extension. + license_id = + filename.substr(0, filename.size() - strlen(kLicenseFileNameSuffix)); + } + return status; +} + CasStatus WidevineCas::RemoveLicense(const std::string& file_name) { // Check if the license is in use. If it is, besides removing the license, // update policy in current license. Else, we just directly remove it. @@ -540,7 +719,13 @@ CasStatus WidevineCas::ProcessCAPrivateData(const CasData& private_data, video_widevine::WidevinePsshData pssh; pssh.set_provider(descriptor.provider()); pssh.set_content_id(descriptor.content_id()); + for (int i = 0; i < descriptor.group_ids_size(); ++i) { + pssh.add_group_ids(descriptor.group_ids(i)); + } pssh.set_type(video_widevine::WidevinePsshData::ENTITLEMENT); + if (is_entitlement_rotation_enabled_) { + pssh.set_entitlement_period_index(entitlement_period_index_); + } pssh.SerializeToString(init_data); return CasStatusCode::kNoError; } @@ -578,9 +763,162 @@ CasStatus WidevineCas::HandleSetParentalControlAge(const CasData& data) { } CasStatus WidevineCas::RecordLicenseId(const std::string& license_id) { - assigned_license_id_ = license_id; - LOGI("License id selected is: %s", assigned_license_id_.c_str()); + requested_license_id_ = license_id; + LOGI("License id selected is: %s", requested_license_id_.c_str()); return CasStatusCode::kNoError; } +std::vector WidevineCas::GenerateFingerprintingEventMessage( + const video_widevine::Fingerprinting& fingerprinting) const { + std::vector message; + for (int i = 0; i < fingerprinting.channels_size(); ++i) { + const std::string& channel = fingerprinting.channels(i); + message.push_back( + static_cast(FingerprintingFieldType::FINGERPRINTING_CHANNEL)); + message.push_back((channel.size() >> 8) & 0xff); + message.push_back(channel.size() & 0xff); + message.insert(message.end(), channel.begin(), channel.end()); + } + + if (fingerprinting.has_control()) { + message.push_back( + static_cast(FingerprintingFieldType::FINGERPRINTING_CONTROL)); + const std::string& control = fingerprinting.control(); + message.push_back((control.size() >> 8) & 0xff); + message.push_back(control.size() & 0xff); + message.insert(message.end(), control.begin(), control.end()); + } + + return message; +} + +std::vector WidevineCas::GenerateServiceBlockingEventMessage( + const video_widevine::ServiceBlocking& service_blocking) const { + std::vector message; + // Process service blocking channels. + for (int i = 0; i < service_blocking.channels_size(); ++i) { + const std::string& channel = service_blocking.channels(i); + message.push_back(static_cast( + ServiceBlockingFieldType::SERVICE_BLOCKING_CHANNEL)); + message.push_back((channel.size() >> 8) & 0xff); + message.push_back(channel.size() & 0xff); + message.insert(message.end(), channel.begin(), channel.end()); + } + + // Process service blocking device_groups. + for (int i = 0; i < service_blocking.device_groups_size(); ++i) { + const std::string& device_group = service_blocking.device_groups(i); + message.push_back(static_cast( + ServiceBlockingFieldType::SERVICE_BLOCKING_DEVICE_GROUP)); + message.push_back((device_group.size() >> 8) & 0xff); + message.push_back(device_group.size() & 0xff); + message.insert(message.end(), device_group.begin(), device_group.end()); + } + + // Process service blocking start_time_sec. + if (service_blocking.has_start_time_sec()) { + message.push_back(static_cast( + ServiceBlockingFieldType::SERVICE_BLOCKING_START_TIME_SECONDS)); + // Timestamp is always 8 bytes (64 bits). + message.push_back(0); + message.push_back(8); + for (int i = 0; i < 8; ++i) { + message.push_back((service_blocking.start_time_sec() >> (8 * (7 - i))) & + 0xff); + } + } + + // Process service blocking end_time_sec. + if (service_blocking.has_end_time_sec()) { + message.push_back(static_cast( + ServiceBlockingFieldType::SERVICE_BLOCKING_END_TIME_SECONDS)); + // Timestamp is always 8 bytes (64 bits). + message.push_back(0); + message.push_back(8); + for (int i = 0; i < 8; ++i) { + message.push_back((service_blocking.end_time_sec() >> (8 * (7 - i))) & + 0xff); + } + } + + return message; +} + +std::unique_ptr WidevineCas::getEmmParser( + const CasEmm& emm) const { + return EmmParser::Create(emm); +} + +void WidevineCas::TryExtractEntitlementPeriodIndex(const CasEcm& ecm) { + std::unique_ptr ecm_parser = getEcmParser(ecm); + if (ecm_parser == nullptr) { + LOGE("ECM parser failed for extracting entitlement period index"); + return; + } + if (ecm_parser->is_entitlement_rotation_enabled()) { + is_entitlement_rotation_enabled_ = true; + entitlement_period_index_ = ecm_parser->entitlement_period_index(); + LOGI("Entitlement key rotation enabled. Current index: %d", + entitlement_period_index_); + } +} + +bool WidevineCas::TryReuseStoredLicense(const std::string& filename) { + // Read the file with |filename| from the file system. + std::string license_file; + if (!ReadFileFromStorage(*file_system_, filename, &license_file)) { + return false; + } + + // If entitlement rotation is enabled, check if the entitlement period in the + // license is outdated. + if (media_id_->is_entitlement_rotation_enabled()) { + uint32_t stored_index; + CasStatus status = CasLicense::GetEntitlementPeriodIndexFromStoredLicense( + license_file, stored_index); + if (!status.ok()) { + LOGW( + "Failed to retrieve entitlement period index from stored license. " + "code: %d, message: %s", + status.status_code(), status.error_string().c_str()); + return false; + } + + if (media_id_->entitlement_period_index() != stored_index) { + LOGI("Stored license has mismatch entitlement period index."); + return false; + } + } + + // Load the stored license to the session. + CasStatus status = + cas_license_->HandleStoredLicense(wrapped_rsa_key_, license_file); + if (!status.ok()) { + LOGW("Failed to load stored license. code: %d, message: %s", + status.status_code(), status.error_string().c_str()); + return false; + } + return true; +} + +void WidevineCas::CheckEntitlementPeriodUpdate(uint32_t period_index, + uint32_t window_left) { + if (period_index == media_id_->entitlement_period_index()) { + return; + } + // If the index changed unexpectedly, we request a new license immediately. If + // it is increased by 1, we decide if a new license should be generated based + // on |window_left|. + if (period_index != media_id_->entitlement_period_index() + 1 || + GetRandom(window_left) == 1) { + video_widevine::WidevinePsshData pssh; + if (!pssh.ParseFromString(media_id_->get_init_data())) { + LOGE("Cannot parse init data"); + return; + } + pssh.set_entitlement_period_index(period_index); + generateEntitlementPeriodUpdateRequest(pssh.SerializeAsString()); + } +} + } // namespace wvcas diff --git a/plugin/src/widevine_cas_session.cpp b/plugin/src/widevine_cas_session.cpp index cfd7241..9714f2f 100644 --- a/plugin/src/widevine_cas_session.cpp +++ b/plugin/src/widevine_cas_session.cpp @@ -30,6 +30,7 @@ WidevineCasSession::~WidevineCasSession() { CasStatus WidevineCasSession::initialize( std::shared_ptr crypto_session, CasEventListener* event_listener, uint32_t* session_id) { + std::unique_lock lock(crypto_lock_); if (crypto_session == nullptr || session_id == nullptr) { LOGE("WidevineCasSession::initialize: missing input parameters"); return CasStatus(CasStatusCode::kInvalidParameter, @@ -42,9 +43,21 @@ CasStatus WidevineCasSession::initialize( return CasStatusCode::kNoError; } +CasStatus WidevineCasSession::resetCryptoSession( + std::shared_ptr crypto_session) { + std::unique_lock lock(crypto_lock_); + if (crypto_session == nullptr) { + return CasStatus(CasStatusCode::kInvalidParameter, + "Can not reset crypto session to null"); + } + crypto_session_ = std::move(crypto_session); + return crypto_session_->ReassociateEntitledKeySession(key_session_id_); +} + CasStatus WidevineCasSession::processEcm(const CasEcm& ecm, uint8_t parental_control_age, const std::string& license_group_id) { + std::unique_lock lock(crypto_lock_); if (ecm != current_ecm_) { LOGD("WidevineCasSession::processEcm: received new ecm"); std::unique_ptr ecm_parser = getEcmParser(ecm); @@ -67,7 +80,7 @@ CasStatus WidevineCasSession::processEcm(const CasEcm& ecm, std::vector message; if (!ecm_parser->fingerprinting().control().empty()) { message.push_back(static_cast( - SessionFingerprintingFieldType::FINGERPRINTING_CONTROL)); + SessionFingerprintingFieldType::SESSION_FINGERPRINTING_CONTROL)); const std::string control = ecm_parser->fingerprinting().control(); message.push_back((control.size() >> 8) & 0xff); message.push_back(control.size() & 0xff); @@ -86,8 +99,9 @@ CasStatus WidevineCasSession::processEcm(const CasEcm& ecm, message.clear(); for (int i = 0; i < ecm_parser->service_blocking().device_groups_size(); ++i) { - message.push_back(static_cast( - SessionServiceBlockingFieldType::SERVICE_BLOCKING_DEVICE_GROUP)); + message.push_back( + static_cast(SessionServiceBlockingFieldType:: + SESSION_SERVICE_BLOCKING_DEVICE_GROUP)); const std::string device_group = ecm_parser->service_blocking().device_groups(i); message.push_back((device_group.size() >> 8) & 0xff); @@ -104,10 +118,12 @@ CasStatus WidevineCasSession::processEcm(const CasEcm& ecm, } } + entitlement_period_index_ = ecm_parser->entitlement_period_index(); + entitlement_rotation_window_left_ = + ecm_parser->entitlement_rotation_window_left(); + bool load_even = false; bool load_odd = false; - - std::unique_lock lock(lock_); KeySlotId keyslot_id = KeySlotId::kEvenKeySlot; // Temporary key slots to only have successfully loaded keys in |keys_|. CasKeySlotData keys; diff --git a/plugin/src/widevine_cas_session_map.cpp b/plugin/src/widevine_cas_session_map.cpp index c78c91d..2582b28 100644 --- a/plugin/src/widevine_cas_session_map.cpp +++ b/plugin/src/widevine_cas_session_map.cpp @@ -34,4 +34,13 @@ void WidevineCasSessionMap::RemoveSession(WvCasSessionId cas_session_id) { map_.erase(cas_session_id); } +std::vector WidevineCasSessionMap::GetAllSessions() const { + std::unique_lock lock(lock_); + std::vector sessions; + for (const auto& session : map_) { + sessions.push_back(session.second); + } + return sessions; +} + } // namespace wvcas diff --git a/plugin/src/widevine_media_cas_plugin.cpp b/plugin/src/widevine_media_cas_plugin.cpp index e3a9623..b3eeb41 100644 --- a/plugin/src/widevine_media_cas_plugin.cpp +++ b/plugin/src/widevine_media_cas_plugin.cpp @@ -12,6 +12,7 @@ #include #include "cas_events.h" +#include "cas_properties.h" #include "cas_status.h" #include "cas_types.h" #include "cas_util.h" @@ -72,6 +73,14 @@ status_t WidevineCasPlugin::initialize() { if (!status.ok()) { return android::ERROR_CAS_UNKNOWN; } + + std::string version; + if (Properties::GetWvCasPluginVersion(version)) { + LOGI("Widevine CAS plugin version: %s", version.c_str()); + } else { + LOGW("Failed to get Widevine CAS plugin version."); + } + return OK; } @@ -85,7 +94,6 @@ status_t WidevineCasPlugin::setStatusCallback( return OK; } -// Will be called directly by player starting Android Q, see b/119039060. status_t WidevineCasPlugin::setPrivateData(const CasData& privateData) { // Can get PSSH from multiple streams and from provision call. // Only need to request a license once. @@ -93,12 +101,12 @@ status_t WidevineCasPlugin::setPrivateData(const CasData& privateData) { return OK; } CasStatus status = - widevine_cas_api_->ProcessCAPrivateData(privateData, &provision_data_); + widevine_cas_api_->ProcessCAPrivateData(privateData, &init_data_); if (!status.ok()) { return android::ERROR_CAS_UNKNOWN; } if (widevine_cas_api_->is_provisioned()) { - return requestLicense(provision_data_); + return requestLicense(init_data_); } return OK; } @@ -153,12 +161,12 @@ status_t WidevineCasPlugin::setSessionPrivateData(const CasSessionId& sessionId, // Doesn't matter which session, CA descriptor applies to all of them. WvCasSessionId wv_session_id = androidSessionIdToWidevine(sessionId); CasStatus status = widevine_cas_api_->ProcessSessionCAPrivateData( - wv_session_id, privateData, &provision_data_); + wv_session_id, privateData, &init_data_); if (!status.ok()) { return android::ERROR_CAS_SESSION_NOT_OPENED; } if (widevine_cas_api_->is_provisioned()) { - return requestLicense(provision_data_); + return requestLicense(init_data_); } return OK; } @@ -190,9 +198,12 @@ status_t WidevineCasPlugin::processEcm(const CasSessionId& sessionId, return OK; } -status_t WidevineCasPlugin::processEmm(const CasEmm& /*emm*/) { - // TODO(jfore): This is used for inband emm. Currently unsupported. - return INVALID_OPERATION; +status_t WidevineCasPlugin::processEmm(const CasEmm& emm) { + LOGI("WidevineCasPlugin::processEmm"); + if (!widevine_cas_api_->processEmm(emm).ok()) { + return android::ERROR_CAS_UNKNOWN; + } + return OK; } status_t WidevineCasPlugin::sendEvent(int32_t event, int32_t arg, @@ -225,15 +236,14 @@ status_t WidevineCasPlugin::sendSessionEvent(const CasSessionId& sessionId, status_t WidevineCasPlugin::provision(const String8& provisionString) { // Store |provisionString| for future use. If |provisionString| is not empty // the value takes priority over data in CA descriptor. - provision_data_ = - std::string(provisionString.c_str(), - provisionString.c_str() + provisionString.length()); + init_data_ = std::string(provisionString.c_str(), + provisionString.c_str() + provisionString.length()); if (is_provisioned()) { CallBack(reinterpret_cast(app_data_), INDIVIDUALIZATION_COMPLETE, 0, nullptr, 0, nullptr); - if (!provision_data_.empty()) { - return requestLicense(provision_data_); + if (!init_data_.empty()) { + return requestLicense(init_data_); } return OK; } @@ -307,6 +317,10 @@ CasStatus WidevineCasPlugin::processEvent(int32_t event, int32_t arg, return HandleLicenseRemoval(eventData); case ASSIGN_LICENSE_ID: return HandleAssignLicenseID(eventData); + case QUERY_WV_CAS_PLUGIN_VERSION: + return HandlePluginVersionQuery(); + case LICENSE_ENTITLEMENT_PERIOD_UPDATE_RESPONSE: + return HandleEntitlementPeriodUpdateResponse(eventData); default: return CasStatusCode::kUnknownEvent; } @@ -328,9 +342,9 @@ CasStatus WidevineCasPlugin::HandleIndividualizationResponse( CallBack(reinterpret_cast(app_data_), INDIVIDUALIZATION_COMPLETE, 0, nullptr, 0, nullptr); - if (!provision_data_.empty() && !is_emm_request_sent_) { + if (!init_data_.empty() && !is_emm_request_sent_) { LOGD("Making license request with provisioned PSSH"); - if (requestLicense(provision_data_) != OK) { + if (requestLicense(init_data_) != OK) { return CasStatus(CasStatusCode::kCasLicenseError, "failed to generate license request"); } @@ -389,6 +403,25 @@ CasStatus WidevineCasPlugin::HandleEntitlementRenewalResponse( return CasStatusCode::kNoError; } +CasStatus WidevineCasPlugin::HandleEntitlementPeriodUpdateResponse( + const CasData& response) { + if (response.empty()) { + return CasStatus(CasStatusCode::kCasLicenseError, "empty license response"); + } + std::string resp_string(response.begin(), response.end()); + std::string license_id; + CasStatus status = widevine_cas_api_->handleEntitlementPeriodUpdateResponse( + resp_string, license_id); + if (!status.ok()) { + return status; + } + CallBack( + reinterpret_cast(app_data_), LICENSE_ENTITLEMENT_PERIOD_UPDATED, + LICENSE_ENTITLEMENT_PERIOD_UPDATED, + reinterpret_cast(&license_id[0]), license_id.size(), nullptr); + return CasStatusCode::kNoError; +} + CasStatus WidevineCasPlugin::HandleUniqueIDQuery() { std::string buffer; CasStatus status = widevine_cas_api_->GetUniqueID(&buffer); @@ -447,6 +480,18 @@ CasStatus WidevineCasPlugin::HandleAssignLicenseID( return CasStatusCode::kNoError; } +CasStatus WidevineCasPlugin::HandlePluginVersionQuery() { + std::string version; + if (!Properties::GetWvCasPluginVersion(version)) { + return CasStatus(CasStatusCode::kUnknownError, + "unable to get plugin version"); + } + CallBack(reinterpret_cast(app_data_), WV_CAS_PLUGIN_VERSION, + static_cast(0), reinterpret_cast(&version[0]), + version.size(), /*sessionId=*/nullptr); + return CasStatusCode::kNoError; +} + void WidevineCasPlugin::OnSessionRenewalNeeded() { LOGI("OnSessionRenewalNeeded"); std::string renewal_request; @@ -535,6 +580,40 @@ void WidevineCasPlugin::OnSessionServiceBlockingUpdated( service_blocking.size(), &android_session_id); } +void WidevineCasPlugin::OnFingerprintingUpdated(const CasData& fingerprinting) { + if (fingerprinting.empty()) { + return; + } + LOGI("OnFingerprintingUpdated"); + CallBack(reinterpret_cast(app_data_), FINGERPRINTING_INFO, /*arg=*/0, + const_cast(&fingerprinting[0]), fingerprinting.size(), + nullptr); +} + +void WidevineCasPlugin::OnServiceBlockingUpdated( + const CasData& service_blocking) { + if (service_blocking.empty()) { + return; + } + LOGI("OnServiceBlockingUpdated"); + CallBack(reinterpret_cast(app_data_), SERVICE_BLOCKING_INFO, /*arg=*/0, + const_cast(&service_blocking[0]), service_blocking.size(), + nullptr); +} + +void WidevineCasPlugin::OnEntitlementPeriodUpdateNeeded( + const std::string& signed_license_request) { + LOGI("OnEntitlementPeriodUpdateNeeded"); + if (!signed_license_request.empty()) { + CallBack(reinterpret_cast(app_data_), + LICENSE_ENTITLEMENT_PERIOD_UPDATE_REQUEST, + LICENSE_ENTITLEMENT_PERIOD_UPDATE_REQUEST, + const_cast( + reinterpret_cast(&signed_license_request[0])), + signed_license_request.size(), nullptr); + } +} + void WidevineCasPlugin::CallBack(void* appData, int32_t event, int32_t arg, uint8_t* data, size_t size, const CasSessionId* sessionId) const { diff --git a/protos/license_protocol.proto b/protos/license_protocol.proto index 8657689..3d2a0f0 100644 --- a/protos/license_protocol.proto +++ b/protos/license_protocol.proto @@ -1035,6 +1035,23 @@ message WidevinePsshData { // PSSHs of type ENTITLED_KEY. repeated EntitledKey entitled_keys = 14; + // Video feature identifier, which is used in conjunction with |content_id| + // to determine the set of keys to be returned in the license. Cannot be + // present in conjunction with |key_ids|. + // Current values are "HDR". + optional string video_feature = 15; + + // Audiofeature identifier, which is used in conjunction with |content_id| + // to determine the set of keys to be returned in the license. Cannot be + // present in conjunction with |key_ids|. + // Current values are "commentary". + optional string audio_feature = 16; + + // Entitlement period index for media using entitlement key rotation. Can only + // present in ENTITLEMENT PSSHs. It always corresponds to the entitlement key + // period. + optional uint32 entitlement_period_index = 17; + //////////////////////////// Deprecated Fields //////////////////////////// enum Algorithm { UNENCRYPTED = 0; diff --git a/protos/media_cas.proto b/protos/media_cas.proto index 79de913..716ab85 100644 --- a/protos/media_cas.proto +++ b/protos/media_cas.proto @@ -16,9 +16,11 @@ message CaDescriptorPrivateData { // Content ID. optional bytes content_id = 2; - // Entitlement key IDs for current content per track. Each track will allow up - // to 2 entitlement key ids (odd and even entitlement keys). - repeated bytes entitlement_key_ids = 3; + // Deprecated. + repeated bytes deprecated_entitlement_key_ids = 3; + + // The groups ids this channel belongs to. + repeated bytes group_ids = 4; } // Widevine fingerprinting. @@ -42,6 +44,21 @@ message ServiceBlocking { optional int64 end_time_sec = 4; } +// The payload field for an EMM. +message EmmPayload { + repeated Fingerprinting fingerprinting = 1; + repeated ServiceBlocking service_blocking = 2; + // Epoch time in seconds. The time when the EMM is generated. + optional int64 timestamp_secs = 3; +} + +message SignedEmmPayload { + // Serialized EmmPayload. + optional bytes serialized_payload = 1; + // ECC (Elliptic Curve Cryptography) signature of |serialized_payload|. + optional bytes signature = 2; +} + message EcmMetaData { enum CipherMode { UNSPECIFIED = 0; @@ -57,6 +74,17 @@ message EcmMetaData { // Optional. The minimum age required to watch the content. The value // represents actual age, with 0 means no restriction. optional uint32 age_restriction = 2 [default = 0]; + // If specified, it means entitlement key rotation is enabled. The value will + // be included in the license request. The server is expected to return + // entitlement keys accordingly (e.g., keys for |entitlement_period_index| and + // |entitlement_period_index| + 1). + optional uint32 entitlement_period_index = 3; + // Used only if entitlement key rotation is enabled. This parameter controls + // the probability of requesting a new license by clients upon receiving this + // ECM. The purpose is to spread out requests to avoid request storms. A + // client will request a new license with possibility = 1 / + // |entitlement_rotation_window_left|. + optional uint32 entitlement_rotation_window_left = 4 [default = 1]; } message EcmKeyData { diff --git a/tests/src/cas_license_test.cpp b/tests/src/cas_license_test.cpp index 8507e39..dd25ab7 100644 --- a/tests/src/cas_license_test.cpp +++ b/tests/src/cas_license_test.cpp @@ -17,6 +17,9 @@ #include "mock_crypto_session.h" #include "string_conversions.h" +namespace wvcas { +namespace { + using ::testing::_; using ::testing::AllOf; using ::testing::DoAll; @@ -367,7 +370,6 @@ TEST_F(CasLicenseTest, HandleEntitlementResponse) { EXPECT_CALL(*cas_license.policy_engine_, CanPersist()) .WillOnce(Return(false)); status = cas_license.HandleEntitlementResponse(entitlement_response, - /*content_id_filter=*/nullptr, /*device_file=*/nullptr); EXPECT_EQ(wvcas::CasStatusCode::kNoError, status.status_code()); // Not a group license. @@ -381,16 +383,16 @@ TEST_F(CasLicenseTest, HandleEntitlementResponse) { EXPECT_CALL(*cas_license.policy_engine_, CanPersist()) .WillOnce(Return(false)); std::string device_file; - status = cas_license.HandleEntitlementResponse( - entitlement_response, /*content_id_filter=*/nullptr, &device_file); + status = + cas_license.HandleEntitlementResponse(entitlement_response, &device_file); EXPECT_TRUE(device_file.empty()); EXPECT_EQ(wvcas::CasStatusCode::kNoError, status.status_code()); // Valid with device file and can_persist = true. EXPECT_CALL(*cas_license.policy_engine_, SetLicense(_)); EXPECT_CALL(*cas_license.policy_engine_, CanPersist()).WillOnce(Return(true)); - status = cas_license.HandleEntitlementResponse( - entitlement_response, /*content_id_filter=*/nullptr, &device_file); + status = + cas_license.HandleEntitlementResponse(entitlement_response, &device_file); EXPECT_FALSE(device_file.empty()); EXPECT_EQ(wvcas::CasStatusCode::kNoError, status.status_code()); @@ -596,9 +598,7 @@ TEST_F(CasLicenseTest, RestoreLicense) { EXPECT_CALL(*cas_license.policy_engine_, SetLicense(_)); EXPECT_CALL(*cas_license.policy_engine_, UpdateLicense(_)); EXPECT_EQ(wvcas::CasStatusCode::kNoError, - cas_license - .HandleStoredLicense(wrapped_rsa_key_, license_file_data, - /*content_id_filter=*/nullptr) + cas_license.HandleStoredLicense(wrapped_rsa_key_, license_file_data) .status_code()); } @@ -662,7 +662,6 @@ TEST_F(CasLicenseTest, HandleMultiContentEntitlementResponse) { EXPECT_CALL(*cas_license.policy_engine_, CanPersist()).WillOnce(Return(true)); status = cas_license.HandleEntitlementResponse(entitlement_response, - /*content_id_filter=*/nullptr, /*device_file=*/nullptr); EXPECT_EQ(status.status_code(), wvcas::CasStatusCode::kNoError); // Not a group license. @@ -671,4 +670,103 @@ TEST_F(CasLicenseTest, HandleMultiContentEntitlementResponse) { testing::ElementsAre("content_id_1", "content_id_2")); EXPECT_FALSE(cas_license.IsGroupLicense()); EXPECT_TRUE(cas_license.IsMultiContentLicense()); -} \ No newline at end of file +} + +TEST(GetEntitlementPeriodIndexTest, ValidIndexSuccess) { + uint32_t expected_index = 123; + // Create a valid pssh with entitlement key period index. + video_widevine::WidevinePsshData pssh; + pssh.set_entitlement_period_index(expected_index); + // Create a license request containing the pssh. + video_widevine::LicenseRequest license_request; + license_request.mutable_content_id()->mutable_cenc_id_deprecated()->add_pssh( + pssh.SerializeAsString()); + // Create a license file. + File file; + file.set_type(File::LICENSE); + license_request.SerializeToString( + file.mutable_license()->mutable_license_request()); + // Hash the created file + HashedFile hashed_file; + file.SerializeToString(hashed_file.mutable_file()); + Hash(hashed_file.file(), hashed_file.mutable_hash()); + uint32_t actual_index; + + EXPECT_TRUE(CasLicense::GetEntitlementPeriodIndexFromStoredLicense( + hashed_file.SerializeAsString(), actual_index) + .ok()); + + EXPECT_EQ(actual_index, expected_index); +} + +TEST(GetEntitlementPeriodIndexTest, PsshHasNoIndexFail) { + // Create a valid pssh without entitlement key period index. + video_widevine::WidevinePsshData pssh; + pssh.set_content_id("content_id"); + // Create a license request containing the pssh. + video_widevine::LicenseRequest license_request; + license_request.mutable_content_id()->mutable_cenc_id_deprecated()->add_pssh( + pssh.SerializeAsString()); + // Create a license file. + File file; + file.set_type(File::LICENSE); + license_request.SerializeToString( + file.mutable_license()->mutable_license_request()); + // Hash the created file + HashedFile hashed_file; + file.SerializeToString(hashed_file.mutable_file()); + Hash(hashed_file.file(), hashed_file.mutable_hash()); + uint32_t actual_index; + + EXPECT_FALSE(CasLicense::GetEntitlementPeriodIndexFromStoredLicense( + hashed_file.SerializeAsString(), actual_index) + .ok()); +} + +TEST(GetEntitlementPeriodIndexTest, NoLicenseDataFail) { + File file; + file.set_type(File::LICENSE); + // Hash the created file + HashedFile hashed_file; + file.SerializeToString(hashed_file.mutable_file()); + Hash(hashed_file.file(), hashed_file.mutable_hash()); + uint32_t actual_index; + + EXPECT_FALSE(CasLicense::GetEntitlementPeriodIndexFromStoredLicense( + hashed_file.SerializeAsString(), actual_index) + .ok()); +} + +TEST(GetEntitlementPeriodIndexTest, InvalidHashFail) { + // Create a valid pssh with entitlement key period index. + video_widevine::WidevinePsshData pssh; + pssh.set_entitlement_period_index(123); + // Create a license request containing the pssh. + video_widevine::LicenseRequest license_request; + license_request.mutable_content_id()->mutable_cenc_id_deprecated()->add_pssh( + pssh.SerializeAsString()); + // Create a license file. + File file; + file.set_type(File::LICENSE); + license_request.SerializeToString( + file.mutable_license()->mutable_license_request()); + // Hash the created file + HashedFile hashed_file; + file.SerializeToString(hashed_file.mutable_file()); + hashed_file.set_hash("invalid_hash"); + uint32_t actual_index; + + EXPECT_FALSE(CasLicense::GetEntitlementPeriodIndexFromStoredLicense( + hashed_file.SerializeAsString(), actual_index) + .ok()); +} + +TEST(GetEntitlementPeriodIndexTest, InvalidFileFail) { + uint32_t actual_index; + EXPECT_FALSE(CasLicense::GetEntitlementPeriodIndexFromStoredLicense( + "invalid file", actual_index) + .ok()); +} + +} // namespace +} // namespace wvcas \ No newline at end of file diff --git a/tests/src/ecm_parser_v3_test.cpp b/tests/src/ecm_parser_v3_test.cpp index 4bd7aaa..4432e94 100644 --- a/tests/src/ecm_parser_v3_test.cpp +++ b/tests/src/ecm_parser_v3_test.cpp @@ -403,5 +403,40 @@ TEST(EcmParserV3Test, ParserGroupKeysWithOmittedFieldsSuccess) { EXPECT_EQ(std::string(result.begin(), result.end()), kWrappedKeyIv2); } +TEST(EcmParserV3Test, EntitlementRotationEnabledSuccess) { + const uint32_t entitlement_period_index = 10; + const uint32_t entitlement_rotation_window_left = 100; + EcmPayload ecm_payload; + ecm_payload.mutable_meta_data()->set_entitlement_period_index( + entitlement_period_index); + ecm_payload.mutable_meta_data()->set_entitlement_rotation_window_left( + entitlement_rotation_window_left); + SignedEcmPayload signed_ecm_payload; + signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); + std::vector ecm = GenerateEcm(signed_ecm_payload); + + std::unique_ptr parser = EcmParserV3::Create(ecm); + + ASSERT_TRUE(parser != nullptr); + EXPECT_TRUE(parser->is_entitlement_rotation_enabled()); + EXPECT_EQ(parser->entitlement_period_index(), entitlement_period_index); + EXPECT_EQ(parser->entitlement_rotation_window_left(), + entitlement_rotation_window_left); +} + +TEST(EcmParserV3Test, EntitlementRotationDefaultDisabledSuccess) { + EcmPayload ecm_payload; + // Put something in the payload just to make the ECM valid. + ecm_payload.mutable_even_key_data()->set_entitlement_key_id(kEntitlementId); + SignedEcmPayload signed_ecm_payload; + signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); + std::vector ecm = GenerateEcm(signed_ecm_payload); + + std::unique_ptr parser = EcmParserV3::Create(ecm); + + ASSERT_TRUE(parser != nullptr); + EXPECT_FALSE(parser->is_entitlement_rotation_enabled()); +} + } // namespace } // namespace wvcas diff --git a/tests/src/mediacas_integration_test.cpp b/tests/src/mediacas_integration_test.cpp index 7131033..450dbd7 100644 --- a/tests/src/mediacas_integration_test.cpp +++ b/tests/src/mediacas_integration_test.cpp @@ -1,9 +1,11 @@ // Dynamically generated header created during build. The Runtest entry point is // defined in gopkg_carchive.go -#include "gowvcas_carchive.h" +#include "golang/src/gowvcas_carchive.h" #include "gtest/gtest.h" +extern "C" int RunTest(GoString); + constexpr int kIntegrationTestPassed = 0; // Invokes a test. Tests are named to allow them to be run individually. This @@ -123,3 +125,30 @@ TEST(IntegrationTests, TestSessionEventPassing) { TEST(IntegrationTests, TestProcessEcmV3) { EXPECT_EQ(kIntegrationTestPassed, RunNamedTest("TestProcessEcmV3")); } + +TEST(IntegrationTests, TestGroupLicense) { + EXPECT_EQ(kIntegrationTestPassed, RunNamedTest("TestGroupLicense")); +} + +TEST(IntegrationTests, TestMultiContentLicense) { + EXPECT_EQ(kIntegrationTestPassed, RunNamedTest("TestMultiContentLicense")); +} + +TEST(IntegrationTests, TestAssignGroupLicense) { + EXPECT_EQ(kIntegrationTestPassed, RunNamedTest("TestAssignGroupLicense")); +} + +TEST(IntegrationTests, TestLicenseRequestWithEntitlementPeriodIndex) { + EXPECT_EQ(kIntegrationTestPassed, + RunNamedTest("TestLicenseRequestWithEntitlementPeriodIndex")); +} + +TEST(IntegrationTests, TestOfflineLicenseWithEntitlementPeriodIndex) { + EXPECT_EQ(kIntegrationTestPassed, + RunNamedTest("TestOfflineLicenseWithEntitlementPeriodIndex")); +} + +TEST(IntegrationTests, TestNewLicenseRequestWithOutdatedOfflineLicense) { + EXPECT_EQ(kIntegrationTestPassed, + RunNamedTest("TestNewLicenseRequestWithOutdatedOfflineLicense")); +} diff --git a/tests/src/mock_crypto_session.h b/tests/src/mock_crypto_session.h index c2db725..f5e4c0c 100644 --- a/tests/src/mock_crypto_session.h +++ b/tests/src/mock_crypto_session.h @@ -82,6 +82,8 @@ class MockCryptoSession : public wvcas::CryptoSession { wvcas::CasStatus(uint32_t* entitled_key_session_id)); MOCK_METHOD1(RemoveEntitledKeySession, wvcas::CasStatus(uint32_t entitled_key_session_id)); + MOCK_METHOD(wvcas::CasStatus, ReassociateEntitledKeySession, + (uint32_t entitled_key_session_id)); }; #endif // MOCK_CRYPTO_SESSION_H diff --git a/tests/src/policy_engine_test.cpp b/tests/src/policy_engine_test.cpp index c08be37..9a20aad 100644 --- a/tests/src/policy_engine_test.cpp +++ b/tests/src/policy_engine_test.cpp @@ -9,6 +9,7 @@ #include "license_key_status.h" #include "license_protocol.pb.h" #include "mock_crypto_session.h" +#include "mock_event_listener.h" using ::testing::_; using ::testing::DoAll; @@ -76,31 +77,6 @@ class MockLicenseKeys : public wvcas::LicenseKeys { void(const std::vector& keys)); }; -class MockEventListener : public wvcas::CasEventListener { - public: - MockEventListener() {} - virtual ~MockEventListener() {} - - MOCK_METHOD0(OnSessionRenewalNeeded, void()); - MOCK_METHOD2(OnSessionKeysChange, - void(const KeyStatusMap& keys_status, bool has_new_usable_key)); - MOCK_METHOD1(OnExpirationUpdate, void(int64_t new_expiry_time_seconds)); - MOCK_METHOD1(OnNewRenewalServerUrl, - void(const std::string& renewal_server_url)); - MOCK_METHOD0(OnLicenseExpiration, void()); - MOCK_METHOD2(OnAgeRestrictionUpdated, - void(const wvcas::WvCasSessionId& sessionId, - uint8_t ecm_age_restriction)); - 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)); -}; - class TestablePolicyEngine : public wvcas::PolicyEngine { std::unique_ptr CreateLicenseKeys() override { std::unique_ptr > license_keys = diff --git a/tests/src/test_properties.cpp b/tests/src/test_properties.cpp index 204b18c..53281ac 100644 --- a/tests/src/test_properties.cpp +++ b/tests/src/test_properties.cpp @@ -63,4 +63,9 @@ bool Properties::GetOEMCryptoPath(std::string* path) { return true; } +bool Properties::GetWvCasPluginVersion(std::string& version) { + version = "unit-test"; + return true; +} + } // namespace wvcas diff --git a/tests/src/widevine_cas_api_test.cpp b/tests/src/widevine_cas_api_test.cpp index 1dc2e18..8caf52a 100644 --- a/tests/src/widevine_cas_api_test.cpp +++ b/tests/src/widevine_cas_api_test.cpp @@ -12,11 +12,18 @@ #include "cas_license.h" #include "cas_util.h" +#include "device_files.pb.h" #include "ecm_parser.h" +#include "media_cas.pb.h" #include "mock_crypto_session.h" +#include "mock_ecm_parser.h" +#include "mock_event_listener.h" #include "string_conversions.h" #include "widevine_cas_session_map.h" +namespace wvcas { +namespace { + using ::testing::_; using ::testing::DoAll; using ::testing::NiceMock; @@ -24,8 +31,9 @@ using ::testing::NotNull; using ::testing::Return; using ::testing::SetArgPointee; using ::testing::StrictMock; +using ::video_widevine_client::sdk::File; +using ::video_widevine_client::sdk::HashedFile; -namespace { constexpr char kBasePathPrefix[] = "/data/vendor/mediacas/IDM/widevine/"; constexpr char kLicenseFileNameSuffix[] = ".lic"; @@ -39,7 +47,6 @@ std::string GenerateTestLicenseFileName(const std::string& mocked_file_name) { SHA256(input, mocked_file_name.size(), output); return kBasePathPrefix + wvutil::b2a_hex(hash) + kLicenseFileNameSuffix; } -} // namespace typedef StrictMock StrictMockCryptoSession; @@ -55,20 +62,16 @@ class MockLicense : public wvcas::CasLicense { const std::string& wrapped_rsa_key, wvcas::LicenseType license_type, std::string* signed_license_request)); - MOCK_METHOD3(HandleStoredLicense, - wvcas::CasStatus(const std::string& wrapped_rsa_key, - const std::string& license_file, - const std::string* content_id_filter)); + MOCK_METHOD(wvcas::CasStatus, HandleStoredLicense, + (const std::string&, const std::string&), (override)); MOCK_METHOD2(GenerateEntitlementRenewalRequest, wvcas::CasStatus(const std::string& device_certificate, std::string* signed_renewal_request)); MOCK_METHOD2(HandleEntitlementRenewalResponse, wvcas::CasStatus(const std::string& renewal_response, std::string* device_file)); - MOCK_METHOD3(HandleEntitlementResponse, - wvcas::CasStatus(const std::string& entitlement_response, - const std::string* content_id_filter, - std::string* device_file)); + MOCK_METHOD(wvcas::CasStatus, HandleEntitlementResponse, + (const std::string&, std::string*), (override)); MOCK_METHOD0(BeginDecryption, void()); MOCK_METHOD0(UpdateLicenseForLicenseRemove, void()); MOCK_METHOD(std::string, GetGroupId, (), (const, override)); @@ -78,30 +81,6 @@ class MockLicense : public wvcas::CasLicense { MOCK_METHOD(bool, IsGroupLicense, (), (const, override)); }; typedef StrictMock StrictMockLicense; - -class MockEventListener : public wvcas::CasEventListener { - public: - MockEventListener() {} - ~MockEventListener() override {} - MOCK_METHOD0(OnSessionRenewalNeeded, void()); - MOCK_METHOD2(OnSessionKeysChange, void(const wvcas::KeyStatusMap& keys_status, - bool has_new_usable_key)); - MOCK_METHOD1(OnExpirationUpdate, void(int64_t new_expiry_time_seconds)); - MOCK_METHOD1(OnNewRenewalServerUrl, - void(const std::string& renewal_server_url)); - MOCK_METHOD0(OnLicenseExpiration, void()); - MOCK_METHOD2(OnAgeRestrictionUpdated, - void(const wvcas::WvCasSessionId& sessionId, - uint8_t ecm_age_restriction)); - 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)); -}; typedef StrictMock StrictMockEventListener; class MockFile : public wvutil::File { @@ -153,6 +132,13 @@ class MockWidevineSession : public wvcas::WidevineCasSession { const wvcas::CasEcm& ecm)); }; +class MockEmmParser : public EmmParser { + public: + MOCK_METHOD(uint64_t, timestamp, (), (const, override)); + MOCK_METHOD(std::string, signature, (), (const, override)); + MOCK_METHOD(EmmPayload, emm_payload, (), (const, override)); +}; + class TestWidevineCas : public wvcas::WidevineCas { public: TestWidevineCas() { @@ -169,10 +155,21 @@ class TestWidevineCas : public wvcas::WidevineCas { return std::move(pass_thru_crypto_session_); } + void setCryptoSession( + std::unique_ptr crypto_session) { + pass_thru_crypto_session_ = std::move(crypto_session); + crypto_session_ = pass_thru_crypto_session_.get(); + } + std::unique_ptr getCasLicense() override { return std::move(pass_thru_license_); } + void setCasLicense(std::unique_ptr license) { + pass_thru_license_ = std::move(license); + license_ = pass_thru_license_.get(); + } + std::unique_ptr getFileSystem() override { return std::move(pass_thru_file_system_); } @@ -181,9 +178,22 @@ class TestWidevineCas : public wvcas::WidevineCas { return std::make_shared >(); } + std::unique_ptr getEmmParser( + const wvcas::CasEmm& emm) const override { + return std::move(pass_thru_emm_parser_); + } + + void setEmmParser(std::unique_ptr parser) { + pass_thru_emm_parser_ = std::move(parser); + } + + MOCK_METHOD(std::unique_ptr, getEcmParser, + (const wvcas::CasEcm& ecm), (const override)); + std::unique_ptr pass_thru_license_; std::unique_ptr pass_thru_crypto_session_; std::unique_ptr pass_thru_file_system_; + mutable std::unique_ptr pass_thru_emm_parser_; StrictMockLicense* license_ = nullptr; StrictMockCryptoSession* crypto_session_ = nullptr; @@ -633,8 +643,8 @@ TEST_F(WidevineCasTest, RemoveLicenseInUse) { .status_code(), wvcas::CasStatusCode::kNoError); // Install the license - EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse(_, _, NotNull())) - .WillOnce(DoAll(SetArgPointee<2>("device_file"), + EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse(_, NotNull())) + .WillOnce(DoAll(SetArgPointee<1>("device_file"), Return(wvcas::CasStatus::OkStatus()))); EXPECT_CALL(*cas_api.license_, IsMultiContentLicense) .WillRepeatedly(Return(false)); @@ -676,8 +686,8 @@ TEST_F(WidevineCasTest, handleMultiContentEntitlementResponse) { std::string license_group_id = "license_group_id"; std::vector content_list = {"content1", "content2"}; - EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse(_, _, NotNull())) - .WillOnce(DoAll(SetArgPointee<2>("device_file"), + EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse(_, NotNull())) + .WillOnce(DoAll(SetArgPointee<1>("device_file"), Return(wvcas::CasStatus::OkStatus()))); EXPECT_CALL(*cas_api.license_, IsMultiContentLicense) .WillRepeatedly(Return(true)); @@ -737,8 +747,8 @@ TEST_F(WidevineCasTest, handleGroupEntitlementResponse) { std::string license_group_id = "license_group_id"; std::vector content_list = {}; - EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse(_, _, NotNull())) - .WillOnce(DoAll(SetArgPointee<2>("device_file"), + EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse(_, NotNull())) + .WillOnce(DoAll(SetArgPointee<1>("device_file"), Return(wvcas::CasStatus::OkStatus()))); EXPECT_CALL(*cas_api.license_, IsMultiContentLicense) .WillRepeatedly(Return(false)); @@ -828,4 +838,529 @@ TEST_F(WidevineCasTest, ECMProcessingWithGroupId) { EXPECT_EQ(cas_api.processEcm(sid, ecm).status_code(), wvcas::CasStatusCode::kNoError); EXPECT_CALL(*(cas_api.crypto_session_), RemoveEntitledKeySession(sid)); -} \ No newline at end of file + EXPECT_EQ(wvcas::CasStatusCode::kNoError, + cas_api.closeSession(sid).status_code()); +} + +TEST_F(WidevineCasTest, ProcessCAPrivateData) { + TestWidevineCas cas_api; + video_widevine::CaDescriptorPrivateData private_data; + private_data.set_provider("provider"); + private_data.set_content_id("content_id"); + private_data.add_group_ids("group1"); + private_data.add_group_ids("group2"); + std::string serialized_private_data; + private_data.SerializeToString(&serialized_private_data); + video_widevine::WidevinePsshData expected_pssh; + expected_pssh.set_provider("provider"); + expected_pssh.set_content_id("content_id"); + expected_pssh.add_group_ids("group1"); + expected_pssh.add_group_ids("group2"); + expected_pssh.set_type(video_widevine::WidevinePsshData::ENTITLEMENT); + std::string expected_serialized_pssh; + expected_pssh.SerializeToString(&expected_serialized_pssh); + std::string init_data; + + ASSERT_EQ(cas_api + .ProcessCAPrivateData({serialized_private_data.begin(), + serialized_private_data.end()}, + &init_data) + .status_code(), + wvcas::CasStatusCode::kNoError); + + EXPECT_EQ(init_data, expected_serialized_pssh); +} + +TEST_F(WidevineCasTest, ProcessEmmEmptyEmmPayload) { + TestWidevineCas cas_api; + MockEventListener event_listener; + EXPECT_CALL(*cas_api.crypto_session_, initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_EQ(cas_api.initialize(&event_listener).status_code(), + wvcas::CasStatusCode::kNoError); + + auto emm_parser = make_unique(); + MockEmmParser* parser = emm_parser.get(); + cas_api.setEmmParser(std::move(emm_parser)); + + video_widevine::EmmPayload emm_payload; + EXPECT_CALL(*parser, emm_payload).WillOnce(Return(emm_payload)); + EXPECT_CALL(event_listener, OnFingerprintingUpdated).Times(0); + EXPECT_CALL(event_listener, OnServiceBlockingUpdated).Times(0); + cas_api.processEmm({}); +} + +class WidevineCasProcessEmmTest : public WidevineCasTest { + protected: + void LoadFingerprinting() { + auto fingerprinting_1 = emm_payload_.add_fingerprinting(); + fingerprinting_1->add_channels("CH1"); + fingerprinting_1->add_channels("CH2"); + fingerprinting_1->set_control("control"); + auto fingerprinting_2 = emm_payload_.add_fingerprinting(); + fingerprinting_2->add_channels("1003"); + fingerprinting_2->set_control("off"); + } + + void LoadServiceBlocking() { + auto service_blocking_1 = emm_payload_.add_service_blocking(); + service_blocking_1->add_channels("CH1"); + service_blocking_1->add_channels("CH2"); + service_blocking_1->add_device_groups("g1"); + service_blocking_1->add_device_groups("g2"); + service_blocking_1->set_start_time_sec(0x12345678); + service_blocking_1->set_end_time_sec(0x87654321); + auto service_blocking_2 = emm_payload_.add_service_blocking(); + service_blocking_2->add_channels("CH3"); + service_blocking_2->add_device_groups("100"); + service_blocking_2->set_end_time_sec(0x987654321); + } + + video_widevine::EmmPayload emm_payload_; +}; + +TEST_F(WidevineCasProcessEmmTest, FingerprintingEmm) { + TestWidevineCas cas_api; + MockEventListener event_listener; + EXPECT_CALL(*cas_api.crypto_session_, initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_EQ(cas_api.initialize(&event_listener).status_code(), + wvcas::CasStatusCode::kNoError); + + LoadFingerprinting(); + const std::vector expected_message_1 = { + 0x00, // Type FINGERPRINTING_CHANNEL + 0x00, 0x03, // Length (bytes) + 'C', 'H', '1', // Value (channel) + 0x00, // Type FINGERPRINTING_CHANNEL + 0x00, 0x03, // Length (bytes) + 'C', 'H', '2', // Value (channel) + 0x01, // Type FINGERPRINTING_CONTROL + 0x00, 0x07, // Length (bytes) + 'c', 'o', 'n', 't', 'r', 'o', 'l' // Value (channel) + }; + const std::vector expected_message_2 = { + 0x00, // Type FINGERPRINTING_CHANNEL + 0x00, 0x04, // Length (bytes) + '1', '0', '0', '3', // Value (channel) + 0x01, // Type FINGERPRINTING_CONTROL + 0x00, 0x03, // Length (bytes) + 'o', 'f', 'f' // Value (channel) + }; + + auto emm_parser = make_unique(); + MockEmmParser* parser = emm_parser.get(); + cas_api.setEmmParser(std::move(emm_parser)); + + EXPECT_CALL(*parser, emm_payload).WillOnce(Return(emm_payload_)); + EXPECT_CALL(event_listener, OnServiceBlockingUpdated).Times(0); + EXPECT_CALL(event_listener, OnFingerprintingUpdated(expected_message_1)) + .Times(1); + EXPECT_CALL(event_listener, OnFingerprintingUpdated(expected_message_2)) + .Times(1); + + EXPECT_EQ(cas_api.processEmm({}).status_code(), + wvcas::CasStatusCode::kNoError); +} + +TEST_F(WidevineCasProcessEmmTest, ServiceBlockingEmm) { + TestWidevineCas cas_api; + MockEventListener event_listener; + EXPECT_CALL(*cas_api.crypto_session_, initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_EQ(cas_api.initialize(&event_listener).status_code(), + wvcas::CasStatusCode::kNoError); + + LoadServiceBlocking(); + const std::vector expected_message_1 = { + 0x00, // Type SERVICE_BLOCKING_CHANNEL + 0x00, 0x03, // Length (bytes) + 'C', 'H', '1', // Value + 0x00, // Type SERVICE_BLOCKING_CHANNEL + 0x00, 0x03, // Length (bytes) + 'C', 'H', '2', // Value + 0x01, // Type SERVICE_BLOCKING_DEVICE_GROUP + 0x00, 0x02, // Length (bytes) + 'g', '1', // Value + 0x01, // Type SERVICE_BLOCKING_DEVICE_GROUP + 0x00, 0x02, // Length (bytes) + 'g', '2', // Value + 0x02, // Type SERVICE_BLOCKING_START_TIME_SECONDS + 0x00, 0x08, // Length (bytes) + 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, // Value + 0x03, // Type SERVICE_BLOCKING_END_TIME_SECONDS + 0x00, 0x08, // Length (bytes) + 0x00, 0x00, 0x00, 0x00, 0x87, 0x65, 0x43, 0x21 // Value + }; + const std::vector expected_message_2 = { + 0x00, // Type SERVICE_BLOCKING_CHANNEL + 0x00, 0x03, // Length (bytes) + 'C', 'H', '3', // Value + 0x01, // Type SERVICE_BLOCKING_DEVICE_GROUP + 0x00, 0x03, // Length (bytes) + '1', '0', '0', // Value + 0x03, // Type SERVICE_BLOCKING_END_TIME_SECONDS + 0x00, 0x08, // Length (bytes) + 0x00, 0x00, 0x00, 0x09, 0x87, 0x65, 0x43, 0x21 // Value + }; + + auto emm_parser = make_unique(); + MockEmmParser* parser = emm_parser.get(); + cas_api.setEmmParser(std::move(emm_parser)); + + EXPECT_CALL(*parser, emm_payload).WillOnce(Return(emm_payload_)); + EXPECT_CALL(event_listener, OnFingerprintingUpdated).Times(0); + EXPECT_CALL(event_listener, OnServiceBlockingUpdated(expected_message_1)) + .Times(1); + EXPECT_CALL(event_listener, OnServiceBlockingUpdated(expected_message_2)) + .Times(1); + + EXPECT_EQ(cas_api.processEmm({}).status_code(), + wvcas::CasStatusCode::kNoError); +} + +TEST_F(WidevineCasProcessEmmTest, MultipleSameEmm) { + TestWidevineCas cas_api; + MockEventListener event_listener; + EXPECT_CALL(*cas_api.crypto_session_, initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_EQ(cas_api.initialize(&event_listener).status_code(), + wvcas::CasStatusCode::kNoError); + + LoadFingerprinting(); + LoadServiceBlocking(); + + auto emm_parser = make_unique(); + MockEmmParser* parser = emm_parser.get(); + cas_api.setEmmParser(std::move(emm_parser)); + EXPECT_CALL(*parser, emm_payload).WillOnce(Return(emm_payload_)); + EXPECT_CALL(event_listener, OnServiceBlockingUpdated).Times(2); + EXPECT_CALL(event_listener, OnFingerprintingUpdated).Times(2); + EXPECT_EQ(cas_api.processEmm({}).status_code(), + wvcas::CasStatusCode::kNoError); + + // Calling again will not trigger the events again. + auto emm_parser2 = make_unique(); + MockEmmParser* parser2 = emm_parser2.get(); + cas_api.setEmmParser(std::move(emm_parser2)); + EXPECT_CALL(*parser2, emm_payload).WillOnce(Return(emm_payload_)); + EXPECT_CALL(event_listener, OnServiceBlockingUpdated).Times(0); + EXPECT_CALL(event_listener, OnFingerprintingUpdated).Times(0); + EXPECT_EQ(cas_api.processEmm({}).status_code(), + wvcas::CasStatusCode::kNoError); +} + +TEST_F(WidevineCasProcessEmmTest, MultipleDifferentEmms) { + TestWidevineCas cas_api; + MockEventListener event_listener; + EXPECT_CALL(*cas_api.crypto_session_, initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_EQ(cas_api.initialize(&event_listener).status_code(), + wvcas::CasStatusCode::kNoError); + + LoadFingerprinting(); + LoadServiceBlocking(); + + auto emm_parser = make_unique(); + MockEmmParser* parser = emm_parser.get(); + cas_api.setEmmParser(std::move(emm_parser)); + EXPECT_CALL(*parser, emm_payload).WillOnce(Return(emm_payload_)); + EXPECT_CALL(event_listener, OnServiceBlockingUpdated).Times(2); + EXPECT_CALL(event_listener, OnFingerprintingUpdated).Times(2); + EXPECT_EQ(cas_api.processEmm({}).status_code(), + wvcas::CasStatusCode::kNoError); + + // Change one of the fingerprinting + emm_payload_.mutable_fingerprinting(0)->set_control("changed"); + emm_payload_.mutable_service_blocking(0)->set_end_time_sec(100); + // Calling again will trigger only one event. + auto emm_parser2 = make_unique(); + MockEmmParser* parser2 = emm_parser2.get(); + cas_api.setEmmParser(std::move(emm_parser2)); + EXPECT_CALL(*parser2, emm_payload).WillOnce(Return(emm_payload_)); + EXPECT_CALL(event_listener, OnServiceBlockingUpdated).Times(1); + EXPECT_CALL(event_listener, OnFingerprintingUpdated).Times(1); + EXPECT_EQ(cas_api.processEmm({}).status_code(), + wvcas::CasStatusCode::kNoError); +} + +TEST_F(WidevineCasTest, ProcessCAPrivateDataWithEntitlementPeriodIndex) { + TestWidevineCas cas_api; + EXPECT_CALL(*(cas_api.crypto_session_), initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_EQ(cas_api.initialize(nullptr).status_code(), + wvcas::CasStatusCode::kNoError); + // Set up mock ecm parser. + const uint32_t expected_entitlement_period_index = 10; + auto ecm_parser = make_unique(); + EXPECT_CALL(*ecm_parser, is_entitlement_rotation_enabled) + .WillOnce(Return(true)); + EXPECT_CALL(*ecm_parser, entitlement_period_index) + .WillOnce(Return(expected_entitlement_period_index)); + EXPECT_CALL(cas_api, getEcmParser) + .WillOnce(Return(testing::ByMove(std::move(ecm_parser)))); + // Construct the private data. + std::string private_data_str; + video_widevine::CaDescriptorPrivateData private_data; + private_data.set_provider("provider"); + private_data.set_content_id("content_id"); + private_data.SerializeToString(&private_data_str); + std::string actual_init_data; + + // Process the ECM without license to will extract the entitlement period + // index. + EXPECT_EQ( + cas_api.processEcm(/*sessionId=*/0, /*ecm=*/{1, 2, 3}).status_code(), + wvcas::CasStatusCode::kDeferedEcmProcessing); + EXPECT_EQ(cas_api + .ProcessCAPrivateData( + {private_data_str.begin(), private_data_str.end()}, + &actual_init_data) + .status_code(), + wvcas::CasStatusCode::kNoError); + + video_widevine::WidevinePsshData pssh; + pssh.ParseFromString(actual_init_data); + EXPECT_EQ(pssh.entitlement_period_index(), expected_entitlement_period_index); +} + +bool Hash(const std::string& data, std::string* hash) { + if (!hash) return false; + hash->resize(SHA256_DIGEST_LENGTH); + const unsigned char* input = + reinterpret_cast(data.data()); + unsigned char* output = reinterpret_cast(&(*hash)[0]); + SHA256(input, data.size(), output); + return true; +} + +TEST_F(WidevineCasTest, GenerateEntitlementRequestWithEntitlementPeriodIndex) { + TestWidevineCas cas_api; + EXPECT_CALL(*(cas_api.crypto_session_), initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_EQ(cas_api.initialize(nullptr).status_code(), + wvcas::CasStatusCode::kNoError); + // pssh is the init data input to generateEntitlementRequest(). + video_widevine::WidevinePsshData pssh; + pssh.set_provider("provider"); + pssh.set_content_id("content_id"); + pssh.set_type(video_widevine::WidevinePsshData::ENTITLEMENT); + pssh.set_entitlement_period_index(123); + // Create a license request containing the pssh. + video_widevine::LicenseRequest license_request; + license_request.mutable_content_id()->mutable_cenc_id_deprecated()->add_pssh( + pssh.SerializeAsString()); + // Create a license file. + File file; + file.set_type(File::LICENSE); + license_request.SerializeToString( + file.mutable_license()->mutable_license_request()); + // Hash the created file + HashedFile hashed_file; + file.SerializeToString(hashed_file.mutable_file()); + Hash(hashed_file.file(), hashed_file.mutable_hash()); + const std::string license_file = hashed_file.SerializeAsString(); + + // Call to Open will return a unique_ptr, freeing this mock_file object. + MockFile* mock_file = new MockFile(); + EXPECT_CALL(*cas_api.file_system_, Exists(_)).WillRepeatedly(Return(true)); + EXPECT_CALL(*cas_api.file_system_, FileSize(_)) + .WillRepeatedly(Return(license_file.size())); + EXPECT_CALL(*cas_api.file_system_, DoOpen(_, _)) + .WillRepeatedly(Return(mock_file)); + EXPECT_CALL(*mock_file, Read(NotNull(), license_file.size())) + .WillRepeatedly([&](char* buffer, size_t bytes) { + memcpy(buffer, license_file.data(), bytes); + return bytes; + }); + EXPECT_CALL(*cas_api.license_, HandleStoredLicense) + .WillRepeatedly(Return(wvcas::CasStatus::OkStatus())); + EXPECT_CALL(*cas_api.license_, IsExpired()).WillRepeatedly(Return(false)); + EXPECT_CALL(*cas_api.license_, IsGroupLicense()) + .WillRepeatedly(Return(false)); + // A new license request will not be generated. + EXPECT_CALL(*cas_api.license_, GenerateEntitlementRequest).Times(0); + std::string entitlement_request; + std::string license_id; + + EXPECT_TRUE(cas_api + .generateEntitlementRequest(pssh.SerializeAsString(), + &entitlement_request, license_id) + .ok()); + + EXPECT_TRUE(entitlement_request.empty()); + EXPECT_FALSE(license_id.empty()); +} + +TEST_F(WidevineCasTest, + GenerateEntitlementRequestWithOutdatedEntitlementPeriodIndex) { + TestWidevineCas cas_api; + EXPECT_CALL(*(cas_api.crypto_session_), initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_EQ(cas_api.initialize(nullptr).status_code(), + wvcas::CasStatusCode::kNoError); + video_widevine::WidevinePsshData pssh; + pssh.set_provider("provider"); + pssh.set_content_id("content_id"); + pssh.set_type(video_widevine::WidevinePsshData::ENTITLEMENT); + pssh.set_entitlement_period_index(123); + video_widevine::WidevinePsshData stored_pssh; + stored_pssh.set_provider("provider"); + stored_pssh.set_content_id("content_id"); + stored_pssh.set_type(video_widevine::WidevinePsshData::ENTITLEMENT); + stored_pssh.set_entitlement_period_index( + 122); // Not equal to 123. + // Create a license request containing the pssh. + video_widevine::LicenseRequest license_request; + license_request.mutable_content_id()->mutable_cenc_id_deprecated()->add_pssh( + stored_pssh.SerializeAsString()); + // Create a license file. + File file; + file.set_type(File::LICENSE); + license_request.SerializeToString( + file.mutable_license()->mutable_license_request()); + // Hash the created file + HashedFile hashed_file; + file.SerializeToString(hashed_file.mutable_file()); + const std::string license_file = hashed_file.SerializeAsString(); + + // Call to Open will return a unique_ptr, freeing this mock_file object. + MockFile* mock_file = new MockFile(); + EXPECT_CALL(*cas_api.file_system_, Exists(_)).WillRepeatedly(Return(true)); + EXPECT_CALL(*cas_api.file_system_, FileSize(_)) + .WillRepeatedly(Return(license_file.size())); + EXPECT_CALL(*cas_api.file_system_, DoOpen(_, _)) + .WillRepeatedly(Return(mock_file)); + EXPECT_CALL(*mock_file, Read(NotNull(), license_file.size())) + .WillRepeatedly([&](char* buffer, size_t bytes) { + memcpy(buffer, license_file.data(), bytes); + return bytes; + }); + EXPECT_CALL(*cas_api.license_, HandleStoredLicense) + .WillRepeatedly(Return(wvcas::CasStatus::OkStatus())); + EXPECT_CALL(*cas_api.license_, IsExpired()).WillRepeatedly(Return(false)); + EXPECT_CALL(*cas_api.license_, IsGroupLicense()) + .WillRepeatedly(Return(false)); + std::string expected_request = "entitlement_request"; + EXPECT_CALL(*cas_api.license_, + GenerateEntitlementRequest(_, _, _, _, NotNull())) + .Times(1) + .WillOnce(DoAll(SetArgPointee<4>(expected_request), + Return(wvcas::CasStatus::OkStatus()))); + std::string entitlement_request; + std::string license_id; + + EXPECT_TRUE(cas_api + .generateEntitlementRequest(pssh.SerializeAsString(), + &entitlement_request, license_id) + .ok()); + + // A license request is generated. + EXPECT_EQ(entitlement_request, expected_request); + EXPECT_TRUE(license_id.empty()); +} + +TEST_F(WidevineCasTest, generateEntitlementPeriodUpdateRequestSuccess) { + TestWidevineCas cas_api; + EXPECT_CALL(*cas_api.crypto_session_, initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_EQ(cas_api.initialize(&event_listener_).status_code(), + wvcas::CasStatusCode::kNoError); + // Prepare for second call for initializing crypto session and cas license. + auto second_license_pass_through = make_unique(); + StrictMockLicense* second_license = second_license_pass_through.get(); + cas_api.setCasLicense(std::move(second_license_pass_through)); + auto second_crypto_pass_through = make_unique(); + StrictMockCryptoSession* second_crypto = second_crypto_pass_through.get(); + cas_api.setCryptoSession(std::move(second_crypto_pass_through)); + EXPECT_CALL(*second_crypto, initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_CALL(*second_license, GenerateEntitlementRequest) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_CALL(event_listener_, OnEntitlementPeriodUpdateNeeded).Times(1); + std::string init_data; + + EXPECT_EQ( + cas_api.generateEntitlementPeriodUpdateRequest(init_data).status_code(), + wvcas::CasStatusCode::kNoError); +} + +TEST_F(WidevineCasTest, + handleEntitlementPeriodUpdateResponseWithoutRequestFail) { + TestWidevineCas cas_api; + EXPECT_CALL(*cas_api.crypto_session_, initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_EQ(cas_api.initialize(nullptr).status_code(), + wvcas::CasStatusCode::kNoError); + std::string response, license_id; + + EXPECT_EQ(cas_api.handleEntitlementPeriodUpdateResponse(response, license_id) + .status_code(), + wvcas::CasStatusCode::kInvalidParameter); +} + +TEST_F(WidevineCasTest, handleEntitlementPeriodUpdateResponseSuccess) { + TestWidevineCas cas_api; + auto first_license_pass_through = make_unique(); + cas_api.setCasLicense(std::move(first_license_pass_through)); + auto first_crypto_pass_through = make_unique(); + StrictMockCryptoSession* first_crypto = first_crypto_pass_through.get(); + cas_api.setCryptoSession(std::move(first_crypto_pass_through)); + EXPECT_CALL(*first_crypto, initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_EQ(cas_api.initialize(&event_listener_).status_code(), + wvcas::CasStatusCode::kNoError); + // Set up two sessions. + wvcas::WvCasSessionId session_id_1; + wvcas::WvCasSessionId session_id_2; + EXPECT_CALL(*first_crypto, CreateEntitledKeySession(NotNull())) + .Times(2) + .WillOnce( + DoAll(SetArgPointee<0>(3), Return(wvcas::CasStatus::OkStatus()))) + .WillOnce( + DoAll(SetArgPointee<0>(4), Return(wvcas::CasStatus::OkStatus()))); + EXPECT_EQ(cas_api.openSession(&session_id_1).status_code(), + wvcas::CasStatusCode::kNoError); + EXPECT_EQ(cas_api.openSession(&session_id_2).status_code(), + wvcas::CasStatusCode::kNoError); + // Prepare for second call for initializing crypto session and cas license. + auto second_license_pass_through = make_unique(); + StrictMockLicense* second_license = second_license_pass_through.get(); + cas_api.setCasLicense(std::move(second_license_pass_through)); + auto second_crypto_pass_through = make_unique(); + StrictMockCryptoSession* second_crypto = second_crypto_pass_through.get(); + cas_api.setCryptoSession(std::move(second_crypto_pass_through)); + EXPECT_CALL(*second_crypto, initialize()) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_CALL(*second_license, GenerateEntitlementRequest) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_CALL(event_listener_, OnEntitlementPeriodUpdateNeeded).Times(1); + std::string init_data; + // Generate switch request. + EXPECT_EQ( + cas_api.generateEntitlementPeriodUpdateRequest(init_data).status_code(), + wvcas::CasStatusCode::kNoError); + std::string response, license_id; + EXPECT_CALL(*second_license, HandleEntitlementResponse) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + // Sessions will be reassociated. + EXPECT_CALL(*second_crypto, ReassociateEntitledKeySession(session_id_1)) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_CALL(*second_crypto, ReassociateEntitledKeySession(session_id_2)) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_CALL(*first_crypto, close) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + + EXPECT_EQ(cas_api.handleEntitlementPeriodUpdateResponse(response, license_id) + .status_code(), + wvcas::CasStatusCode::kNoError); + + EXPECT_CALL(*(cas_api.crypto_session_), RemoveEntitledKeySession).Times(2); + EXPECT_EQ(wvcas::CasStatusCode::kNoError, + cas_api.closeSession(session_id_1).status_code()); + EXPECT_EQ(wvcas::CasStatusCode::kNoError, + cas_api.closeSession(session_id_2).status_code()); +} + +} // namespace +} // namespace wvcas diff --git a/tests/src/widevine_cas_session_test.cpp b/tests/src/widevine_cas_session_test.cpp index f8ff5b5..7d969b3 100644 --- a/tests/src/widevine_cas_session_test.cpp +++ b/tests/src/widevine_cas_session_test.cpp @@ -16,6 +16,8 @@ #include "cas_util.h" #include "media_cas.pb.h" #include "mock_crypto_session.h" +#include "mock_ecm_parser.h" +#include "mock_event_listener.h" #include "string_conversions.h" namespace { @@ -112,29 +114,6 @@ MATCHER(IsValidKeyOddSlotData, "") { return true; } -class MockEcmParser : public wvcas::EcmParser { - public: - MOCK_CONST_METHOD0(version, uint8_t()); - MOCK_CONST_METHOD0(age_restriction, uint8_t()); - MOCK_CONST_METHOD0(crypto_mode, wvcas::CryptoMode()); - MOCK_CONST_METHOD0(rotation_enabled, bool()); - MOCK_CONST_METHOD0(content_iv_size, size_t()); - MOCK_CONST_METHOD1(entitlement_key_id, - std::vector(wvcas::KeySlotId id)); - MOCK_CONST_METHOD1(content_key_id, std::vector(wvcas::KeySlotId id)); - MOCK_CONST_METHOD1(wrapped_key_data, - std::vector(wvcas::KeySlotId id)); - MOCK_CONST_METHOD1(wrapped_key_iv, std::vector(wvcas::KeySlotId id)); - MOCK_CONST_METHOD1(content_iv, std::vector(wvcas::KeySlotId id)); - MOCK_METHOD(bool, set_group_id, (const std::string& group_id), (override)); - MOCK_CONST_METHOD0(has_fingerprinting, bool()); - MOCK_CONST_METHOD0(fingerprinting, video_widevine::Fingerprinting()); - MOCK_CONST_METHOD0(has_service_blocking, bool()); - MOCK_CONST_METHOD0(service_blocking, video_widevine::ServiceBlocking()); - MOCK_CONST_METHOD0(ecm_serialized_payload, std::string()); - MOCK_CONST_METHOD0(signature, std::string()); -}; - class CasSessionTest : public ::testing::Test { public: CasSessionTest() {} @@ -318,30 +297,6 @@ TEST_F(CasSessionTest, parentalControl) { EXPECT_CALL(*mock, RemoveEntitledKeySession(session_id)); } -class MockEventListener : public wvcas::CasEventListener { - public: - MockEventListener() {} - ~MockEventListener() override {} - MOCK_METHOD0(OnSessionRenewalNeeded, void()); - MOCK_METHOD2(OnSessionKeysChange, void(const wvcas::KeyStatusMap& keys_status, - bool has_new_usable_key)); - MOCK_METHOD1(OnExpirationUpdate, void(int64_t new_expiry_time_seconds)); - MOCK_METHOD1(OnNewRenewalServerUrl, - void(const std::string& renewal_server_url)); - MOCK_METHOD0(OnLicenseExpiration, void()); - MOCK_METHOD2(OnAgeRestrictionUpdated, - void(const wvcas::WvCasSessionId& sessionId, - uint8_t ecm_age_restriction)); - 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)); -}; - TEST_F(CasSessionTest, FingerprintingSuccess) { TestCasSession session; auto mock_crypto = std::make_shared(); diff --git a/tests/src/widevine_media_cas_plugin_test.cpp b/tests/src/widevine_media_cas_plugin_test.cpp index e2dc5fa..888f90c 100644 --- a/tests/src/widevine_media_cas_plugin_test.cpp +++ b/tests/src/widevine_media_cas_plugin_test.cpp @@ -209,5 +209,17 @@ TEST(WidevineCasPluginTest, HandleEntitlementResponseEmptyResponseFail) { android::OK); } +TEST(WidevineCasPluginTest, HandlePluginVersionQuerySuccess) { + TestWidevineCasPlugin plugin; + std::string expected_version = "uint-test"; + EXPECT_CALL(plugin, CallBack(_, WV_CAS_PLUGIN_VERSION, _, NotNull(), + expected_version.size(), _)) + .Times(1); + + EXPECT_EQ(plugin.sendEvent(QUERY_WV_CAS_PLUGIN_VERSION, /*arg=*/0, + /*eventData=*/{}), + android::OK); +} + } // namespace } // namespace wvcas \ No newline at end of file diff --git a/wvutil/include/cas_properties.h b/wvutil/include/cas_properties.h index 1312640..202f199 100644 --- a/wvutil/include/cas_properties.h +++ b/wvutil/include/cas_properties.h @@ -36,6 +36,9 @@ class Properties { // Returned path could be either absolute or relative. // Returns false if unable to set the value. static bool GetOEMCryptoPath(std::string* path); + // Sets |version| to Widevine CAS plugin version. Returns false if unable to + // set the value. + static bool GetWvCasPluginVersion(std::string& version); }; } // namespace wvcas diff --git a/wvutil/src/android_properties.cpp b/wvutil/src/android_properties.cpp index f8d86d8..6aa5a57 100644 --- a/wvutil/src/android_properties.cpp +++ b/wvutil/src/android_properties.cpp @@ -2,13 +2,16 @@ // source code may only be used and distributed under the Widevine Master // License Agreement. +#include + #include "cas_properties.h" #include "log.h" -#include - namespace { +// Version format: OEMCrypto_major.OEMCrypto_minor.Plugin_version +constexpr char kWvCasPluginVersion[] = "16.4.1"; + bool GetAndroidProperty(const char* key, std::string* value) { char val[PROPERTY_VALUE_MAX]; if (!key) { @@ -77,4 +80,9 @@ bool Properties::GetOEMCryptoPath(std::string* path) { return true; } +bool Properties::GetWvCasPluginVersion(std::string& version) { + version = kWvCasPluginVersion; + return true; +} + } // namespace wvcas