diff --git a/plugin/include/cas_events.h b/plugin/include/cas_events.h index 8c9ec92..2a6e401 100644 --- a/plugin/include/cas_events.h +++ b/plugin/include/cas_events.h @@ -32,6 +32,11 @@ typedef enum { LICENSE_CAS_RENEWAL_READY, LICENSE_REMOVAL, LICENSE_REMOVED, + ASSIGN_LICENSE_ID, + LICENSE_ID_ASSIGNED, + LICENSE_NEW_EXPIRY_TIME, + MULTI_CONTENT_LICENSE_INFO, + GROUP_LICENSE_INFO, // TODO(jfore): Evaluate removing this event in favor of return status codes // from @@ -87,4 +92,16 @@ typedef enum { SERVICE_BLOCKING_DEVICE_GROUP = 0, } SessionServiceBlockingFieldType; +// Types used inside a MULTI_CONTENT_LICENSE_INFO event. +typedef enum { + MULTI_CONTENT_LICENSE_ID = 0, + MULTI_CONTENT_LICENSE_CONTENT_ID, +} MultiContentLicenseFieldType; + +// Types used inside a GROUP_LICENSE_INFO event. +typedef enum { + GROUP_LICENSE_ID = 0, + GROUP_LICENSE_GROUP_ID, +} GroupLicenseFieldType; + #endif // CAS_EVENTS_H diff --git a/plugin/include/cas_license.h b/plugin/include/cas_license.h index 0e0f6c4..26e179a 100644 --- a/plugin/include/cas_license.h +++ b/plugin/include/cas_license.h @@ -53,15 +53,22 @@ 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& license_file, + const std::string* content_id_filter); - // Process a server response containing a EMM for use in the - // processing of ECM(s). 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. + // 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, std::string* device_file); + const std::string& entitlement_response, + const std::string* content_id_filter, std::string* device_file); // Process a previously stored device |certificate| and make it available // for use in an EMM request. @@ -92,6 +99,22 @@ class CasLicense : public wvutil::TimerHandler, public wvcas::CasEventListener { // Query the license to see if storage is allowed. virtual bool CanStoreLicense() const; + // Returns the group id specified in the license. Group id is expected to be + // non-empty if the license is MULTI_CONTENT_LICENSE or GROUP_LICENSE; and + // empty if the license is SINGLE_CONTENT_LICENSE_DEFAULT. + virtual std::string GetGroupId() const; + + // If the license is MULTI_CONTENT_LICENSE, the returned vector contains all + // content ids that the license is for. Returns empty if the license if not + // MULTI_CONTENT_LICENSE. + virtual std::vector GetContentIdList() const; + + // Returns true if the license is MULTI_CONTENT_LICENSE, and false otherwise. + virtual bool IsMultiContentLicense() const; + + // Returns true if the license is GROUP_LICENSE, and false otherwise. + virtual bool IsGroupLicense() const; + // Policy timer implentation. void OnTimerEvent() override; @@ -133,7 +156,8 @@ 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& signature, + const std::string* content_id_filter); CasStatus InstallLicenseRenewal(const std::string& serialized_license, const std::string& core_message, const std::string& signature); diff --git a/plugin/include/crypto_key.h b/plugin/include/crypto_key.h deleted file mode 100644 index 9341557..0000000 --- a/plugin/include/crypto_key.h +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary -// source code may only be used and distributed under the Widevine Master -// License Agreement. - -#ifndef CRYPTO_KEY_H_ -#define CRYPTO_KEY_H_ - -namespace wvcas { - -class CryptoKey { - public: - CryptoKey(){}; - ~CryptoKey(){}; - - const std::string& key_id() const { return key_id_; } - const std::string& key_data() const { return key_data_; } - const std::string& key_data_iv() const { return key_data_iv_; } - const std::string& key_control() const { return key_control_; } - const std::string& key_control_iv() const { return key_control_iv_; } - const std::string& entitlement_key_id() const { return entitlement_key_id_; } - const std::string& track_label() const { return track_label_; } - void set_key_id(const std::string& key_id) { key_id_ = key_id; } - void set_key_data(const std::string& key_data) { key_data_ = key_data; } - void set_key_data_iv(const std::string& iv) { key_data_iv_ = iv; } - void set_key_control(const std::string& ctl) { key_control_ = ctl; } - void set_key_control_iv(const std::string& ctl_iv) { - key_control_iv_ = ctl_iv; - } - void set_track_label(const std::string& track_label) { - track_label_ = track_label; - } - void set_entitlement_key_id(const std::string& entitlement_key_id) { - entitlement_key_id_ = entitlement_key_id; - } - - bool HasKeyControl() const { return key_control_.size() >= 16; } - - private: - std::string key_id_; - std::string key_data_iv_; - std::string key_data_; - std::string key_control_; - std::string key_control_iv_; - std::string track_label_; - std::string entitlement_key_id_; -}; - -} // namespace wvcas - -#endif // CRYPTO_KEY_H_ diff --git a/plugin/include/crypto_session.h b/plugin/include/crypto_session.h index f262ab8..1410f9f 100644 --- a/plugin/include/crypto_session.h +++ b/plugin/include/crypto_session.h @@ -13,7 +13,6 @@ #include "OEMCryptoCAS.h" #include "cas_status.h" #include "cas_types.h" -#include "crypto_key.h" #include "oemcrypto_interface.h" #include "rw_lock.h" @@ -144,8 +143,6 @@ class CryptoInterface { size_t enc_session_key_length, const uint8_t* mac_key_context, size_t mac_key_context_length, const uint8_t* enc_key_context, size_t enc_key_context_length); - virtual OEMCryptoResult OEMCrypto_LoadKeys( - const LoadKeysParams& load_key_params); virtual OEMCryptoResult OEMCrypto_LoadLicense(OEMCrypto_SESSION session, const uint8_t* message, size_t message_length, @@ -168,10 +165,6 @@ class CryptoInterface { OEMCryptoCipherMode cipher_mode); virtual OEMCryptoResult OEMCrypto_GetHDCPCapability( OEMCrypto_HDCP_Capability* current, OEMCrypto_HDCP_Capability* max); - virtual OEMCryptoResult OEMCrypto_RefreshKeys( - OEMCrypto_SESSION session, const uint8_t* message, size_t message_length, - const uint8_t* signature, size_t signature_length, size_t num_keys, - const OEMCrypto_KeyRefreshObject* key_array); virtual OEMCryptoResult OEMCrypto_GetDeviceID(uint8_t* deviceID, size_t* idLength); virtual const char* OEMCrypto_SecurityLevel(); @@ -267,13 +260,6 @@ class CryptoSession { size_t mac_key_context_length, const uint8_t* enc_key_context, size_t enc_key_context_length); - virtual CasStatus LoadKeys(const std::string& message, - const std::string& signature, - const std::string& mac_key_iv, - const std::string& mac_key, - const std::vector& key_array, - const std::string& pst, - const std::string& srm_requirement); virtual CasStatus LoadLicense(const std::string& signed_message, const std::string& core_message, const std::string& signature); @@ -292,9 +278,6 @@ class CryptoSession { CryptoMode crypto_mode); virtual bool GetHdcpCapabilities(HdcpCapability* current, HdcpCapability* max); - virtual CasStatus RefreshKeys(const std::string& message, - const std::string& signature, - const std::vector& key_array); virtual CasStatus GetDeviceID(std::string* buffer); virtual const char* SecurityLevel(); virtual CasStatus CreateEntitledKeySession( diff --git a/plugin/include/ecm_parser.h b/plugin/include/ecm_parser.h index 982fd89..e26480c 100644 --- a/plugin/include/ecm_parser.h +++ b/plugin/include/ecm_parser.h @@ -24,7 +24,7 @@ class EcmParser { // The EcmParser factory method. // Validates the ecm. If validations is successful returns true and constructs // an EcmParser in |parser| using |ecm|. - static std::unique_ptr Create(const CasEcm& ecm); + static std::unique_ptr Create(const CasEcm& ecm); // Accessor methods. virtual uint8_t version() const = 0; @@ -38,6 +38,9 @@ class EcmParser { virtual std::vector wrapped_key_iv(KeySlotId id) const = 0; virtual std::vector content_iv(KeySlotId id) const = 0; + // Process group content keys instead of the normal content keys. + virtual bool set_group_id(const std::string& group_id) = 0; + virtual bool has_fingerprinting() const = 0; virtual video_widevine::Fingerprinting fingerprinting() const = 0; virtual bool has_service_blocking() const = 0; diff --git a/plugin/include/ecm_parser_v2.h b/plugin/include/ecm_parser_v2.h index 8a807c1..9037eb9 100644 --- a/plugin/include/ecm_parser_v2.h +++ b/plugin/include/ecm_parser_v2.h @@ -30,7 +30,7 @@ class EcmParserV2 : public EcmParser { // successful returns true and constructs an EcmParserV2 in |parser| using // |ecm|. static bool create(const CasEcm& cas_ecm, - std::unique_ptr* parser); + std::unique_ptr* parser); // Accessor methods. uint8_t version() const override; @@ -44,6 +44,11 @@ class EcmParserV2 : public EcmParser { std::vector wrapped_key_iv(KeySlotId id) const override; std::vector content_iv(KeySlotId id) const override; + // Group keys not supported in v2. + bool set_group_id(const std::string& group_id) override { + return group_id.empty(); + }; + // ECM v2 or under does not have these fields. bool has_fingerprinting() const override { return false; } video_widevine::Fingerprinting fingerprinting() const override { diff --git a/plugin/include/ecm_parser_v3.h b/plugin/include/ecm_parser_v3.h index bc374d5..f3a15cf 100644 --- a/plugin/include/ecm_parser_v3.h +++ b/plugin/include/ecm_parser_v3.h @@ -25,7 +25,7 @@ class EcmParserV3 : public EcmParser { // |ecm| must be Widevine ECM v3 (or higher if compatible) without section // header. Validates the ecm. If validations is successful returns an // EcmParserV3, otherwise an nullptr. - static std::unique_ptr Create(const CasEcm& ecm); + static std::unique_ptr Create(const CasEcm& ecm); // Accessor methods. uint8_t version() const override; @@ -39,6 +39,8 @@ class EcmParserV3 : public EcmParser { std::vector wrapped_key_iv(KeySlotId id) const override; std::vector content_iv(KeySlotId id) const override; + bool set_group_id(const std::string& group_id) override; + bool has_fingerprinting() const override; video_widevine::Fingerprinting fingerprinting() const override; bool has_service_blocking() const override; @@ -53,6 +55,8 @@ class EcmParserV3 : public EcmParser { video_widevine::EcmPayload ecm_payload); video_widevine::SignedEcmPayload signed_ecm_payload_; video_widevine::EcmPayload ecm_payload_; + video_widevine::EcmKeyData even_key_data_; + video_widevine::EcmKeyData odd_key_data_; }; } // namespace wvcas diff --git a/plugin/include/oemcrypto_interface.h b/plugin/include/oemcrypto_interface.h index 1d28529..c8148da 100644 --- a/plugin/include/oemcrypto_interface.h +++ b/plugin/include/oemcrypto_interface.h @@ -10,28 +10,9 @@ #include #include "OEMCryptoCAS.h" -#include "crypto_key.h" namespace wvcas { -// LoadKeysParams mirrors the parameters in the OEMCrypto API. It's purpose is -// to allow OEMCrypto_LoadKeys to be mocked. OEMCrypto_LoadKeys takes 13 -// parameters as of API V14. GoogleMock allows a maximum of 10. -struct LoadKeysParams { - OEMCrypto_SESSION session = 0; - const uint8_t* message = nullptr; - size_t message_length = 0; - const uint8_t* signature = nullptr; - size_t signature_length = 0; - OEMCrypto_Substring enc_mac_keys_iv = {}; - OEMCrypto_Substring enc_mac_keys = {}; - size_t num_keys = 0; - const OEMCrypto_KeyObject* key_array = nullptr; - OEMCrypto_Substring pst = {}; - OEMCrypto_Substring srm_requirement = {}; - OEMCrypto_LicenseType license_type = OEMCrypto_ContentLicense; -}; - // InputStreamParams mirrors the parameters in OEMCrypto API. The // purpose is to allow OEMCrypto_Descramble to be mocked. OEMCrypto_Descramble // takes 11 parameters as of API V15. GoogleMock allows a maximum of 10. @@ -107,8 +88,6 @@ class OEMCryptoInterface { size_t enc_session_key_length, const uint8_t* mac_key_context, size_t mac_key_context_length, const uint8_t* enc_key_context, size_t enc_key_context_length) const; - virtual OEMCryptoResult OEMCrypto_LoadKeys( - const LoadKeysParams& load_key_params) const; virtual OEMCryptoResult OEMCrypto_LoadLicense(OEMCrypto_SESSION session, const uint8_t* message, size_t message_length, @@ -130,10 +109,6 @@ class OEMCryptoInterface { size_t content_key_id_length, OEMCryptoCipherMode cipher_mode) const; virtual OEMCryptoResult OEMCrypto_GetHDCPCapability( OEMCrypto_HDCP_Capability* current, OEMCrypto_HDCP_Capability* max) const; - virtual OEMCryptoResult OEMCrypto_RefreshKeys( - OEMCrypto_SESSION session, const uint8_t* message, size_t message_length, - const uint8_t* signature, size_t signature_length, size_t num_keys, - const OEMCrypto_KeyRefreshObject* key_array); virtual OEMCryptoResult OEMCrypto_GetDeviceID(uint8_t* deviceID, size_t* idLength); virtual OEMCryptoResult OEMCrypto_LoadTestKeybox(const uint8_t* buffer, diff --git a/plugin/include/widevine_cas_api.h b/plugin/include/widevine_cas_api.h index 2db8d27..f2edeb0 100644 --- a/plugin/include/widevine_cas_api.h +++ b/plugin/include/widevine_cas_api.h @@ -54,8 +54,15 @@ class WidevineCas : public wvutil::TimerHandler { std::string& license_id); // Processes the entitlement |response| to a entitlement license request. - virtual CasStatus handleEntitlementResponse(const std::string& response, - std::string& license_id); + // |license_id| is the id of the license installed. Can be used to select + // which license to install. + // |multi_content_license_info| contains the message that can be sent to the + // app if the installed license is a multi content license. + // |group_license_info| contains the message that can be sent to the app if + // the installed license is a group license. + virtual CasStatus handleEntitlementResponse( + const std::string& response, std::string& license_id, + std::string& multi_content_license_info, std::string& group_license_info); // Generates an entitlement license request in |entitlement_request| for the // media described in |init_data|. @@ -87,8 +94,11 @@ class WidevineCas : public wvutil::TimerHandler { // Set the minimum age required to process ECM. virtual CasStatus HandleSetParentalControlAge(const CasData& data); - // Remove the in used license. If successful content id is returned. - virtual CasStatus RemoveLicense(const std::string file_name); + // Remove the license file given the filename user provides. + virtual CasStatus RemoveLicense(const std::string& file_name); + + // Record the license id that user provides. + virtual CasStatus RecordLicenseId(const std::string& license_id); void OnTimerEvent() override; @@ -127,6 +137,15 @@ 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 + // content will use if multiple licenses exist. + std::string assigned_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_; }; // namespace wvcas } // namespace wvcas diff --git a/plugin/include/widevine_cas_session.h b/plugin/include/widevine_cas_session.h index 1b95472..314edb6 100644 --- a/plugin/include/widevine_cas_session.h +++ b/plugin/include/widevine_cas_session.h @@ -49,16 +49,15 @@ class WidevineCasSession { CasStatus initialize(std::shared_ptr crypto_session, CasEventListener* event_listener, uint32_t* session_id); - // Get the current key information. This method will be used by a descrambler - // plugin to obtain the current key information. - const KeySlot& key(KeySlotId slot_id) const; - // 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 // age_restriction field specified in |ecm|. Otherwise, ECM will not be // processed and error will be returned. - virtual CasStatus processEcm(const CasEcm& ecm, uint8_t parental_control_age); + // |license_group_id| if non empty, processEcm will decrypt content keys that + // are specified by |license_group_id|. + virtual CasStatus processEcm(const CasEcm& ecm, uint8_t parental_control_age, + const std::string& license_group_id); // Returns the security level retrieved from OEMCrypto. const char* securityLevel(); @@ -71,8 +70,7 @@ class WidevineCasSession { private: // Creates an EcmParser. - virtual std::unique_ptr getEcmParser( - const CasEcm& ecm) const; + virtual std::unique_ptr getEcmParser(const CasEcm& ecm) const; CasKeySlotData keys_; // Odd and even key slots. std::string entitlement_key_id_; diff --git a/plugin/include/widevine_media_cas_plugin.h b/plugin/include/widevine_media_cas_plugin.h index 229f5ea..f9f7039 100644 --- a/plugin/include/widevine_media_cas_plugin.h +++ b/plugin/include/widevine_media_cas_plugin.h @@ -30,12 +30,9 @@ class WidevineCasPlugin : public CasPlugin, public CasEventListener { // MediaCas platform api documentation. WidevineCasPlugin(void* appData, CasPluginCallback callback); WidevineCasPlugin(void* appData, CasPluginCallbackExt callback); - ~WidevineCasPlugin() override {} + virtual ~WidevineCasPlugin() override {} - virtual status_t initialize(); - - // Returns true if the device has been provisioned with a device certificate. - bool is_provisioned(); + status_t initialize(); // Provide a callback to report plugin status. status_t setStatusCallback(CasPluginStatusCallback callback) override; @@ -88,19 +85,28 @@ class WidevineCasPlugin : public CasPlugin, public CasEventListener { WidevineCasPlugin(const WidevineCasPlugin&) = delete; WidevineCasPlugin& operator=(const WidevineCasPlugin&) = delete; + protected: + // For unit test only. + virtual void SetWidevineCasApi( + std::unique_ptr widevine_cas_api) { + widevine_cas_api_ = std::move(widevine_cas_api); + } + WidevineCasPlugin(){}; + private: - virtual std::shared_ptr getCryptoSession(); // |sessionId| is nullptr if the event is not a session event. - virtual CasStatus processEvent(int32_t event, int32_t arg, - const CasData& eventData, - const CasSessionId* sessionId); - virtual CasStatus HandleIndividualizationResponse(const CasData& response); - virtual CasStatus HandleEntitlementResponse(const CasData& response); - virtual status_t requestLicense(const std::string& init_data); - virtual CasStatus HandleEntitlementRenewalResponse(const CasData& response); - virtual CasStatus HandleUniqueIDQuery(); - virtual CasStatus HandleSetParentalControlAge(const CasData& data); - virtual CasStatus HandleLicenseRemoval(const CasData& license_id); + CasStatus processEvent(int32_t event, int32_t arg, const CasData& eventData, + const CasSessionId* sessionId); + CasStatus HandleIndividualizationResponse(const CasData& response); + CasStatus HandleEntitlementResponse(const CasData& response); + status_t requestLicense(const std::string& init_data); + CasStatus HandleEntitlementRenewalResponse(const CasData& response); + CasStatus HandleUniqueIDQuery(); + CasStatus HandleSetParentalControlAge(const CasData& data); + CasStatus HandleLicenseRemoval(const CasData& license_id); + CasStatus HandleAssignLicenseID(const CasData& license_id); + // Returns true if the device has been provisioned with a device certificate. + bool is_provisioned() const; // Event listener implementation void OnSessionRenewalNeeded() override; @@ -120,8 +126,9 @@ class WidevineCasPlugin : public CasPlugin, public CasEventListener { // Choose to use |callback_| or |callback_ext_| to send back information. // |sessionId| is ignored if |callback_ext_| is null, - void CallBack(void* appData, int32_t event, int32_t arg, uint8_t* data, - size_t size, const CasSessionId* sessionId) const; + virtual void CallBack(void* appData, int32_t event, int32_t arg, + uint8_t* data, size_t size, + const CasSessionId* sessionId) const; void* app_data_; CasPluginCallback callback_; @@ -132,7 +139,7 @@ class WidevineCasPlugin : public CasPlugin, public CasEventListener { // is used to build a PSSH, and others are discarded. bool is_emm_request_sent_ = false; std::string provision_data_; - WidevineCas widevine_cas_; + std::unique_ptr widevine_cas_api_; }; } // namespace wvcas diff --git a/plugin/src/cas_license.cpp b/plugin/src/cas_license.cpp index 11b43b0..8b88de3 100644 --- a/plugin/src/cas_license.cpp +++ b/plugin/src/cas_license.cpp @@ -7,12 +7,13 @@ #include #include +#include #include +#include #include #include "cas_properties.h" #include "cas_util.h" -#include "crypto_key.h" #include "crypto_session.h" #include "device_files.pb.h" #include "license_protocol.pb.h" @@ -167,51 +168,6 @@ bool Hash(const std::string& data, std::string* hash) { } // namespace -std::vector ExtractEntitlementKeys(const License& license) { - std::vector key_array; - key_array.reserve(license.key_size()); - // Extract entitlement type key(s). - for (const auto& license_key : license.key()) { - if (license_key.type() == License_KeyContainer::ENTITLEMENT) { - key_array.emplace_back(); - auto& key = key_array.back(); - // Strip off PKCS#5 padding - since we know the key is 32 or 48 bytes, - // the padding will always be 16 bytes. - size_t length = 0; - if (license_key.key().size() > 32) { - length = license_key.key().size() - 16; - } - key.set_key_data(license_key.key().substr(0, length)); - key.set_key_data_iv(license_key.iv()); - key.set_key_id(license_key.id()); - key.set_track_label(license_key.track_label()); - if (license_key.has_key_control()) { - key.set_key_control(license_key.key_control().key_control_block()); - key.set_key_control_iv(license_key.key_control().iv()); - } - } - } - return key_array; -} - -std::vector ExtractKeyControlKeys(const License& license) { - std::vector key_array; - key_array.reserve(license.key_size()); - // Extract key control type key(s). - for (const auto& license_key : license.key()) { - if (license_key.type() == License_KeyContainer::KEY_CONTROL) { - key_array.emplace_back(); - auto& key = key_array.back(); - if (license_key.has_key_control()) { - key.set_key_control(license_key.key_control().key_control_block()); - key.set_key_control_iv(license_key.key_control().iv()); - } - key.set_track_label(license_key.track_label()); - } - } - return key_array; -} - CasStatus GenerateLicenseFile( const std::string& emm_request, const std::string& emm_response, const std::string& renewal_request, const std::string& renewal_response, @@ -477,8 +433,9 @@ CasStatus CasLicense::GenerateEntitlementRequest( return CasStatusCode::kNoError; } -CasStatus CasLicense::HandleStoredLicense(const std::string& wrapped_rsa_key, - const std::string& license_file) { +CasStatus CasLicense::HandleStoredLicense( + const std::string& wrapped_rsa_key, const std::string& license_file, + const std::string* content_id_filter) { HashedFile hash_file; if (!hash_file.ParseFromString(license_file)) { return CasStatus(CasStatusCode::kLicenseFileParseError, @@ -545,7 +502,7 @@ CasStatus CasLicense::HandleStoredLicense(const std::string& wrapped_rsa_key, status = InstallLicense(signed_message.session_key(), signed_message.msg(), signed_message.oemcrypto_core_message(), - signed_message.signature()); + signed_message.signature(), content_id_filter); if (!status.ok()) { return status; } @@ -568,7 +525,8 @@ CasStatus CasLicense::HandleStoredLicense(const std::string& wrapped_rsa_key, } CasStatus CasLicense::HandleEntitlementResponse( - const std::string& entitlement_response, std::string* device_file) { + const std::string& entitlement_response, + const std::string* content_id_filter, std::string* device_file) { video_widevine::SignedMessage signed_message; if (!signed_message.ParseFromString(entitlement_response)) { return CasStatus(CasStatusCode::kCasLicenseError, @@ -591,9 +549,10 @@ 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()); + CasStatus status = + InstallLicense(signed_message.session_key(), signed_message.msg(), + signed_message.oemcrypto_core_message(), + signed_message.signature(), content_id_filter); if (!status.ok()) { return status; } @@ -721,7 +680,8 @@ 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& signature, + const std::string* /*content_id_filter*/) { video_widevine::License license; if (!license.ParseFromString(serialized_license)) { return CasStatus(CasStatusCode::kCasLicenseError, @@ -768,12 +728,7 @@ CasStatus CasLicense::InstallLicense(const std::string& session_key, mac_key_str.resize(2 * kMacKeySizeBytes); } - std::vector key_array = ExtractEntitlementKeys(license); - if (key_array.empty()) { - return CasStatus(CasStatusCode::kCasLicenseError, - "the entitlement contains no keys"); - } - + // TODO: apply content_id_filter status = crypto_session_->LoadLicense(serialized_license, core_message, signature); if (!status.ok()) { @@ -856,6 +811,36 @@ bool CasLicense::CanStoreLicense() const { return policy_engine_->CanPersist(); } +std::string CasLicense::GetGroupId() const { + return license_.license_category_spec().group_id(); +} + +std::vector CasLicense::GetContentIdList() const { + std::set content_ids; + if (IsMultiContentLicense()) { + for (const auto& license_key : license_.key()) { + if (license_key.type() == License_KeyContainer::ENTITLEMENT) { + if (license_key.key_category_spec().has_content_id()) { + content_ids.insert(license_key.key_category_spec().content_id()); + } else if (license_key.key_category_spec().has_group_id()) { + content_ids.insert(license_key.key_category_spec().group_id()); + } + } + } + } + return {content_ids.begin(), content_ids.end()}; +} + +bool CasLicense::IsMultiContentLicense() const { + return license_.license_category_spec().license_category() == + video_widevine::LicenseCategorySpec::MULTI_CONTENT_LICENSE; +} + +bool CasLicense::IsGroupLicense() const { + return license_.license_category_spec().license_category() == + video_widevine::LicenseCategorySpec::GROUP_LICENSE; +} + CasStatus CasLicense::GenerateEntitlementRenewalRequest( const std::string& device_certificate, std::string* signed_renewal_request) { diff --git a/plugin/src/crypto_session.cpp b/plugin/src/crypto_session.cpp index d667fc1..e337415 100644 --- a/plugin/src/crypto_session.cpp +++ b/plugin/src/crypto_session.cpp @@ -265,13 +265,6 @@ OEMCryptoResult CryptoInterface::OEMCrypto_DeriveKeysFromSessionKey( }); } -OEMCryptoResult CryptoInterface::OEMCrypto_LoadKeys( - const LoadKeysParams& load_key_params) { - return lock_->WithOecSessionLock("LoadKeys", [&] { - return oemcrypto_interface_->OEMCrypto_LoadKeys(load_key_params); - }); -} - OEMCryptoResult CryptoInterface::OEMCrypto_LoadLicense( OEMCrypto_SESSION session, const uint8_t* message, size_t message_length, size_t core_message_length, const uint8_t* signature, @@ -320,17 +313,6 @@ OEMCryptoResult CryptoInterface::OEMCrypto_GetHDCPCapability( }); } -OEMCryptoResult CryptoInterface::OEMCrypto_RefreshKeys( - OEMCrypto_SESSION session, const uint8_t* message, size_t message_length, - const uint8_t* signature, size_t signature_length, size_t num_keys, - const OEMCrypto_KeyRefreshObject* key_array) { - return lock_->WithOecSessionLock("RefreshKeys", [&] { - return oemcrypto_interface_->OEMCrypto_RefreshKeys( - session, message, message_length, signature, signature_length, num_keys, - key_array); - }); -} - OEMCryptoResult CryptoInterface::OEMCrypto_GetDeviceID(uint8_t* deviceID, size_t* idLength) { return lock_->WithOecReadLock("GetDeviceID", [&] { @@ -828,64 +810,6 @@ CasStatus CryptoSession::DeriveKeysFromSessionKey( return CasStatus::OkStatus(); } -CasStatus CryptoSession::LoadKeys(const std::string& message, - const std::string& signature, - const std::string& mac_key_iv, - const std::string& mac_key, - const std::vector& key_array, - const std::string& pst, - const std::string& srm_requirement) { - if (key_array.empty()) { - return CasStatus::OkStatus(); - } - - LoadKeysParams load_key_params; - load_key_params.session = session_; - load_key_params.message = reinterpret_cast(message.data()); - load_key_params.message_length = message.size(); - load_key_params.signature = - reinterpret_cast(signature.data()); - load_key_params.signature_length = signature.size(); - load_key_params.enc_mac_keys_iv.offset = GetOffset(message, mac_key_iv); - load_key_params.enc_mac_keys_iv.length = mac_key_iv.size(); - load_key_params.enc_mac_keys.offset = GetOffset(message, mac_key); - load_key_params.enc_mac_keys.length = mac_key.size(); - load_key_params.pst.offset = GetOffset(message, pst); - load_key_params.pst.length = pst.size(); - load_key_params.srm_requirement.offset = GetOffset(message, srm_requirement); - load_key_params.srm_requirement.length = srm_requirement.size(); - std::vector key_objects; - key_objects.reserve(key_array.size()); - for (const auto& ki : key_array) { - key_objects.resize(key_objects.size() + 1); - OEMCrypto_KeyObject& ko = key_objects.back(); - ko.key_id.offset = GetOffset(message, ki.key_id()); - ko.key_id.length = ki.key_id().length(); - ko.key_data_iv.offset = GetOffset(message, ki.key_data_iv()); - ko.key_data_iv.length = ki.key_data_iv().length(); - ko.key_data.offset = GetOffset(message, ki.key_data()); - ko.key_data.length = ki.key_data().length(); - if (ki.HasKeyControl()) { - ko.key_control_iv.offset = GetOffset(message, ki.key_control_iv()); - ko.key_control_iv.length = ki.key_control_iv().length(); - ko.key_control.offset = GetOffset(message, ki.key_control()); - ko.key_control.length = ki.key_control().length(); - } - } - load_key_params.key_array = &key_objects[0]; - load_key_params.num_keys = key_objects.size(); - load_key_params.license_type = OEMCrypto_EntitlementLicense; - - OEMCryptoResult result = - crypto_interface_->OEMCrypto_LoadKeys(load_key_params); - if (result != OEMCrypto_SUCCESS) { - std::ostringstream err_string; - err_string << "OEMCrypto_LoadKeys returned " << result; - return CasStatus(CasStatusCode::kCryptoSessionError, err_string.str()); - } - return CasStatus::OkStatus(); -} - CasStatus CryptoSession::LoadLicense(const std::string& signed_message, const std::string& core_message, const std::string& signature) { @@ -1040,42 +964,6 @@ OEMCryptoResult CryptoSession::getCryptoInterface( return CryptoInterface::create(interface); } -CasStatus CryptoSession::RefreshKeys(const std::string& message, - const std::string& signature, - const std::vector& key_array) { - if (key_array.empty()) { - return CasStatus::OkStatus(); - } - // Assume the message and signature parameters are valid. - const uint8_t* message_ptr = reinterpret_cast(message.data()); - const uint8_t* signature_ptr = - reinterpret_cast(signature.data()); - std::vector load_key_array; - load_key_array.reserve(key_array.size()); - for (const auto& key : key_array) { - load_key_array.emplace_back(); - auto& key_object = load_key_array.back(); - key_object.key_id.offset = GetOffset(message, key.key_id()); - key_object.key_id.length = key.key_id().length(); - if (!key.key_control().empty()) { - key_object.key_control_iv.offset = - GetOffset(message, key.key_control_iv()); - key_object.key_control_iv.length = key.key_control_iv().length(); - key_object.key_control.offset = GetOffset(message, key.key_control()); - key_object.key_control.length = key.key_control().length(); - } - } - OEMCryptoResult result = crypto_interface_->OEMCrypto_RefreshKeys( - session_, message_ptr, message.size(), signature_ptr, signature.size(), - load_key_array.size(), &load_key_array[0]); - if (result != OEMCrypto_SUCCESS) { - std::ostringstream err_string; - err_string << "OEMCrypto_RefreshKeys returned " << result; - return CasStatus(CasStatusCode::kCryptoSessionError, err_string.str()); - } - return CasStatus::OkStatus(); -} - CasStatus CryptoSession::GetDeviceID(std::string* buffer) { if (!crypto_interface_) { return CasStatus(CasStatusCode::kCryptoSessionError, diff --git a/plugin/src/ecm_parser.cpp b/plugin/src/ecm_parser.cpp index 86f1df1..b3e43b2 100644 --- a/plugin/src/ecm_parser.cpp +++ b/plugin/src/ecm_parser.cpp @@ -51,7 +51,7 @@ int find_ecm_start_index(const CasEcm& cas_ecm) { } // namespace -std::unique_ptr EcmParser::Create(const CasEcm& cas_ecm) { +std::unique_ptr EcmParser::Create(const CasEcm& cas_ecm) { // Detect and strip optional section header. const int offset = find_ecm_start_index(cas_ecm); if (offset < 0 || @@ -71,7 +71,7 @@ std::unique_ptr EcmParser::Create(const CasEcm& cas_ecm) { } if (ecm[kVersionIndex] <= 2) { - std::unique_ptr parser; + std::unique_ptr parser; if (!EcmParserV2::create(ecm, &parser)) { return nullptr; } diff --git a/plugin/src/ecm_parser_v2.cpp b/plugin/src/ecm_parser_v2.cpp index 996380e..c351665 100644 --- a/plugin/src/ecm_parser_v2.cpp +++ b/plugin/src/ecm_parser_v2.cpp @@ -80,13 +80,12 @@ const EcmKeyData* EcmParserV2::key_slot_data(KeySlotId id) const { } bool EcmParserV2::create(const CasEcm& cas_ecm, - std::unique_ptr* parser) { + std::unique_ptr* parser) { if (parser == nullptr) { return false; } // Using 'new' to access a non-public constructor. - auto new_parser = - std::unique_ptr(new EcmParserV2(cas_ecm)); + auto new_parser = std::unique_ptr(new EcmParserV2(cas_ecm)); if (!new_parser->is_valid_size()) { return false; } diff --git a/plugin/src/ecm_parser_v3.cpp b/plugin/src/ecm_parser_v3.cpp index 65e1d0e..79f65cb 100644 --- a/plugin/src/ecm_parser_v3.cpp +++ b/plugin/src/ecm_parser_v3.cpp @@ -51,9 +51,12 @@ CryptoMode ConvertProtoCipherMode(EcmMetaData::CipherMode cipher_mode) { EcmParserV3::EcmParserV3(SignedEcmPayload signed_ecm_payload, EcmPayload ecm_payload) : signed_ecm_payload_(std::move(signed_ecm_payload)), - ecm_payload_(std::move(ecm_payload)) {} + ecm_payload_(std::move(ecm_payload)) { + even_key_data_ = ecm_payload_.even_key_data(); + odd_key_data_ = ecm_payload_.odd_key_data(); +} -std::unique_ptr EcmParserV3::Create(const CasEcm& cas_ecm) { +std::unique_ptr EcmParserV3::Create(const CasEcm& cas_ecm) { if (cas_ecm.size() <= kEcmHeaderSize) { LOGE("ECM is too short. Size: %u", cas_ecm.size()); return nullptr; @@ -74,7 +77,7 @@ std::unique_ptr EcmParserV3::Create(const CasEcm& cas_ecm) { } // Using 'new' to access a non-public constructor. - return std::unique_ptr( + return std::unique_ptr( new EcmParserV3(signed_ecm_payload, ecm_payload)); } @@ -100,9 +103,9 @@ std::vector EcmParserV3::entitlement_key_id(KeySlotId id) const { // Use the even entitlement_key_id if the odd one is empty (omitted). const EcmKeyData& key_data = id == KeySlotId::kOddKeySlot && - !ecm_payload_.odd_key_data().entitlement_key_id().empty() - ? ecm_payload_.odd_key_data() - : ecm_payload_.even_key_data(); + !odd_key_data_.entitlement_key_id().empty() + ? odd_key_data_ + : even_key_data_; return {key_data.entitlement_key_id().begin(), key_data.entitlement_key_id().end()}; @@ -118,9 +121,8 @@ std::vector EcmParserV3::content_key_id(KeySlotId id) const { } std::vector EcmParserV3::wrapped_key_data(KeySlotId id) const { - const EcmKeyData& key_data = id == KeySlotId::kOddKeySlot - ? ecm_payload_.odd_key_data() - : ecm_payload_.even_key_data(); + const EcmKeyData& key_data = + id == KeySlotId::kOddKeySlot ? odd_key_data_ : even_key_data_; return {key_data.wrapped_key_data().begin(), key_data.wrapped_key_data().end()}; @@ -128,24 +130,36 @@ std::vector EcmParserV3::wrapped_key_data(KeySlotId id) const { std::vector EcmParserV3::wrapped_key_iv(KeySlotId id) const { // Use the even wrapped_key_iv if the odd one is empty (omitted). - const EcmKeyData& key_data = - id == KeySlotId::kOddKeySlot && - !ecm_payload_.odd_key_data().wrapped_key_iv().empty() - ? ecm_payload_.odd_key_data() - : ecm_payload_.even_key_data(); + const EcmKeyData* key_data = + id == KeySlotId::kOddKeySlot && !odd_key_data_.wrapped_key_iv().empty() + ? &odd_key_data_ + : &even_key_data_; + // Wrapped key IV may be omitted for group keys. + if (key_data->wrapped_key_iv().empty()) { + key_data = id == KeySlotId::kOddKeySlot && + !ecm_payload_.odd_key_data().wrapped_key_iv().empty() + ? &ecm_payload_.odd_key_data() + : &ecm_payload_.even_key_data(); + } - return {key_data.wrapped_key_iv().begin(), key_data.wrapped_key_iv().end()}; + return {key_data->wrapped_key_iv().begin(), key_data->wrapped_key_iv().end()}; } std::vector EcmParserV3::content_iv(KeySlotId id) const { // Use the even content_iv if the odd one is empty (omitted). - const EcmKeyData& key_data = - id == KeySlotId::kOddKeySlot && - !ecm_payload_.odd_key_data().content_iv().empty() - ? ecm_payload_.odd_key_data() - : ecm_payload_.even_key_data(); + const EcmKeyData* key_data = + id == KeySlotId::kOddKeySlot && !odd_key_data_.content_iv().empty() + ? &odd_key_data_ + : &even_key_data_; + // Content IV may be omitted for group keys. + if (key_data->content_iv().empty()) { + key_data = id == KeySlotId::kOddKeySlot && + !ecm_payload_.odd_key_data().content_iv().empty() + ? &ecm_payload_.odd_key_data() + : &ecm_payload_.even_key_data(); + } - return {key_data.content_iv().begin(), key_data.content_iv().end()}; + return {key_data->content_iv().begin(), key_data->content_iv().end()}; } bool EcmParserV3::has_fingerprinting() const { @@ -171,4 +185,24 @@ std::string EcmParserV3::signature() const { return signed_ecm_payload_.signature(); } +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(); + } + + bool found = false; + for (int i = 0; i < ecm_payload_.group_key_data_size(); ++i) { + const video_widevine::EcmGroupKeyData& group_key_data = + ecm_payload_.group_key_data(i); + if (group_key_data.group_id() == group_id) { + found = true; + even_key_data_ = group_key_data.even_key_data(); + odd_key_data_ = group_key_data.odd_key_data(); + break; + } + } + return found; +} + } // namespace wvcas diff --git a/plugin/src/oemcrypto_interface.cpp b/plugin/src/oemcrypto_interface.cpp index c09e972..01738b4 100644 --- a/plugin/src/oemcrypto_interface.cpp +++ b/plugin/src/oemcrypto_interface.cpp @@ -81,11 +81,6 @@ class OEMCryptoInterface::Impl { const uint8_t*, size_t, const uint8_t*, size_t, const uint8_t*, size_t); - typedef OEMCryptoResult (*LoadKeys_t)( - OEMCrypto_SESSION, const uint8_t*, size_t, const uint8_t*, size_t, - OEMCrypto_Substring, OEMCrypto_Substring, size_t, - const OEMCrypto_KeyObject*, OEMCrypto_Substring, OEMCrypto_Substring, - OEMCrypto_LicenseType); typedef OEMCryptoResult (*LoadLicense_t)(OEMCrypto_SESSION session, const uint8_t* message, size_t message_length, @@ -106,10 +101,6 @@ class OEMCryptoInterface::Impl { size_t, OEMCryptoCipherMode); typedef OEMCryptoResult (*GetHDCPCapability_t)(OEMCrypto_HDCP_Capability*, OEMCrypto_HDCP_Capability*); - typedef OEMCryptoResult (*RefreshKeys_t)( - OEMCrypto_SESSION session, const uint8_t* message, size_t message_length, - const uint8_t* signature, size_t signature_length, size_t num_keys, - const OEMCrypto_KeyRefreshObject* key_array); typedef OEMCryptoResult (*GetDeviceID_t)(uint8_t* deviceID, size_t* idLength); typedef OEMCryptoResult (*LoadTestKeybox_t)(const uint8_t* buffer, size_t length); @@ -137,13 +128,11 @@ class OEMCryptoInterface::Impl { LoadDRMPrivateKey_t LoadDRMPrivateKey = nullptr; GenerateRSASignature_t GenerateRSASignature = nullptr; DeriveKeysFromSessionKey_t DeriveKeysFromSessionKey = nullptr; - LoadKeys_t LoadKeys = nullptr; LoadLicense_t LoadLicense = nullptr; LoadRenewal_t LoadRenewal = nullptr; LoadCasECMKeys_t LoadCasECMKeys = nullptr; SelectKey_t SelectKey = nullptr; GetHDCPCapability_t GetHDCPCapability = nullptr; - RefreshKeys_t RefreshKeys = nullptr; GetDeviceID_t GetDeviceID = nullptr; LoadTestKeybox_t LoadTestKeybox = nullptr; SecurityLevel_t SecurityLevel = nullptr; @@ -182,13 +171,11 @@ class OEMCryptoInterface::Impl { LOAD_SYM(LoadDRMPrivateKey); LOAD_SYM(GenerateRSASignature); LOAD_SYM(DeriveKeysFromSessionKey); - LOAD_SYM(LoadKeys); LOAD_SYM(LoadLicense); LOAD_SYM(LoadRenewal); LOAD_SYM(LoadCasECMKeys); LOAD_SYM(SelectKey); LOAD_SYM(GetHDCPCapability); - LOAD_SYM(RefreshKeys); LOAD_SYM(GetDeviceID); LOAD_SYM(SecurityLevel); LOAD_SYM(CreateEntitledKeySession); @@ -328,17 +315,6 @@ OEMCryptoResult OEMCryptoInterface::OEMCrypto_DeriveKeysFromSessionKey( mac_key_context_length, enc_key_context, enc_key_context_length); } -OEMCryptoResult OEMCryptoInterface::OEMCrypto_LoadKeys( - const LoadKeysParams& load_key_params) const { - return impl_->LoadKeys( - load_key_params.session, load_key_params.message, - load_key_params.message_length, load_key_params.signature, - load_key_params.signature_length, load_key_params.enc_mac_keys_iv, - load_key_params.enc_mac_keys, load_key_params.num_keys, - load_key_params.key_array, load_key_params.pst, - load_key_params.srm_requirement, load_key_params.license_type); -} - OEMCryptoResult OEMCryptoInterface::OEMCrypto_LoadLicense( OEMCrypto_SESSION session, const uint8_t* message, size_t message_length, size_t core_message_length, const uint8_t* signature, @@ -375,14 +351,6 @@ OEMCryptoResult OEMCryptoInterface::OEMCrypto_GetHDCPCapability( return impl_->GetHDCPCapability(current, max); } -OEMCryptoResult OEMCryptoInterface::OEMCrypto_RefreshKeys( - OEMCrypto_SESSION session, const uint8_t* message, size_t message_length, - const uint8_t* signature, size_t signature_length, size_t num_keys, - const OEMCrypto_KeyRefreshObject* key_array) { - return impl_->RefreshKeys(session, message, message_length, signature, - signature_length, num_keys, key_array); -} - OEMCryptoResult OEMCryptoInterface::OEMCrypto_GetDeviceID(uint8_t* deviceID, size_t* idLength) { return impl_->GetDeviceID(deviceID, idLength); diff --git a/plugin/src/widevine_cas_api.cpp b/plugin/src/widevine_cas_api.cpp index 6be8758..eae4d75 100644 --- a/plugin/src/widevine_cas_api.cpp +++ b/plugin/src/widevine_cas_api.cpp @@ -76,6 +76,46 @@ std::string GenerateLicenseFilename(const std::string& content_id, return std::string(std::string(kBasePathPrefix) + wvutil::b2a_hex(hash) + std::string(kLicenseFileNameSuffix)); } + +std::string GenerateMultiContentLicenseInfo( + const std::string& license_id, + const std::vector& content_list) { + std::string message; + if (license_id.empty() || content_list.empty()) { + return message; + } + message.push_back(MultiContentLicenseFieldType::MULTI_CONTENT_LICENSE_ID); + message.push_back((license_id.size() >> 8) & 0xff); + message.push_back(license_id.size() & 0xff); + message.append(license_id); + for (const auto& content_id : content_list) { + message.push_back( + MultiContentLicenseFieldType::MULTI_CONTENT_LICENSE_CONTENT_ID); + message.push_back((content_id.size() >> 8) & 0xff); + message.push_back(content_id.size() & 0xff); + message.append(content_id); + } + return message; +} + +std::string GenerateGroupLicenseInfo(const std::string& license_id, + const std::string group_id) { + std::string message; + if (license_id.empty() || group_id.empty()) { + return message; + } + message.push_back(GroupLicenseFieldType::GROUP_LICENSE_ID); + message.push_back((license_id.size() >> 8) & 0xff); + message.push_back(license_id.size() & 0xff); + message.append(license_id); + + message.push_back(GroupLicenseFieldType::GROUP_LICENSE_GROUP_ID); + message.push_back((group_id.size() >> 8) & 0xff); + message.push_back(group_id.size() & 0xff); + message.append(group_id); + return message; +} + } // namespace namespace wvcas { @@ -129,8 +169,7 @@ void WidevineCas::OnTimerEvent() { // Delete expired license after firing expired event in policy_engine if (cas_license_->IsExpired() && (media_id_.get() != nullptr)) { - std::string filename = GenerateLicenseFilename(media_id_->content_id(), - media_id_->provider_id()); + std::string filename = license_id_ + kLicenseFileNameSuffix; if (!file_system_->Exists(filename)) { LOGI("No expired license file stored in disk"); } else { @@ -238,7 +277,8 @@ CasStatus WidevineCas::HandleProcessEcm(const WvCasSessionId& sessionId, } uint8_t ecm_age_previous = session->GetEcmAgeRestriction(); - CasStatus status = session->processEcm(ecm, parental_control_age_); + CasStatus status = + session->processEcm(ecm, parental_control_age_, license_group_id_); uint8_t ecm_age_current = session->GetEcmAgeRestriction(); if (event_listener_ != nullptr && ecm_age_current != ecm_age_previous) { event_listener_->OnAgeRestrictionUpdated(sessionId, ecm_age_current); @@ -302,12 +342,24 @@ CasStatus WidevineCas::generateEntitlementRequest( if (!status.ok()) { return status; } + + std::string filename; + // Backward compatible. If the license_filename is unassigned by app, plugin + // will directly use the single_content_license named "content_id + + // provider_id" by default. + if (assigned_license_id_.empty()) { + filename = GenerateLicenseFilename(media_id_->content_id(), + media_id_->provider_id()); + } else { + filename = assigned_license_id_ + kLicenseFileNameSuffix; + // Clean up the assigned_license_filename for next round use. + assigned_license_id_.clear(); + } + std::string license_file; - std::string filename = GenerateLicenseFilename(media_id_->content_id(), - media_id_->provider_id()); if (ReadFileFromStorage(*file_system_, filename, &license_file)) { - CasStatus status = - cas_license_->HandleStoredLicense(wrapped_rsa_key_, 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. @@ -323,6 +375,13 @@ CasStatus WidevineCas::generateEntitlementRequest( } 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(); @@ -340,20 +399,29 @@ CasStatus WidevineCas::generateEntitlementRequest( entitlement_request); } -CasStatus WidevineCas::handleEntitlementResponse(const std::string& response, - std::string& license_id) { +CasStatus WidevineCas::handleEntitlementResponse( + const std::string& response, std::string& license_id, + std::string& multi_content_license_info, std::string& group_license_info) { if (response.empty()) { return CasStatus(CasStatusCode::kCasLicenseError, "empty entitlement response"); } + if (media_id_ == nullptr) { + return CasStatus(CasStatusCode::kCasLicenseError, "No media id"); + } + std::string device_file; std::unique_lock locker(lock_); - CasStatus status = - cas_license_->HandleEntitlementResponse(response, &device_file); + CasStatus status = cas_license_->HandleEntitlementResponse( + response, /*content_id_filter=*/nullptr, &device_file); if (status.ok()) { // A license has been successfully loaded. Load any ecms that may have been // deferred waiting for the license. + if (cas_license_->IsGroupLicense()) { + license_group_id_ = cas_license_->GetGroupId(); + } has_license_ = true; + status = HandleDeferredECMs(); if (!status.ok()) { return status; @@ -361,17 +429,27 @@ CasStatus WidevineCas::handleEntitlementResponse(const std::string& response, policy_timer_.Start(this, 1); - if (device_file.empty()) { - return status; - } - if (!device_file.empty()) { - std::string filename = GenerateLicenseFilename(media_id_->content_id(), - media_id_->provider_id()); + const std::string license_group_id = cas_license_->GetGroupId(); + std::string filename = GenerateLicenseFilename( + license_group_id.empty() ? media_id_->content_id() : license_group_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)); + // Save the license id. + license_id_ = license_id; + + // License info is only needed if the license is stored. + if (cas_license_->IsMultiContentLicense()) { + multi_content_license_info = GenerateMultiContentLicenseInfo( + license_id, cas_license_->GetContentIdList()); + } + if (cas_license_->IsGroupLicense()) { + group_license_info = + GenerateGroupLicenseInfo(license_id, license_group_id); + } } } return status; @@ -400,21 +478,25 @@ CasStatus WidevineCas::handleEntitlementRenewalResponse( if (!status.ok()) { return status; } - if (!device_file.empty()) { - std::string filename = GenerateLicenseFilename(media_id_->content_id(), - media_id_->provider_id()); + if (!device_file.empty() && media_id_ != nullptr) { + const std::string license_group_id = cas_license_->GetGroupId(); + std::string filename = GenerateLicenseFilename( + license_group_id.empty() ? media_id_->content_id() : license_group_id, + media_id_->provider_id()); StoreFile(*file_system_, filename, device_file); + // TODO(chelu): The license id should not change, right? // license_id will be the filename without ".lic" extension. license_id = - filename.substr(0, filename.size() - std::string(".lic").size()); + filename.substr(0, filename.size() - strlen(kLicenseFileNameSuffix)); + license_id_ = license_id; } return CasStatusCode::kNoError; } -CasStatus WidevineCas::RemoveLicense(const std::string file_name) { +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. - if (nullptr == media_id_.get()) { + if (media_id_ == nullptr) { return CasStatus(CasStatusCode::kCasLicenseError, "No media id"); } // Remove the license file given the file_name user provides. @@ -423,9 +505,9 @@ CasStatus WidevineCas::RemoveLicense(const std::string file_name) { "unable to remove license file from disk"); } LOGI("Remove license file from disk successfully."); - std::string used_license_filename = GenerateLicenseFilename( - media_id_->content_id(), media_id_->provider_id()); - if (file_name.compare(used_license_filename) == 0) { + + std::string current_license_filename = license_id_ + kLicenseFileNameSuffix; + if (file_name == current_license_filename) { // Update license policy for the in-used license. Plugin will not allowed to // play stream, store and renew license unless a new plugin instance is // created. @@ -491,7 +573,13 @@ CasStatus WidevineCas::HandleSetParentalControlAge(const CasData& data) { "missing value of parental control min age"); } parental_control_age_ = data[0]; - LOGI("Parental control age set to: ", parental_control_age_); + LOGI("Parental control age set to: %d", parental_control_age_); + return CasStatusCode::kNoError; +} + +CasStatus WidevineCas::RecordLicenseId(const std::string& license_id) { + assigned_license_id_ = license_id; + LOGI("License id selected is: %s", assigned_license_id_.c_str()); return CasStatusCode::kNoError; } diff --git a/plugin/src/widevine_cas_session.cpp b/plugin/src/widevine_cas_session.cpp index 2c72197..cfd7241 100644 --- a/plugin/src/widevine_cas_session.cpp +++ b/plugin/src/widevine_cas_session.cpp @@ -42,21 +42,19 @@ CasStatus WidevineCasSession::initialize( return CasStatusCode::kNoError; } -const KeySlot& WidevineCasSession::key(KeySlotId slot_id) const { - // TODO(): Make this function private and assume the mutex is locked. - // std::unique_lock lock(lock_); - const KeySlot& key_slot = keys_[slot_id]; - return key_slot; -} - CasStatus WidevineCasSession::processEcm(const CasEcm& ecm, - uint8_t parental_control_age) { + uint8_t parental_control_age, + const std::string& license_group_id) { if (ecm != current_ecm_) { LOGD("WidevineCasSession::processEcm: received new ecm"); - std::unique_ptr ecm_parser = getEcmParser(ecm); + std::unique_ptr ecm_parser = getEcmParser(ecm); if (ecm_parser == nullptr) { return CasStatus(CasStatusCode::kInvalidParameter, "invalid ecm"); } + if (!license_group_id.empty() && + !ecm_parser->set_group_id(license_group_id)) { + return CasStatus(CasStatusCode::kInvalidParameter, "invalid group id"); + } ecm_age_restriction_ = ecm_parser->age_restriction(); // Parental control check. @@ -114,7 +112,8 @@ CasStatus WidevineCasSession::processEcm(const CasEcm& ecm, // Temporary key slots to only have successfully loaded keys in |keys_|. CasKeySlotData keys; do { - if (keys_[keyslot_id].key_id != ecm_parser->content_key_id(keyslot_id)) { + if (keys_[keyslot_id].wrapped_key != + ecm_parser->wrapped_key_data(keyslot_id)) { KeySlot& key = keys[keyslot_id]; key.key_id = ecm_parser->content_key_id(keyslot_id); key.wrapped_key = ecm_parser->wrapped_key_data(keyslot_id); @@ -161,7 +160,7 @@ CasStatus WidevineCasSession::processEcm(const CasEcm& ecm, return CasStatusCode::kNoError; } -std::unique_ptr WidevineCasSession::getEcmParser( +std::unique_ptr WidevineCasSession::getEcmParser( const CasEcm& ecm) const { return EcmParser::Create(ecm); } diff --git a/plugin/src/widevine_media_cas_plugin.cpp b/plugin/src/widevine_media_cas_plugin.cpp index 268b493..e3a9623 100644 --- a/plugin/src/widevine_media_cas_plugin.cpp +++ b/plugin/src/widevine_media_cas_plugin.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -54,22 +55,28 @@ CasSessionId widevineSessionIdToAndroid(const WvCasSessionId& wv_session_id) { } WidevineCasPlugin::WidevineCasPlugin(void* appData, CasPluginCallback callback) - : app_data_(appData), callback_(callback), callback_ext_(nullptr) {} + : app_data_(appData), + callback_(callback), + callback_ext_(nullptr), + widevine_cas_api_(make_unique()) {} WidevineCasPlugin::WidevineCasPlugin(void* appData, CasPluginCallbackExt callback) - : app_data_(appData), callback_(nullptr), callback_ext_(callback) {} + : app_data_(appData), + callback_(nullptr), + callback_ext_(callback), + widevine_cas_api_(make_unique()) {} status_t WidevineCasPlugin::initialize() { - CasStatus status = widevine_cas_.initialize(this); + CasStatus status = widevine_cas_api_->initialize(this); if (!status.ok()) { return android::ERROR_CAS_UNKNOWN; } return OK; } -bool WidevineCasPlugin::is_provisioned() { - return widevine_cas_.is_provisioned(); +bool WidevineCasPlugin::is_provisioned() const { + return widevine_cas_api_->is_provisioned(); } status_t WidevineCasPlugin::setStatusCallback( @@ -86,11 +93,11 @@ status_t WidevineCasPlugin::setPrivateData(const CasData& privateData) { return OK; } CasStatus status = - widevine_cas_.ProcessCAPrivateData(privateData, &provision_data_); + widevine_cas_api_->ProcessCAPrivateData(privateData, &provision_data_); if (!status.ok()) { return android::ERROR_CAS_UNKNOWN; } - if (widevine_cas_.is_provisioned()) { + if (widevine_cas_api_->is_provisioned()) { return requestLicense(provision_data_); } return OK; @@ -100,8 +107,13 @@ status_t WidevineCasPlugin::openSession(CasSessionId* sessionId) { if (nullptr == sessionId) { return BAD_VALUE; } + if (!is_provisioned()) { + LOGE("Sessions can only be opened after privisioned."); + return android::ERROR_CAS_NOT_PROVISIONED; + } + WvCasSessionId new_session_id; - CasStatus status = widevine_cas_.openSession(&new_session_id); + CasStatus status = widevine_cas_api_->openSession(&new_session_id); if (!status.ok()) { return android::ERROR_CAS_SESSION_NOT_OPENED; } @@ -123,7 +135,7 @@ status_t WidevineCasPlugin::openSession(uint32_t intent, uint32_t mode, status_t WidevineCasPlugin::closeSession(const CasSessionId& sessionId) { WvCasSessionId wv_session_id = androidSessionIdToWidevine(sessionId); - CasStatus status = widevine_cas_.closeSession(wv_session_id); + CasStatus status = widevine_cas_api_->closeSession(wv_session_id); if (!status.ok()) { return android::ERROR_CAS_SESSION_NOT_OPENED; } @@ -140,12 +152,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_.ProcessSessionCAPrivateData( + CasStatus status = widevine_cas_api_->ProcessSessionCAPrivateData( wv_session_id, privateData, &provision_data_); if (!status.ok()) { return android::ERROR_CAS_SESSION_NOT_OPENED; } - if (widevine_cas_.is_provisioned()) { + if (widevine_cas_api_->is_provisioned()) { return requestLicense(provision_data_); } return OK; @@ -155,7 +167,7 @@ status_t WidevineCasPlugin::processEcm(const CasSessionId& sessionId, const CasEcm& ecm) { LOGI("WidevineCasPlugin::processEcm"); WvCasSessionId wv_session_id = androidSessionIdToWidevine(sessionId); - CasStatus status = widevine_cas_.processEcm(wv_session_id, ecm); + CasStatus status = widevine_cas_api_->processEcm(wv_session_id, ecm); if (!status.ok()) { CasData error(status.error_string().begin(), status.error_string().end()); switch (status.status_code()) { @@ -227,8 +239,8 @@ status_t WidevineCasPlugin::provision(const String8& provisionString) { } std::string provisioning_request; - CasStatus status = - widevine_cas_.generateDeviceProvisioningRequest(&provisioning_request); + CasStatus status = widevine_cas_api_->generateDeviceProvisioningRequest( + &provisioning_request); if (!status.ok()) { return INVALID_OPERATION; } @@ -246,7 +258,7 @@ status_t WidevineCasPlugin::provision(const String8& provisionString) { status_t WidevineCasPlugin::requestLicense(const std::string& init_data) { std::string signed_license_request; std::string license_id; - CasStatus status = widevine_cas_.generateEntitlementRequest( + CasStatus status = widevine_cas_api_->generateEntitlementRequest( init_data, &signed_license_request, license_id); if (!status.ok()) { return INVALID_OPERATION; @@ -273,10 +285,6 @@ status_t WidevineCasPlugin::refreshEntitlements( return OK; } -std::shared_ptr WidevineCasPlugin::getCryptoSession() { - return std::make_shared(); -} - CasStatus WidevineCasPlugin::processEvent(int32_t event, int32_t arg, const CasData& eventData, const CasSessionId* sessionId) { @@ -297,6 +305,8 @@ CasStatus WidevineCasPlugin::processEvent(int32_t event, int32_t arg, return HandleSetParentalControlAge(eventData); case LICENSE_REMOVAL: return HandleLicenseRemoval(eventData); + case ASSIGN_LICENSE_ID: + return HandleAssignLicenseID(eventData); default: return CasStatusCode::kUnknownEvent; } @@ -310,7 +320,7 @@ CasStatus WidevineCasPlugin::HandleIndividualizationResponse( "empty individualization response"); } std::string resp_string(response.begin(), response.end()); - CasStatus status = widevine_cas_.handleProvisioningResponse(resp_string); + CasStatus status = widevine_cas_api_->handleProvisioningResponse(resp_string); if (!status.ok()) { return status; } @@ -335,14 +345,28 @@ CasStatus WidevineCasPlugin::HandleEntitlementResponse( } std::string resp_string(response.begin(), response.end()); std::string license_id; - CasStatus status = - widevine_cas_.handleEntitlementResponse(resp_string, license_id); + std::string multi_content_license_info; + std::string group_license_info; + CasStatus status = widevine_cas_api_->handleEntitlementResponse( + resp_string, license_id, multi_content_license_info, group_license_info); if (!status.ok()) { return status; } CallBack(reinterpret_cast(app_data_), LICENSE_CAS_READY, LICENSE_CAS_READY, reinterpret_cast(&license_id[0]), license_id.size(), nullptr); + if (!multi_content_license_info.empty()) { + CallBack(reinterpret_cast(app_data_), MULTI_CONTENT_LICENSE_INFO, + MULTI_CONTENT_LICENSE_INFO, + reinterpret_cast(&multi_content_license_info[0]), + multi_content_license_info.size(), nullptr); + } + if (!group_license_info.empty()) { + CallBack(reinterpret_cast(app_data_), GROUP_LICENSE_INFO, + GROUP_LICENSE_INFO, + reinterpret_cast(&group_license_info[0]), + group_license_info.size(), nullptr); + } return CasStatusCode::kNoError; } @@ -353,8 +377,8 @@ CasStatus WidevineCasPlugin::HandleEntitlementRenewalResponse( } std::string resp_string(response.begin(), response.end()); std::string license_id; - CasStatus status = - widevine_cas_.handleEntitlementRenewalResponse(resp_string, license_id); + CasStatus status = widevine_cas_api_->handleEntitlementRenewalResponse( + resp_string, license_id); if (!status.ok()) { return status; } @@ -367,7 +391,7 @@ CasStatus WidevineCasPlugin::HandleEntitlementRenewalResponse( CasStatus WidevineCasPlugin::HandleUniqueIDQuery() { std::string buffer; - CasStatus status = widevine_cas_.GetUniqueID(&buffer); + CasStatus status = widevine_cas_api_->GetUniqueID(&buffer); if (!status.ok()) { CasData error(status.error_string().begin(), status.error_string().end()); CallBack(reinterpret_cast(app_data_), CAS_ERROR, @@ -383,7 +407,7 @@ CasStatus WidevineCasPlugin::HandleUniqueIDQuery() { } CasStatus WidevineCasPlugin::HandleSetParentalControlAge(const CasData& data) { - return widevine_cas_.HandleSetParentalControlAge(data); + return widevine_cas_api_->HandleSetParentalControlAge(data); } CasStatus WidevineCasPlugin::HandleLicenseRemoval(const CasData& license_id) { @@ -393,7 +417,7 @@ CasStatus WidevineCasPlugin::HandleLicenseRemoval(const CasData& license_id) { std::string license_id_str(license_id.begin(), license_id.end()); std::string file_name = license_id_str + ".lic"; - CasStatus status = widevine_cas_.RemoveLicense(file_name); + CasStatus status = widevine_cas_api_->RemoveLicense(file_name); if (!status.ok()) { CasData error(status.error_string().begin(), status.error_string().end()); CallBack(reinterpret_cast(app_data_), CAS_ERROR, @@ -407,11 +431,27 @@ CasStatus WidevineCasPlugin::HandleLicenseRemoval(const CasData& license_id) { return CasStatusCode::kNoError; } +CasStatus WidevineCasPlugin::HandleAssignLicenseID( + const wvcas::CasData& license_id) { + if (license_id.empty()) { + return CasStatus(CasStatusCode::kInvalidParameter, "empty license id"); + } + std::string license_id_str(license_id.begin(), license_id.end()); + CasStatus status = widevine_cas_api_->RecordLicenseId(license_id_str); + if (!status.ok()) { + return status; + } + CallBack(reinterpret_cast(app_data_), LICENSE_ID_ASSIGNED, + LICENSE_ID_ASSIGNED, reinterpret_cast(&license_id_str[0]), + license_id_str.size(), nullptr); + return CasStatusCode::kNoError; +} + void WidevineCasPlugin::OnSessionRenewalNeeded() { LOGI("OnSessionRenewalNeeded"); std::string renewal_request; CasStatus status = - widevine_cas_.generateEntitlementRenewalRequest(&renewal_request); + widevine_cas_api_->generateEntitlementRenewalRequest(&renewal_request); if (!status.ok()) { LOGE("unable to generate a license renewal request: %s", status.error_string().c_str()); @@ -435,8 +475,15 @@ void WidevineCasPlugin::OnSessionKeysChange(const KeyStatusMap& keys_status, } } +// Send the license expiration timestamp via this existing callback to app. +// Callback will be triggered once license is installed or license expiration +// time getting update. +// TODO(b/163427255): Should we combine with license_id? void WidevineCasPlugin::OnExpirationUpdate(int64_t new_expiry_time_seconds) { LOGI("OnExpirationUpdate"); + CallBack(reinterpret_cast(app_data_), LICENSE_NEW_EXPIRY_TIME, + LICENSE_NEW_EXPIRY_TIME, + reinterpret_cast(&new_expiry_time_seconds), 8, nullptr); } void WidevineCasPlugin::OnNewRenewalServerUrl( diff --git a/protos/license_protocol.proto b/protos/license_protocol.proto index 07d6bd1..8657689 100644 --- a/protos/license_protocol.proto +++ b/protos/license_protocol.proto @@ -49,6 +49,34 @@ message LicenseIdentification { optional bytes provider_session_token = 6; } +// This message is used to indicate the license cateogry spec for a license as +// a part of initial license issuance. +message LicenseCategorySpec { + // Possible license categories. + enum LicenseCategory { + // By default, License is used for single content. + SINGLE_CONTENT_LICENSE_DEFAULT = 0; + // License is used for multiple contents (could be a combination of + // single contents and groups of contents). + MULTI_CONTENT_LICENSE = 1; + // License is used for contents logically grouped. + GROUP_LICENSE = 2; + } + // Optional. License category indicates if license is used for single + // content, multiple contents (could be a combination of + // single contents and groups of contents) or a group of contents. + optional LicenseCategory license_category = 1; + // Optional. Content or group ID covered by the license. + oneof content_or_group_id { + // Content_id would be present if it is a license for single content. + bytes content_id = 2; + // Group_id would be present if the license is a multi_content_license or + // group_license. Group Id could be the name of a group of contents, + // defined by licensor. + bytes group_id = 3; + } +} + message License { message Policy { // Indicates that playback of the content is allowed. @@ -217,6 +245,27 @@ message License { optional bool allow_signature_verify = 4 [default = false]; } + // KeyCategorySpec message is used to identify if current key is generated + // for a single content or a group of contents. + message KeyCategorySpec { + // Represents what kind of content a key is used for. + enum KeyCategory { + // By default, key is created for single content. + SINGLE_CONTENT_KEY_DEFAULT = 0; + // Key is created for a group of contents. + GROUP_KEY = 1; + } + // Indicate if the current key is created for single content or for group + // use. + optional KeyCategory key_category = 1; + // Id for key category. If it is a key for single content, this id + // represents the content_id. Otherwise, it represents a group_id. + oneof content_or_group_id { + bytes content_id = 2; + bytes group_id = 3; + } + } + optional bytes id = 1; optional bytes iv = 2; optional bytes key = 3; @@ -242,6 +291,9 @@ message License { // Optional not limited to commonly known track types such as SD, HD. // It can be some provider defined label to identify the track. optional string track_label = 12; + // A Key Category Spec is used to identify if current key is generated for a + // single content or a group of contents. + optional KeyCategorySpec key_category_spec = 13; } optional LicenseIdentification id = 1; @@ -275,6 +327,10 @@ message License { [default = PLATFORM_NO_VERIFICATION]; // IDs of the groups for which keys are delivered in this license, if any. repeated bytes group_ids = 11; + // Optional. LicenseCategorySpec is used to indicate the license category for + // a license. It could be used as a part of initial license issuance or shown + // as a part of license in license response. + optional LicenseCategorySpec license_category_spec = 12; } enum ProtocolVersion { @@ -1038,6 +1094,15 @@ message CASDrmLicenseRequest { // Optionally specify even, odd or single slot for key rotation. repeated CASEncryptionResponse.KeyInfo entitlement_keys = 4; optional License.KeyContainer.KeyType key_type = 5; + // A track type is used to represent a set of tracks that share the same + // content key and security level. Common values are SD, HD, UHD1, UHD2 + // and AUDIO. Content providers may use arbitrary strings for track type + // as long as they are consistent with the track types used at packaging + // time. + optional string track_type = 6; + // A Key Category Spec is used to identify if current key is generated for a + // single content or a group of contents. + optional License.KeyContainer.KeyCategorySpec key_category_spec = 7; } repeated ContentKeySpec content_key_specs = 4; // Policy for the entire license such as playback duration. @@ -1053,6 +1118,10 @@ message CASEncryptionRequest { // return one key for EVEN and one key for ODD, otherwise only a single key is // returned. optional bool key_rotation = 4; + // Optional value which can be used to indicate a group. + // If present the CasEncryptionResponse will return key based on the group + // id. + optional bytes group_id = 5; } message CASEncryptionResponse { @@ -1084,6 +1153,9 @@ message CASEncryptionResponse { optional string status_message = 2; optional bytes content_id = 3; repeated KeyInfo entitlement_keys = 4; + // If keys shown in the encryption response are for group usage, this is the + // group identifier. + optional bytes group_id = 5; } message SignedCASEncryptionRequest { diff --git a/protos/media_cas.proto b/protos/media_cas.proto index ad715ec..79de913 100644 --- a/protos/media_cas.proto +++ b/protos/media_cas.proto @@ -15,6 +15,10 @@ 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; } // Widevine fingerprinting. @@ -76,6 +80,18 @@ message EcmKeyData { optional bytes content_iv = 4; } +message EcmGroupKeyData { + // Group id of this key data. + optional bytes group_id = 1; + // Required. The key data for the even slot. Fields wrapped_key_iv and + // content_iv may be omitted if it is the same as EcmPayload.even_key_data. + optional EcmKeyData even_key_data = 2; + // Optional. The key data for the odd slot if key rotation is enabled. Fields + // wrapped_key_iv and content_iv may be omitted if it is the same as + // EcmPayload.odd_key_data. + optional EcmKeyData odd_key_data = 3; +} + message EcmPayload { // Required. Meta info carried by the ECM. optional EcmMetaData meta_data = 1; @@ -87,6 +103,9 @@ message EcmPayload { optional Fingerprinting fingerprinting = 4; // Optional. Widevine service blocking information. optional ServiceBlocking service_blocking = 5; + // If a channel belongs to a group, the content keys can additionally be + // encrypted by the group entitlement keys. + repeated EcmGroupKeyData group_key_data = 6; } // The payload field for an ECM with signature. diff --git a/tests/src/cas_license_test.cpp b/tests/src/cas_license_test.cpp index 7a3b93e..8507e39 100644 --- a/tests/src/cas_license_test.cpp +++ b/tests/src/cas_license_test.cpp @@ -12,19 +12,11 @@ #include "cas_status.h" #include "cas_util.h" -#include "crypto_key.h" #include "device_files.pb.h" #include "license_protocol.pb.h" #include "mock_crypto_session.h" #include "string_conversions.h" -// Prototype for ExtractEntitlementKeys. This prototype is added here to allow -// this method to be unit tested without being added to CasLicense header. -namespace wvcas { -std::vector ExtractEntitlementKeys( - const video_widevine::License& license); -} // namespace wvcas - using ::testing::_; using ::testing::AllOf; using ::testing::DoAll; @@ -211,41 +203,6 @@ std::string CreateLicenseFileData() { return hashed_file.SerializeAsString(); } -TEST(CasLicenseUtilityTest, ExtractEntitlementKeys) { - video_widevine::License license; - - auto* key = license.add_key(); - key->set_type(video_widevine::License::KeyContainer::ENTITLEMENT); - key->set_id(kKeyIDVideo); - key->set_iv(kKeyVideoIV); - key->mutable_key_control()->set_key_control_block(kKeyControlVideo); - key->mutable_key_control()->set_iv(kKeyControlIVVideo); - key->set_track_label(kTrackTypeVideo); - - key = license.add_key(); - key->set_type(video_widevine::License::KeyContainer::ENTITLEMENT); - key->set_id(kKeyIDAudio); - key->set_iv(kKeyAudioIV); - key->mutable_key_control()->set_key_control_block(kKeyControlAudio); - key->mutable_key_control()->set_iv(kKeyControlIVAudio); - key->set_track_label(kTrackTypeAudio); - - std::vector keys = wvcas::ExtractEntitlementKeys(license); - ASSERT_EQ(2, keys.size()); - - EXPECT_EQ(kKeyIDVideo, keys[0].key_id()); - EXPECT_EQ(kKeyVideoIV, keys[0].key_data_iv()); - EXPECT_EQ(kKeyControlVideo, keys[0].key_control()); - EXPECT_EQ(kKeyControlIVVideo, keys[0].key_control_iv()); - EXPECT_EQ(kTrackTypeVideo, keys[0].track_label()); - - EXPECT_EQ(kKeyIDAudio, keys[1].key_id()); - EXPECT_EQ(kKeyAudioIV, keys[1].key_data_iv()); - EXPECT_EQ(kKeyControlAudio, keys[1].key_control()); - EXPECT_EQ(kKeyControlIVAudio, keys[1].key_control_iv()); - EXPECT_EQ(kTrackTypeAudio, keys[1].track_label()); -} - TEST_F(CasLicenseTest, GenerateDeviceProvisioningRequest) { strict_mock_ = std::make_shared(); TestCasLicense cas_license; @@ -409,24 +366,31 @@ TEST_F(CasLicenseTest, HandleEntitlementResponse) { EXPECT_CALL(*cas_license.policy_engine_, SetLicense(_)); EXPECT_CALL(*cas_license.policy_engine_, CanPersist()) .WillOnce(Return(false)); - status = cas_license.HandleEntitlementResponse(entitlement_response, nullptr); + 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. + EXPECT_TRUE(cas_license.GetGroupId().empty()); + EXPECT_TRUE(cas_license.GetContentIdList().empty()); + EXPECT_FALSE(cas_license.IsGroupLicense()); + EXPECT_FALSE(cas_license.IsMultiContentLicense()); // Valid with device file. EXPECT_CALL(*cas_license.policy_engine_, SetLicense(_)); EXPECT_CALL(*cas_license.policy_engine_, CanPersist()) .WillOnce(Return(false)); std::string device_file; - status = - cas_license.HandleEntitlementResponse(entitlement_response, &device_file); + status = cas_license.HandleEntitlementResponse( + entitlement_response, /*content_id_filter=*/nullptr, &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, &device_file); + status = cas_license.HandleEntitlementResponse( + entitlement_response, /*content_id_filter=*/nullptr, &device_file); EXPECT_FALSE(device_file.empty()); EXPECT_EQ(wvcas::CasStatusCode::kNoError, status.status_code()); @@ -632,6 +596,79 @@ 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) + cas_license + .HandleStoredLicense(wrapped_rsa_key_, license_file_data, + /*content_id_filter=*/nullptr) .status_code()); +} + +TEST_F(CasLicenseTest, HandleMultiContentEntitlementResponse) { + strict_mock_ = std::make_shared(); + TestCasLicense cas_license; + EXPECT_CALL(*cas_license.policy_engine_, initialize(_, _)); + EXPECT_EQ(wvcas::CasStatusCode::kNoError, + cas_license.initialize(strict_mock_, nullptr).status_code()); + EXPECT_CALL(*strict_mock_, GetOEMPublicCertificate(_, _)) + .WillOnce(Return(wvcas::CasStatusCode::kCryptoSessionError)); + EXPECT_CALL(*strict_mock_, APIVersion(_)) + .WillOnce(Return(wvcas::CasStatusCode::kNoError)); + EXPECT_CALL(*strict_mock_, GenerateNonce(_)) + .WillOnce(Return(wvcas::CasStatusCode::kNoError)); + EXPECT_CALL(*strict_mock_, + PrepareAndSignLicenseRequest(_, NotNull(), NotNull())) + .WillOnce(DoAll(SetArgPointee<2>(kExpectedSignature), + Return(wvcas::CasStatusCode::kNoError))); + EXPECT_CALL(*strict_mock_, LoadDeviceRSAKey(_, _)); + + std::string serialized_entitlement_request; + wvcas::CasStatus status = cas_license.GenerateEntitlementRequest( + kInitializationData, device_certificate_, wrapped_rsa_key_, + wvcas::LicenseType::kStreaming, &serialized_entitlement_request); + EXPECT_EQ(wvcas::CasStatusCode::kNoError, status.status_code()); + + // Create multi content entitlement response. + video_widevine::LicenseCategorySpec license_category_spec; + license_category_spec.set_license_category( + video_widevine::LicenseCategorySpec::MULTI_CONTENT_LICENSE); + license_category_spec.set_group_id("group_id"); + video_widevine::License license; + *license.mutable_license_category_spec() = license_category_spec; + video_widevine::License::KeyContainer::KeyCategorySpec key_category_spec; + key_category_spec.set_content_id("content_id_1"); + auto* key = license.add_key(); + key->set_type(video_widevine::License_KeyContainer::ENTITLEMENT); + *key->mutable_key_category_spec() = key_category_spec; + key = license.add_key(); + key->set_type(video_widevine::License_KeyContainer::ENTITLEMENT); + *key->mutable_key_category_spec() = key_category_spec; + key = license.add_key(); + key->set_type(video_widevine::License_KeyContainer::ENTITLEMENT); + key_category_spec.set_content_id("content_id_2"); + *key->mutable_key_category_spec() = key_category_spec; + + video_widevine::SignedMessage signed_message; + license.SerializeToString(signed_message.mutable_msg()); + signed_message.set_type(video_widevine::SignedMessage::CAS_LICENSE); + signed_message.set_signature(kExpectedSignature); + signed_message.set_session_key(kExpectedSignature); + signed_message.set_oemcrypto_core_message(kCoreMessage); + std::string entitlement_response = signed_message.SerializeAsString(); + + EXPECT_CALL(*strict_mock_, DeriveKeysFromSessionKey(_, _, _, _, _, _)) + .WillRepeatedly(Return(wvcas::CasStatusCode::kNoError)); + EXPECT_CALL(*strict_mock_, LoadLicense(_, _, _)) + .WillRepeatedly(Return(wvcas::CasStatusCode::kNoError)); + 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=*/nullptr); + EXPECT_EQ(status.status_code(), wvcas::CasStatusCode::kNoError); + // Not a group license. + EXPECT_EQ(cas_license.GetGroupId(), "group_id"); + EXPECT_THAT(cas_license.GetContentIdList(), + testing::ElementsAre("content_id_1", "content_id_2")); + EXPECT_FALSE(cas_license.IsGroupLicense()); + EXPECT_TRUE(cas_license.IsMultiContentLicense()); } \ No newline at end of file diff --git a/tests/src/crypto_session_test.cpp b/tests/src/crypto_session_test.cpp index 81f1549..6621f03 100644 --- a/tests/src/crypto_session_test.cpp +++ b/tests/src/crypto_session_test.cpp @@ -49,12 +49,6 @@ static const std::string kOddWrappedKeyIv("odd_wrapped_content_key_iv"); static const std::string kEvenContentIv("even_content_iv"); static const std::string kOddContentIv("odd_content_iv"); -// Defined in cas_license.cpp. -namespace wvcas { -extern std::vector ExtractKeyControlKeys( - const video_widevine::License& license); -} // namespace wvcas - // TODO(jfore): Add validation of arg->buffer based on type. Type is assumed to // be clear. MATCHER_P2(IsValidOutputBuffer, type, dest, "") { @@ -271,12 +265,6 @@ class MockedOEMCrypto : public wvcas::OEMCryptoInterface { const uint8_t* content_key_id, size_t content_key_id_length, OEMCryptoCipherMode cipher_mode)); - MOCK_METHOD7(OEMCrypto_RefreshKeys, - OEMCryptoResult(OEMCrypto_SESSION session, - const uint8_t* message, size_t message_length, - const uint8_t* signature, - size_t signature_length, size_t num_keys, - const OEMCrypto_KeyRefreshObject* key_array)); MOCK_METHOD2(OEMCrypto_GetDeviceID, OEMCryptoResult(uint8_t* deviceID, size_t* idLength)); MOCK_METHOD2(OEMCrypto_CreateEntitledKeySession, @@ -937,56 +925,35 @@ TEST_F(CryptoSessionTest, SelectKeys) { } } -TEST_F(CryptoSessionTest, RefreshKeys) { - TestCryptoSession > crypto_session( - strict_oemcrypto_interface_); - - EXPECT_CALL(strict_oemcrypto_interface_, OEMCrypto_Initialize()); - EXPECT_CALL(strict_oemcrypto_interface_, OEMCrypto_Terminate()); - EXPECT_CALL(strict_oemcrypto_interface_, OEMCrypto_OpenSession(_)) +TEST_F(CryptoSessionTest, LoadRenewal) { + TestCryptoSession > crypto_session( + nice_oemcrypto_interface_); + EXPECT_CALL(nice_oemcrypto_interface_, OEMCrypto_OpenSession(_)) .WillOnce( DoAll(SetArgPointee<0>(kOemcSessionId), Return(OEMCrypto_SUCCESS))); - - ASSERT_EQ(wvcas::CasStatusCode::kNoError, + EXPECT_EQ(wvcas::CasStatusCode::kNoError, crypto_session.initialize().status_code()); - video_widevine::License license; - - // Empty key array - no keys. - ASSERT_EQ(wvcas::CasStatusCode::kNoError, - crypto_session - .RefreshKeys(license.SerializeAsString(), "signature", - std::vector()) - .status_code()); - - auto* key_1 = license.add_key(); - key_1->set_type(video_widevine::License_KeyContainer::KEY_CONTROL); - auto* key_2 = license.add_key(); - key_2->set_type(video_widevine::License_KeyContainer::KEY_CONTROL); - auto* key_3 = license.add_key(); - key_3->set_type(video_widevine::License_KeyContainer::KEY_CONTROL); - - std::vector key_array = - wvcas::ExtractKeyControlKeys(license); - - EXPECT_CALL(strict_oemcrypto_interface_, - OEMCrypto_RefreshKeys(_, _, _, _, _, 3, _)) + const std::string signed_message("signed_message"); + const std::string core_message("core_message"); + const std::string signature("signature"); + EXPECT_CALL( + nice_oemcrypto_interface_, + OEMCrypto_LoadRenewal(kOemcSessionId, NotNull(), + signed_message.size() + core_message.size(), + core_message.size(), NotNull(), signature.size())) .WillOnce(Return(OEMCrypto_ERROR_SIGNATURE_FAILURE)) .WillOnce(Return(OEMCrypto_SUCCESS)); // OEMCrypto error. - ASSERT_EQ( - wvcas::CasStatusCode::kCryptoSessionError, - crypto_session - .RefreshKeys(license.SerializeAsString(), "signature", key_array) - .status_code()); + ASSERT_EQ(wvcas::CasStatusCode::kCryptoSessionError, + crypto_session.LoadRenewal(signed_message, core_message, signature) + .status_code()); // Valid. - ASSERT_EQ( - wvcas::CasStatusCode::kNoError, - crypto_session - .RefreshKeys(license.SerializeAsString(), "signature", key_array) - .status_code()); + ASSERT_EQ(wvcas::CasStatusCode::kNoError, + crypto_session.LoadRenewal(signed_message, core_message, signature) + .status_code()); } TEST_F(CryptoSessionTest, ReadUniqueId) { diff --git a/tests/src/ecm_parser_v2_test.cpp b/tests/src/ecm_parser_v2_test.cpp index a32464b..9b6d899 100644 --- a/tests/src/ecm_parser_v2_test.cpp +++ b/tests/src/ecm_parser_v2_test.cpp @@ -55,7 +55,7 @@ class EcmParserV2Test : public testing::Test { void BuildEcm(bool with_rotation, bool content_iv_flag); std::vector ecm_data_; - std::unique_ptr parser_; + std::unique_ptr parser_; }; size_t EcmParserV2Test::ContentKeyIVSize(bool content_iv_flag) { diff --git a/tests/src/ecm_parser_v3_test.cpp b/tests/src/ecm_parser_v3_test.cpp index 63555c6..4bd7aaa 100644 --- a/tests/src/ecm_parser_v3_test.cpp +++ b/tests/src/ecm_parser_v3_test.cpp @@ -14,6 +14,7 @@ namespace wvcas { namespace { +using video_widevine::EcmGroupKeyData; using video_widevine::EcmMetaData; using video_widevine::EcmPayload; using video_widevine::SignedEcmPayload; @@ -84,7 +85,7 @@ TEST(EcmParserV3Test, CreateWithEvenKeySuccess) { signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); std::vector ecm = GenerateEcm(signed_ecm_payload); - std::unique_ptr parser = EcmParserV3::Create(ecm); + std::unique_ptr parser = EcmParserV3::Create(ecm); ASSERT_TRUE(parser != nullptr); EXPECT_EQ(parser->version(), kEcmVersion); @@ -121,7 +122,7 @@ TEST(EcmParserV3Test, CreateWithEvenOddKeysSuccess) { signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); std::vector ecm = GenerateEcm(signed_ecm_payload); - std::unique_ptr parser = EcmParserV3::Create(ecm); + std::unique_ptr parser = EcmParserV3::Create(ecm); ASSERT_TRUE(parser != nullptr); EXPECT_TRUE(parser->rotation_enabled()); @@ -155,7 +156,7 @@ TEST(EcmParserV3Test, CreateWithOmittedOddKeyFieldsSuccess) { signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); std::vector ecm = GenerateEcm(signed_ecm_payload); - std::unique_ptr parser = EcmParserV3::Create(ecm); + std::unique_ptr parser = EcmParserV3::Create(ecm); ASSERT_TRUE(parser != nullptr); EXPECT_TRUE(parser->rotation_enabled()); @@ -187,7 +188,7 @@ TEST(EcmParserV3Test, AgeRestrictionSuccess) { signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); std::vector ecm = GenerateEcm(signed_ecm_payload); - std::unique_ptr parser = EcmParserV3::Create(ecm); + std::unique_ptr parser = EcmParserV3::Create(ecm); ASSERT_TRUE(parser != nullptr); EXPECT_EQ(parser->age_restriction(), expected_age_restriction); @@ -206,7 +207,7 @@ TEST_P(EcmParserV3AgeRestrictionTest, ExpectedAgeRestriction) { signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); std::vector ecm = GenerateEcm(signed_ecm_payload); - std::unique_ptr parser = EcmParserV3::Create(ecm); + std::unique_ptr parser = EcmParserV3::Create(ecm); ASSERT_TRUE(parser != nullptr); EXPECT_EQ(parser->age_restriction(), expected_age_restriction); @@ -229,7 +230,7 @@ TEST_P(EcmParserV3CipherModeTest, ExpectedCipherMode) { signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); std::vector ecm = GenerateEcm(signed_ecm_payload); - std::unique_ptr parser = EcmParserV3::Create(ecm); + std::unique_ptr parser = EcmParserV3::Create(ecm); ASSERT_TRUE(parser != nullptr); EXPECT_EQ(parser->crypto_mode(), expected); @@ -252,7 +253,7 @@ TEST(EcmParserV3Test, FingerprintingSuccess) { signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); std::vector ecm = GenerateEcm(signed_ecm_payload); - std::unique_ptr parser = EcmParserV3::Create(ecm); + std::unique_ptr parser = EcmParserV3::Create(ecm); ASSERT_TRUE(parser != nullptr); EXPECT_TRUE(parser->has_fingerprinting()); @@ -267,7 +268,7 @@ TEST(EcmParserV3Test, ServiceBlockingSuccess) { signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); std::vector ecm = GenerateEcm(signed_ecm_payload); - std::unique_ptr parser = EcmParserV3::Create(ecm); + std::unique_ptr parser = EcmParserV3::Create(ecm); ASSERT_TRUE(parser != nullptr); EXPECT_TRUE(parser->has_service_blocking()); @@ -281,11 +282,126 @@ TEST(EcmParserV3Test, SignatureSuccess) { signed_ecm_payload.set_signature(expected_signature); std::vector ecm = GenerateEcm(signed_ecm_payload); - std::unique_ptr parser = EcmParserV3::Create(ecm); + std::unique_ptr parser = EcmParserV3::Create(ecm); ASSERT_TRUE(parser != nullptr); EXPECT_EQ(parser->signature(), expected_signature); } +TEST(EcmParserV3Test, SetGroupIdSuccess) { + const std::string group_id = "group_id"; + const std::string group_id2 = "another_group"; + EcmPayload ecm_payload; + EcmGroupKeyData* group_key_data = ecm_payload.add_group_key_data(); + group_key_data->set_group_id(group_id); + group_key_data = ecm_payload.add_group_key_data(); + group_key_data->set_group_id(group_id2); + SignedEcmPayload signed_ecm_payload; + signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); + std::vector ecm = GenerateEcm(signed_ecm_payload); + + std::unique_ptr parser = EcmParserV3::Create(ecm); + + ASSERT_TRUE(parser != nullptr); + EXPECT_TRUE(parser->set_group_id(group_id)); + EXPECT_TRUE(parser->set_group_id(group_id2)); +} + +TEST(EcmParserV3Test, SetUnknownGroupIdFail) { + EcmPayload ecm_payload; + EcmGroupKeyData* group_key_data = ecm_payload.add_group_key_data(); + group_key_data->set_group_id("group_id"); + SignedEcmPayload signed_ecm_payload; + signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); + std::vector ecm = GenerateEcm(signed_ecm_payload); + + std::unique_ptr parser = EcmParserV3::Create(ecm); + + ASSERT_TRUE(parser != nullptr); + EXPECT_FALSE(parser->set_group_id("unknown")); +} + +TEST(EcmParserV3Test, ParserWithGroupIdSuccess) { + const std::string group_id = "group_id"; + EcmPayload ecm_payload; + EcmGroupKeyData* group_key_data = ecm_payload.add_group_key_data(); + group_key_data->set_group_id(group_id); + group_key_data->mutable_even_key_data()->set_entitlement_key_id( + kEntitlementId); + group_key_data->mutable_even_key_data()->set_wrapped_key_data( + kWrappedContentKey); + group_key_data->mutable_even_key_data()->set_content_iv(kContentIv); + group_key_data->mutable_even_key_data()->set_wrapped_key_iv(kWrappedKeyIv); + ecm_payload.mutable_even_key_data()->set_entitlement_key_id(kEntitlementId2); + ecm_payload.mutable_even_key_data()->set_wrapped_key_data( + kWrappedContentKey2); + ecm_payload.mutable_even_key_data()->set_content_iv(kContentIv2); + ecm_payload.mutable_even_key_data()->set_wrapped_key_iv(kWrappedKeyIv2); + + SignedEcmPayload signed_ecm_payload; + signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); + std::vector ecm = GenerateEcm(signed_ecm_payload); + + std::unique_ptr parser = EcmParserV3::Create(ecm); + + ASSERT_TRUE(parser != nullptr); + // If group Id is not set, the normal keys will be returned. + std::vector result = + parser->entitlement_key_id(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kEntitlementId2); + result = parser->wrapped_key_data(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kWrappedContentKey2); + result = parser->content_iv(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kContentIv2); + result = parser->wrapped_key_iv(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kWrappedKeyIv2); + + // Now set the group id. + EXPECT_TRUE(parser->set_group_id(group_id)); + result = parser->entitlement_key_id(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kEntitlementId); + result = parser->wrapped_key_data(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kWrappedContentKey); + result = parser->content_iv(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kContentIv); + result = parser->wrapped_key_iv(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kWrappedKeyIv); +} + +TEST(EcmParserV3Test, ParserGroupKeysWithOmittedFieldsSuccess) { + const std::string group_id = "group_id"; + EcmPayload ecm_payload; + EcmGroupKeyData* group_key_data = ecm_payload.add_group_key_data(); + group_key_data->set_group_id(group_id); + group_key_data->mutable_even_key_data()->set_entitlement_key_id( + kEntitlementId); + group_key_data->mutable_even_key_data()->set_wrapped_key_data( + kWrappedContentKey); + // Content IV and wrapped key iv is omitted in |group_key_data|/ + ecm_payload.mutable_even_key_data()->set_entitlement_key_id(kEntitlementId2); + ecm_payload.mutable_even_key_data()->set_wrapped_key_data( + kWrappedContentKey2); + ecm_payload.mutable_even_key_data()->set_content_iv(kContentIv2); + ecm_payload.mutable_even_key_data()->set_wrapped_key_iv(kWrappedKeyIv2); + SignedEcmPayload signed_ecm_payload; + signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); + std::vector ecm = GenerateEcm(signed_ecm_payload); + + std::unique_ptr parser = EcmParserV3::Create(ecm); + ASSERT_TRUE(parser != nullptr); + EXPECT_TRUE(parser->set_group_id(group_id)); + + std::vector result = + parser->entitlement_key_id(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kEntitlementId); + result = parser->wrapped_key_data(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kWrappedContentKey); + // Content IV and wrapped key iv are from normal non-group key. + result = parser->content_iv(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kContentIv2); + result = parser->wrapped_key_iv(KeySlotId::kEvenKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kWrappedKeyIv2); +} + } // namespace } // namespace wvcas diff --git a/tests/src/mediacas_integration_test.cpp b/tests/src/mediacas_integration_test.cpp index 095fb09..7131033 100644 --- a/tests/src/mediacas_integration_test.cpp +++ b/tests/src/mediacas_integration_test.cpp @@ -55,6 +55,11 @@ TEST(IntegrationTests, TestCasPluginEventPassing) { EXPECT_EQ(kIntegrationTestPassed, RunNamedTest("TestCasPluginEventPassing")); } +TEST(IntegrationTests, TestSessionFailWithoutProvisioning) { + EXPECT_EQ(kIntegrationTestPassed, + RunNamedTest("TestSessionFailWithoutProvisioning")); +} + TEST(IntegrationTests, TestUniqueIdQuery) { EXPECT_EQ(kIntegrationTestPassed, RunNamedTest("TestUniqueIdQuery")); } diff --git a/tests/src/widevine_cas_api_test.cpp b/tests/src/widevine_cas_api_test.cpp index 2d27011..1dc2e18 100644 --- a/tests/src/widevine_cas_api_test.cpp +++ b/tests/src/widevine_cas_api_test.cpp @@ -20,6 +20,7 @@ using ::testing::_; using ::testing::DoAll; using ::testing::NiceMock; +using ::testing::NotNull; using ::testing::Return; using ::testing::SetArgPointee; using ::testing::StrictMock; @@ -54,20 +55,27 @@ class MockLicense : public wvcas::CasLicense { const std::string& wrapped_rsa_key, wvcas::LicenseType license_type, std::string* signed_license_request)); - MOCK_METHOD2(HandleStoredLicense, + MOCK_METHOD3(HandleStoredLicense, wvcas::CasStatus(const std::string& wrapped_rsa_key, - const std::string& license_file)); + const std::string& license_file, + const std::string* content_id_filter)); 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_METHOD2(HandleEntitlementResponse, + MOCK_METHOD3(HandleEntitlementResponse, wvcas::CasStatus(const std::string& entitlement_response, + const std::string* content_id_filter, std::string* device_file)); MOCK_METHOD0(BeginDecryption, void()); MOCK_METHOD0(UpdateLicenseForLicenseRemove, void()); + MOCK_METHOD(std::string, GetGroupId, (), (const, override)); + MOCK_METHOD(std::vector, GetContentIdList, (), + (const, override)); + MOCK_METHOD(bool, IsMultiContentLicense, (), (const, override)); + MOCK_METHOD(bool, IsGroupLicense, (), (const, override)); }; typedef StrictMock StrictMockLicense; @@ -136,8 +144,10 @@ class MockWidevineSession : public wvcas::WidevineCasSession { public: MockWidevineSession() {} ~MockWidevineSession() override {} - MOCK_METHOD2(processEcm, wvcas::CasStatus(const wvcas::CasEcm& ecm, - uint8_t parental_control_age)); + MOCK_METHOD(wvcas::CasStatus, processEcm, + (const wvcas::CasEcm& ecm, uint8_t parental_control_age, + const std::string& license_group_id), + (override)); MOCK_METHOD2(HandleProcessEcm, wvcas::CasStatus(const wvcas::WvCasSessionId& sessionId, const wvcas::CasEcm& ecm)); @@ -238,6 +248,7 @@ TEST_F(WidevineCasTest, generateEntitlementRequest) { .WillOnce(Return(wvcas::CasStatus::OkStatus())); EXPECT_EQ(wvcas::CasStatusCode::kNoError, cas_api.initialize(nullptr).status_code()); + EXPECT_CALL(*cas_api.license_, IsGroupLicense).WillRepeatedly(Return(false)); // Invalid parameter. std::string request, init_data, license_id; @@ -267,7 +278,7 @@ TEST_F(WidevineCasTest, generateEntitlementRequest) { .WillRepeatedly(Return(mock_file)); EXPECT_CALL(*mock_file, Read(_, _)).WillRepeatedly(Return(mock_filesize)); - EXPECT_CALL(*cas_api.license_, HandleStoredLicense(_, _)) + EXPECT_CALL(*cas_api.license_, HandleStoredLicense) .WillRepeatedly(Return(wvcas::CasStatus( wvcas::CasStatusCode::kCasLicenseError, "forced failure"))); EXPECT_EQ(wvcas::CasStatusCode::kCasLicenseError, @@ -281,7 +292,7 @@ TEST_F(WidevineCasTest, generateEntitlementRequest) { EXPECT_CALL(*mock_file, Read(_, _)).WillRepeatedly(Return(mock_filesize)); // For expired license file, remove it successfully // and return CasLicenseError. - EXPECT_CALL(*cas_api.license_, HandleStoredLicense(_, _)) + EXPECT_CALL(*cas_api.license_, HandleStoredLicense) .WillRepeatedly(Return(wvcas::CasStatus::OkStatus())); EXPECT_CALL(*cas_api.license_, IsExpired()).WillRepeatedly(Return(true)); EXPECT_CALL(*cas_api.file_system_, Remove(_)).WillRepeatedly(Return(true)); @@ -442,6 +453,7 @@ TEST_P(WidevineCasTest, ECMProcessing) { EXPECT_CALL(*cas_api.license_, IsExpired()).WillRepeatedly(Return(false)); EXPECT_EQ(wvcas::CasStatusCode::kNoError, cas_api.initialize(nullptr).status_code()); + EXPECT_CALL(*cas_api.license_, IsGroupLicense).WillRepeatedly(Return(false)); wvcas::WvCasSessionId video_sid; wvcas::WvCasSessionId audio_sid; @@ -487,10 +499,10 @@ TEST_P(WidevineCasTest, ECMProcessing) { int expected_begin_decryption_calls = expected_process_ecm_calls * 2; EXPECT_CALL(*reinterpret_cast(video_session.get()), - processEcm(video_ecm, 0)) + processEcm(video_ecm, 0, "")) .Times(expected_process_ecm_calls); EXPECT_CALL(*reinterpret_cast(audio_session.get()), - processEcm(audio_ecm, 0)) + processEcm(audio_ecm, 0, "")) .Times(expected_process_ecm_calls); EXPECT_CALL(*cas_api.license_, BeginDecryption()) .Times(expected_begin_decryption_calls); @@ -507,7 +519,7 @@ TEST_P(WidevineCasTest, ECMProcessing) { .WillOnce(Return(file_handle)); EXPECT_CALL(*file_handle, Read(_, _)).WillOnce(Return(file_data.size())); - EXPECT_CALL(*cas_api.license_, HandleStoredLicense(_, _)); + EXPECT_CALL(*cas_api.license_, HandleStoredLicense); EXPECT_EQ( wvcas::CasStatusCode::kNoError, cas_api.generateEntitlementRequest("init_data", &request, license_id) @@ -520,12 +532,26 @@ TEST_P(WidevineCasTest, ECMProcessing) { license_id); } else { // Empty response. - std::string init_data; - EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse(_, _)) - .WillOnce(Return(wvcas::CasStatusCode::kNoError)); + // Initialize CaMediaId. + EXPECT_CALL(*cas_api.file_system_, Exists(_)).WillOnce(Return(false)); + EXPECT_CALL(*cas_api.license_, GenerateEntitlementRequest(_, _, _, _, _)) + .WillRepeatedly(Return(wvcas::CasStatus::OkStatus())); + std::string request, init_data, license_id; EXPECT_EQ( wvcas::CasStatusCode::kNoError, - cas_api.handleEntitlementResponse("response", init_data).status_code()); + cas_api.generateEntitlementRequest(init_data, &request, license_id) + .status_code()); + + EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse) + .WillOnce(Return(wvcas::CasStatusCode::kNoError)); + std::string multi_content_license_info; + std::string group_license_info; + EXPECT_EQ(wvcas::CasStatusCode::kNoError, + cas_api + .handleEntitlementResponse("response", init_data, + multi_content_license_info, + group_license_info) + .status_code()); } EXPECT_EQ(wvcas::CasStatusCode::kNoError, @@ -589,17 +615,217 @@ TEST_F(WidevineCasTest, RemoveLicense) { EXPECT_EQ(wvcas::CasStatusCode::kNoError, cas_api.RemoveLicense(mocked_file_name).status_code()); +} - // Happy case: remove the in used license file - std::string hash; - std::string kBasePathPrefix = "/data/vendor/mediacas/IDM/widevine/"; - hash.resize(SHA256_DIGEST_LENGTH); - const auto* input = - reinterpret_cast(mocked_file_name.data()); - auto* output = reinterpret_cast(&hash[0]); - SHA256(input, mocked_file_name.size(), output); - std::string full_filename = GenerateTestLicenseFileName(mocked_file_name); +TEST_F(WidevineCasTest, RemoveLicenseInUse) { + 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); + EXPECT_CALL(*cas_api.license_, IsExpired()).WillRepeatedly(Return(false)); + + // Initialize media_id_. + EXPECT_CALL(*cas_api.license_, GenerateEntitlementRequest) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + std::string request, init_data, license_id; + EXPECT_EQ(cas_api.generateEntitlementRequest(init_data, &request, license_id) + .status_code(), + wvcas::CasStatusCode::kNoError); + // Install the license + EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse(_, _, NotNull())) + .WillOnce(DoAll(SetArgPointee<2>("device_file"), + Return(wvcas::CasStatus::OkStatus()))); + EXPECT_CALL(*cas_api.license_, IsMultiContentLicense) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*cas_api.license_, IsGroupLicense).WillRepeatedly(Return(false)); + EXPECT_CALL(*cas_api.license_, GetGroupId).WillRepeatedly(Return("")); + std::string multi_content_license_info; + std::string group_license_info; + EXPECT_EQ(cas_api + .handleEntitlementResponse("response", license_id, + multi_content_license_info, + group_license_info) + .status_code(), + wvcas::CasStatusCode::kNoError); + EXPECT_FALSE(license_id.empty()); + + MockFile mock_file; + EXPECT_CALL(*cas_api.file_system_, Exists(_)).WillRepeatedly(Return(true)); + EXPECT_CALL(*cas_api.file_system_, Remove(_)).WillRepeatedly(Return(true)); EXPECT_CALL(*cas_api.license_, UpdateLicenseForLicenseRemove()).Times(1); + EXPECT_EQ(cas_api.RemoveLicense(license_id + ".lic").status_code(), + wvcas::CasStatusCode::kNoError); +} + +TEST_F(WidevineCasTest, handleMultiContentEntitlementResponse) { + 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); + EXPECT_CALL(*cas_api.license_, IsExpired()).WillRepeatedly(Return(false)); + + // Initialize media_id_. + EXPECT_CALL(*cas_api.license_, GenerateEntitlementRequest) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + std::string request, init_data, license_id; + EXPECT_EQ(cas_api.generateEntitlementRequest(init_data, &request, license_id) + .status_code(), + wvcas::CasStatusCode::kNoError); + + std::string license_group_id = "license_group_id"; + std::vector content_list = {"content1", "content2"}; + EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse(_, _, NotNull())) + .WillOnce(DoAll(SetArgPointee<2>("device_file"), + Return(wvcas::CasStatus::OkStatus()))); + EXPECT_CALL(*cas_api.license_, IsMultiContentLicense) + .WillRepeatedly(Return(true)); + EXPECT_CALL(*cas_api.license_, IsGroupLicense).WillRepeatedly(Return(false)); + EXPECT_CALL(*cas_api.license_, GetGroupId) + .WillRepeatedly(Return(license_group_id)); + EXPECT_CALL(*cas_api.license_, GetContentIdList) + .WillRepeatedly(Return(content_list)); + + std::string multi_content_license_info; + std::string group_license_info; + EXPECT_EQ(cas_api + .handleEntitlementResponse("response", license_id, + multi_content_license_info, + group_license_info) + .status_code(), + wvcas::CasStatusCode::kNoError); + + std::string expected_license_id = + GenerateTestLicenseFileName(license_group_id); + expected_license_id = expected_license_id.substr( + 0, expected_license_id.size() - strlen(kLicenseFileNameSuffix)); + EXPECT_EQ(license_id, expected_license_id); + + std::string expected_info; + expected_info.push_back(0); + expected_info.push_back(0); + expected_info.push_back(license_id.size()); + expected_info.append(license_id); + expected_info.push_back(1); + expected_info.push_back(0); + expected_info.push_back(content_list[0].size()); + expected_info.append(content_list[0]); + expected_info.push_back(1); + expected_info.push_back(0); + expected_info.push_back(content_list[1].size()); + expected_info.append(content_list[1]); + EXPECT_EQ(multi_content_license_info, expected_info); + EXPECT_TRUE(group_license_info.empty()); +} + +TEST_F(WidevineCasTest, handleGroupEntitlementResponse) { + 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); + EXPECT_CALL(*cas_api.license_, IsExpired()).WillRepeatedly(Return(false)); + + // Initialize media_id_. + EXPECT_CALL(*cas_api.license_, GenerateEntitlementRequest) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + std::string request, init_data, license_id; + EXPECT_EQ(cas_api.generateEntitlementRequest(init_data, &request, license_id) + .status_code(), + wvcas::CasStatusCode::kNoError); + + std::string license_group_id = "license_group_id"; + std::vector content_list = {}; + EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse(_, _, NotNull())) + .WillOnce(DoAll(SetArgPointee<2>("device_file"), + Return(wvcas::CasStatus::OkStatus()))); + EXPECT_CALL(*cas_api.license_, IsMultiContentLicense) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*cas_api.license_, IsGroupLicense).WillRepeatedly(Return(true)); + EXPECT_CALL(*cas_api.license_, GetGroupId) + .WillRepeatedly(Return(license_group_id)); + EXPECT_CALL(*cas_api.license_, GetContentIdList) + .WillRepeatedly(Return(content_list)); + + std::string multi_content_license_info; + std::string group_license_info; + EXPECT_EQ(cas_api + .handleEntitlementResponse("response", license_id, + multi_content_license_info, + group_license_info) + .status_code(), + wvcas::CasStatusCode::kNoError); + + std::string expected_license_id = + GenerateTestLicenseFileName(license_group_id); + expected_license_id = expected_license_id.substr( + 0, expected_license_id.size() - strlen(kLicenseFileNameSuffix)); + EXPECT_EQ(license_id, expected_license_id); + + std::string expected_info; + expected_info.push_back(0); + expected_info.push_back(0); + expected_info.push_back(license_id.size()); + expected_info.append(license_id); + expected_info.push_back(1); + expected_info.push_back(0); + expected_info.push_back(license_group_id.size()); + expected_info.append(license_group_id); + EXPECT_EQ(group_license_info, expected_info); + EXPECT_TRUE(multi_content_license_info.empty()); +} + +TEST_F(WidevineCasTest, ECMProcessingWithGroupId) { + 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); + EXPECT_CALL(*cas_api.license_, IsExpired()).WillRepeatedly(Return(false)); + + // Initialize media_id_. + EXPECT_CALL(*cas_api.license_, GenerateEntitlementRequest) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + std::string request, init_data, license_id; + EXPECT_EQ(cas_api.generateEntitlementRequest(init_data, &request, license_id) + .status_code(), + wvcas::CasStatusCode::kNoError); + // Install license + const std::string license_group_id = "license_group_id"; + EXPECT_CALL(*cas_api.license_, HandleEntitlementResponse) + .WillOnce(Return(wvcas::CasStatus::OkStatus())); + EXPECT_CALL(*cas_api.license_, IsMultiContentLicense) + .WillRepeatedly(Return(false)); + EXPECT_CALL(*cas_api.license_, IsGroupLicense).WillRepeatedly(Return(true)); + EXPECT_CALL(*cas_api.license_, GetGroupId) + .WillRepeatedly(Return(license_group_id)); + std::string multi_content_license_info; + std::string group_license_info; + EXPECT_EQ(cas_api + .handleEntitlementResponse("response", license_id, + multi_content_license_info, + group_license_info) + .status_code(), + wvcas::CasStatusCode::kNoError); + // Init a session + wvcas::WvCasSessionId sid; + EXPECT_CALL(*(cas_api.crypto_session_), CreateEntitledKeySession(_)) + .WillOnce( + DoAll(SetArgPointee<0>(10), Return(wvcas::CasStatus::OkStatus()))); EXPECT_EQ(wvcas::CasStatusCode::kNoError, - cas_api.RemoveLicense(full_filename).status_code()); + cas_api.openSession(&sid).status_code()); + wvcas::CasSessionPtr session = + wvcas::WidevineCasSessionMap::instance().GetSession(sid); + ASSERT_TRUE(session != nullptr); + const wvcas::CasEcm ecm = {1, 2, 3}; + // It is expected that process ecm with group_id + EXPECT_CALL(*reinterpret_cast(session.get()), + processEcm(ecm, 0, license_group_id)) + .Times(1); + EXPECT_CALL(*cas_api.license_, BeginDecryption()); + + EXPECT_EQ(cas_api.processEcm(sid, ecm).status_code(), + wvcas::CasStatusCode::kNoError); + EXPECT_CALL(*(cas_api.crypto_session_), RemoveEntitledKeySession(sid)); } \ No newline at end of file diff --git a/tests/src/widevine_cas_session_test.cpp b/tests/src/widevine_cas_session_test.cpp index 9eb7aef..f8ff5b5 100644 --- a/tests/src/widevine_cas_session_test.cpp +++ b/tests/src/widevine_cas_session_test.cpp @@ -40,6 +40,7 @@ static const char kOddWrappedKeyIv[] = "odd_wrapped_content_key_iv"; static const char kEvenContentIv[] = "even_content_iv"; static const char kOddContentIv[] = "odd_content_iv"; static const OEMCrypto_SESSION kEntitledKeySessionId = 0x1111; +constexpr char kEmptyGroupId[] = ""; MATCHER(IsValidKeyEvenSlotData, "") { if (nullptr == arg) { @@ -125,6 +126,7 @@ class MockEcmParser : public wvcas::EcmParser { std::vector(wvcas::KeySlotId id)); MOCK_CONST_METHOD1(wrapped_key_iv, std::vector(wvcas::KeySlotId id)); MOCK_CONST_METHOD1(content_iv, std::vector(wvcas::KeySlotId id)); + MOCK_METHOD(bool, set_group_id, (const std::string& group_id), (override)); MOCK_CONST_METHOD0(has_fingerprinting, bool()); MOCK_CONST_METHOD0(fingerprinting, video_widevine::Fingerprinting()); MOCK_CONST_METHOD0(has_service_blocking, bool()); @@ -145,7 +147,7 @@ class TestCasSession : public wvcas::WidevineCasSession { TestCasSession() {} virtual ~TestCasSession() {} - std::unique_ptr getEcmParser( + std::unique_ptr getEcmParser( const wvcas::CasEcm& ecm) const override; std::vector entitlement_key_id(wvcas::KeySlotId id) const { @@ -222,7 +224,7 @@ class TestCasSession : public wvcas::WidevineCasSession { video_widevine::ServiceBlocking service_blocking_; }; -std::unique_ptr TestCasSession::getEcmParser( +std::unique_ptr TestCasSession::getEcmParser( const wvcas::CasEcm& ecm) const { std::unique_ptr> mock_ecm_parser( new NiceMock); @@ -241,6 +243,7 @@ std::unique_ptr TestCasSession::getEcmParser( .WillByDefault(Invoke(this, &TestCasSession::wrapped_key_iv)); ON_CALL(*mock_ecm_parser, content_iv(_)) .WillByDefault(Invoke(this, &TestCasSession::content_iv)); + ON_CALL(*mock_ecm_parser, set_group_id(_)).WillByDefault(Return(true)); ON_CALL(*mock_ecm_parser, has_fingerprinting()) .WillByDefault(Return(fingerprinting_.has_control())); ON_CALL(*mock_ecm_parser, fingerprinting()) @@ -249,7 +252,7 @@ std::unique_ptr TestCasSession::getEcmParser( .WillByDefault(Return(service_blocking_.device_groups_size() > 0)); ON_CALL(*mock_ecm_parser, service_blocking()) .WillByDefault(Return(service_blocking_)); - return std::unique_ptr(mock_ecm_parser.release()); + return std::unique_ptr(mock_ecm_parser.release()); } TEST_F(CasSessionTest, processEcm) { @@ -269,7 +272,7 @@ TEST_F(CasSessionTest, processEcm) { wvcas::CasEcm ecm(184); EXPECT_CALL(*mock, LoadCasECMKeys(session_id, IsValidKeyEvenSlotData(), IsValidKeyOddSlotData())); - session.processEcm(ecm, 0); + session.processEcm(ecm, 0, kEmptyGroupId); EXPECT_CALL(*mock, RemoveEntitledKeySession(session_id)); } @@ -293,25 +296,25 @@ TEST_F(CasSessionTest, parentalControl) { // Different Ecm to make sure processEcm() processes this ecm. std::generate(ecm.begin(), ecm.end(), std::rand); ASSERT_EQ(wvcas::CasStatusCode::kNoError, - session.processEcm(ecm, 0).status_code()); + session.processEcm(ecm, 0, kEmptyGroupId).status_code()); std::generate(ecm.begin(), ecm.end(), std::rand); ASSERT_EQ(wvcas::CasStatusCode::kNoError, - session.processEcm(ecm, 13).status_code()); + session.processEcm(ecm, 13, kEmptyGroupId).status_code()); // Parental control age must >= 10 (if non-zero). session.set_age_restriction(10); std::generate(ecm.begin(), ecm.end(), std::rand); ASSERT_EQ(wvcas::CasStatusCode::kNoError, - session.processEcm(ecm, 0).status_code()); + session.processEcm(ecm, 0, kEmptyGroupId).status_code()); std::generate(ecm.begin(), ecm.end(), std::rand); ASSERT_EQ(wvcas::CasStatusCode::kNoError, - session.processEcm(ecm, 10).status_code()); + session.processEcm(ecm, 10, kEmptyGroupId).status_code()); std::generate(ecm.begin(), ecm.end(), std::rand); ASSERT_EQ(wvcas::CasStatusCode::kNoError, - session.processEcm(ecm, 13).status_code()); + session.processEcm(ecm, 13, kEmptyGroupId).status_code()); std::generate(ecm.begin(), ecm.end(), std::rand); ASSERT_EQ(wvcas::CasStatusCode::kAccessDeniedByParentalControl, - session.processEcm(ecm, 3).status_code()); + session.processEcm(ecm, 3, kEmptyGroupId).status_code()); EXPECT_CALL(*mock, RemoveEntitledKeySession(session_id)); } @@ -354,7 +357,7 @@ TEST_F(CasSessionTest, FingerprintingSuccess) { OnSessionFingerprintingUpdated(session_id, expected_message)) .Times(1); - session.processEcm(wvcas::CasEcm(184, '0'), 0); + session.processEcm(wvcas::CasEcm(184, '0'), 0, kEmptyGroupId); } TEST_F(CasSessionTest, RepeatedFingerprintingNoEventSuccess) { @@ -368,10 +371,10 @@ TEST_F(CasSessionTest, RepeatedFingerprintingNoEventSuccess) { session.set_fingerprinting_control("control"); EXPECT_CALL(mock_listener, OnSessionFingerprintingUpdated).Times(1); - session.processEcm(wvcas::CasEcm(184, '0'), 0); + session.processEcm(wvcas::CasEcm(184, '0'), 0, kEmptyGroupId); // Same fingerprinting will not trigger event. EXPECT_CALL(mock_listener, OnSessionFingerprintingUpdated).Times(0); - session.processEcm(wvcas::CasEcm(184, '1'), 0); + session.processEcm(wvcas::CasEcm(184, '1'), 0, kEmptyGroupId); } TEST_F(CasSessionTest, DifferentFingerprintingTriggerEventSuccess) { @@ -385,15 +388,15 @@ TEST_F(CasSessionTest, DifferentFingerprintingTriggerEventSuccess) { session.set_fingerprinting_control("control"); EXPECT_CALL(mock_listener, OnSessionFingerprintingUpdated).Times(1); - session.processEcm(wvcas::CasEcm(184, '0'), 0); + session.processEcm(wvcas::CasEcm(184, '0'), 0, kEmptyGroupId); // Different fingerprinting will trigger event. session.set_fingerprinting_control("control2"); EXPECT_CALL(mock_listener, OnSessionFingerprintingUpdated).Times(1); - session.processEcm(wvcas::CasEcm(184, '1'), 0); + session.processEcm(wvcas::CasEcm(184, '1'), 0, kEmptyGroupId); // Different fingerprinting (including empty) will trigger event. session.set_fingerprinting_control(""); EXPECT_CALL(mock_listener, OnSessionFingerprintingUpdated).Times(1); - session.processEcm(wvcas::CasEcm(184, '2'), 0); + session.processEcm(wvcas::CasEcm(184, '2'), 0, kEmptyGroupId); } TEST_F(CasSessionTest, ServiceBlockingSuccess) { @@ -412,7 +415,7 @@ TEST_F(CasSessionTest, ServiceBlockingSuccess) { OnSessionServiceBlockingUpdated(session_id, expected_message)) .Times(1); - session.processEcm(wvcas::CasEcm(184, '0'), 0); + session.processEcm(wvcas::CasEcm(184, '0'), 0, kEmptyGroupId); } TEST_F(CasSessionTest, RepeatedServiceBlockingNoEventSuccess) { @@ -426,9 +429,9 @@ TEST_F(CasSessionTest, RepeatedServiceBlockingNoEventSuccess) { session.set_service_blocking_groups({"Group1", "g2"}); EXPECT_CALL(mock_listener, OnSessionServiceBlockingUpdated).Times(1); - session.processEcm(wvcas::CasEcm(184, '0'), 0); + session.processEcm(wvcas::CasEcm(184, '0'), 0, kEmptyGroupId); EXPECT_CALL(mock_listener, OnSessionServiceBlockingUpdated).Times(0); - session.processEcm(wvcas::CasEcm(184, '1'), 0); + session.processEcm(wvcas::CasEcm(184, '1'), 0, kEmptyGroupId); } TEST_F(CasSessionTest, DifferentServiceBlockingTriggerEventSuccess) { @@ -442,13 +445,13 @@ TEST_F(CasSessionTest, DifferentServiceBlockingTriggerEventSuccess) { session.set_service_blocking_groups({"Group1", "g2"}); EXPECT_CALL(mock_listener, OnSessionServiceBlockingUpdated).Times(1); - session.processEcm(wvcas::CasEcm(184, '0'), 0); + session.processEcm(wvcas::CasEcm(184, '0'), 0, kEmptyGroupId); EXPECT_CALL(mock_listener, OnSessionServiceBlockingUpdated).Times(1); session.set_service_blocking_groups({"Group1"}); - session.processEcm(wvcas::CasEcm(184, '1'), 0); + session.processEcm(wvcas::CasEcm(184, '1'), 0, kEmptyGroupId); EXPECT_CALL(mock_listener, OnSessionServiceBlockingUpdated).Times(1); session.set_service_blocking_groups({}); - session.processEcm(wvcas::CasEcm(184, '2'), 0); + session.processEcm(wvcas::CasEcm(184, '2'), 0, kEmptyGroupId); } } // namespace diff --git a/tests/src/widevine_media_cas_plugin_test.cpp b/tests/src/widevine_media_cas_plugin_test.cpp new file mode 100644 index 0000000..e2dc5fa --- /dev/null +++ b/tests/src/widevine_media_cas_plugin_test.cpp @@ -0,0 +1,213 @@ +// Copyright 2021 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "widevine_media_cas_plugin.h" + +#include +#include +#include + +#include "cas_events.h" +#include "cas_status.h" +#include "media/cas/CasAPI.h" +#include "media/stagefright/MediaErrors.h" +#include "widevine_cas_api.h" + +namespace android { +// Minimalist implementation of Android string class to support test. +std::map > string8s; + +String8::String8(const String8& value) + : String8(value.c_str(), value.length()) {} + +String8::String8(char const* data, size_t data_length) { + auto result = + string8s.emplace(this, make_unique(data, data_length)); + mString = result.first->second->data(); +} + +size_t String8::length() const { + auto it = string8s.find(this); + return it == string8s.end() ? 0 : it->second->size(); +} + +String8::~String8() { string8s.erase(this); } + +} // namespace android + +namespace wvcas { +namespace { + +using ::testing::_; +using ::testing::DoAll; +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::NotNull; +using ::testing::Return; +using ::testing::SetArgPointee; +using ::testing::SetArgReferee; + +class MockWidevineCas : public WidevineCas { + public: + MockWidevineCas() {} + ~MockWidevineCas() override {} + + MOCK_METHOD(CasStatus, openSession, (WvCasSessionId * sessionId), (override)); + MOCK_METHOD(bool, is_provisioned, (), (const, override)); + MOCK_METHOD(CasStatus, generateEntitlementRequest, + (const std::string& init_data, std::string* entitlement_request, + std::string& license_id), + (override)); + MOCK_METHOD(CasStatus, RecordLicenseId, (const std::string& license_id), + (override)); + MOCK_METHOD(CasStatus, handleEntitlementResponse, + (const std::string& response, std::string& license_id, + std::string& multi_content_license_info, + std::string& group_license_info), + (override)); +}; + +// Override WidevineCasPlugin to set WidevineCas and mock callbacks. +class TestWidevineCasPlugin : public WidevineCasPlugin { + public: + TestWidevineCasPlugin() : WidevineCasPlugin() {} + ~TestWidevineCasPlugin() override {} + + void SetWidevineCasApi( + std::unique_ptr widevine_cas_api) override { + WidevineCasPlugin::SetWidevineCasApi(std::move(widevine_cas_api)); + } + + MOCK_METHOD(void, CallBack, + (void* appData, int32_t event, int32_t arg, uint8_t* data, + size_t size, const CasSessionId* sessionId), + (const, override)); +}; + +TEST(WidevineCasPluginTest, openSessionSuccess) { + TestWidevineCasPlugin plugin; + auto pass_through_cas_api = make_unique(); + MockWidevineCas* cas_api = pass_through_cas_api.get(); + plugin.SetWidevineCasApi(std::move(pass_through_cas_api)); + const int32_t created_session_id = 0x12345678; + const std::vector expected_android_session_id = {0x78, 0x56, 0x34, + 0x12}; + EXPECT_CALL(*cas_api, is_provisioned).WillOnce(Return(true)); + EXPECT_CALL(*cas_api, openSession(NotNull())) + .WillOnce(DoAll(SetArgPointee<0>(created_session_id), + Return(CasStatus::OkStatus()))); + EXPECT_CALL(plugin, CallBack(_, CAS_SESSION_ID, created_session_id, IsNull(), + 0, IsNull())); + std::vector session_id; + + EXPECT_EQ(plugin.openSession(&session_id), android::OK); + + EXPECT_EQ(session_id, expected_android_session_id); +} + +TEST(WidevineCasPluginTest, openSessionWithoutProvisionFail) { + TestWidevineCasPlugin plugin; + auto pass_through_cas_api = make_unique(); + MockWidevineCas* cas_api = pass_through_cas_api.get(); + plugin.SetWidevineCasApi(std::move(pass_through_cas_api)); + EXPECT_CALL(*cas_api, is_provisioned).WillOnce(Return(false)); + EXPECT_CALL(*cas_api, openSession(NotNull())).Times((0)); + std::vector session_id; + + EXPECT_EQ(plugin.openSession(&session_id), + android::ERROR_CAS_NOT_PROVISIONED); +} + +TEST(WidevineCasPluginTest, + provisionWithProvisionStringAlreadyProvisionedSuccess) { + TestWidevineCasPlugin plugin; + auto pass_through_cas_api = make_unique(); + MockWidevineCas* cas_api = pass_through_cas_api.get(); + plugin.SetWidevineCasApi(std::move(pass_through_cas_api)); + const std::string provision_string = "init_data"; + EXPECT_CALL(*cas_api, is_provisioned).WillOnce(Return(true)); + EXPECT_CALL(plugin, CallBack(_, INDIVIDUALIZATION_COMPLETE, _, _, _, _)); + // Provision string is init data; it triggers license request. + EXPECT_CALL(*cas_api, + generateEntitlementRequest(Eq(provision_string), NotNull(), _)) + .WillOnce(DoAll(SetArgPointee<1>("signed_license_request"), + Return(CasStatus::OkStatus()))); + EXPECT_CALL(plugin, CallBack(_, LICENSE_REQUEST, _, _, _, _)); + + const android::String8 provision_msg(); + + EXPECT_EQ(plugin.provision(android::String8(provision_string.c_str(), + provision_string.size())), + android::OK); +} + +TEST(WidevineCasPluginTest, HandleAssignLicenseIDSuccess) { + TestWidevineCasPlugin plugin; + auto pass_through_cas_api = make_unique(); + MockWidevineCas* cas_api = pass_through_cas_api.get(); + plugin.SetWidevineCasApi(std::move(pass_through_cas_api)); + const std::string license_id = "license_id"; + EXPECT_CALL(*cas_api, RecordLicenseId(license_id)) + .WillOnce(Return(CasStatus::OkStatus())); + EXPECT_CALL(plugin, CallBack(_, LICENSE_ID_ASSIGNED, _, _, _, _)).Times(1); + + EXPECT_EQ(plugin.sendEvent(ASSIGN_LICENSE_ID, /*arg=*/0, + {license_id.begin(), license_id.end()}), + android::OK); +} + +TEST(WidevineCasPluginTest, HandleAssignLicenseIDApiError) { + TestWidevineCasPlugin plugin; + auto pass_through_cas_api = make_unique(); + MockWidevineCas* cas_api = pass_through_cas_api.get(); + plugin.SetWidevineCasApi(std::move(pass_through_cas_api)); + const std::string license_id = "license_id"; + EXPECT_CALL(*cas_api, RecordLicenseId) + .WillOnce(Return(CasStatus(CasStatusCode::kInvalidParameter, "invalid"))); + EXPECT_CALL(plugin, CallBack(_, CAS_ERROR, _, _, _, _)).Times(1); + + EXPECT_NE(plugin.sendEvent(ASSIGN_LICENSE_ID, /*arg=*/0, + {license_id.begin(), license_id.end()}), + android::OK); +} + +TEST(WidevineCasPluginTest, HandleEntitlementResponseSuccess) { + TestWidevineCasPlugin plugin; + auto pass_through_cas_api = make_unique(); + MockWidevineCas* cas_api = pass_through_cas_api.get(); + plugin.SetWidevineCasApi(std::move(pass_through_cas_api)); + const std::string license = "license"; + const std::string license_id = "id"; + const std::string multi_content_license_info = "info"; + const std::string group_license_info = "info2"; + EXPECT_CALL(*cas_api, handleEntitlementResponse(_, _, _, _)) + .WillOnce(DoAll(SetArgReferee<1>(license_id), + SetArgReferee<2>(multi_content_license_info), + SetArgReferee<3>(group_license_info), + Return(CasStatus::OkStatus()))); + EXPECT_CALL(plugin, + CallBack(_, LICENSE_CAS_READY, _, _, license_id.size(), _)) + .Times(1); + EXPECT_CALL(plugin, CallBack(_, MULTI_CONTENT_LICENSE_INFO, _, _, + multi_content_license_info.size(), _)) + .Times(1); + EXPECT_CALL(plugin, CallBack(_, GROUP_LICENSE_INFO, _, _, + group_license_info.size(), _)) + .Times(1); + + EXPECT_EQ(plugin.sendEvent(LICENSE_RESPONSE, /*arg=*/0, + {license.begin(), license.end()}), + android::OK); +} + +TEST(WidevineCasPluginTest, HandleEntitlementResponseEmptyResponseFail) { + TestWidevineCasPlugin plugin; + EXPECT_CALL(plugin, CallBack(_, CAS_ERROR, _, _, _, _)).Times(1); + + EXPECT_NE(plugin.sendEvent(LICENSE_RESPONSE, /*arg=*/0, /*eventData=*/{}), + android::OK); +} + +} // namespace +} // namespace wvcas \ No newline at end of file