Cas Client repo update-2.

-Parse EMM in Cas plugin
-Entitlement key rotation support
-Multi_content_license support
This commit is contained in:
huihli
2021-06-28 11:34:11 -07:00
parent 4e4f8c468f
commit 065ca035c9
42 changed files with 1748 additions and 234 deletions

View File

@@ -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

View File

@@ -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<std::mutex> 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<std::mutex> lock(session_table_lock_);
if (entitled_key_session_table_ == nullptr) {

View File

@@ -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(); }

View File

@@ -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<std::mutex> 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<std::mutex> lock(session_lock_);
if (key_session_to_oec_session_.find(key_sid) ==

View File

@@ -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);

View File

@@ -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

View File

@@ -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());

View File

@@ -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",

View File

@@ -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.

View File

@@ -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);

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View File

@@ -7,13 +7,17 @@
#include <memory>
#include <mutex>
#include <set>
#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 sessions 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<CryptoSession> getCryptoSession();
virtual std::unique_ptr<CasLicense> getCasLicense();
virtual std::unique_ptr<wvutil::FileSystem> getFileSystem();
virtual std::shared_ptr<WidevineCasSession> newCasSession();
virtual std::unique_ptr<EcmParser> getEcmParser(const CasEcm& ecm) const;
// Creates an EmmParser. Marked as virtual for easier unit test.
virtual std::unique_ptr<const EmmParser> getEmmParser(
const CasEmm& emm) const;
std::vector<uint8_t> GenerateFingerprintingEventMessage(
const video_widevine::Fingerprinting& fingerprinting) const;
std::vector<uint8_t> 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<CasData> last_fingerprinting_events_;
// Service blocking events sent in processing last ECM/EMM. Used to avoid
// sending a same event again.
std::set<CasData> 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<CryptoSession> next_crypto_session_;
std::unique_ptr<CasLicense> next_cas_license_;
std::unique_ptr<CasMediaId> next_media_id_;
}; // namespace wvcas
} // namespace wvcas

View File

@@ -49,6 +49,8 @@ class WidevineCasSession {
CasStatus initialize(std::shared_ptr<CryptoSession> crypto_session,
CasEventListener* event_listener, uint32_t* session_id);
CasStatus resetCryptoSession(std::shared_ptr<CryptoSession> 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<CryptoSession> 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<uint8_t> 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

View File

@@ -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<CasSessionPtr> GetAllSessions() const;
// Returns a reference to the map.
static WidevineCasSessionMap& instance();

View File

@@ -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<WidevineCas> widevine_cas_api_;
};

View File

@@ -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<CryptoSession> 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

View File

@@ -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,

View File

@@ -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

View File

@@ -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();
}

View File

@@ -1,7 +1,10 @@
#include "widevine_cas_api.h"
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <limits>
#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<uint32_t>::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<uint8_t*>(&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<WidevineCasSession> WidevineCas::newCasSession() {
return std::make_shared<WidevineCasSession>();
}
std::unique_ptr<EcmParser> WidevineCas::getEcmParser(const CasEcm& ecm) const {
return EcmParser::Create(ecm);
}
void WidevineCas::OnTimerEvent() {
std::unique_lock<std::mutex> 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<const EmmParser> 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<CasData> 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<CasData> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<uint8_t> WidevineCas::GenerateFingerprintingEventMessage(
const video_widevine::Fingerprinting& fingerprinting) const {
std::vector<uint8_t> message;
for (int i = 0; i < fingerprinting.channels_size(); ++i) {
const std::string& channel = fingerprinting.channels(i);
message.push_back(
static_cast<uint8_t>(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<uint8_t>(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<uint8_t> WidevineCas::GenerateServiceBlockingEventMessage(
const video_widevine::ServiceBlocking& service_blocking) const {
std::vector<uint8_t> 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<uint8_t>(
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<uint8_t>(
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<uint8_t>(
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<uint8_t>(
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<const EmmParser> WidevineCas::getEmmParser(
const CasEmm& emm) const {
return EmmParser::Create(emm);
}
void WidevineCas::TryExtractEntitlementPeriodIndex(const CasEcm& ecm) {
std::unique_ptr<EcmParser> 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

View File

@@ -30,6 +30,7 @@ WidevineCasSession::~WidevineCasSession() {
CasStatus WidevineCasSession::initialize(
std::shared_ptr<CryptoSession> crypto_session,
CasEventListener* event_listener, uint32_t* session_id) {
std::unique_lock<std::mutex> 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<CryptoSession> crypto_session) {
std::unique_lock<std::mutex> 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<std::mutex> lock(crypto_lock_);
if (ecm != current_ecm_) {
LOGD("WidevineCasSession::processEcm: received new ecm");
std::unique_ptr<EcmParser> ecm_parser = getEcmParser(ecm);
@@ -67,7 +80,7 @@ CasStatus WidevineCasSession::processEcm(const CasEcm& ecm,
std::vector<uint8_t> message;
if (!ecm_parser->fingerprinting().control().empty()) {
message.push_back(static_cast<uint8_t>(
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<uint8_t>(
SessionServiceBlockingFieldType::SERVICE_BLOCKING_DEVICE_GROUP));
message.push_back(
static_cast<uint8_t>(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<std::mutex> lock(lock_);
KeySlotId keyslot_id = KeySlotId::kEvenKeySlot;
// Temporary key slots to only have successfully loaded keys in |keys_|.
CasKeySlotData keys;

View File

@@ -34,4 +34,13 @@ void WidevineCasSessionMap::RemoveSession(WvCasSessionId cas_session_id) {
map_.erase(cas_session_id);
}
std::vector<CasSessionPtr> WidevineCasSessionMap::GetAllSessions() const {
std::unique_lock<std::mutex> lock(lock_);
std::vector<CasSessionPtr> sessions;
for (const auto& session : map_) {
sessions.push_back(session.second);
}
return sessions;
}
} // namespace wvcas

View File

@@ -12,6 +12,7 @@
#include <memory>
#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<void*>(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<void*>(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<void*>(app_data_), LICENSE_ENTITLEMENT_PERIOD_UPDATED,
LICENSE_ENTITLEMENT_PERIOD_UPDATED,
reinterpret_cast<uint8_t*>(&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<void*>(app_data_), WV_CAS_PLUGIN_VERSION,
static_cast<int32_t>(0), reinterpret_cast<uint8_t*>(&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<void*>(app_data_), FINGERPRINTING_INFO, /*arg=*/0,
const_cast<uint8_t*>(&fingerprinting[0]), fingerprinting.size(),
nullptr);
}
void WidevineCasPlugin::OnServiceBlockingUpdated(
const CasData& service_blocking) {
if (service_blocking.empty()) {
return;
}
LOGI("OnServiceBlockingUpdated");
CallBack(reinterpret_cast<void*>(app_data_), SERVICE_BLOCKING_INFO, /*arg=*/0,
const_cast<uint8_t*>(&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<void*>(app_data_),
LICENSE_ENTITLEMENT_PERIOD_UPDATE_REQUEST,
LICENSE_ENTITLEMENT_PERIOD_UPDATE_REQUEST,
const_cast<uint8_t*>(
reinterpret_cast<const uint8_t*>(&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 {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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());
}
}
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

View File

@@ -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<uint8_t> ecm = GenerateEcm(signed_ecm_payload);
std::unique_ptr<EcmParserV3> 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<uint8_t> ecm = GenerateEcm(signed_ecm_payload);
std::unique_ptr<EcmParserV3> parser = EcmParserV3::Create(ecm);
ASSERT_TRUE(parser != nullptr);
EXPECT_FALSE(parser->is_entitlement_rotation_enabled());
}
} // namespace
} // namespace wvcas

View File

@@ -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"));
}

View File

@@ -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

View File

@@ -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<WidevinePsshData_EntitledKey>& 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<uint8_t>& fingerprinting),
(override));
MOCK_METHOD(void, OnSessionServiceBlockingUpdated,
(const int32_t& sessionId,
const std::vector<uint8_t>& service_blocking),
(override));
};
class TestablePolicyEngine : public wvcas::PolicyEngine {
std::unique_ptr<wvcas::LicenseKeys> CreateLicenseKeys() override {
std::unique_ptr<StrictMock<MockLicenseKeys> > license_keys =

View File

@@ -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

View File

@@ -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<MockCryptoSession> 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<MockLicense> 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<uint8_t>& fingerprinting),
(override));
MOCK_METHOD(void, OnSessionServiceBlockingUpdated,
(const int32_t& sessionId,
const std::vector<uint8_t>& service_blocking),
(override));
};
typedef StrictMock<MockEventListener> 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<StrictMockCryptoSession> crypto_session) {
pass_thru_crypto_session_ = std::move(crypto_session);
crypto_session_ = pass_thru_crypto_session_.get();
}
std::unique_ptr<wvcas::CasLicense> getCasLicense() override {
return std::move(pass_thru_license_);
}
void setCasLicense(std::unique_ptr<StrictMockLicense> license) {
pass_thru_license_ = std::move(license);
license_ = pass_thru_license_.get();
}
std::unique_ptr<wvutil::FileSystem> getFileSystem() override {
return std::move(pass_thru_file_system_);
}
@@ -181,9 +178,22 @@ class TestWidevineCas : public wvcas::WidevineCas {
return std::make_shared<StrictMock<MockWidevineSession> >();
}
std::unique_ptr<const EmmParser> getEmmParser(
const wvcas::CasEmm& emm) const override {
return std::move(pass_thru_emm_parser_);
}
void setEmmParser(std::unique_ptr<MockEmmParser> parser) {
pass_thru_emm_parser_ = std::move(parser);
}
MOCK_METHOD(std::unique_ptr<wvcas::EcmParser>, getEcmParser,
(const wvcas::CasEcm& ecm), (const override));
std::unique_ptr<StrictMockLicense> pass_thru_license_;
std::unique_ptr<StrictMockCryptoSession> pass_thru_crypto_session_;
std::unique_ptr<NiceMockFileSystem> pass_thru_file_system_;
mutable std::unique_ptr<MockEmmParser> 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<std::string> 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<std::string> 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));
}
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>();
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<uint8_t> 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<uint8_t> 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>();
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<uint8_t> 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<uint8_t> 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>();
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>();
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>();
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>();
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>();
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<MockEcmParser>();
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<const unsigned char*>(data.data());
unsigned char* output = reinterpret_cast<unsigned char*>(&(*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>();
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>();
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<StrictMockLicense>();
cas_api.setCasLicense(std::move(first_license_pass_through));
auto first_crypto_pass_through = make_unique<StrictMockCryptoSession>();
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>();
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>();
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

View File

@@ -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<uint8_t>(wvcas::KeySlotId id));
MOCK_CONST_METHOD1(content_key_id, std::vector<uint8_t>(wvcas::KeySlotId id));
MOCK_CONST_METHOD1(wrapped_key_data,
std::vector<uint8_t>(wvcas::KeySlotId id));
MOCK_CONST_METHOD1(wrapped_key_iv, std::vector<uint8_t>(wvcas::KeySlotId id));
MOCK_CONST_METHOD1(content_iv, std::vector<uint8_t>(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<uint8_t>& fingerprinting),
(override));
MOCK_METHOD(void, OnSessionServiceBlockingUpdated,
(const int32_t& sessionId,
const std::vector<uint8_t>& service_blocking),
(override));
};
TEST_F(CasSessionTest, FingerprintingSuccess) {
TestCasSession session;
auto mock_crypto = std::make_shared<MockCryptoSession>();

View File

@@ -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

View File

@@ -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

View File

@@ -2,13 +2,16 @@
// source code may only be used and distributed under the Widevine Master
// License Agreement.
#include <cutils/properties.h>
#include "cas_properties.h"
#include "log.h"
#include <cutils/properties.h>
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