diff --git a/oemcrypto/include/OEMCryptoCAS.h b/oemcrypto/include/OEMCryptoCAS.h index 7d1852e..8fc3220 100644 --- a/oemcrypto/include/OEMCryptoCAS.h +++ b/oemcrypto/include/OEMCryptoCAS.h @@ -268,10 +268,10 @@ typedef struct { * padding. * entitlement_key_id - entitlement key id to be matched to key table. * content_key_id - content key id to be loaded into key table. - * content_key_data_iv - the IV for performing AES-256-CBC decryption of the key data. - * content_key_data - encrypted content key data. - * content_iv - the 16 byte iv used to decrypt content. - * cipher_mode - the cipher mode to be used to decrypt the content. + * content_key_data_iv - the IV for performing AES-256-CBC decryption of the key + * data. content_key_data - encrypted content key data. content_iv - the 16 byte + * iv used to decrypt content. cipher_mode - the cipher mode to be used to + * decrypt the content. */ typedef struct { OEMCrypto_Substring entitlement_key_id; @@ -1737,42 +1737,44 @@ OEMCryptoResult OEMCrypto_LoadEntitledContentKeys( * OEMCrypto_LoadCasECMKeys * * Description: - * Load content keys into a session which already has entitlement - * keys loaded. This function will only be called for a session after a call - * to OEMCrypto_LoadKeys with the parameter type license_type equal to - * OEMCrypto_EntitlementLicense. This function may be called multiple times - * for the same session. + * The OEMCrypto_LoadCasECMKeys method is added to load content keys into an + * entitled key session, which already has entitlement keys loaded. * - * If the session does not have license_type equal to - * OEMCrypto_EntitlementLicense, return OEMCrypto_ERROR_INVALID_CONTEXT and - * perform no work. + * This function will only be called for a session after a call to + * OEMCrypto_LoadKeys with the license_type equal to + * OEMCrypto_EntitlementLicense, and a call to + * OEMCrypto_CreateEntitledKeySession initializing the entitled key session. + * This function may be called multiple times for the same session. * - * For each key object in key_array, OEMCrypto shall look up the entry in the - * key table with the corresponding entitlement_key_id. + * For each key object, odd and even, OEMCrypto shall look up the entry in the + * key table with the corresponding entitlement_key_id. Before the + * entitlement_key is used: * 1) If no entry is found, return OEMCrypto_KEY_NOT_ENTITLED. - * 2) If the entry already has a content_key_id and content_key_data, that id - * and data are erased. - * 3) The content_key_id from the key_array is copied to the entry's - * content_key_id. + * 2) Check the entitlement key’s key control block use. If failed, return + * corresponding error code such as OEMCrypto_ERROR_ANALOG_OUTPUT, + * OEMCrypto_ERROR_INSUFFICIENT_HDCP. + * 3) If the entitlement key’s control block has a nonzero Duration field, + * then the API shall verify that the duration is greater than the + * session’s elapsed time clock before the key is used. OEMCrypto will + * return OEMCrypto_ERROR_KEY_EXPIRED. * 4) The content_key_data decrypted using the entitlement_key_data as a key - * for AES-256-CBC with an IV of content_key_data_iv, and using PKCS#7 - * padding. Notice that the entitlement key will be an AES 256 bit key. - * The clear content key data will be stored in the entry's - * content_key_data. - * 5) The decrypted content key data may be set in a hardware descrambler - * if present. - * - * Entries in the key table that do not correspond to anything in the - * key_array are not modified or removed. - * - * For devices that use a hardware key ladder, it may be more appropriate to - * store the encrypted content key data in the key table, and defer decrypting - * it until the function SelectKey is called. + * for AES-256-CBC with an IV of content_key_data_iv. Wrapped content is + * padded using PKCS#7 padding. Notice that the entitlement key will be an + * AES 256 bit key. The clear content key data will be stored in the + * entry’s content_key_data. + * 5) The decrypted content key data may be set in a hardware KeySlot, + * together with content iv and cipher mode information, which can be used + * by the Descrambler in TunerHal. The entitled key session ID may be used + * as the key token to uniquely identify the content key in KeySlot. * * Parameters: - * session (in) - handle for the session to be used. - * even_key (in) - key update for the even ecm key. - * odd_key (in) - key update for the odd ecm key. + * [in] session: handle for the entitled key session to be used. + * [in] message: pointer to memory containing message to be verified. + * [in] message_length: length of the message, in bytes. + * [in] even_key: key update for the even ecm key. May be null if the key + * does not change. + * [in] odd_key: key update for the odd ecm key. May be null if the key does + * not change. * * Returns * OEMCrypto_SUCCESS success @@ -1781,6 +1783,11 @@ OEMCryptoResult OEMCrypto_LoadEntitledContentKeys( * OEMCrypto_ERROR_INSUFFICIENT_RESOURCES * OEMCrypto_ERROR_UNKNOWN_FAILURE * OEMCrypto_KEY_NOT_ENTITLED + * OEMCrypto_ERROR_INVALID_ENTITLED_KEY_SESSION + * OEMCrypto_ERROR_KEY_EXPIRED + * OEMCrypto_ERROR_ANALOG_OUTPUT + * OEMCrypto_ERROR_INSUFFICIENT_HDCP + * * Threading * * This function may be called simultaneously with functions on other diff --git a/oemcrypto/odk/test/fuzzing/Android.bp b/oemcrypto/odk/test/fuzzing/Android.bp index d6b1f2a..f1c7d25 100644 --- a/oemcrypto/odk/test/fuzzing/Android.bp +++ b/oemcrypto/odk/test/fuzzing/Android.bp @@ -3,7 +3,7 @@ // License Agreement. cc_defaults { - name: "odk_fuzz_library_defaults", + name: "cas_odk_fuzz_library_defaults", srcs: [ "odk_fuzz_helper.cpp", ], @@ -15,7 +15,7 @@ cc_defaults { } cc_fuzz { - name: "odk_license_request_fuzz", + name: "cas_odk_license_request_fuzz", srcs: [ "odk_license_request_fuzz.cpp", ], @@ -24,15 +24,15 @@ cc_fuzz { }, corpus: ["corpus/little_endian_64bit/license_request_corpus/*"], static_libs: [ - "libwv_kdo", - "libwv_odk", + "libwvcas_kdo", + "libwvcas_odk", ], - defaults: ["odk_fuzz_library_defaults"], + defaults: ["cas_odk_fuzz_library_defaults"], proprietary: true, } cc_fuzz { - name: "odk_renewal_request_fuzz", + name: "cas_odk_renewal_request_fuzz", srcs: [ "odk_renewal_request_fuzz.cpp", ], @@ -41,15 +41,15 @@ cc_fuzz { }, corpus: ["corpus/little_endian_64bit/renewal_request_corpus/*"], static_libs: [ - "libwv_kdo", - "libwv_odk", + "libwvcas_kdo", + "libwvcas_odk", ], - defaults: ["odk_fuzz_library_defaults"], + defaults: ["cas_odk_fuzz_library_defaults"], proprietary: true, } cc_fuzz { - name: "odk_provisioning_request_fuzz", + name: "cas_odk_provisioning_request_fuzz", srcs: [ "odk_provisioning_request_fuzz.cpp", ], @@ -58,15 +58,15 @@ cc_fuzz { }, corpus: ["corpus/little_endian_64bit/provisioning_request_corpus/*"], static_libs: [ - "libwv_kdo", - "libwv_odk", + "libwvcas_kdo", + "libwvcas_odk", ], - defaults: ["odk_fuzz_library_defaults"], + defaults: ["cas_odk_fuzz_library_defaults"], proprietary: true, } cc_fuzz { - name: "odk_license_response_fuzz", + name: "cas_odk_license_response_fuzz", srcs: [ "odk_license_response_fuzz.cpp", ], @@ -75,15 +75,15 @@ cc_fuzz { }, corpus: ["corpus/little_endian_64bit/license_response_corpus/*"], static_libs: [ - "libwv_kdo", - "libwv_odk", + "libwvcas_kdo", + "libwvcas_odk", ], - defaults: ["odk_fuzz_library_defaults"], + defaults: ["cas_odk_fuzz_library_defaults"], proprietary: true, } cc_fuzz { - name: "odk_renewal_response_fuzz", + name: "cas_odk_renewal_response_fuzz", srcs: [ "odk_renewal_response_fuzz.cpp", ], @@ -92,15 +92,15 @@ cc_fuzz { }, corpus: ["corpus/little_endian_64bit/renewal_response_corpus/*"], static_libs: [ - "libwv_kdo", - "libwv_odk", + "libwvcas_kdo", + "libwvcas_odk", ], - defaults: ["odk_fuzz_library_defaults"], + defaults: ["cas_odk_fuzz_library_defaults"], proprietary: true, } cc_fuzz { - name: "odk_provisioning_response_fuzz", + name: "cas_odk_provisioning_response_fuzz", srcs: [ "odk_provisioning_response_fuzz.cpp", ], @@ -109,15 +109,15 @@ cc_fuzz { }, corpus: ["corpus/little_endian_64bit/provisioning_response_corpus/*"], static_libs: [ - "libwv_kdo", - "libwv_odk", + "libwvcas_kdo", + "libwvcas_odk", ], - defaults: ["odk_fuzz_library_defaults"], + defaults: ["cas_odk_fuzz_library_defaults"], proprietary: true, } cc_fuzz { - name: "odk_license_response_fuzz_with_mutator", + name: "cas_odk_license_response_fuzz_with_mutator", srcs: [ "odk_license_response_fuzz_with_mutator.cpp", ], @@ -126,15 +126,15 @@ cc_fuzz { }, corpus: ["corpus/little_endian_64bit/license_response_corpus/*"], static_libs: [ - "libwv_kdo", - "libwv_odk", + "libwvcas_kdo", + "libwvcas_odk", ], - defaults: ["odk_fuzz_library_defaults"], + defaults: ["cas_odk_fuzz_library_defaults"], proprietary: true, } cc_fuzz { - name: "odk_renewal_response_fuzz_with_mutator", + name: "cas_odk_renewal_response_fuzz_with_mutator", srcs: [ "odk_renewal_response_fuzz_with_mutator.cpp", ], @@ -143,15 +143,15 @@ cc_fuzz { }, corpus: ["corpus/little_endian_64bit/renewal_response_corpus/*"], static_libs: [ - "libwv_kdo", - "libwv_odk", + "libwvcas_kdo", + "libwvcas_odk", ], - defaults: ["odk_fuzz_library_defaults"], + defaults: ["cas_odk_fuzz_library_defaults"], proprietary: true, } cc_fuzz { - name: "odk_provisioning_response_fuzz_with_mutator", + name: "cas_odk_provisioning_response_fuzz_with_mutator", srcs: [ "odk_provisioning_response_fuzz_with_mutator.cpp", ], @@ -160,9 +160,9 @@ cc_fuzz { }, corpus: ["corpus/little_endian_64bit/provisioning_response_corpus/*"], static_libs: [ - "libwv_kdo", - "libwv_odk", + "libwvcas_kdo", + "libwvcas_odk", ], - defaults: ["odk_fuzz_library_defaults"], + defaults: ["cas_odk_fuzz_library_defaults"], proprietary: true, } \ No newline at end of file diff --git a/oemcrypto/odk/test/fuzzing/corpus_generator/Android.bp b/oemcrypto/odk/test/fuzzing/corpus_generator/Android.bp index cdd672e..5ff757f 100644 --- a/oemcrypto/odk/test/fuzzing/corpus_generator/Android.bp +++ b/oemcrypto/odk/test/fuzzing/corpus_generator/Android.bp @@ -10,7 +10,7 @@ // Builds libwv_odk.so, The ODK shared Library (libwv_odk) is used // by the OEMCrypto unit tests to generate corpus for ODK fuzz scrips. cc_library_shared { - name: "libwv_odk_corpus_generator", + name: "libwvcas_odk_corpus_generator", include_dirs: [ "vendor/widevine/libwvmediacas/oemcrypto/include", "vendor/widevine/libwvmediacas/oemcrypto/odk/include", diff --git a/oemcrypto/ref/src/oemcrypto_entitled_key_session.cpp b/oemcrypto/ref/src/oemcrypto_entitled_key_session.cpp index b1d3ff1..8d307d8 100644 --- a/oemcrypto/ref/src/oemcrypto_entitled_key_session.cpp +++ b/oemcrypto/ref/src/oemcrypto_entitled_key_session.cpp @@ -4,22 +4,87 @@ #include "oemcrypto_entitled_key_session.h" +#include +#include +#include +#include + +#include +#include + +#include "log.h" + namespace wvoec_ref { +namespace { +constexpr size_t kContentKeySize = 16; +constexpr size_t kContentIvSize = 16; + +// Returns the open file descriptor. +int OpenHardwareKeySlot() { + // Must be hardware location in production, instead of a file that is publicly + // visible. + // Not able to create (permission denied). The file must already exist. + int fd = open("/data/local/tmp/wv", O_RDWR); + if (fd < 0) { + LOGE("File for shared memory open failed: %d", errno); + } + return fd; +} + +// Returns true if the lock is success. +bool LockHardwareKeySlot(int fd) { + flock lock; + lock.l_type = F_WRLCK; /* read/write (exclusive versus shared) lock */ + lock.l_whence = SEEK_SET; /* base for seek offsets */ + lock.l_start = 0; /* 1st byte in file */ + lock.l_len = 0; /* 0 here means 'until EOF' */ + lock.l_pid = getpid(); /* process id */ + + // Wait until we get the lock. + if (fcntl(fd, F_SETLKW, &lock) < 0) { + LOGE("Failed to lock file: %d", errno); + return false; + } + return true; +} + +void UnlockAndCloseHardwareKeySlot(int fd) { + if (fd < 0) { + return; + } + flock lock; + lock.l_type = F_UNLCK; + lock.l_whence = SEEK_SET; /* base for seek offsets */ + lock.l_start = 0; /* 1st byte in file */ + lock.l_len = 0; /* 0 here means 'until EOF' */ + lock.l_pid = getpid(); /* process id */ + if (fcntl(fd, F_SETLK, &lock) < 0) { + LOGE("explicit unlocking failed."); + } + close(fd); /* close the file: would unlock if needed */ +} + +} // namespace + +EntitledKeySession::~EntitledKeySession() { ClearKeySlot(); } + const Key* EntitledKeySession::GetEntitlementKey( const KeyId& content_key_id) const { - if (entitlement_key_map_.find(content_key_id) == entitlement_key_map_.end()) { + if (content_key_info_map_.find(content_key_id) == + content_key_info_map_.end()) { return nullptr; } - return entitlement_key_map_.at(content_key_id); + return content_key_info_map_.at(content_key_id)->entitlement_key; } EntitledKey* EntitledKeySession::GetContentKey( const KeyId& content_key_id) const { - if (content_key_map_.find(content_key_id) == content_key_map_.end()) { + if (content_key_info_map_.find(content_key_id) == + content_key_info_map_.end()) { return nullptr; } - return content_key_map_.at(content_key_id).get(); + return content_key_info_map_.at(content_key_id)->content_key.get(); } bool EntitledKeySession::AddOrUpdateContentKey( @@ -29,32 +94,151 @@ bool EntitledKeySession::AddOrUpdateContentKey( content_key == nullptr) { return false; } - // Remove the entry if |entitlement_key| already exists. Each entitlement key - // can only be referred by one content key within an entitled key session. - for (auto const& content_id_entitlement : entitlement_key_map_) { - if (content_id_entitlement.second == entitlement_key) { - RemoveContentKey(content_id_entitlement.first); + // Remove the entry if |entitlement_key| already referenced by a content key + // with the same parity. Each entitlement key can only be referred by one + // even and one odd content key within an entitled key session. + for (auto const& content_key_entry : content_key_info_map_) { + const ContentKeyInfo* const key_info = content_key_entry.second.get(); + if (key_info->entitlement_key == entitlement_key && + key_info->content_key->is_even_key() == content_key->is_even_key()) { + RemoveContentKey(content_key_entry.first); break; } } // |content_key_id| must be unique. - if (content_key_map_.find(content_key_id) != content_key_map_.end()) { + if (content_key_info_map_.find(content_key_id) != + content_key_info_map_.end()) { return false; } - content_key_map_[content_key_id] = std::move(content_key); - entitlement_key_map_[content_key_id] = entitlement_key; + + content_key_info_map_[content_key_id] = + std::unique_ptr(new ContentKeyInfo()); + content_key_info_map_[content_key_id]->content_key = std::move(content_key); + content_key_info_map_[content_key_id]->entitlement_key = entitlement_key; return true; } bool EntitledKeySession::RemoveContentKey(const KeyId& content_key_id) { - if (content_key_map_.find(content_key_id) == content_key_map_.end()) { + if (content_key_info_map_.find(content_key_id) == + content_key_info_map_.end()) { return false; } - content_key_map_.erase(content_key_id); - entitlement_key_map_.erase(content_key_id); + content_key_info_map_.erase(content_key_id); return true; } +EntitledKeySession::ContentKeySlot* EntitledKeySession::GetKeySlotToUpdate( + uint8_t* buffer) const { + if (buffer == nullptr) { + return nullptr; + } + + // Read the current content to know where to write. + auto content_key_slots = reinterpret_cast(buffer); + int use_index = -1; + for (size_t i = 0; i < sizeof(ContentKeySlots) / sizeof(ContentKeySlot); + ++i) { + const ContentKeySlot& current_key_slot = content_key_slots->key_slots[i]; + // Use the existing slot, or the first available slot if there is no + // existing one. + if (current_key_slot.entitled_key_session_id == key_sid_) { + use_index = i; + break; + } + if (current_key_slot.entitled_key_session_id == 0 && use_index < 0) { + use_index = i; + } + } + if (use_index < 0) { + LOGE("No key slot available to write keys."); + return nullptr; + } + return &content_key_slots->key_slots[use_index]; +} + +bool EntitledKeySession::WriteContentKeyToKeySlot( + const KeyId& content_key_id, ContentKeySlot* key_slot) const { + if (key_slot == nullptr) { + LOGE("key_slot is null"); + return false; + } + EntitledKey* entitled_key = GetContentKey(content_key_id); + if (entitled_key == nullptr) { + LOGE("Content key is not found."); + return false; + } + + key_slot->entitled_key_session_id = key_sid_; + key_slot->cipher_mode = static_cast(entitled_key->cipher_mode()); + memcpy(entitled_key->is_even_key() ? &key_slot->even_key[0] + : &key_slot->odd_key[0], + entitled_key->value().data(), + std::min(entitled_key->value().size(), kContentKeySize)); + memcpy(entitled_key->is_even_key() ? &key_slot->even_iv[0] + : &key_slot->odd_iv[0], + entitled_key->content_iv().data(), + std::min(entitled_key->content_iv().size(), kContentIvSize)); + msync(reinterpret_cast(key_slot), sizeof(ContentKeySlot), MS_SYNC); + return true; +} + +bool EntitledKeySession::WriteToKeySlot(const KeyId& content_key_id) const { + bool result = false; + const int fd = OpenHardwareKeySlot(); + if (fd < 0) { + return result; + } + if (!LockHardwareKeySlot(fd)) { + close(fd); + return result; + } + ftruncate(fd, sizeof(ContentKeySlots)); + uint8_t* buffer = static_cast(mmap(nullptr, sizeof(ContentKeySlots), + PROT_READ | PROT_WRITE, + MAP_SHARED, fd, /*offset=*/0)); + if (buffer == MAP_FAILED) { + LOGE("Fail to get mmap buffer %d", errno); + close(fd); + return result; + } + + ContentKeySlot* key_slot = GetKeySlotToUpdate(buffer); + if (key_slot != nullptr) { + result = WriteContentKeyToKeySlot(content_key_id, key_slot); + } + munmap(reinterpret_cast(buffer), sizeof(ContentKeySlots)); + UnlockAndCloseHardwareKeySlot(fd); + return result; +} + +void EntitledKeySession::ClearKeySlot() const { + const int fd = OpenHardwareKeySlot(); + if (fd < 0) { + return; + } + if (!LockHardwareKeySlot(fd)) { + close(fd); + return; + } + ftruncate(fd, sizeof(ContentKeySlots)); + uint8_t* buffer = static_cast(mmap(nullptr, sizeof(ContentKeySlots), + PROT_READ | PROT_WRITE, + MAP_SHARED, fd, /*offset=*/0)); + if (buffer == MAP_FAILED) { + LOGE("Fail to get mmap buffer %d", errno); + close(fd); + return; + } + + ContentKeySlot* key_slot = GetKeySlotToUpdate(buffer); + if (key_slot != nullptr) { + memset(key_slot, 0, sizeof(ContentKeySlot)); + msync(reinterpret_cast(key_slot), sizeof(ContentKeySlot), MS_SYNC); + } + munmap(reinterpret_cast(buffer), sizeof(ContentKeySlots)); + UnlockAndCloseHardwareKeySlot(fd); +} + /***************************************/ EntitledKeySession* EntitledKeySessionTable::CreateEntitledKeySession( diff --git a/oemcrypto/ref/src/oemcrypto_entitled_key_session.h b/oemcrypto/ref/src/oemcrypto_entitled_key_session.h index 5c0a5cf..fe1b2df 100644 --- a/oemcrypto/ref/src/oemcrypto_entitled_key_session.h +++ b/oemcrypto/ref/src/oemcrypto_entitled_key_session.h @@ -28,7 +28,7 @@ class EntitledKeySession { current_entitlement_key_(nullptr){}; EntitledKeySession(const EntitledKeySession&) = delete; EntitledKeySession(EntitledKeySession&&) = delete; - ~EntitledKeySession() = default; + ~EntitledKeySession(); // Get id of this entitled key session . SessionId GetSessionId() const { return key_sid_; } @@ -55,17 +55,47 @@ class EntitledKeySession { // Remove a content key |content_key_id|. bool RemoveContentKey(const KeyId& content_key_id); + // Write the content key info to hardware key slot, which can be retrieved + // from Android TV Tuner Hal implementation. + bool WriteToKeySlot(const KeyId& content_key_id) const; + // Clear the content keys written to hardware key slots by this entitled key + // session. The function is called by destructor. + void ClearKeySlot() const; + // Total number of content keys this entitled key session holds. - size_t size() const { return content_key_map_.size(); } + size_t size() const { return content_key_info_map_.size(); } private: + struct ContentKeyInfo { + std::unique_ptr content_key; + Key* entitlement_key; + }; + + // Must have the same exact definition on the tuner hal side. + typedef struct { + uint32_t entitled_key_session_id; + uint8_t cipher_mode; + uint8_t even_key[16]; + uint8_t even_iv[16]; + uint8_t odd_key[16]; + uint8_t odd_iv[16]; + } ContentKeySlot; + + // Must have the same exact definition on the tuner hal side. + typedef struct { + // Assume max number of slots is 16. + ContentKeySlot key_slots[16]; + } ContentKeySlots; + + ContentKeySlot* GetKeySlotToUpdate(uint8_t* buffer) const; + bool WriteContentKeyToKeySlot(const KeyId& content_key_id, + ContentKeySlot* key_slot) const; + const SessionId key_sid_; EntitledKey* current_content_key_; const Key* current_entitlement_key_; - // Map from content key id to content key. - std::map> content_key_map_; - // Map from content key id to referenced entitlement key. - std::map entitlement_key_map_; + // Map from content key id to content key info. + std::map> content_key_info_map_; }; class EntitledKeySessionTable { diff --git a/oemcrypto/ref/src/oemcrypto_key_ref.h b/oemcrypto/ref/src/oemcrypto_key_ref.h index 67b1da3..0759962 100644 --- a/oemcrypto/ref/src/oemcrypto_key_ref.h +++ b/oemcrypto/ref/src/oemcrypto_key_ref.h @@ -68,7 +68,9 @@ class Key { class EntitledKey { public: explicit EntitledKey(std::vector key_string) - : value_(std::move(key_string)), cipher_mode_(OEMCrypto_CipherMode_CTR){}; + : value_(std::move(key_string)), + cipher_mode_(OEMCrypto_CipherMode_CTR), + is_even_key_(true){}; EntitledKey(const EntitledKey&) = default; EntitledKey(EntitledKey&&) = default; ~EntitledKey() = default; @@ -78,11 +80,14 @@ class EntitledKey { void set_cipher_mode(OEMCryptoCipherMode mode) { cipher_mode_ = mode; } void set_content_iv(const std::vector& iv) { content_iv_ = iv; } const std::vector& content_iv() const { return content_iv_; } + bool is_even_key() const { return is_even_key_; } + void set_is_even_key(bool is_even_key) { is_even_key_ = is_even_key; } private: std::vector value_; OEMCryptoCipherMode cipher_mode_; std::vector content_iv_; + bool is_even_key_; }; } // namespace wvoec_ref diff --git a/oemcrypto/ref/src/oemcrypto_ref.cpp b/oemcrypto/ref/src/oemcrypto_ref.cpp index 4904c08..f34b1e9 100644 --- a/oemcrypto/ref/src/oemcrypto_ref.cpp +++ b/oemcrypto/ref/src/oemcrypto_ref.cpp @@ -470,16 +470,16 @@ OEMCRYPTO_API OEMCryptoResult OEMCrypto_LoadCasECMKeys( LOGE("[OEMCrypto_LoadCasECMKeys(): ERROR_INVALID_SESSION]"); return OEMCrypto_ERROR_INVALID_SESSION; } - std::vector key_array; + std::vector key_array; key_array.reserve(2); - if (even_key) key_array.push_back(*even_key); - if (odd_key) key_array.push_back(*odd_key); + if (even_key) key_array.push_back(even_key); + if (odd_key) key_array.push_back(odd_key); for (unsigned int i = 0; i < key_array.size(); i++) { - if (!RangeCheck(message_length, key_array[i].entitlement_key_id, false) || - !RangeCheck(message_length, key_array[i].content_key_id, false) || - !RangeCheck(message_length, key_array[i].content_key_data_iv, false) || - !RangeCheck(message_length, key_array[i].content_key_data, false) || - !RangeCheck(message_length, key_array[i].content_iv, false)) { + if (!RangeCheck(message_length, key_array[i]->entitlement_key_id, false) || + !RangeCheck(message_length, key_array[i]->content_key_id, false) || + !RangeCheck(message_length, key_array[i]->content_key_data_iv, false) || + !RangeCheck(message_length, key_array[i]->content_key_data, false) || + !RangeCheck(message_length, key_array[i]->content_iv, true)) { LOGE( "[OEMCrypto_LoadCasECMKeys(): " "OEMCrypto_ERROR_INVALID_CONTEXT -range " @@ -489,8 +489,7 @@ OEMCRYPTO_API OEMCryptoResult OEMCrypto_LoadCasECMKeys( } } return session_ctx->LoadEntitledCasKeys(entitled_key_session, message, - message_length, key_array.size(), - &key_array[0]); + message_length, even_key, odd_key); } OEMCRYPTO_API OEMCryptoResult OEMCrypto_LoadRenewal(OEMCrypto_SESSION session, diff --git a/oemcrypto/ref/src/oemcrypto_session.cpp b/oemcrypto/ref/src/oemcrypto_session.cpp index 084bd88..5774c14 100644 --- a/oemcrypto/ref/src/oemcrypto_session.cpp +++ b/oemcrypto/ref/src/oemcrypto_session.cpp @@ -840,9 +840,9 @@ OEMCryptoResult SessionContext::LoadEntitledContentKeys( OEMCryptoResult SessionContext::LoadEntitledCasKeys( EntitledKeySession* key_session, const uint8_t* message, - size_t message_length, size_t key_array_length, - const OEMCrypto_EntitledCasKeyObject* key_array) { - if (!key_array) { + size_t message_length, const OEMCrypto_EntitledCasKeyObject* even_key, + const OEMCrypto_EntitledCasKeyObject* odd_key) { + if (even_key == nullptr && odd_key == nullptr) { return OEMCrypto_ERROR_UNKNOWN_FAILURE; } if (session_keys_ == nullptr || @@ -853,8 +853,13 @@ OEMCryptoResult SessionContext::LoadEntitledCasKeys( return OEMCrypto_ERROR_INVALID_ENTITLED_KEY_SESSION; } - for (size_t i = 0; i < key_array_length; ++i) { - const OEMCrypto_EntitledCasKeyObject* key_data = &key_array[i]; + const std::vector key_array = { + even_key, odd_key}; + for (size_t i = 0; i < key_array.size(); ++i) { + if (key_array[i] == nullptr) { + continue; + } + const OEMCrypto_EntitledCasKeyObject* key_data = key_array[i]; std::vector entitlement_key_id; entitlement_key_id.assign(message + key_data->entitlement_key_id.offset, message + key_data->entitlement_key_id.offset + @@ -864,6 +869,15 @@ OEMCryptoResult SessionContext::LoadEntitledCasKeys( if (entitlement_key == nullptr) { return OEMCrypto_KEY_NOT_ENTITLED; } + + // Assume in Tuner Hal, buffer type is always secure. + OEMCryptoResult result = CheckKeyControlBlockUse( + entitlement_key->control(), "LoadEntitledCasKeys", /*use_type=*/0, + OEMCrypto_BufferType_Secure); + if (result != OEMCrypto_SUCCESS) { + return result; + } + std::vector content_key; std::vector iv; std::vector encrypted_content_key; @@ -893,11 +907,19 @@ OEMCryptoResult SessionContext::LoadEntitledCasKeys( content_key_obj->set_content_iv(content_iv); } content_key_obj->set_cipher_mode(key_data->cipher_mode); + content_key_obj->set_is_even_key(i % 2 == 0); if (!key_session->AddOrUpdateContentKey(entitlement_key, content_key_id, std::move(content_key_obj))) { return OEMCrypto_ERROR_UNKNOWN_FAILURE; } + + if (!key_session->WriteToKeySlot(content_key_id)) { + // Currently assume this is acceptable. + LOGE("Failed to update content keys to hardware KeySlots."); + } else { + LOGI("Updated content keys to hardware KeySlots."); + } } return OEMCrypto_SUCCESS; } diff --git a/oemcrypto/ref/src/oemcrypto_session.h b/oemcrypto/ref/src/oemcrypto_session.h index 35ab5ea..7fbf030 100644 --- a/oemcrypto/ref/src/oemcrypto_session.h +++ b/oemcrypto/ref/src/oemcrypto_session.h @@ -118,8 +118,8 @@ class SessionContext { const OEMCrypto_EntitledContentKeyObject* key_array); virtual OEMCryptoResult LoadEntitledCasKeys( EntitledKeySession* key_session, const uint8_t* message, - size_t message_length, size_t key_array_length, - const OEMCrypto_EntitledCasKeyObject* key_array); + size_t message_length, const OEMCrypto_EntitledCasKeyObject* even_key, + const OEMCrypto_EntitledCasKeyObject* odd_key); virtual OEMCryptoResult InstallKey( const KeyId& key_id, const std::vector& key_data, const std::vector& key_data_iv, diff --git a/oemcrypto/test/oec_session_util.cpp b/oemcrypto/test/oec_session_util.cpp index d62201f..149d5b6 100644 --- a/oemcrypto/test/oec_session_util.cpp +++ b/oemcrypto/test/oec_session_util.cpp @@ -794,33 +794,98 @@ void EntitledMessage::LoadKeys(OEMCryptoResult expected_sts) { VerifyEntitlementTestKeys(); } +void EntitledMessage::LoadCasKeys(OEMCryptoResult expected_sts, bool load_even, + bool load_odd) { + for (size_t i = 0; i < num_keys_; ++i) { + EntitledContentKeyData* key_data = &entitled_key_data_[i]; + const size_t entitlement_key_index = key_data->key_index; + MessageKeyData* entitlement_key = + &license_messages_->response_data().keys[entitlement_key_index]; + + // Load the entitlement key from |key_array_|. + AES_KEY aes_key; + AES_set_encrypt_key(entitlement_key->key_data, 256, &aes_key); + + // Encrypt the content key with the entitlement key. + uint8_t iv[16]; + memcpy(&iv[0], key_data->content_key_data_iv, KEY_IV_SIZE); + AES_cbc_encrypt(key_data->content_key_data, + key_data->encrypted_content_key_data, KEY_SIZE, &aes_key, + iv, AES_ENCRYPT); + } + + // Convert the OEMCrypto_EntitledContentKeyObject to + // OEMCrypto_EntitledCasKeyObject. Only the first two key object is used. + OEMCrypto_EntitledCasKeyObject even_key; + OEMCrypto_EntitledCasKeyObject odd_key; + bool has_even = load_even && num_keys_ >= 1; + bool has_odd = load_odd && num_keys_ >= 2; + if (has_even) { + even_key.entitlement_key_id = entitled_key_array_[0].entitlement_key_id; + even_key.content_key_id = entitled_key_array_[0].content_key_id; + even_key.content_key_data_iv = entitled_key_array_[0].content_key_data_iv; + even_key.content_key_data = entitled_key_array_[0].content_key_data; + even_key.content_iv.length = 0; + } + if (has_odd) { + odd_key.entitlement_key_id = entitled_key_array_[1].entitlement_key_id; + odd_key.content_key_id = entitled_key_array_[1].content_key_id; + odd_key.content_key_data_iv = entitled_key_array_[1].content_key_data_iv; + odd_key.content_key_data = entitled_key_array_[1].content_key_data; + odd_key.content_iv.length = 0; + } + + ASSERT_EQ( + expected_sts, + OEMCrypto_LoadCasECMKeys( + key_session_, reinterpret_cast(entitled_key_data_), + sizeof(entitled_key_data_), has_even ? &even_key : nullptr, + has_odd ? &odd_key : nullptr)); + if (expected_sts != OEMCrypto_SUCCESS) { + return; + } + + if (has_even) { + VerifyEntitlementTestKey(0); + } + if (has_odd) { + VerifyEntitlementTestKey(1); + } +} + // This function verifies that the key control block reported by OEMCrypto agree // with the truth key control block. Failures in this function probably // indicate the OEMCrypto_LoadEntitledKeys did not correctly process the key // control block. void EntitledMessage::VerifyEntitlementTestKeys() { for (unsigned int i = 0; i < num_keys_; i++) { - EntitledContentKeyData* key_data = &entitled_key_data_[i]; - const size_t entitlement_key_index = key_data->key_index; - MessageKeyData* entitlement_key = - &license_messages_->response_data().keys[entitlement_key_index]; - KeyControlBlock block; - size_t size = sizeof(block); - OEMCryptoResult sts = OEMCrypto_QueryKeyControl( - key_session_, key_data->content_key_id, key_data->content_key_id_length, - reinterpret_cast(&block), &size); - if (sts != OEMCrypto_ERROR_NOT_IMPLEMENTED) { - ASSERT_EQ(OEMCrypto_SUCCESS, sts); - ASSERT_EQ(sizeof(block), size); - // control duration and bits stored in network byte order. For printing - // we change to host byte order. - ASSERT_EQ((htonl_fnc(entitlement_key->control.duration)), - (htonl_fnc(block.duration))) - << "For key " << i; - ASSERT_EQ(htonl_fnc(entitlement_key->control.control_bits), - htonl_fnc(block.control_bits)) - << "For key " << i; - } + VerifyEntitlementTestKey(i); + } +} + +void EntitledMessage::VerifyEntitlementTestKey(size_t index) { + ASSERT_GE(num_keys_, index); + + EntitledContentKeyData* key_data = &entitled_key_data_[index]; + const size_t entitlement_key_index = key_data->key_index; + MessageKeyData* entitlement_key = + &license_messages_->response_data().keys[entitlement_key_index]; + KeyControlBlock block; + size_t size = sizeof(block); + OEMCryptoResult sts = OEMCrypto_QueryKeyControl( + key_session_, key_data->content_key_id, key_data->content_key_id_length, + reinterpret_cast(&block), &size); + if (sts != OEMCrypto_ERROR_NOT_IMPLEMENTED) { + ASSERT_EQ(OEMCrypto_SUCCESS, sts); + ASSERT_EQ(sizeof(block), size); + // control duration and bits stored in network byte order. For printing + // we change to host byte order. + ASSERT_EQ((htonl_fnc(entitlement_key->control.duration)), + (htonl_fnc(block.duration))) + << "For key " << index; + ASSERT_EQ(htonl_fnc(entitlement_key->control.control_bits), + htonl_fnc(block.control_bits)) + << "For key " << index; } } diff --git a/oemcrypto/test/oec_session_util.h b/oemcrypto/test/oec_session_util.h index d694e0f..49fa8be 100644 --- a/oemcrypto/test/oec_session_util.h +++ b/oemcrypto/test/oec_session_util.h @@ -419,12 +419,14 @@ class EntitledMessage { key_session_ = key_session; } void LoadKeys(OEMCryptoResult expected_sts); + void LoadCasKeys(OEMCryptoResult expected_sts, bool load_even, bool load_odd); void set_num_keys(uint32_t num_keys) { num_keys_ = num_keys; } uint32_t num_keys() const { return num_keys_; } void SetEntitlementKeyId(unsigned int index, const std::string& key_id); void SetContentKeyId(unsigned int index, const std::string& key_id); // Verify that key control blocks of the loaded keys. void VerifyEntitlementTestKeys(); + void VerifyEntitlementTestKey(size_t index); private: // Find the offset of the give pointer, relative to |entitled_key_data_|. diff --git a/oemcrypto/test/oemcrypto_test.cpp b/oemcrypto/test/oemcrypto_test.cpp index 3f62e08..8641049 100644 --- a/oemcrypto/test/oemcrypto_test.cpp +++ b/oemcrypto/test/oemcrypto_test.cpp @@ -896,7 +896,7 @@ TEST_P(OEMCryptoLicenseTest, LoadKeyWithNonceTwiceAPI16) { } // This verifies that entitlement keys and entitled content keys can be loaded. -TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysAPI17) { +TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); license_messages_.set_license_type(OEMCrypto_EntitlementLicense); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); @@ -906,25 +906,24 @@ TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysAPI17) { uint32_t key_session_id = 0; OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( session_.session_id(), &key_session_id); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_EQ(OEMCrypto_SUCCESS, sts); ASSERT_NE(key_session_id, 0u); EntitledMessage entitled_message_1(&license_messages_); entitled_message_1.FillKeyArray(); entitled_message_1.SetEntitledKeySession(key_session_id); - ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadKeys(OEMCrypto_SUCCESS)); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); EntitledMessage entitled_message_2(&license_messages_); entitled_message_2.FillKeyArray(); entitled_message_2.SetEntitledKeySession(key_session_id); - ASSERT_NO_FATAL_FAILURE(entitled_message_2.LoadKeys(OEMCrypto_SUCCESS)); + ASSERT_NO_FATAL_FAILURE(entitled_message_2.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); } // This verifies that entitled content keys cannot be loaded if we have not yet // loaded the entitlement keys. -TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysNoEntitlementKeysAPI17) { +TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysNoEntitlementKeysAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); license_messages_.set_license_type(OEMCrypto_EntitlementLicense); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); @@ -933,22 +932,19 @@ TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysNoEntitlementKeysAPI17) { uint32_t key_session_id = 0; OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( session_.session_id(), &key_session_id); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_EQ(OEMCrypto_SUCCESS, sts); ASSERT_NE(key_session_id, 0u); EntitledMessage entitled_message_1(&license_messages_); entitled_message_1.FillKeyArray(); entitled_message_1.SetEntitledKeySession(key_session_id); - ASSERT_NO_FATAL_FAILURE( - entitled_message_1.LoadKeys(OEMCrypto_ERROR_INVALID_CONTEXT)); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_ERROR_INVALID_CONTEXT, /*load_even=*/true, /*load_odd=*/true)); } // This verifies that entitled content keys cannot be loaded if we have loaded // the wrong entitlement keys. -TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysWrongEntitlementKeysAPI17) { +TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysWrongEntitlementKeysAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); license_messages_.set_license_type(OEMCrypto_EntitlementLicense); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); @@ -958,9 +954,6 @@ TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysWrongEntitlementKeysAPI17) { uint32_t key_session_id = 0; OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( session_.session_id(), &key_session_id); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_EQ(OEMCrypto_SUCCESS, sts); ASSERT_NE(key_session_id, 0u); @@ -969,13 +962,13 @@ TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysWrongEntitlementKeysAPI17) { const std::string key_id = "no_key"; entitled_message_1.SetEntitlementKeyId(0, key_id); entitled_message_1.SetEntitledKeySession(key_session_id); - ASSERT_NO_FATAL_FAILURE( - entitled_message_1.LoadKeys(OEMCrypto_KEY_NOT_ENTITLED)); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_KEY_NOT_ENTITLED, /*load_even=*/true, /*load_odd=*/true)); } // This verifies that entitled content keys cannot be loaded if we specify an // entitled key session that has not been created. -TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysWrongEntitledKeySessionAPI17) { +TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysWrongEntitledKeySessionAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); license_messages_.set_license_type(OEMCrypto_EntitlementLicense); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); @@ -985,22 +978,20 @@ TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysWrongEntitledKeySessionAPI17) { uint32_t key_session_id = 0; OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( session_.session_id(), &key_session_id); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_EQ(OEMCrypto_SUCCESS, sts); ASSERT_NE(key_session_id, 0u); EntitledMessage entitled_message_1(&license_messages_); entitled_message_1.FillKeyArray(); entitled_message_1.SetEntitledKeySession(0); - ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadKeys( - OEMCrypto_ERROR_INVALID_ENTITLED_KEY_SESSION)); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_ERROR_INVALID_ENTITLED_KEY_SESSION, /*load_even=*/true, + /*load_odd=*/true)); } // This verifies that entitled content keys cannot be loaded if we specify an // entitled key session that is actually an oemcrypto session. -TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysOemcryptoSessionAPI17) { +TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysOemcryptoSessionAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); license_messages_.set_license_type(OEMCrypto_EntitlementLicense); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); @@ -1010,17 +1001,15 @@ TEST_P(OEMCryptoLicenseTest, LoadEntitlementKeysOemcryptoSessionAPI17) { uint32_t key_session_id = 0; OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( session_.session_id(), &key_session_id); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_EQ(OEMCrypto_SUCCESS, sts); ASSERT_NE(key_session_id, 0u); EntitledMessage entitled_message_1(&license_messages_); entitled_message_1.FillKeyArray(); entitled_message_1.SetEntitledKeySession(session_.session_id()); - ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadKeys( - OEMCrypto_ERROR_INVALID_ENTITLED_KEY_SESSION)); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_ERROR_INVALID_ENTITLED_KEY_SESSION, /*load_even=*/true, + /*load_odd=*/true)); } // This tests load license with an 8k license response. @@ -1479,7 +1468,7 @@ TEST_P(OEMCryptoLicenseTest, SelectKeyNotThereAPI16) { } } -TEST_P(OEMCryptoLicenseTest, SelectKeyEntitledKeyAPI17) { +TEST_P(OEMCryptoLicenseTest, SelectKeyEntitledKeyAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); license_messages_.set_license_type(OEMCrypto_EntitlementLicense); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); @@ -1490,16 +1479,14 @@ TEST_P(OEMCryptoLicenseTest, SelectKeyEntitledKeyAPI17) { uint32_t key_session_id; OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( session_.session_id(), &key_session_id); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_EQ(OEMCrypto_SUCCESS, sts); EntitledMessage entitled_message_1(&license_messages_); entitled_message_1.FillKeyArray(); entitled_message_1.SetEntitledKeySession(key_session_id); const char* content_key_id = "content_key_id"; entitled_message_1.SetContentKeyId(0, content_key_id); - ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadKeys(OEMCrypto_SUCCESS)); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/false)); ASSERT_EQ( OEMCrypto_SUCCESS, @@ -1510,7 +1497,7 @@ TEST_P(OEMCryptoLicenseTest, SelectKeyEntitledKeyAPI17) { // SelectEntitledKey should fail if we attempt to select a key that has not been // loaded. Also, the error should be NO_CONTENT_KEY. -TEST_P(OEMCryptoLicenseTest, SelectKeyEntitledKeyNotThereAPI17) { +TEST_P(OEMCryptoLicenseTest, SelectKeyEntitledKeyNotThereAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); license_messages_.set_license_type(OEMCrypto_EntitlementLicense); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); @@ -1521,14 +1508,12 @@ TEST_P(OEMCryptoLicenseTest, SelectKeyEntitledKeyNotThereAPI17) { uint32_t key_session_id; OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( session_.session_id(), &key_session_id); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_EQ(OEMCrypto_SUCCESS, sts); EntitledMessage entitled_message_1(&license_messages_); entitled_message_1.FillKeyArray(); entitled_message_1.SetEntitledKeySession(key_session_id); - ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadKeys(OEMCrypto_SUCCESS)); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); const char* content_key_id = "no_key"; ASSERT_EQ( @@ -1540,7 +1525,7 @@ TEST_P(OEMCryptoLicenseTest, SelectKeyEntitledKeyNotThereAPI17) { // Select key with entitlement license fails if the key id is entitilement key // id. -TEST_P(OEMCryptoLicenseTest, SelectKeyEntitlementKeyAPI17) { +TEST_P(OEMCryptoLicenseTest, SelectKeyEntitlementKeyAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); license_messages_.set_license_type(OEMCrypto_EntitlementLicense); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); @@ -1551,14 +1536,12 @@ TEST_P(OEMCryptoLicenseTest, SelectKeyEntitlementKeyAPI17) { uint32_t key_session_id; OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( session_.session_id(), &key_session_id); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_EQ(OEMCrypto_SUCCESS, sts); EntitledMessage entitled_message_1(&license_messages_); entitled_message_1.FillKeyArray(); entitled_message_1.SetEntitledKeySession(key_session_id); - ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadKeys(OEMCrypto_SUCCESS)); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); ASSERT_EQ(OEMCrypto_ERROR_INVALID_CONTEXT, OEMCrypto_SelectKey(session_.session_id(), @@ -1568,7 +1551,7 @@ TEST_P(OEMCryptoLicenseTest, SelectKeyEntitlementKeyAPI17) { } // This verifies that entitled key sessions can be created and removed. -TEST_P(OEMCryptoLicenseTest, EntitledKeySessionsAPI17) { +TEST_P(OEMCryptoLicenseTest, EntitledKeySessionsAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); license_messages_.set_license_type(OEMCrypto_EntitlementLicense); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); @@ -1579,9 +1562,6 @@ TEST_P(OEMCryptoLicenseTest, EntitledKeySessionsAPI17) { uint32_t key_session_id_1; OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( session_.session_id(), &key_session_id_1); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_NE(key_session_id_1, 0u); // 0 is a reserved id number. uint32_t key_session_id_2; @@ -1599,7 +1579,7 @@ TEST_P(OEMCryptoLicenseTest, EntitledKeySessionsAPI17) { // This verifies that multiple entitled key sessions can be created. They can // load and select keys independently. -TEST_P(OEMCryptoLicenseTest, EntitledKeySessionMultipleKeySessionsAPI17) { +TEST_P(OEMCryptoLicenseTest, EntitledKeySessionMultipleKeySessionsAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); license_messages_.set_license_type(OEMCrypto_EntitlementLicense); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); @@ -1610,16 +1590,14 @@ TEST_P(OEMCryptoLicenseTest, EntitledKeySessionMultipleKeySessionsAPI17) { uint32_t key_session_id_1; OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( session_.session_id(), &key_session_id_1); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_EQ(OEMCrypto_SUCCESS, sts); EntitledMessage entitled_message_1(&license_messages_); entitled_message_1.FillKeyArray(); entitled_message_1.SetEntitledKeySession(key_session_id_1); const char* content_key_id_1 = "content_key_id_1"; entitled_message_1.SetContentKeyId(0, content_key_id_1); - ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadKeys(OEMCrypto_SUCCESS)); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); // We can select content key 1 in entitled key session 1. ASSERT_EQ( OEMCrypto_SUCCESS, @@ -1638,7 +1616,8 @@ TEST_P(OEMCryptoLicenseTest, EntitledKeySessionMultipleKeySessionsAPI17) { entitled_message_2.SetEntitledKeySession(key_session_id_2); const char* content_key_id_2 = "content_key_id_2"; entitled_message_2.SetContentKeyId(0, content_key_id_2); - ASSERT_NO_FATAL_FAILURE(entitled_message_2.LoadKeys(OEMCrypto_SUCCESS)); + ASSERT_NO_FATAL_FAILURE(entitled_message_2.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); // We can select content key 2 in entitled key session 2. ASSERT_EQ( OEMCrypto_SUCCESS, @@ -1660,9 +1639,9 @@ TEST_P(OEMCryptoLicenseTest, EntitledKeySessionMultipleKeySessionsAPI17) { } // This verifies that within an entitled key session, each entitlement key can -// corresponds to only one content key at most. +// corresponds to only one even content key at most. TEST_P(OEMCryptoLicenseTest, - EntitledKeySessionOneContentKeyPerEntitlementAPI17) { + EntitledKeySessionOneContentKeyPerEntitlementAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); license_messages_.set_license_type(OEMCrypto_EntitlementLicense); ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); @@ -1673,9 +1652,6 @@ TEST_P(OEMCryptoLicenseTest, uint32_t key_session_id; OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( session_.session_id(), &key_session_id); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_EQ(OEMCrypto_SUCCESS, sts); // Construct and load content keys to entitled key session. EntitledMessage entitled_message_1(&license_messages_); @@ -1683,7 +1659,8 @@ TEST_P(OEMCryptoLicenseTest, entitled_message_1.SetEntitledKeySession(key_session_id); const char* content_key_id_1 = "content_key_id_1"; entitled_message_1.SetContentKeyId(0, content_key_id_1); - ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadKeys(OEMCrypto_SUCCESS)); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); // We can select content key 1 in entitled key session. ASSERT_EQ( OEMCrypto_SUCCESS, @@ -1693,7 +1670,8 @@ TEST_P(OEMCryptoLicenseTest, // Load content key with new content id. const char* content_key_id_2 = "content_key_id_2"; entitled_message_1.SetContentKeyId(0, content_key_id_2); - ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadKeys(OEMCrypto_SUCCESS)); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); // We can select content key 2 in entitled key session. ASSERT_EQ( OEMCrypto_SUCCESS, @@ -1709,6 +1687,55 @@ TEST_P(OEMCryptoLicenseTest, strlen(content_key_id_1), OEMCrypto_CipherMode_CTR)); } +// This verifies that within an entitled key session, each entitlement key can +// be shared by both even and odd content keys. +TEST_P(OEMCryptoLicenseTest, + EntitledKeySessionEvenOddContentKeysPerEntitlementAPI16) { + ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); + license_messages_.set_license_type(OEMCrypto_EntitlementLicense); + ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); + ASSERT_NO_FATAL_FAILURE(license_messages_.CreateDefaultResponse()); + ASSERT_NO_FATAL_FAILURE(license_messages_.EncryptAndSignResponse()); + ASSERT_EQ(OEMCrypto_SUCCESS, license_messages_.LoadResponse()); + + uint32_t key_session_id; + OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( + session_.session_id(), &key_session_id); + ASSERT_EQ(OEMCrypto_SUCCESS, sts); + // Construct and load even content keys to entitled key session. + EntitledMessage entitled_message_1(&license_messages_); + entitled_message_1.FillKeyArray(); + entitled_message_1.SetEntitledKeySession(key_session_id); + const char* content_key_id_1 = "content_key_id_1"; + entitled_message_1.SetContentKeyId(0, content_key_id_1); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/false)); + // We can select content key 1 (even key) in entitled key session. + ASSERT_EQ( + OEMCrypto_SUCCESS, + OEMCrypto_SelectKey(key_session_id, + reinterpret_cast(content_key_id_1), + strlen(content_key_id_1), OEMCrypto_CipherMode_CTR)); + // Load odd content key with new content id. + const char* content_key_id_2 = "content_key_id_2"; + entitled_message_1.SetContentKeyId(1, content_key_id_2); + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/false, /*load_odd=*/true)); + // We can select content key 2 (odd key) in entitled key session. + ASSERT_EQ( + OEMCrypto_SUCCESS, + OEMCrypto_SelectKey(key_session_id, + reinterpret_cast(content_key_id_2), + strlen(content_key_id_2), OEMCrypto_CipherMode_CTR)); + // Content key one is still in the entitled key session as they are even and + // odd keys that can use the same entitlement key. + ASSERT_EQ( + OEMCrypto_SUCCESS, + OEMCrypto_SelectKey(key_session_id, + reinterpret_cast(content_key_id_1), + strlen(content_key_id_1), OEMCrypto_CipherMode_CTR)); +} + // 'cens' mode is no longer supported in v16 TEST_P(OEMCryptoLicenseTest, RejectCensAPI16) { ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); @@ -1819,9 +1846,6 @@ TEST_P(OEMCryptoLicenseTest, QueryKeyControl) { session_.session_id(), license_messages_.response_data().keys[0].key_id, license_messages_.response_data().keys[0].key_id_length, reinterpret_cast(&block), &size); - if (sts == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - return; - } ASSERT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, sts); const char* key_id = "no_key"; size = sizeof(block); @@ -2029,6 +2053,60 @@ TEST_P(OEMCryptoSessionTestDecryptWithHDCP, DecryptAPI09) { INSTANTIATE_TEST_CASE_P(TestHDCP, OEMCryptoSessionTestDecryptWithHDCP, Range(1, 6)); +// Used to test the different HDCP versions. This test is parameterized by the +// required HDCP version in the key control block. +class OEMCryptoSessionTestLoadCasKeysWithHDCP : public OEMCryptoSessionTests, + public WithParamInterface { + protected: + void LoadCasKeysWithHDCP(OEMCrypto_HDCP_Capability version) { + OEMCryptoResult sts; + OEMCrypto_HDCP_Capability current, maximum; + sts = OEMCrypto_GetHDCPCapability(¤t, &maximum); + ASSERT_EQ(OEMCrypto_SUCCESS, sts); + Session s; + ASSERT_NO_FATAL_FAILURE(s.open()); + ASSERT_NO_FATAL_FAILURE(InstallTestRSAKey(&s)); + LicenseRoundTrip license_messages(&s); + license_messages.set_control((version << wvoec::kControlHDCPVersionShift) | + wvoec::kControlObserveHDCP | + wvoec::kControlHDCPRequired); + license_messages.set_license_type(OEMCrypto_EntitlementLicense); + ASSERT_NO_FATAL_FAILURE(license_messages.SignAndVerifyRequest()); + ASSERT_NO_FATAL_FAILURE(license_messages.CreateDefaultResponse()); + ASSERT_NO_FATAL_FAILURE(license_messages.EncryptAndSignResponse()); + ASSERT_EQ(OEMCrypto_SUCCESS, license_messages.LoadResponse()); + + uint32_t key_session_id; + sts = OEMCrypto_CreateEntitledKeySession(s.session_id(), &key_session_id); + ASSERT_EQ(OEMCrypto_SUCCESS, sts); + EntitledMessage entitled_message_1(&license_messages); + entitled_message_1.FillKeyArray(); + entitled_message_1.SetEntitledKeySession(key_session_id); + + if (version > current) { + ASSERT_NO_FATAL_FAILURE( + entitled_message_1.LoadCasKeys(OEMCrypto_ERROR_INSUFFICIENT_HDCP, + /*load_even=*/true, /*load_odd=*/true)) + << "Failed when current HDCP = " << HDCPCapabilityAsString(current) + << ", maximum HDCP = " << HDCPCapabilityAsString(maximum) + << ", license HDCP = " << HDCPCapabilityAsString(version); + } else { + ASSERT_NO_FATAL_FAILURE(entitled_message_1.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)) + << "Failed when current HDCP = " << HDCPCapabilityAsString(current) + << ", maximum HDCP = " << HDCPCapabilityAsString(maximum) + << ", license HDCP = " << HDCPCapabilityAsString(version); + } + } +}; + +TEST_P(OEMCryptoSessionTestLoadCasKeysWithHDCP, LoadCasECMKeysAPI16) { + // Test parameterized by HDCP version. + LoadCasKeysWithHDCP(static_cast(GetParam())); +} +INSTANTIATE_TEST_CASE_P(TestHDCP, OEMCryptoSessionTestLoadCasKeysWithHDCP, + Range(1, 6)); + // // Load, Refresh Keys Test // @@ -2924,6 +3002,54 @@ TEST_P(OEMCryptoLicenseTest, KeyDuration) { ASSERT_NO_FATAL_FAILURE(session_.TestSelectExpired(0)); } +TEST_P(OEMCryptoLicenseTest, LoadCasKeysNoAnalogAPI16) { + ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); + license_messages_.set_license_type(OEMCrypto_EntitlementLicense); + license_messages_.set_control(wvoec::kControlDisableAnalogOutput); + ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); + ASSERT_NO_FATAL_FAILURE(license_messages_.CreateDefaultResponse()); + ASSERT_NO_FATAL_FAILURE(license_messages_.EncryptAndSignResponse()); + ASSERT_EQ(OEMCrypto_SUCCESS, license_messages_.LoadResponse()); + uint32_t key_session_id; + OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( + session_.session_id(), &key_session_id); + ASSERT_EQ(OEMCrypto_SUCCESS, sts); + + EntitledMessage entitled_message(&license_messages_); + entitled_message.FillKeyArray(); + entitled_message.SetEntitledKeySession(key_session_id); + ASSERT_NO_FATAL_FAILURE(entitled_message.LoadCasKeys( + OEMCrypto_ERROR_ANALOG_OUTPUT, /*load_even=*/true, /*load_odd=*/true)); +} + +// Test that key duration is honored. +TEST_P(OEMCryptoLicenseTest, LoadCasKeysKeyDurationAPI16) { + ASSERT_NO_FATAL_FAILURE(session_.GenerateNonce()); + license_messages_.set_license_type(OEMCrypto_EntitlementLicense); + license_messages_.core_response() + .timer_limits.total_playback_duration_seconds = kDuration; + ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest()); + ASSERT_NO_FATAL_FAILURE(license_messages_.CreateDefaultResponse()); + ASSERT_NO_FATAL_FAILURE(license_messages_.EncryptAndSignResponse()); + ASSERT_EQ(OEMCrypto_SUCCESS, license_messages_.LoadResponse()); + uint32_t key_session_id; + OEMCryptoResult sts = OEMCrypto_CreateEntitledKeySession( + session_.session_id(), &key_session_id); + ASSERT_EQ(OEMCrypto_SUCCESS, sts); + EntitledMessage entitled_message(&license_messages_); + entitled_message.FillKeyArray(); + entitled_message.SetEntitledKeySession(key_session_id); + + ASSERT_NO_FATAL_FAILURE(entitled_message.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); + wvutil::TestSleep::Sleep(kShortSleep); // Should still be valid key. + ASSERT_NO_FATAL_FAILURE(entitled_message.LoadCasKeys( + OEMCrypto_SUCCESS, /*load_even=*/true, /*load_odd=*/true)); + wvutil::TestSleep::Sleep(kLongSleep); // Should be expired key. + ASSERT_NO_FATAL_FAILURE(entitled_message.LoadCasKeys( + OEMCrypto_ERROR_KEY_EXPIRED, /*load_even=*/true, /*load_odd=*/true)); +} + INSTANTIATE_TEST_CASE_P(TestAll, OEMCryptoLicenseTest, Range(kCurrentAPI - 1, kCurrentAPI + 1)); diff --git a/plugin/Android.bp b/plugin/Android.bp index b3ea5e1..3d8c016 100644 --- a/plugin/Android.bp +++ b/plugin/Android.bp @@ -10,6 +10,8 @@ cc_library_static { "src/widevine_media_cas.cpp", "src/cas_license.cpp", "src/ecm_parser.cpp", + "src/ecm_parser_v2.cpp", + "src/ecm_parser_v3.cpp", "src/license_key_status.cpp", "src/oemcrypto_interface.cpp", "src/policy_engine.cpp", diff --git a/plugin/include/cas_events.h b/plugin/include/cas_events.h index 6a2a4fa..8c9ec92 100644 --- a/plugin/include/cas_events.h +++ b/plugin/include/cas_events.h @@ -11,6 +11,8 @@ #define CAS_QUERY_EVENT_START 4000 #define CAS_ERROR_EVENT_START 5000 #define CAS_PARENTAL_CONTROL_EVENT_START 6000 +#define CAS_FINGERPRINTING_EVENT_START 6100 +#define CAS_SERVICE_BLOCKING_EVENT_START 6200 #define CAS_TEST_EVENT_START 10000 typedef enum { @@ -48,9 +50,41 @@ typedef enum { ACCESS_DENIED_BY_PARENTAL_CONTROL, AGE_RESTRICTION_UPDATED, + // The content of FINGERPRINTING_INFO events follows TLV (Type (1 byte) - + // Length (2 bytes) - Value) format. See FingerprintingFieldType for possible + // types. A FINGERPRINTING_INFO event contains {one or more CHANNEL, one + // CONTROL}. + FINGERPRINTING_INFO = CAS_FINGERPRINTING_EVENT_START, + // Fingerprinting control info for a session. The content of the event follows + // TLV (Type (1 byte) - Length (2 bytes) - Value) format. See + // SessionFingerprintingFieldType for possible types. It will contain {one + // FINGERPRINTING_CONTROL}. + SESSION_FINGERPRINTING_INFO, + + // The content of SERVICE_BLOCKING_INFO events follows TLV (Type (1 byte) - + // Length (2 bytes) - Value) format. See ServiceBlockingFieldType for possible + // types. A SERVICE_BLOCKING_INFO event contains {one or more CHANNEL, one or + // more DEVICE_GROUP, zero or one START_TIME_SECONDS, one END_TIME_SECONDS}. + SERVICE_BLOCKING_INFO = CAS_SERVICE_BLOCKING_EVENT_START, + // Service blocking device group for a session. The content of the event + // follows TLV (Type (1 byte) - Length (2 bytes) - Value) format. See + // SessionServiceBlockingFieldType for possible types. It will contain {one or + // more SERVICE_BLOCKING_DEVICE_GROUP}. + SESSION_SERVICE_BLOCKING_INFO, + TEST_FOR_ECHO = CAS_TEST_EVENT_START, // Request an ECHO response to test events passing. ECHO, // Respond to TEST_FOR_ECHO. } CasEventId; +// Types used inside an SESSION_FINGERPRINTING_CONTROL event. +typedef enum { + FINGERPRINTING_CONTROL = 0, +} SessionFingerprintingFieldType; + +// Types used inside an SESSION_SERVICE_BLOCKING_GROUPS event. +typedef enum { + SERVICE_BLOCKING_DEVICE_GROUP = 0, +} SessionServiceBlockingFieldType; + #endif // CAS_EVENTS_H diff --git a/plugin/include/cas_license.h b/plugin/include/cas_license.h index e4e2c1f..0e0f6c4 100644 --- a/plugin/include/cas_license.h +++ b/plugin/include/cas_license.h @@ -110,6 +110,13 @@ class CasLicense : public wvutil::TimerHandler, public wvcas::CasEventListener { void OnAgeRestrictionUpdated(const WvCasSessionId& sessionId, uint8_t ecm_age_restriction) override; + void OnSessionFingerprintingUpdated(const WvCasSessionId& sessionId, + const CasData& fingerprinting) override; + + void OnSessionServiceBlockingUpdated( + const WvCasSessionId& sessionId, + const CasData& service_blocking) override; + // Query to see if the license is expired. virtual bool IsExpired() const; diff --git a/plugin/include/cas_types.h b/plugin/include/cas_types.h index 111f623..c688e7e 100644 --- a/plugin/include/cas_types.h +++ b/plugin/include/cas_types.h @@ -89,6 +89,14 @@ class CasEventListener { virtual void OnAgeRestrictionUpdated(const WvCasSessionId& sessionId, uint8_t ecm_age_restriction) = 0; + // Notifies listeners of new fingerprinting info. + virtual void OnSessionFingerprintingUpdated( + const WvCasSessionId& sessionId, const CasData& fingerprinting) = 0; + + // Notifies listeners of new service blocking info. + virtual void OnSessionServiceBlockingUpdated( + const WvCasSessionId& sessionId, const CasData& service_blocking) = 0; + CasEventListener(const CasEventListener&) = delete; CasEventListener& operator=(const CasEventListener&) = delete; }; diff --git a/plugin/include/ecm_parser.h b/plugin/include/ecm_parser.h index 17795b4..982fd89 100644 --- a/plugin/include/ecm_parser.h +++ b/plugin/include/ecm_parser.h @@ -9,56 +9,42 @@ #include #include "cas_types.h" +#include "media_cas.pb.h" namespace wvcas { enum class KeySlotId { kEvenKeySlot, kOddKeySlot }; -struct EcmKeyData; - // EcmParser allows random access to the fields of an ECM. -// The only validation performed is to ensure that the ecm -// passed in is large enough to hold a single key entry. class EcmParser { - protected: - EcmParser() {} - public: + EcmParser() = default; virtual ~EcmParser() {} // The EcmParser factory method. // Validates the ecm. If validations is successful returns true and constructs // an EcmParser in |parser| using |ecm|. - static bool create(const CasEcm& ecm, - std::unique_ptr* parser); + static std::unique_ptr Create(const CasEcm& ecm); // Accessor methods. - virtual uint8_t version() const; - virtual uint8_t sequence_count() const; - virtual CryptoMode crypto_mode() const; - virtual bool rotation_enabled() const; - virtual size_t content_iv_size() const; - virtual uint8_t age_restriction() const; - virtual const std::vector entitlement_key_id(KeySlotId id) const; - virtual const std::vector content_key_id(KeySlotId id) const; - virtual const std::vector wrapped_key_data(KeySlotId id) const; - virtual const std::vector wrapped_key_iv(KeySlotId id) const; - virtual const std::vector content_iv(KeySlotId id) const; + virtual uint8_t version() const = 0; + virtual CryptoMode crypto_mode() const = 0; + virtual bool rotation_enabled() const = 0; + virtual size_t content_iv_size() const = 0; + virtual uint8_t age_restriction() const = 0; + virtual std::vector entitlement_key_id(KeySlotId id) const = 0; + virtual std::vector content_key_id(KeySlotId id) const = 0; + virtual std::vector wrapped_key_data(KeySlotId id) const = 0; + virtual std::vector wrapped_key_iv(KeySlotId id) const = 0; + virtual std::vector content_iv(KeySlotId id) const = 0; - EcmParser(const EcmParser&) = delete; - EcmParser& operator=(const EcmParser&) = delete; - - private: - // Constructs an EcmParser using |ecm|. - explicit EcmParser(const CasEcm& ecm); - - size_t key_data_size() const; - // Returns false if the ecm used to construct the object is not a valid size. - // TODO(jfore): Add validation using the version field. - bool is_valid_size() const; - const EcmKeyData* key_slot_data(KeySlotId id) const; - - CasEcm ecm_; + virtual bool has_fingerprinting() const = 0; + virtual video_widevine::Fingerprinting fingerprinting() const = 0; + virtual bool has_service_blocking() const = 0; + virtual video_widevine::ServiceBlocking service_blocking() const = 0; + // The serialized payload that the signature is calculated on. + virtual std::string ecm_serialized_payload() const = 0; + virtual std::string signature() const = 0; }; } // namespace wvcas diff --git a/plugin/include/ecm_parser_v2.h b/plugin/include/ecm_parser_v2.h new file mode 100644 index 0000000..8a807c1 --- /dev/null +++ b/plugin/include/ecm_parser_v2.h @@ -0,0 +1,76 @@ +// 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 ECM_PARSER_V2_H +#define ECM_PARSER_V2_H + +#include +#include + +#include "cas_types.h" +#include "ecm_parser.h" + +namespace wvcas { + +struct EcmKeyData; + +// EcmParserV2 allows random access to the fields of an ECM version 2 and under. +// It should be initialized via EcmParser factory create only. +class EcmParserV2 : public EcmParser { + public: + ~EcmParserV2() override = default; + EcmParserV2(const EcmParserV2&) = delete; + EcmParserV2& operator=(const EcmParserV2&) = delete; + + // The EcmParserV2 factory method. + // |ecm| must be Widevine ECM v2 or under without section header. + // Validates the ecm. The only validation performed is to ensure that the ecm + // passed in is large enough to hold a single key entry. If validations is + // successful returns true and constructs an EcmParserV2 in |parser| using + // |ecm|. + static bool create(const CasEcm& cas_ecm, + std::unique_ptr* parser); + + // Accessor methods. + uint8_t version() const override; + CryptoMode crypto_mode() const override; + bool rotation_enabled() const override; + size_t content_iv_size() const override; + uint8_t age_restriction() const override; + std::vector entitlement_key_id(KeySlotId id) const override; + std::vector content_key_id(KeySlotId id) const override; + std::vector wrapped_key_data(KeySlotId id) const override; + std::vector wrapped_key_iv(KeySlotId id) const override; + std::vector content_iv(KeySlotId id) const override; + + // ECM v2 or under does not have these fields. + bool has_fingerprinting() const override { return false; } + video_widevine::Fingerprinting fingerprinting() const override { + video_widevine::Fingerprinting fingerprinting; + return fingerprinting; + } + bool has_service_blocking() const override { return false; }; + video_widevine::ServiceBlocking service_blocking() const override { + video_widevine::ServiceBlocking service_blocking; + return service_blocking; + } + std::string ecm_serialized_payload() const override { return ""; } + std::string signature() const override { return ""; } + + private: + // Constructs an EcmParserV2 using |ecm|. + explicit EcmParserV2(const CasEcm& ecm); + + size_t key_data_size() const; + // Returns false if the ecm used to construct the object is not a valid size. + // TODO(jfore): Add validation using the version field. + bool is_valid_size() const; + const EcmKeyData* key_slot_data(KeySlotId id) const; + + CasEcm ecm_; +}; + +} // namespace wvcas + +#endif // ECM_PARSER_V2_H diff --git a/plugin/include/ecm_parser_v3.h b/plugin/include/ecm_parser_v3.h new file mode 100644 index 0000000..bc374d5 --- /dev/null +++ b/plugin/include/ecm_parser_v3.h @@ -0,0 +1,60 @@ +// 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 ECM_PARSER_V3_H +#define ECM_PARSER_V3_H + +#include +#include + +#include "cas_types.h" +#include "ecm_parser.h" +#include "media_cas.pb.h" + +namespace wvcas { + +// EcmParser allows random access to the fields of an ECM. +class EcmParserV3 : public EcmParser { + public: + ~EcmParserV3() override = default; + EcmParserV3(const EcmParserV3&) = delete; + EcmParserV3& operator=(const EcmParserV3&) = delete; + + // The EcmParserV3 factory method. + // |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); + + // Accessor methods. + uint8_t version() const override; + CryptoMode crypto_mode() const override; + bool rotation_enabled() const override; + size_t content_iv_size() const override; + uint8_t age_restriction() const override; + std::vector entitlement_key_id(KeySlotId id) const override; + std::vector content_key_id(KeySlotId id) const override; + std::vector wrapped_key_data(KeySlotId id) const override; + std::vector wrapped_key_iv(KeySlotId id) const override; + std::vector content_iv(KeySlotId id) const override; + + bool has_fingerprinting() const override; + video_widevine::Fingerprinting fingerprinting() const override; + bool has_service_blocking() const override; + video_widevine::ServiceBlocking service_blocking() const override; + // The serialized payload that the signature is calculated on. + std::string ecm_serialized_payload() const override; + std::string signature() const override; + + private: + // Constructs an EcmParserV3 using |ecm|. + EcmParserV3(video_widevine::SignedEcmPayload signed_ecm_payload, + video_widevine::EcmPayload ecm_payload); + video_widevine::SignedEcmPayload signed_ecm_payload_; + video_widevine::EcmPayload ecm_payload_; +}; + +} // namespace wvcas + +#endif // ECM_PARSER_V3_H diff --git a/plugin/include/widevine_cas_session.h b/plugin/include/widevine_cas_session.h index 05e3300..1b95472 100644 --- a/plugin/include/widevine_cas_session.h +++ b/plugin/include/widevine_cas_session.h @@ -7,11 +7,13 @@ #include #include +#include #include #include "cas_types.h" #include "crypto_session.h" #include "ecm_parser.h" +#include "media_cas.pb.h" namespace wvcas { @@ -45,7 +47,7 @@ class WidevineCasSession { virtual ~WidevineCasSession(); CasStatus initialize(std::shared_ptr crypto_session, - uint32_t* session_id); + 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. @@ -80,6 +82,13 @@ class WidevineCasSession { std::shared_ptr crypto_session_; // Id of the entitled key session in OEMCrypto associated with this session. uint32_t key_session_id_; + CasEventListener* event_listener_ = nullptr; + // Fingerprinting events sent in processing last ECM/EMM. Used to avoid + // sending a same event again. + std::vector last_fingerprinting_message_; + // Service blocking events sent in processing last ECM/EMM. Used to avoid + // sending a same event again. + std::vector last_service_blocking_message_; }; } // namespace wvcas diff --git a/plugin/include/widevine_media_cas_plugin.h b/plugin/include/widevine_media_cas_plugin.h index d28fae5..229f5ea 100644 --- a/plugin/include/widevine_media_cas_plugin.h +++ b/plugin/include/widevine_media_cas_plugin.h @@ -112,6 +112,11 @@ class WidevineCasPlugin : public CasPlugin, public CasEventListener { void OnLicenseExpiration() override; void OnAgeRestrictionUpdated(const WvCasSessionId& sessionId, uint8_t ecm_age_restriction) override; + void OnSessionFingerprintingUpdated(const WvCasSessionId& sessionId, + const CasData& fingerprinting) override; + void OnSessionServiceBlockingUpdated( + const WvCasSessionId& sessionId, + const CasData& service_blocking) override; // Choose to use |callback_| or |callback_ext_| to send back information. // |sessionId| is ignored if |callback_ext_| is null, diff --git a/plugin/src/cas_license.cpp b/plugin/src/cas_license.cpp index bfc82a4..11b43b0 100644 --- a/plugin/src/cas_license.cpp +++ b/plugin/src/cas_license.cpp @@ -1018,4 +1018,20 @@ void CasLicense::OnAgeRestrictionUpdated(const WvCasSessionId& sessionId, event_listener_->OnAgeRestrictionUpdated(sessionId, ecm_age_restriction); } } + +void CasLicense::OnSessionFingerprintingUpdated(const WvCasSessionId& sessionId, + const CasData& fingerprinting) { + if (event_listener_ != nullptr) { + event_listener_->OnSessionFingerprintingUpdated(sessionId, fingerprinting); + } +} + +void CasLicense::OnSessionServiceBlockingUpdated( + const WvCasSessionId& sessionId, const CasData& service_blocking) { + if (event_listener_ != nullptr) { + event_listener_->OnSessionServiceBlockingUpdated(sessionId, + service_blocking); + } +} + } // namespace wvcas diff --git a/plugin/src/crypto_session.cpp b/plugin/src/crypto_session.cpp index b53de5b..d667fc1 100644 --- a/plugin/src/crypto_session.cpp +++ b/plugin/src/crypto_session.cpp @@ -85,39 +85,6 @@ void FillEntitledContentKeyObjectFromKeyData( dest->cipher_mode = CipherModeFromKeyData(src.cipher_mode); } -// Increment the IV based on the number of encrypted block. IV size is assumed -// to be 16 bytes. -static constexpr uint32_t kIvSizeBytesBytes = 16; -static constexpr uint32_t kCencIvSize = 8; -static const uint32_t kAesBlockSizeBytes = 16; -bool Ctr128Add(size_t block_count, uint8_t* counter) { - if (counter == nullptr) return false; - if (0 == block_count) return true; - - uint8_t carry = 0; - uint8_t n = kIvSizeBytesBytes - 1; - // Update the counter one byte at a time. - while (n >= kCencIvSize) { - // Grab a single byte of the block_count. - uint32_t temp = block_count & 0xff; - // Add the corresponding byte from the counter value. - temp += counter[n]; - // Add in the carry. - temp += carry; - // Write back the updated counter byte and set the carry value as needed. - counter[n] = temp & 0xff; - carry = (temp & 0x100) ? 1 : 0; - // Update block_count and set the counter index for the next byte. - block_count = block_count >> 8; - n--; - // Early exit if nothing to do. - if (!block_count && !carry) { - break; - } - } - return true; -} - } // namespace shared_mutex CryptoLock::static_field_mutex_; diff --git a/plugin/src/ecm_parser.cpp b/plugin/src/ecm_parser.cpp index f89d124..86f1df1 100644 --- a/plugin/src/ecm_parser.cpp +++ b/plugin/src/ecm_parser.cpp @@ -4,29 +4,17 @@ #include "ecm_parser.h" -#include - -#include - +#include "ecm_parser_v2.h" +#include "ecm_parser_v3.h" #include "log.h" namespace wvcas { namespace { -// ECM constants -constexpr uint8_t kSequenceCountMask = 0xF8; // Mode bits 3..7 -constexpr uint8_t kCryptoModeFlags = (0x3 << 1); // Mode bits 1..2 -constexpr uint8_t kCryptoModeFlagsV2 = (0xF << 1); // Mode bits 1..4 -constexpr uint8_t kRotationFlag = (0x1 << 0); // Mode bit 0 -constexpr uint8_t kContentIVSizeFlag = (0x1 << 6); -constexpr uint8_t kAgeRestrictionMask = (0x1F << 1); - -constexpr size_t kEntitlementKeyIDSizeBytes = 16; -constexpr size_t kContentKeyIDSizeBytes = 16; -constexpr size_t kContentKeyDataSizeBytes = 16; -constexpr size_t kWrappedKeyIVSizeBytes = 16; -// Size is either 8 or 16 bytes, depending on ContentIVSize flag. -constexpr size_t kContentKeyMaxIVSizeBytes = 16; +// 2 bytes cas id, 1 byte version. +constexpr int kEcmHeaderSize = 3; +constexpr int kCasIdIndex = 0; +constexpr int kVersionIndex = 2; // Legacy Widevine CAS ID constexpr uint16_t kWidevineCasId = 0x4AD4; @@ -43,8 +31,6 @@ constexpr size_t kSectionHeaderSize = 3; constexpr size_t kSectionHeaderWithPointerSize = 4; constexpr uint8_t kPointerFieldZero = 0x00; -constexpr size_t kMaxTsPayloadSizeBytes = 184; - // Returns the possible starting index of ECM. It assumes the pointer field will // always set to 0, if present. int find_ecm_start_index(const CasEcm& cas_ecm) { @@ -65,183 +51,34 @@ int find_ecm_start_index(const CasEcm& cas_ecm) { } // namespace -#pragma pack(push, 1) // No padding in ecm struct definition. -struct EcmKeyData { - const uint8_t entitlement_key_id[kEntitlementKeyIDSizeBytes]; - const uint8_t content_key_id[kContentKeyIDSizeBytes]; - const uint8_t control_word[kContentKeyDataSizeBytes]; - const uint8_t control_word_iv[kWrappedKeyIVSizeBytes]; - // Actual size can be either 8 or 16 bytes. - const uint8_t content_iv[kContentKeyMaxIVSizeBytes]; -}; - -struct EcmDescriptor { - const uint16_t ca_id; - const uint8_t version; - const uint8_t flags_cipher_rotation; - const uint8_t flags_iv_age; -}; - -static_assert((sizeof(EcmDescriptor) + 2 * sizeof(EcmKeyData)) <= - kMaxTsPayloadSizeBytes, - "Maximum possible ecm size is larger than a ts payload"); -#pragma pack(pop) // Revert padding value to previous. - -EcmParser::EcmParser(const CasEcm& ecm) : ecm_(ecm) {} - -size_t EcmParser::key_data_size() const { - return sizeof(EcmKeyData) + content_iv_size() - kContentKeyMaxIVSizeBytes; -} - -bool EcmParser::is_valid_size() const { - size_t expected_size = - sizeof(EcmDescriptor) + key_data_size() * (rotation_enabled() ? 2 : 1); - // Parser always receives entire ts payload of 184 bytes. - return ecm_.size() >= expected_size; -} - -const EcmKeyData* EcmParser::key_slot_data(KeySlotId id) const { - // ECM descriptor is followed by either one or two ECM key data. - size_t key_data_offset = sizeof(EcmDescriptor); - if (rotation_enabled()) { - if (id == KeySlotId::kOddKeySlot) { - key_data_offset += key_data_size(); - } else if (id != KeySlotId::kEvenKeySlot) { - return nullptr; - } - } else { - // No rotation enabled. - if (id != KeySlotId::kEvenKeySlot) { - return nullptr; - } - } - return reinterpret_cast(&ecm_[key_data_offset]); -} - -bool EcmParser::create(const CasEcm& cas_ecm, - std::unique_ptr* parser) { - if (parser == nullptr) { - return false; - } +std::unique_ptr EcmParser::Create(const CasEcm& cas_ecm) { // Detect and strip optional section header. - int offset = find_ecm_start_index(cas_ecm); - if (offset < 0 || (static_cast(cas_ecm.size()) - offset < - static_cast(sizeof(EcmDescriptor)))) { - return false; + const int offset = find_ecm_start_index(cas_ecm); + if (offset < 0 || + (offset + kEcmHeaderSize > static_cast(cas_ecm.size()))) { + LOGE("Unable to find start of ECM"); + return nullptr; } - const CasEcm ecm(cas_ecm.begin() + offset, cas_ecm.end()); - // Reconfirm ecm data should start with valid Widevine CAS ID. - uint16_t cas_id_val = ntohs(*reinterpret_cast(ecm.data())); + // Confirm ecm data starts with valid Widevine CAS ID. + uint16_t cas_id_val = (ecm[kCasIdIndex] << 8) | ecm[kCasIdIndex + 1]; if (cas_id_val != kWidevineCasId && (cas_id_val < kWidevineNewCasIdLowerBound || cas_id_val > kWidevineNewCasIdUpperBound)) { LOGE("Supported Widevine CAS IDs not found at the start of ECM. Found: %u", cas_id_val); - return false; + return nullptr; } - // Using 'new' to access a non-public constructor. - std::unique_ptr new_parser = - std::unique_ptr(new EcmParser(ecm)); - if (!new_parser->is_valid_size()) { - return false; + if (ecm[kVersionIndex] <= 2) { + std::unique_ptr parser; + if (!EcmParserV2::create(ecm, &parser)) { + return nullptr; + } + return parser; + } else { + return EcmParserV3::Create(ecm); } - *parser = std::move(new_parser); - return true; -} - -uint8_t EcmParser::version() const { - const EcmDescriptor* ecm = reinterpret_cast(&ecm_[0]); - return ecm->version; -} - -uint8_t EcmParser::sequence_count() const { - if (version() == 1) { - const EcmDescriptor* ecm = reinterpret_cast(&ecm_[0]); - return (ecm->flags_cipher_rotation & kSequenceCountMask) >> 3; - } - return 0; -} - -CryptoMode EcmParser::crypto_mode() const { - const EcmDescriptor* ecm = reinterpret_cast(&ecm_[0]); - if (version() == 1) { - return static_cast( - (ecm->flags_cipher_rotation & kCryptoModeFlags) >> 1); - } - return static_cast( - (ecm->flags_cipher_rotation & kCryptoModeFlagsV2) >> 1); -} - -bool EcmParser::rotation_enabled() const { - const EcmDescriptor* ecm = reinterpret_cast(&ecm_[0]); - return (ecm->flags_cipher_rotation & kRotationFlag); -} - -size_t EcmParser::content_iv_size() const { - const EcmDescriptor* ecm = reinterpret_cast(&ecm_[0]); - // Content key IV size is 8 bytes if flag is zero, and 16 if flag is set. - return (ecm->flags_iv_age & kContentIVSizeFlag) ? 16 : 8; -} - -uint8_t EcmParser::age_restriction() const { - if (version() == 1) { - return 0; - } - const EcmDescriptor* ecm = reinterpret_cast(&ecm_[0]); - return (ecm->flags_iv_age & kAgeRestrictionMask) >> 1; -} - -const std::vector EcmParser::entitlement_key_id(KeySlotId id) const { - std::vector ekey_id; - const EcmKeyData* key_data = key_slot_data(id); - if (key_data) { - ekey_id.assign( - &key_data->entitlement_key_id[0], - &key_data->entitlement_key_id[0] + kEntitlementKeyIDSizeBytes); - } - return ekey_id; -} - -const std::vector EcmParser::content_key_id(KeySlotId id) const { - std::vector ckey_id; - const EcmKeyData* key_data = key_slot_data(id); - if (key_data) { - ckey_id.assign(&key_data->content_key_id[0], - &key_data->content_key_id[0] + kContentKeyIDSizeBytes); - } - return ckey_id; -} - -const std::vector EcmParser::wrapped_key_data(KeySlotId id) const { - std::vector ckey_data; - const EcmKeyData* key_data = key_slot_data(id); - if (key_data) { - ckey_data.assign(&key_data->control_word[0], - &key_data->control_word[0] + kContentKeyDataSizeBytes); - } - return ckey_data; -} - -const std::vector EcmParser::wrapped_key_iv(KeySlotId id) const { - std::vector iv; - const EcmKeyData* key_data = key_slot_data(id); - if (key_data) { - iv.assign(&key_data->control_word_iv[0], - &key_data->control_word_iv[0] + kWrappedKeyIVSizeBytes); - } - return iv; -} - -const std::vector EcmParser::content_iv(KeySlotId id) const { - std::vector iv; - const EcmKeyData* key_data = key_slot_data(id); - if (key_data) { - iv.assign(&key_data->content_iv[0], - &key_data->content_iv[0] + content_iv_size()); - } - return iv; } } // namespace wvcas diff --git a/plugin/src/ecm_parser_v2.cpp b/plugin/src/ecm_parser_v2.cpp new file mode 100644 index 0000000..996380e --- /dev/null +++ b/plugin/src/ecm_parser_v2.cpp @@ -0,0 +1,182 @@ +// 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. + +#include "ecm_parser_v2.h" + +#include + +namespace wvcas { + +namespace { +// ECM constants +constexpr uint8_t kCryptoModeFlags = (0x3 << 1); // Mode bits 1..2 +constexpr uint8_t kCryptoModeFlagsV2 = (0xF << 1); // Mode bits 1..4 +constexpr uint8_t kRotationFlag = (0x1 << 0); // Mode bit 0 +constexpr uint8_t kContentIVSizeFlag = (0x1 << 6); +constexpr uint8_t kAgeRestrictionMask = (0x1F << 1); + +constexpr size_t kEntitlementKeyIDSizeBytes = 16; +constexpr size_t kContentKeyIDSizeBytes = 16; +constexpr size_t kContentKeyDataSizeBytes = 16; +constexpr size_t kWrappedKeyIVSizeBytes = 16; +// Size is either 8 or 16 bytes, depending on ContentIVSize flag. +constexpr size_t kContentKeyMaxIVSizeBytes = 16; + +constexpr size_t kMaxTsPayloadSizeBytes = 184; +} // namespace + +#pragma pack(push, 1) // No padding in ecm struct definition. +struct EcmKeyData { + const uint8_t entitlement_key_id[kEntitlementKeyIDSizeBytes]; + const uint8_t content_key_id[kContentKeyIDSizeBytes]; + const uint8_t control_word[kContentKeyDataSizeBytes]; + const uint8_t control_word_iv[kWrappedKeyIVSizeBytes]; + // Actual size can be either 8 or 16 bytes. + const uint8_t content_iv[kContentKeyMaxIVSizeBytes]; +}; + +struct EcmDescriptor { + const uint16_t ca_id; + const uint8_t version; + const uint8_t flags_cipher_rotation; + const uint8_t flags_iv_age; +}; + +static_assert((sizeof(EcmDescriptor) + 2 * sizeof(EcmKeyData)) <= + kMaxTsPayloadSizeBytes, + "Maximum possible ecm size is larger than a ts payload"); +#pragma pack(pop) // Revert padding value to previous. + +EcmParserV2::EcmParserV2(const CasEcm& ecm) : ecm_(ecm) {} + +size_t EcmParserV2::key_data_size() const { + return sizeof(EcmKeyData) + content_iv_size() - kContentKeyMaxIVSizeBytes; +} + +bool EcmParserV2::is_valid_size() const { + size_t expected_size = + sizeof(EcmDescriptor) + key_data_size() * (rotation_enabled() ? 2 : 1); + // Parser always receives entire ts payload of 184 bytes. + return ecm_.size() >= expected_size; +} + +const EcmKeyData* EcmParserV2::key_slot_data(KeySlotId id) const { + // ECM descriptor is followed by either one or two ECM key data. + size_t key_data_offset = sizeof(EcmDescriptor); + if (rotation_enabled()) { + if (id == KeySlotId::kOddKeySlot) { + key_data_offset += key_data_size(); + } else if (id != KeySlotId::kEvenKeySlot) { + return nullptr; + } + } else { + // No rotation enabled. + if (id != KeySlotId::kEvenKeySlot) { + return nullptr; + } + } + return reinterpret_cast(&ecm_[key_data_offset]); +} + +bool EcmParserV2::create(const CasEcm& cas_ecm, + 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)); + if (!new_parser->is_valid_size()) { + return false; + } + *parser = std::move(new_parser); + return true; +} + +uint8_t EcmParserV2::version() const { + const EcmDescriptor* ecm = reinterpret_cast(&ecm_[0]); + return ecm->version; +} + +CryptoMode EcmParserV2::crypto_mode() const { + const EcmDescriptor* ecm = reinterpret_cast(&ecm_[0]); + if (version() == 1) { + return static_cast( + (ecm->flags_cipher_rotation & kCryptoModeFlags) >> 1); + } + return static_cast( + (ecm->flags_cipher_rotation & kCryptoModeFlagsV2) >> 1); +} + +bool EcmParserV2::rotation_enabled() const { + const EcmDescriptor* ecm = reinterpret_cast(&ecm_[0]); + return (ecm->flags_cipher_rotation & kRotationFlag); +} + +size_t EcmParserV2::content_iv_size() const { + const EcmDescriptor* ecm = reinterpret_cast(&ecm_[0]); + // Content key IV size is 8 bytes if flag is zero, and 16 if flag is set. + return (ecm->flags_iv_age & kContentIVSizeFlag) ? 16 : 8; +} + +uint8_t EcmParserV2::age_restriction() const { + if (version() == 1) { + return 0; + } + const EcmDescriptor* ecm = reinterpret_cast(&ecm_[0]); + return (ecm->flags_iv_age & kAgeRestrictionMask) >> 1; +} + +std::vector EcmParserV2::entitlement_key_id(KeySlotId id) const { + std::vector ekey_id; + const EcmKeyData* key_data = key_slot_data(id); + if (key_data) { + ekey_id.assign( + &key_data->entitlement_key_id[0], + &key_data->entitlement_key_id[0] + kEntitlementKeyIDSizeBytes); + } + return ekey_id; +} + +std::vector EcmParserV2::content_key_id(KeySlotId id) const { + std::vector ckey_id; + const EcmKeyData* key_data = key_slot_data(id); + if (key_data) { + ckey_id.assign(&key_data->content_key_id[0], + &key_data->content_key_id[0] + kContentKeyIDSizeBytes); + } + return ckey_id; +} + +std::vector EcmParserV2::wrapped_key_data(KeySlotId id) const { + std::vector ckey_data; + const EcmKeyData* key_data = key_slot_data(id); + if (key_data) { + ckey_data.assign(&key_data->control_word[0], + &key_data->control_word[0] + kContentKeyDataSizeBytes); + } + return ckey_data; +} + +std::vector EcmParserV2::wrapped_key_iv(KeySlotId id) const { + std::vector iv; + const EcmKeyData* key_data = key_slot_data(id); + if (key_data) { + iv.assign(&key_data->control_word_iv[0], + &key_data->control_word_iv[0] + kWrappedKeyIVSizeBytes); + } + return iv; +} + +std::vector EcmParserV2::content_iv(KeySlotId id) const { + std::vector iv; + const EcmKeyData* key_data = key_slot_data(id); + if (key_data) { + iv.assign(&key_data->content_iv[0], + &key_data->content_iv[0] + content_iv_size()); + } + return iv; +} + +} // namespace wvcas diff --git a/plugin/src/ecm_parser_v3.cpp b/plugin/src/ecm_parser_v3.cpp new file mode 100644 index 0000000..65e1d0e --- /dev/null +++ b/plugin/src/ecm_parser_v3.cpp @@ -0,0 +1,174 @@ +// 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. + +#include "ecm_parser_v3.h" + +#include + +#include "log.h" + +namespace wvcas { + +namespace { +using video_widevine::EcmKeyData; +using video_widevine::EcmMetaData; +using video_widevine::EcmPayload; +using video_widevine::SignedEcmPayload; + +constexpr int kEcmHeaderSize = 3; +constexpr uint8_t kEcmVersion = 3; +// 16 bytes fixed content key ids +constexpr uint8_t kEvenContentKeyId[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00}; +constexpr uint8_t kOddContentKeyId[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01}; + +CryptoMode ConvertProtoCipherMode(EcmMetaData::CipherMode cipher_mode) { + switch (cipher_mode) { + case EcmMetaData::AES_CBC: + return CryptoMode::kAesCBC; + case EcmMetaData::AES_CTR: + return CryptoMode::kAesCTR; + case EcmMetaData::DVB_CSA2: + return CryptoMode::kDvbCsa2; + case EcmMetaData::DVB_CSA3: + return CryptoMode::kDvbCsa3; + case EcmMetaData::AES_OFB: + return CryptoMode::kAesOFB; + case EcmMetaData::AES_SCTE52: + return CryptoMode::kAesSCTE; + case EcmMetaData::UNSPECIFIED: + default: + return CryptoMode::kInvalid; + } +} + +} // namespace + +EcmParserV3::EcmParserV3(SignedEcmPayload signed_ecm_payload, + EcmPayload ecm_payload) + : signed_ecm_payload_(std::move(signed_ecm_payload)), + ecm_payload_(std::move(ecm_payload)) {} + +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; + } + + SignedEcmPayload signed_ecm_payload; + // The 3 byte ecm header is ignored. + if (!signed_ecm_payload.ParseFromArray(cas_ecm.data() + kEcmHeaderSize, + cas_ecm.size() - kEcmHeaderSize)) { + LOGE("Unable to parse signed ecm payload"); + return nullptr; + } + + EcmPayload ecm_payload; + if (!ecm_payload.ParseFromString(signed_ecm_payload.serialized_payload())) { + LOGE("Unable to parse ecm payload"); + return nullptr; + } + + // Using 'new' to access a non-public constructor. + return std::unique_ptr( + new EcmParserV3(signed_ecm_payload, ecm_payload)); +} + +uint8_t EcmParserV3::version() const { return kEcmVersion; } + +CryptoMode EcmParserV3::crypto_mode() const { + return ConvertProtoCipherMode(ecm_payload_.meta_data().cipher_mode()); +} + +bool EcmParserV3::rotation_enabled() const { + return ecm_payload_.has_odd_key_data(); +} + +size_t EcmParserV3::content_iv_size() const { + return ecm_payload_.even_key_data().content_iv().size(); +} + +uint8_t EcmParserV3::age_restriction() const { + return static_cast(ecm_payload_.meta_data().age_restriction()); +} + +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(); + + return {key_data.entitlement_key_id().begin(), + key_data.entitlement_key_id().end()}; +} + +std::vector EcmParserV3::content_key_id(KeySlotId id) const { + if (id == KeySlotId::kEvenKeySlot && ecm_payload_.has_even_key_data()) { + return {kEvenContentKeyId, kEvenContentKeyId + sizeof(kEvenContentKeyId)}; + } else if (id == KeySlotId::kOddKeySlot && ecm_payload_.has_odd_key_data()) { + return {kOddContentKeyId, kOddContentKeyId + sizeof(kOddContentKeyId)}; + } + return {}; +} + +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(); + + return {key_data.wrapped_key_data().begin(), + key_data.wrapped_key_data().end()}; +} + +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(); + + 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(); + + return {key_data.content_iv().begin(), key_data.content_iv().end()}; +} + +bool EcmParserV3::has_fingerprinting() const { + return ecm_payload_.has_fingerprinting(); +} + +video_widevine::Fingerprinting EcmParserV3::fingerprinting() const { + return ecm_payload_.fingerprinting(); +} + +bool EcmParserV3::has_service_blocking() const { + return ecm_payload_.has_service_blocking(); +} + +video_widevine::ServiceBlocking EcmParserV3::service_blocking() const { + return ecm_payload_.service_blocking(); +} + +std::string EcmParserV3::ecm_serialized_payload() const { + return signed_ecm_payload_.serialized_payload(); +} +std::string EcmParserV3::signature() const { + return signed_ecm_payload_.signature(); +} + +} // namespace wvcas diff --git a/plugin/src/widevine_cas_api.cpp b/plugin/src/widevine_cas_api.cpp index 0362366..6be8758 100644 --- a/plugin/src/widevine_cas_api.cpp +++ b/plugin/src/widevine_cas_api.cpp @@ -190,7 +190,7 @@ CasStatus WidevineCas::openSession(WvCasSessionId* sessionId) { CasSessionPtr session = newCasSession(); CasStatus status = session->initialize( - crypto_session_, reinterpret_cast(sessionId)); + crypto_session_, event_listener_, reinterpret_cast(sessionId)); if (CasStatusCode::kNoError != status.status_code()) { return status; } diff --git a/plugin/src/widevine_cas_session.cpp b/plugin/src/widevine_cas_session.cpp index cf363a2..2c72197 100644 --- a/plugin/src/widevine_cas_session.cpp +++ b/plugin/src/widevine_cas_session.cpp @@ -3,10 +3,13 @@ // License Agreement. #include "widevine_cas_session.h" +#include + #include #include #include "log.h" +#include "media_cas.pb.h" namespace wvcas { @@ -25,7 +28,8 @@ WidevineCasSession::~WidevineCasSession() { } CasStatus WidevineCasSession::initialize( - std::shared_ptr crypto_session, uint32_t* session_id) { + std::shared_ptr crypto_session, + CasEventListener* event_listener, uint32_t* session_id) { if (crypto_session == nullptr || session_id == nullptr) { LOGE("WidevineCasSession::initialize: missing input parameters"); return CasStatus(CasStatusCode::kInvalidParameter, @@ -34,6 +38,7 @@ CasStatus WidevineCasSession::initialize( crypto_session_ = std::move(crypto_session); crypto_session_->CreateEntitledKeySession(&key_session_id_); *session_id = key_session_id_; + event_listener_ = event_listener; return CasStatusCode::kNoError; } @@ -49,7 +54,7 @@ CasStatus WidevineCasSession::processEcm(const CasEcm& ecm, if (ecm != current_ecm_) { LOGD("WidevineCasSession::processEcm: received new ecm"); std::unique_ptr ecm_parser = getEcmParser(ecm); - if (!ecm_parser) { + if (ecm_parser == nullptr) { return CasStatus(CasStatusCode::kInvalidParameter, "invalid ecm"); } @@ -61,6 +66,46 @@ CasStatus WidevineCasSession::processEcm(const CasEcm& ecm, return CasStatus(CasStatusCode::kAccessDeniedByParentalControl, message); } + std::vector message; + if (!ecm_parser->fingerprinting().control().empty()) { + message.push_back(static_cast( + SessionFingerprintingFieldType::FINGERPRINTING_CONTROL)); + const std::string control = ecm_parser->fingerprinting().control(); + message.push_back((control.size() >> 8) & 0xff); + message.push_back(control.size() & 0xff); + message.insert(message.end(), control.begin(), control.end()); + } + if (message != last_fingerprinting_message_) { + last_fingerprinting_message_ = message; + if (event_listener_ == nullptr) { + LOGW("event_listener is null. Fingerprinting info ignored!"); + } else { + event_listener_->OnSessionFingerprintingUpdated(key_session_id_, + message); + } + } + + message.clear(); + for (int i = 0; i < ecm_parser->service_blocking().device_groups_size(); + ++i) { + message.push_back(static_cast( + SessionServiceBlockingFieldType::SERVICE_BLOCKING_DEVICE_GROUP)); + const std::string device_group = + ecm_parser->service_blocking().device_groups(i); + message.push_back((device_group.size() >> 8) & 0xff); + message.push_back(device_group.size() & 0xff); + message.insert(message.end(), device_group.begin(), device_group.end()); + } + if (message != last_service_blocking_message_) { + last_service_blocking_message_ = message; + if (event_listener_ == nullptr) { + LOGW("event_listener is null. Service blocking info ignored!"); + } else { + event_listener_->OnSessionServiceBlockingUpdated(key_session_id_, + message); + } + } + bool load_even = false; bool load_odd = false; @@ -118,11 +163,7 @@ CasStatus WidevineCasSession::processEcm(const CasEcm& ecm, std::unique_ptr WidevineCasSession::getEcmParser( const CasEcm& ecm) const { - std::unique_ptr new_ecm_parser; - if (!EcmParser::create(ecm, &new_ecm_parser)) { - return std::unique_ptr(); - } - return new_ecm_parser; + return EcmParser::Create(ecm); } const char* WidevineCasSession::securityLevel() { diff --git a/plugin/src/widevine_media_cas_plugin.cpp b/plugin/src/widevine_media_cas_plugin.cpp index 5c013a6..268b493 100644 --- a/plugin/src/widevine_media_cas_plugin.cpp +++ b/plugin/src/widevine_media_cas_plugin.cpp @@ -461,6 +461,33 @@ void WidevineCasPlugin::OnAgeRestrictionUpdated(const WvCasSessionId& sessionId, sessionId, &ecm_age_restriction, 1, &android_session_id); } +void WidevineCasPlugin::OnSessionFingerprintingUpdated( + const WvCasSessionId& sessionId, const CasData& fingerprinting) { + LOGI("OnSessionFingerprintingUpdated"); + const CasSessionId& android_session_id = + widevineSessionIdToAndroid(sessionId); + CallBack(reinterpret_cast(app_data_), + SESSION_FINGERPRINTING_INFO, /*arg=*/ + 0, + fingerprinting.empty() ? nullptr + : const_cast(&fingerprinting[0]), + fingerprinting.size(), &android_session_id); +} + +void WidevineCasPlugin::OnSessionServiceBlockingUpdated( + const WvCasSessionId& sessionId, const CasData& service_blocking) { + LOGI("OnSessionServiceBlockingUpdated"); + const CasSessionId& android_session_id = + widevineSessionIdToAndroid(sessionId); + CallBack(reinterpret_cast(app_data_), + SESSION_SERVICE_BLOCKING_INFO, /*arg=*/ + 0, + service_blocking.empty() + ? nullptr + : const_cast(&service_blocking[0]), + service_blocking.size(), &android_session_id); +} + void WidevineCasPlugin::CallBack(void* appData, int32_t event, int32_t arg, uint8_t* data, size_t size, const CasSessionId* sessionId) const { diff --git a/protos/media_cas.proto b/protos/media_cas.proto index 72b81d1..ad715ec 100644 --- a/protos/media_cas.proto +++ b/protos/media_cas.proto @@ -16,3 +16,83 @@ message CaDescriptorPrivateData { // Content ID. optional bytes content_id = 2; } + +// Widevine fingerprinting. +message Fingerprinting { + // Channels that will be applied with the controls. + repeated bytes channels = 1; + // Fingerprinting controls are opaque to Widevine. + optional bytes control = 2; +} + +// Widevine service blocking. +message ServiceBlocking { + // Channels that will be blocked. + repeated bytes channels = 1; + // Device groups that will be blocked. Group definition is opaque to Widevine. + repeated bytes device_groups = 2; + // Blocking start time in seconds since epoch. Start time is "immediate" if + // this field is not set. + optional int64 start_time_sec = 3; + // Required. Blocking end time in seconds since epoch. + optional int64 end_time_sec = 4; +} + +message EcmMetaData { + enum CipherMode { + UNSPECIFIED = 0; + AES_CBC = 1; + AES_CTR = 2; + DVB_CSA2 = 3; + DVB_CSA3 = 4; + AES_OFB = 5; + AES_SCTE52 = 6; + } + // Required. The cipher mode used to encrypt/decrypt the content. + optional CipherMode cipher_mode = 1; + // Optional. The minimum age required to watch the content. The value + // represents actual age, with 0 means no restriction. + optional uint32 age_restriction = 2 [default = 0]; +} + +message EcmKeyData { + // The wrapped content key data (aka control word). + // Required. + optional bytes wrapped_key_data = 1; + // The ID of the entitlement key used to wrap the content key. The secure key + // data associated with this ID is held by the license server. The client gets + // the key from the license server through a license request. + // Required for the even key data, optional for the odd key data if it is the + // same as the even key data. + optional bytes entitlement_key_id = 2; + // IV for decrypting the wrapped_key_data. + // Required for the even key data, optional for the odd key data if it is the + // same as the even key data. + optional bytes wrapped_key_iv = 3; + // IV for decrypting the content stream. + // Optional. If not specified in the even key data, 8 bytes 0x00 will be used; + // If not specified in the odd key data, the same content iv in the even key + // data will be used. + optional bytes content_iv = 4; +} + +message EcmPayload { + // Required. Meta info carried by the ECM. + optional EcmMetaData meta_data = 1; + // Required. The key data for the even slot. + optional EcmKeyData even_key_data = 2; + // Optional. The key data for the odd slot if key rotation is enabled. + optional EcmKeyData odd_key_data = 3; + // Optional. Widevine fingerprinting information. + optional Fingerprinting fingerprinting = 4; + // Optional. Widevine service blocking information. + optional ServiceBlocking service_blocking = 5; +} + +// The payload field for an ECM with signature. +message SignedEcmPayload { + // Serialized EcmPayload. + optional bytes serialized_payload = 1; + // ECC (Elliptic Curve Cryptography) signature of |serialized_payload|. + optional bytes signature = 2; +} \ No newline at end of file diff --git a/tests/Android.bp b/tests/Android.bp index 1ab0d50..fd207f4 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -6,6 +6,8 @@ cc_binary { "src/cas_license_test.cpp", "src/crypto_session_test.cpp", "src/ecm_parser_test.cpp", + "src/ecm_parser_v2_test.cpp", + "src/ecm_parser_v3_test.cpp", "src/test_properties.cpp", "src/widevine_cas_session_test.cpp", "src/cas_session_map_test.cpp", diff --git a/tests/src/ecm_parser_test.cpp b/tests/src/ecm_parser_test.cpp index 4f5259e..079ebc5 100644 --- a/tests/src/ecm_parser_test.cpp +++ b/tests/src/ecm_parser_test.cpp @@ -4,49 +4,23 @@ #include "ecm_parser.h" -#include #include #include -#include #include +#include "media_cas.pb.h" + +namespace wvcas { namespace { constexpr int kCasIdSizeBytes = 2; -constexpr int kModeSizeBytes = 1; constexpr int kVersionSizeBytes = 1; -constexpr int kIVFlagsSizeBytes = 1; -constexpr int kEntitlementKeyIDSizeBytes = 16; -constexpr int kContentKeyIDSizeBytes = 16; -constexpr int kContentKeyDataSize = 16; -constexpr int kWrappedKeyIVSizeBytes = 16; +constexpr int kEcmHeaderSizeBytes = kCasIdSizeBytes + kVersionSizeBytes; +constexpr int kEcmVersion2 = 2; +constexpr int kEcmVersion3 = 3; -constexpr int kEcmDescriptorSizeBytes = - kCasIdSizeBytes + kModeSizeBytes + kVersionSizeBytes + kIVFlagsSizeBytes; - -constexpr int kECMVersion = 2; -// The cipher mode flags field in the ECM V2 is 4 bits. -constexpr uint8_t kAESCBCCryptoModeFlagsVal = (0x0 << 1); -constexpr uint8_t kAESCTRCryptoModeFlagsVal = (0x1 << 1); -constexpr uint8_t kDvbCsa2CryptoModeFlagsVal = (0x2 << 1); -constexpr uint8_t kDvbCsa3CryptoModeFlagsVal = (0x3 << 1); -constexpr uint8_t kDvbOFBCryptoModeFlagsVal = (0x4 << 1); -constexpr uint8_t kDvbSCTECryptoModeFlagsVal = (0x5 << 1); -constexpr uint8_t kRotationFlag = (0x1 << 0); -constexpr uint8_t kContentIVSizeFlag = (0x1 << 6); - -constexpr uint8_t kEntitlementKeyIDFill = '1'; -constexpr uint8_t kEvenContentKeyIDFill = '2'; -constexpr uint8_t kEvenContentKeyDataFill = '3'; -constexpr uint8_t kEvenWrappedKeyIVFill = '4'; -constexpr uint8_t kEvenContentKeyIVFill = '5'; -constexpr uint8_t kOddContentKeyIDFill = '6'; -constexpr uint8_t kOddContentKeyDataFill = '7'; -constexpr uint8_t kOddWrappedKeyIVFill = '8'; -constexpr uint8_t kOddContentKeyIVFill = '9'; - -constexpr size_t kMaxEcmSizeBytes = 184; +constexpr size_t kValidEcmV2SizeBytes = 165; constexpr uint16_t kSectionHeader1 = 0x80; constexpr uint16_t kSectionHeader2 = 0x81; @@ -56,242 +30,56 @@ constexpr uint16_t kWidevineCasId = 0x4AD4; // New Widevine CAS IDs 0x56C0 to 0x56C9 (all inclusive). constexpr uint16_t kWidevineNewCasIdLowerBound = 0x56C0; constexpr uint16_t kWidevineNewCasIdUpperBound = 0x56C9; -} // namespace -class EcmParserTest : public testing::Test { - protected: - void SetUp() { - BuildEcm(kWidevineCasId, /*with_rotation=*/true, /*content_iv_flag=*/false); +std::vector BuildEcm(uint16_t cas_id, uint8_t version) { + std::vector ecm_data; + ecm_data.resize(kEcmHeaderSizeBytes); + ecm_data[0] = cas_id >> 8; + ecm_data[1] = cas_id & 0xff; + ecm_data[2] = version; + + // Put some dummy data to make the ECM a valid one. + if (version <= 2) { + ecm_data.resize(kValidEcmV2SizeBytes); + } else { + video_widevine::EcmPayload ecm_payload; + ecm_payload.mutable_even_key_data()->set_entitlement_key_id("123"); + video_widevine::SignedEcmPayload signed_ecm_payload; + signed_ecm_payload.set_serialized_payload(ecm_payload.SerializeAsString()); + ecm_data.resize(ecm_data.size() + signed_ecm_payload.ByteSize()); + signed_ecm_payload.SerializeToArray(ecm_data.data() + kEcmHeaderSizeBytes, + signed_ecm_payload.ByteSize()); } - size_t ContentKeyIVSize(bool content_iv_flag); - size_t CalculateEcmSize(bool with_rotation, bool content_iv_flag = false); - void BuildEcm(uint16_t cas_id, bool with_rotation, bool content_iv_flag); - - std::vector ecm_data_; - std::unique_ptr parser_; -}; - -size_t EcmParserTest::ContentKeyIVSize(bool content_iv_flag) { - // Content key iv is 8 bytes if Content_IV flag is zero, and 16 bytes - // othersize. - return content_iv_flag ? 16 : 8; + return ecm_data; } -size_t EcmParserTest::CalculateEcmSize(bool with_rotation, - bool content_iv_flag) { - size_t ecm_key_data_size = - kContentKeyIDSizeBytes + kContentKeyDataSize + kWrappedKeyIVSizeBytes + - kEntitlementKeyIDSizeBytes + ContentKeyIVSize(content_iv_flag); - return kEcmDescriptorSizeBytes + ecm_key_data_size * (with_rotation ? 2 : 1); +// Verifies ECM parser can be created with different version. +class EcmParserVersionTest : public testing::Test, + public ::testing::WithParamInterface {}; + +TEST_P(EcmParserVersionTest, CreateSuccess) { + std::vector ecm_data = BuildEcm(kWidevineCasId, GetParam()); + ASSERT_TRUE(wvcas::EcmParser::Create(ecm_data) != nullptr); } -void EcmParserTest::BuildEcm(uint16_t cas_id, bool with_rotation, - bool content_iv_flag) { - ecm_data_.clear(); - ecm_data_.reserve(CalculateEcmSize(with_rotation, content_iv_flag)); - ecm_data_.resize(kCasIdSizeBytes, 0); - ecm_data_[0] = cas_id >> 8; - ecm_data_[1] = cas_id & 0xff; - ecm_data_.resize(ecm_data_.size() + kVersionSizeBytes, kECMVersion); - ecm_data_.resize(ecm_data_.size() + kModeSizeBytes, - kAESCBCCryptoModeFlagsVal); - uint8_t iv_flag_value = content_iv_flag ? kContentIVSizeFlag : 0; - ecm_data_.resize(ecm_data_.size() + kIVFlagsSizeBytes, iv_flag_value); - ASSERT_EQ(kEcmDescriptorSizeBytes, ecm_data_.size()); - - // Even key fields. - ecm_data_.resize(ecm_data_.size() + kEntitlementKeyIDSizeBytes, - kEntitlementKeyIDFill); - ecm_data_.resize(ecm_data_.size() + kContentKeyIDSizeBytes, - kEvenContentKeyIDFill); - ecm_data_.resize(ecm_data_.size() + kContentKeyDataSize, - kEvenContentKeyDataFill); - ecm_data_.resize(ecm_data_.size() + kWrappedKeyIVSizeBytes, - kEvenWrappedKeyIVFill); - ecm_data_.resize(ecm_data_.size() + ContentKeyIVSize(content_iv_flag), - kEvenContentKeyIVFill); - ASSERT_EQ(CalculateEcmSize(false, content_iv_flag), ecm_data_.size()); - - if (with_rotation) { - // Entitlement key id field for odd key. - ecm_data_.resize(ecm_data_.size() + kEntitlementKeyIDSizeBytes, - kEntitlementKeyIDFill); - ecm_data_.resize(ecm_data_.size() + kContentKeyIDSizeBytes, - kOddContentKeyIDFill); - ecm_data_.resize(ecm_data_.size() + kContentKeyDataSize, - kOddContentKeyDataFill); - ecm_data_.resize(ecm_data_.size() + kWrappedKeyIVSizeBytes, - kOddWrappedKeyIVFill); - ecm_data_.resize(ecm_data_.size() + ContentKeyIVSize(content_iv_flag), - kOddContentKeyIVFill); - ASSERT_EQ(CalculateEcmSize(true, content_iv_flag), ecm_data_.size()); - } -} - -TEST_F(EcmParserTest, FieldsWithoutKeyRotation) { - bool content_key_iv_16b = false; - ecm_data_.resize(CalculateEcmSize(false, content_key_iv_16b)); - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - ASSERT_FALSE(parser_->rotation_enabled()); - - std::vector test_data; - test_data.resize(kEntitlementKeyIDSizeBytes, kEntitlementKeyIDFill); - EXPECT_EQ(test_data, - parser_->entitlement_key_id(wvcas::KeySlotId::kEvenKeySlot)); - - test_data.clear(); - test_data.resize(kContentKeyIDSizeBytes, kEvenContentKeyIDFill); - EXPECT_EQ(test_data, parser_->content_key_id(wvcas::KeySlotId::kEvenKeySlot)); - - test_data.clear(); - test_data.resize(kContentKeyIDSizeBytes, kEvenContentKeyDataFill); - EXPECT_EQ(test_data, - parser_->wrapped_key_data(wvcas::KeySlotId::kEvenKeySlot)); - - test_data.clear(); - test_data.resize(kWrappedKeyIVSizeBytes, kEvenWrappedKeyIVFill); - EXPECT_EQ(test_data, parser_->wrapped_key_iv(wvcas::KeySlotId::kEvenKeySlot)); - - test_data.clear(); - test_data.resize(ContentKeyIVSize(content_key_iv_16b), kEvenContentKeyIVFill); - EXPECT_EQ(test_data, parser_->content_iv(wvcas::KeySlotId::kEvenKeySlot)); - - EXPECT_TRUE(parser_->content_key_id(wvcas::KeySlotId::kOddKeySlot).empty()); - EXPECT_TRUE(parser_->wrapped_key_data(wvcas::KeySlotId::kOddKeySlot).empty()); - EXPECT_TRUE(parser_->wrapped_key_iv(wvcas::KeySlotId::kOddKeySlot).empty()); - EXPECT_TRUE(parser_->content_iv(wvcas::KeySlotId::kOddKeySlot).empty()); -} - -TEST_F(EcmParserTest, FieldsWithKeyRotation) { - ecm_data_[3] |= kRotationFlag; - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - ASSERT_TRUE(parser_->rotation_enabled()); - - std::vector test_data; - test_data.resize(kEntitlementKeyIDSizeBytes, kEntitlementKeyIDFill); - EXPECT_EQ(test_data, - parser_->entitlement_key_id(wvcas::KeySlotId::kEvenKeySlot)); - - test_data.clear(); - test_data.resize(kContentKeyIDSizeBytes, kEvenContentKeyIDFill); - EXPECT_EQ(test_data, parser_->content_key_id(wvcas::KeySlotId::kEvenKeySlot)); - - test_data.clear(); - test_data.resize(kContentKeyIDSizeBytes, kEvenContentKeyDataFill); - EXPECT_EQ(test_data, - parser_->wrapped_key_data(wvcas::KeySlotId::kEvenKeySlot)); - - test_data.clear(); - test_data.resize(kWrappedKeyIVSizeBytes, kEvenWrappedKeyIVFill); - EXPECT_EQ(test_data, parser_->wrapped_key_iv(wvcas::KeySlotId::kEvenKeySlot)); - - test_data.clear(); - bool content_key_iv_16b = false; - test_data.resize(ContentKeyIVSize(content_key_iv_16b), kEvenContentKeyIVFill); - EXPECT_EQ(test_data, parser_->content_iv(wvcas::KeySlotId::kEvenKeySlot)); - - test_data.clear(); - test_data.resize(kContentKeyIDSizeBytes, kOddContentKeyIDFill); - EXPECT_EQ(test_data, parser_->content_key_id(wvcas::KeySlotId::kOddKeySlot)); - - test_data.clear(); - test_data.resize(kContentKeyIDSizeBytes, kOddContentKeyDataFill); - EXPECT_EQ(test_data, - parser_->wrapped_key_data(wvcas::KeySlotId::kOddKeySlot)); - - test_data.clear(); - test_data.resize(kWrappedKeyIVSizeBytes, kOddWrappedKeyIVFill); - EXPECT_EQ(test_data, parser_->wrapped_key_iv(wvcas::KeySlotId::kOddKeySlot)); - - test_data.clear(); - test_data.resize(ContentKeyIVSize(content_key_iv_16b), kOddContentKeyIVFill); - EXPECT_EQ(test_data, parser_->content_iv(wvcas::KeySlotId::kOddKeySlot)); -} - -TEST_F(EcmParserTest, create) { - EXPECT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - - ecm_data_.resize(4); - EXPECT_FALSE(wvcas::EcmParser::create(ecm_data_, &parser_)); - - ecm_data_.resize(4 + CalculateEcmSize(false)); - EXPECT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - - ecm_data_.resize(kMaxEcmSizeBytes); - EXPECT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - - ecm_data_.resize(CalculateEcmSize(true)); - EXPECT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_FALSE(wvcas::EcmParser::create(ecm_data_, nullptr)); - - ecm_data_.resize(CalculateEcmSize(true)); - EXPECT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_FALSE(wvcas::EcmParser::create(ecm_data_, nullptr)); -} - -TEST_F(EcmParserTest, crypto_mode) { - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kAesCBC); - - ecm_data_[3] = kAESCTRCryptoModeFlagsVal; - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kAesCTR); - - ecm_data_[3] = kDvbCsa2CryptoModeFlagsVal; - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kDvbCsa2); - - ecm_data_[3] = kDvbCsa3CryptoModeFlagsVal; - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kDvbCsa3); - - ecm_data_[3] = kDvbOFBCryptoModeFlagsVal; - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kAesOFB); - - ecm_data_[3] = kDvbSCTECryptoModeFlagsVal; - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kAesSCTE); -} - -TEST_F(EcmParserTest, ContentKeyIVSizes) { - bool with_rotation = true; - bool iv_flag = false; - ecm_data_.resize(CalculateEcmSize(with_rotation, iv_flag)); - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_EQ(parser_->content_iv_size(), ContentKeyIVSize(iv_flag)); - - iv_flag = true; - ecm_data_[4] = kContentIVSizeFlag; - ecm_data_.resize(CalculateEcmSize(with_rotation, iv_flag)); - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_EQ(parser_->content_iv_size(), ContentKeyIVSize(iv_flag)); -} - -TEST_F(EcmParserTest, AgeRestriction) { - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_EQ(0, parser_->age_restriction()); - - uint8_t age_restriction = 16; - ecm_data_[4] |= age_restriction << 1; - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); - EXPECT_EQ(age_restriction, parser_->age_restriction()); -} +INSTANTIATE_TEST_SUITE_P(EcmParserVersionTest, EcmParserVersionTest, + ::testing::Values(kEcmVersion2, kEcmVersion3)); // Verifies CAS ID returned by the parser must be expected ones. class EcmParserCasIdTest - : public EcmParserTest, - public ::testing::WithParamInterface<::testing::tuple> { - protected: - void SetUp() override { - const uint16_t cas_id = ::testing::get<0>(GetParam()); - BuildEcm(cas_id, /*with_rotation=*/true, /*content_iv_flag=*/false); - } -}; + : public testing::Test, + public ::testing::WithParamInterface<::testing::tuple> {}; TEST_P(EcmParserCasIdTest, ValidateCasIds) { - bool expected_result = ::testing::get<1>(GetParam()); - ASSERT_EQ(wvcas::EcmParser::create(ecm_data_, &parser_), expected_result); + const uint16_t cas_id = ::testing::get<0>(GetParam()); + std::vector ecm_data = BuildEcm(cas_id, kEcmVersion2); + + const bool is_valid_id = ::testing::get<1>(GetParam()); + if (is_valid_id) { + ASSERT_TRUE(wvcas::EcmParser::Create(ecm_data) != nullptr); + } else { + ASSERT_TRUE(wvcas::EcmParser::Create(ecm_data) == nullptr); + } } INSTANTIATE_TEST_SUITE_P(EcmWithLegacyCasId, EcmParserCasIdTest, @@ -315,23 +103,28 @@ INSTANTIATE_TEST_SUITE_P( // Verifies Section header and pointer field may be prepended to ECM. class EcmParserSectionHeaderTest - : public EcmParserTest, + : public testing::Test, public ::testing::WithParamInterface {}; TEST_P(EcmParserSectionHeaderTest, EcmWithSectionHeaderOnly) { + std::vector ecm_data = BuildEcm(kWidevineCasId, kEcmVersion2); const std::vector section_header = {GetParam(), 0, 0}; - ecm_data_.insert(ecm_data_.begin(), section_header.begin(), - section_header.end()); - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); + ecm_data.insert(ecm_data.begin(), section_header.begin(), + section_header.end()); + ASSERT_TRUE(wvcas::EcmParser::Create(ecm_data) != nullptr); } TEST_P(EcmParserSectionHeaderTest, EcmWithSectionHeaderAndPointerField) { + std::vector ecm_data = BuildEcm(kWidevineCasId, kEcmVersion2); const std::vector section_header = {kPointerFieldZero, GetParam(), 0, 0}; - ecm_data_.insert(ecm_data_.begin(), section_header.begin(), - section_header.end()); - ASSERT_TRUE(wvcas::EcmParser::create(ecm_data_, &parser_)); + ecm_data.insert(ecm_data.begin(), section_header.begin(), + section_header.end()); + ASSERT_TRUE(wvcas::EcmParser::Create(ecm_data) != nullptr); } INSTANTIATE_TEST_SUITE_P(EcmWithSectionHeader, EcmParserSectionHeaderTest, ::testing::Values(kSectionHeader1, kSectionHeader2)); + +} // namespace +} // namespace wvcas \ No newline at end of file diff --git a/tests/src/ecm_parser_v2_test.cpp b/tests/src/ecm_parser_v2_test.cpp new file mode 100644 index 0000000..a32464b --- /dev/null +++ b/tests/src/ecm_parser_v2_test.cpp @@ -0,0 +1,270 @@ +// 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. + +#include "ecm_parser_v2.h" + +#include +#include + +namespace { + +constexpr int kCasIdSizeBytes = 2; +constexpr int kModeSizeBytes = 1; +constexpr int kVersionSizeBytes = 1; +constexpr int kIVFlagsSizeBytes = 1; +constexpr int kEntitlementKeyIDSizeBytes = 16; +constexpr int kContentKeyIDSizeBytes = 16; +constexpr int kContentKeyDataSize = 16; +constexpr int kWrappedKeyIVSizeBytes = 16; + +constexpr int kEcmDescriptorSizeBytes = + kCasIdSizeBytes + kModeSizeBytes + kVersionSizeBytes + kIVFlagsSizeBytes; + +constexpr int kECMVersion = 2; +// The cipher mode flags field in the ECM V2 is 4 bits. +constexpr uint8_t kAESCBCCryptoModeFlagsVal = (0x0 << 1); +constexpr uint8_t kAESCTRCryptoModeFlagsVal = (0x1 << 1); +constexpr uint8_t kDvbCsa2CryptoModeFlagsVal = (0x2 << 1); +constexpr uint8_t kDvbCsa3CryptoModeFlagsVal = (0x3 << 1); +constexpr uint8_t kDvbOFBCryptoModeFlagsVal = (0x4 << 1); +constexpr uint8_t kDvbSCTECryptoModeFlagsVal = (0x5 << 1); +constexpr uint8_t kRotationFlag = (0x1 << 0); +constexpr uint8_t kContentIVSizeFlag = (0x1 << 6); + +constexpr uint8_t kEntitlementKeyIDFill = '1'; +constexpr uint8_t kEvenContentKeyIDFill = '2'; +constexpr uint8_t kEvenContentKeyDataFill = '3'; +constexpr uint8_t kEvenWrappedKeyIVFill = '4'; +constexpr uint8_t kEvenContentKeyIVFill = '5'; +constexpr uint8_t kOddContentKeyIDFill = '6'; +constexpr uint8_t kOddContentKeyDataFill = '7'; +constexpr uint8_t kOddWrappedKeyIVFill = '8'; +constexpr uint8_t kOddContentKeyIVFill = '9'; + +constexpr size_t kMaxEcmSizeBytes = 184; +constexpr uint16_t kWidevineCasId = 0x4AD4; + +} // namespace + +class EcmParserV2Test : public testing::Test { + protected: + void SetUp() { BuildEcm(/*with_rotation=*/true, /*content_iv_flag=*/false); } + size_t ContentKeyIVSize(bool content_iv_flag); + size_t CalculateEcmSize(bool with_rotation, bool content_iv_flag = false); + void BuildEcm(bool with_rotation, bool content_iv_flag); + + std::vector ecm_data_; + std::unique_ptr parser_; +}; + +size_t EcmParserV2Test::ContentKeyIVSize(bool content_iv_flag) { + // Content key iv is 8 bytes if Content_IV flag is zero, and 16 bytes + // othersize. + return content_iv_flag ? 16 : 8; +} + +size_t EcmParserV2Test::CalculateEcmSize(bool with_rotation, + bool content_iv_flag) { + size_t ecm_key_data_size = + kContentKeyIDSizeBytes + kContentKeyDataSize + kWrappedKeyIVSizeBytes + + kEntitlementKeyIDSizeBytes + ContentKeyIVSize(content_iv_flag); + return kEcmDescriptorSizeBytes + ecm_key_data_size * (with_rotation ? 2 : 1); +} + +void EcmParserV2Test::BuildEcm(bool with_rotation, bool content_iv_flag) { + ecm_data_.clear(); + ecm_data_.reserve(CalculateEcmSize(with_rotation, content_iv_flag)); + ecm_data_.resize(kCasIdSizeBytes, 0); + ecm_data_[0] = kWidevineCasId >> 8; + ecm_data_[1] = kWidevineCasId & 0xff; + ecm_data_.resize(ecm_data_.size() + kVersionSizeBytes, kECMVersion); + ecm_data_.resize(ecm_data_.size() + kModeSizeBytes, + kAESCBCCryptoModeFlagsVal); + uint8_t iv_flag_value = content_iv_flag ? kContentIVSizeFlag : 0; + ecm_data_.resize(ecm_data_.size() + kIVFlagsSizeBytes, iv_flag_value); + ASSERT_EQ(kEcmDescriptorSizeBytes, ecm_data_.size()); + + // Even key fields. + ecm_data_.resize(ecm_data_.size() + kEntitlementKeyIDSizeBytes, + kEntitlementKeyIDFill); + ecm_data_.resize(ecm_data_.size() + kContentKeyIDSizeBytes, + kEvenContentKeyIDFill); + ecm_data_.resize(ecm_data_.size() + kContentKeyDataSize, + kEvenContentKeyDataFill); + ecm_data_.resize(ecm_data_.size() + kWrappedKeyIVSizeBytes, + kEvenWrappedKeyIVFill); + ecm_data_.resize(ecm_data_.size() + ContentKeyIVSize(content_iv_flag), + kEvenContentKeyIVFill); + ASSERT_EQ(CalculateEcmSize(false, content_iv_flag), ecm_data_.size()); + + if (with_rotation) { + // Entitlement key id field for odd key. + ecm_data_.resize(ecm_data_.size() + kEntitlementKeyIDSizeBytes, + kEntitlementKeyIDFill); + ecm_data_.resize(ecm_data_.size() + kContentKeyIDSizeBytes, + kOddContentKeyIDFill); + ecm_data_.resize(ecm_data_.size() + kContentKeyDataSize, + kOddContentKeyDataFill); + ecm_data_.resize(ecm_data_.size() + kWrappedKeyIVSizeBytes, + kOddWrappedKeyIVFill); + ecm_data_.resize(ecm_data_.size() + ContentKeyIVSize(content_iv_flag), + kOddContentKeyIVFill); + ASSERT_EQ(CalculateEcmSize(true, content_iv_flag), ecm_data_.size()); + } +} + +TEST_F(EcmParserV2Test, FieldsWithoutKeyRotation) { + bool content_key_iv_16b = false; + ecm_data_.resize(CalculateEcmSize(false, content_key_iv_16b)); + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + ASSERT_FALSE(parser_->rotation_enabled()); + + std::vector test_data; + test_data.resize(kEntitlementKeyIDSizeBytes, kEntitlementKeyIDFill); + EXPECT_EQ(test_data, + parser_->entitlement_key_id(wvcas::KeySlotId::kEvenKeySlot)); + + test_data.clear(); + test_data.resize(kContentKeyIDSizeBytes, kEvenContentKeyIDFill); + EXPECT_EQ(test_data, parser_->content_key_id(wvcas::KeySlotId::kEvenKeySlot)); + + test_data.clear(); + test_data.resize(kContentKeyIDSizeBytes, kEvenContentKeyDataFill); + EXPECT_EQ(test_data, + parser_->wrapped_key_data(wvcas::KeySlotId::kEvenKeySlot)); + + test_data.clear(); + test_data.resize(kWrappedKeyIVSizeBytes, kEvenWrappedKeyIVFill); + EXPECT_EQ(test_data, parser_->wrapped_key_iv(wvcas::KeySlotId::kEvenKeySlot)); + + test_data.clear(); + test_data.resize(ContentKeyIVSize(content_key_iv_16b), kEvenContentKeyIVFill); + EXPECT_EQ(test_data, parser_->content_iv(wvcas::KeySlotId::kEvenKeySlot)); + + EXPECT_TRUE(parser_->content_key_id(wvcas::KeySlotId::kOddKeySlot).empty()); + EXPECT_TRUE(parser_->wrapped_key_data(wvcas::KeySlotId::kOddKeySlot).empty()); + EXPECT_TRUE(parser_->wrapped_key_iv(wvcas::KeySlotId::kOddKeySlot).empty()); + EXPECT_TRUE(parser_->content_iv(wvcas::KeySlotId::kOddKeySlot).empty()); +} + +TEST_F(EcmParserV2Test, FieldsWithKeyRotation) { + ecm_data_[3] |= kRotationFlag; + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + ASSERT_TRUE(parser_->rotation_enabled()); + + std::vector test_data; + test_data.resize(kEntitlementKeyIDSizeBytes, kEntitlementKeyIDFill); + EXPECT_EQ(test_data, + parser_->entitlement_key_id(wvcas::KeySlotId::kEvenKeySlot)); + + test_data.clear(); + test_data.resize(kContentKeyIDSizeBytes, kEvenContentKeyIDFill); + EXPECT_EQ(test_data, parser_->content_key_id(wvcas::KeySlotId::kEvenKeySlot)); + + test_data.clear(); + test_data.resize(kContentKeyIDSizeBytes, kEvenContentKeyDataFill); + EXPECT_EQ(test_data, + parser_->wrapped_key_data(wvcas::KeySlotId::kEvenKeySlot)); + + test_data.clear(); + test_data.resize(kWrappedKeyIVSizeBytes, kEvenWrappedKeyIVFill); + EXPECT_EQ(test_data, parser_->wrapped_key_iv(wvcas::KeySlotId::kEvenKeySlot)); + + test_data.clear(); + bool content_key_iv_16b = false; + test_data.resize(ContentKeyIVSize(content_key_iv_16b), kEvenContentKeyIVFill); + EXPECT_EQ(test_data, parser_->content_iv(wvcas::KeySlotId::kEvenKeySlot)); + + test_data.clear(); + test_data.resize(kContentKeyIDSizeBytes, kOddContentKeyIDFill); + EXPECT_EQ(test_data, parser_->content_key_id(wvcas::KeySlotId::kOddKeySlot)); + + test_data.clear(); + test_data.resize(kContentKeyIDSizeBytes, kOddContentKeyDataFill); + EXPECT_EQ(test_data, + parser_->wrapped_key_data(wvcas::KeySlotId::kOddKeySlot)); + + test_data.clear(); + test_data.resize(kWrappedKeyIVSizeBytes, kOddWrappedKeyIVFill); + EXPECT_EQ(test_data, parser_->wrapped_key_iv(wvcas::KeySlotId::kOddKeySlot)); + + test_data.clear(); + test_data.resize(ContentKeyIVSize(content_key_iv_16b), kOddContentKeyIVFill); + EXPECT_EQ(test_data, parser_->content_iv(wvcas::KeySlotId::kOddKeySlot)); +} + +TEST_F(EcmParserV2Test, create) { + EXPECT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + + ecm_data_.resize(4); + EXPECT_FALSE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + + ecm_data_.resize(4 + CalculateEcmSize(false)); + EXPECT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + + ecm_data_.resize(kMaxEcmSizeBytes); + EXPECT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + + ecm_data_.resize(CalculateEcmSize(true)); + EXPECT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_FALSE(wvcas::EcmParserV2::create(ecm_data_, nullptr)); + + ecm_data_.resize(CalculateEcmSize(true)); + EXPECT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_FALSE(wvcas::EcmParserV2::create(ecm_data_, nullptr)); +} + +TEST_F(EcmParserV2Test, crypto_mode) { + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kAesCBC); + + ecm_data_[3] = kAESCTRCryptoModeFlagsVal; + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kAesCTR); + + ecm_data_[3] = kDvbCsa2CryptoModeFlagsVal; + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kDvbCsa2); + + ecm_data_[3] = kDvbCsa3CryptoModeFlagsVal; + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kDvbCsa3); + + ecm_data_[3] = kDvbOFBCryptoModeFlagsVal; + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kAesOFB); + + ecm_data_[3] = kDvbSCTECryptoModeFlagsVal; + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_EQ(parser_->crypto_mode(), wvcas::CryptoMode::kAesSCTE); +} + +TEST_F(EcmParserV2Test, ContentKeyIVSizes) { + bool with_rotation = true; + bool iv_flag = false; + ecm_data_.resize(CalculateEcmSize(with_rotation, iv_flag)); + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_EQ(parser_->content_iv_size(), ContentKeyIVSize(iv_flag)); + + iv_flag = true; + ecm_data_[4] = kContentIVSizeFlag; + ecm_data_.resize(CalculateEcmSize(with_rotation, iv_flag)); + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_EQ(parser_->content_iv_size(), ContentKeyIVSize(iv_flag)); +} + +TEST_F(EcmParserV2Test, AgeRestriction) { + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_EQ(0, parser_->age_restriction()); + + uint8_t age_restriction = 16; + ecm_data_[4] |= age_restriction << 1; + ASSERT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_EQ(age_restriction, parser_->age_restriction()); +} + +TEST_F(EcmParserV2Test, Version) { + EXPECT_TRUE(wvcas::EcmParserV2::create(ecm_data_, &parser_)); + EXPECT_EQ(parser_->version(), kECMVersion); +} diff --git a/tests/src/ecm_parser_v3_test.cpp b/tests/src/ecm_parser_v3_test.cpp new file mode 100644 index 0000000..63555c6 --- /dev/null +++ b/tests/src/ecm_parser_v3_test.cpp @@ -0,0 +1,291 @@ +// 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. + +#include "ecm_parser_v3.h" + +#include +#include + +#include + +#include "media_cas.pb.h" + +namespace wvcas { +namespace { + +using video_widevine::EcmMetaData; +using video_widevine::EcmPayload; +using video_widevine::SignedEcmPayload; + +constexpr int kEcmVersion = 3; +constexpr uint16_t kWidevineCasId = 0x4AD4; +constexpr int kEcmHeaderSizeByte = 3; + +constexpr char kWrappedKeyIv[] = "wrapped_key_iv.."; +constexpr char kWrappedKeyIv2[] = "wrapped_key_iv.2"; +constexpr char kEntitlementId[] = "entitlement_id.."; +constexpr char kEntitlementId2[] = "entitlement_id.2"; +constexpr char kContentIv[] = "c_iv....c_iv...."; +constexpr char kContentIv2[] = "c_iv....c_iv...2"; +constexpr char kWrappedContentKey[] = "wrapped_key....."; +constexpr char kWrappedContentKey2[] = "wrapped_key....2"; + +void WriteEcmHeader(std::vector* ecm) { + ecm->push_back(kWidevineCasId >> 8); + ecm->push_back(kWidevineCasId & 0xff); + ecm->push_back(kEcmVersion); +} + +std::vector GenerateEcm(const SignedEcmPayload& signed_ecm_payload) { + std::vector ecm; + WriteEcmHeader(&ecm); + ecm.resize(kEcmHeaderSizeByte + signed_ecm_payload.ByteSize()); + signed_ecm_payload.SerializeToArray(ecm.data() + kEcmHeaderSizeByte, + signed_ecm_payload.ByteSize()); + return ecm; +} + +TEST(EcmParserV3Test, CreateWithEmptyEcmFail) { + std::vector ecm; + EXPECT_TRUE(EcmParserV3::Create(ecm) == nullptr); +} + +TEST(EcmParserV3Test, CreateWithOnlyEcmHeaderFail) { + std::vector ecm; + WriteEcmHeader(&ecm); + EXPECT_TRUE(EcmParserV3::Create(ecm) == nullptr); +} + +TEST(EcmParserV3Test, CreateWithInvalidSignedEcmPayloadFail) { + std::vector ecm; + WriteEcmHeader(&ecm); + // appends some chars as payload + ecm.resize(100, 'x'); + + EXPECT_TRUE(EcmParserV3::Create(ecm) == nullptr); +} + +TEST(EcmParserV3Test, CreateWithInvalidSerializedEcmFail) { + SignedEcmPayload signed_ecm_payload; + signed_ecm_payload.set_serialized_payload("invalid"); + std::vector ecm = GenerateEcm(signed_ecm_payload); + + EXPECT_TRUE(EcmParserV3::Create(ecm) == nullptr); +} + +TEST(EcmParserV3Test, CreateWithEvenKeySuccess) { + EcmPayload ecm_payload; + ecm_payload.mutable_even_key_data()->set_entitlement_key_id(kEntitlementId); + ecm_payload.mutable_even_key_data()->set_wrapped_key_data(kWrappedContentKey); + ecm_payload.mutable_even_key_data()->set_content_iv(kContentIv); + ecm_payload.mutable_even_key_data()->set_wrapped_key_iv(kWrappedKeyIv); + 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_EQ(parser->version(), kEcmVersion); + EXPECT_EQ(parser->age_restriction(), 0); + EXPECT_EQ(parser->crypto_mode(), CryptoMode::kInvalid); + EXPECT_FALSE(parser->has_fingerprinting()); + EXPECT_FALSE(parser->has_service_blocking()); + EXPECT_EQ(parser->ecm_serialized_payload(), ecm_payload.SerializeAsString()); + EXPECT_TRUE(parser->signature().empty()); + EXPECT_FALSE(parser->rotation_enabled()); + EXPECT_EQ(parser->content_iv_size(), 16); + 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); + 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, CreateWithEvenOddKeysSuccess) { + EcmPayload ecm_payload; + ecm_payload.mutable_even_key_data()->set_entitlement_key_id(kEntitlementId); + ecm_payload.mutable_even_key_data()->set_wrapped_key_data(kWrappedContentKey); + ecm_payload.mutable_even_key_data()->set_content_iv(kContentIv); + ecm_payload.mutable_even_key_data()->set_wrapped_key_iv(kWrappedKeyIv); + ecm_payload.mutable_odd_key_data()->set_entitlement_key_id(kEntitlementId2); + ecm_payload.mutable_odd_key_data()->set_wrapped_key_data(kWrappedContentKey2); + ecm_payload.mutable_odd_key_data()->set_content_iv(kContentIv2); + ecm_payload.mutable_odd_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->rotation_enabled()); + 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); + 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); + result = parser->entitlement_key_id(KeySlotId::kOddKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kEntitlementId2); + result = parser->wrapped_key_data(KeySlotId::kOddKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kWrappedContentKey2); + result = parser->content_iv(KeySlotId::kOddKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kContentIv2); + result = parser->wrapped_key_iv(KeySlotId::kOddKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kWrappedKeyIv2); +} + +TEST(EcmParserV3Test, CreateWithOmittedOddKeyFieldsSuccess) { + EcmPayload ecm_payload; + ecm_payload.mutable_even_key_data()->set_entitlement_key_id(kEntitlementId); + ecm_payload.mutable_even_key_data()->set_wrapped_key_data(kWrappedContentKey); + ecm_payload.mutable_even_key_data()->set_content_iv(kContentIv); + ecm_payload.mutable_even_key_data()->set_wrapped_key_iv(kWrappedKeyIv); + ecm_payload.mutable_odd_key_data()->set_wrapped_key_data(kWrappedContentKey2); + 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->rotation_enabled()); + 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); + 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); + result = parser->entitlement_key_id(KeySlotId::kOddKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kEntitlementId); + result = parser->wrapped_key_data(KeySlotId::kOddKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kWrappedContentKey2); + result = parser->content_iv(KeySlotId::kOddKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kContentIv); + result = parser->wrapped_key_iv(KeySlotId::kOddKeySlot); + EXPECT_EQ(std::string(result.begin(), result.end()), kWrappedKeyIv); +} + +TEST(EcmParserV3Test, AgeRestrictionSuccess) { + const int expected_age_restriction = 3; + EcmPayload ecm_payload; + ecm_payload.mutable_meta_data()->set_age_restriction( + expected_age_restriction); + 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_EQ(parser->age_restriction(), expected_age_restriction); +} + +class EcmParserV3AgeRestrictionTest + : public testing::Test, + public testing::WithParamInterface {}; + +TEST_P(EcmParserV3AgeRestrictionTest, ExpectedAgeRestriction) { + const uint8_t expected_age_restriction = GetParam(); + EcmPayload ecm_payload; + ecm_payload.mutable_meta_data()->set_age_restriction( + expected_age_restriction); + 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_EQ(parser->age_restriction(), expected_age_restriction); +} + +INSTANTIATE_TEST_SUITE_P(EcmParserV3AgeRestrictionTest, + EcmParserV3AgeRestrictionTest, + testing::Values(0, 3, 18)); + +class EcmParserV3CipherModeTest + : public testing::Test, + public testing::WithParamInterface< + testing::tuple> {}; + +TEST_P(EcmParserV3CipherModeTest, ExpectedCipherMode) { + const CryptoMode expected = std::get<0>(GetParam()); + EcmPayload ecm_payload; + ecm_payload.mutable_meta_data()->set_cipher_mode(std::get<1>(GetParam())); + 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_EQ(parser->crypto_mode(), expected); +} + +INSTANTIATE_TEST_SUITE_P( + EcmParserV3CipherModeTest, EcmParserV3CipherModeTest, + testing::Values( + std::make_tuple(CryptoMode::kAesCBC, EcmMetaData::AES_CBC), + std::make_tuple(CryptoMode::kAesCTR, EcmMetaData::AES_CTR), + std::make_tuple(CryptoMode::kDvbCsa2, EcmMetaData::DVB_CSA2), + std::make_tuple(CryptoMode::kDvbCsa3, EcmMetaData::DVB_CSA3), + std::make_tuple(CryptoMode::kAesOFB, EcmMetaData::AES_OFB), + std::make_tuple(CryptoMode::kAesSCTE, EcmMetaData::AES_SCTE52))); + +TEST(EcmParserV3Test, FingerprintingSuccess) { + EcmPayload ecm_payload; + ecm_payload.mutable_fingerprinting()->set_control("control"); + 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->has_fingerprinting()); + EXPECT_EQ(parser->fingerprinting().SerializeAsString(), + ecm_payload.fingerprinting().SerializeAsString()); +} + +TEST(EcmParserV3Test, ServiceBlockingSuccess) { + EcmPayload ecm_payload; + ecm_payload.mutable_service_blocking()->add_device_groups("group"); + 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->has_service_blocking()); + EXPECT_EQ(parser->service_blocking().SerializeAsString(), + ecm_payload.service_blocking().SerializeAsString()); +} + +TEST(EcmParserV3Test, SignatureSuccess) { + const std::string expected_signature = "signature"; + SignedEcmPayload signed_ecm_payload; + signed_ecm_payload.set_signature(expected_signature); + std::vector ecm = GenerateEcm(signed_ecm_payload); + + std::unique_ptr parser = EcmParserV3::Create(ecm); + + ASSERT_TRUE(parser != nullptr); + EXPECT_EQ(parser->signature(), expected_signature); +} + +} // namespace +} // namespace wvcas diff --git a/tests/src/mediacas_integration_test.cpp b/tests/src/mediacas_integration_test.cpp index 4a1da8f..095fb09 100644 --- a/tests/src/mediacas_integration_test.cpp +++ b/tests/src/mediacas_integration_test.cpp @@ -115,3 +115,6 @@ TEST(IntegrationTests, TestSessionEventPassing) { EXPECT_EQ(kIntegrationTestPassed, RunNamedTest("TestSessionEventPassing")); } +TEST(IntegrationTests, TestProcessEcmV3) { + EXPECT_EQ(kIntegrationTestPassed, RunNamedTest("TestProcessEcmV3")); +} diff --git a/tests/src/policy_engine_test.cpp b/tests/src/policy_engine_test.cpp index 1d7b677..c08be37 100644 --- a/tests/src/policy_engine_test.cpp +++ b/tests/src/policy_engine_test.cpp @@ -91,6 +91,14 @@ class MockEventListener : public wvcas::CasEventListener { MOCK_METHOD2(OnAgeRestrictionUpdated, void(const wvcas::WvCasSessionId& sessionId, uint8_t ecm_age_restriction)); + MOCK_METHOD(void, OnSessionFingerprintingUpdated, + (const int32_t& sessionId, + const std::vector& fingerprinting), + (override)); + MOCK_METHOD(void, OnSessionServiceBlockingUpdated, + (const int32_t& sessionId, + const std::vector& service_blocking), + (override)); }; class TestablePolicyEngine : public wvcas::PolicyEngine { diff --git a/tests/src/widevine_cas_api_test.cpp b/tests/src/widevine_cas_api_test.cpp index 00ea9e0..2d27011 100644 --- a/tests/src/widevine_cas_api_test.cpp +++ b/tests/src/widevine_cas_api_test.cpp @@ -85,6 +85,14 @@ class MockEventListener : public wvcas::CasEventListener { MOCK_METHOD2(OnAgeRestrictionUpdated, void(const wvcas::WvCasSessionId& sessionId, uint8_t ecm_age_restriction)); + MOCK_METHOD(void, OnSessionFingerprintingUpdated, + (const int32_t& sessionId, + const std::vector& fingerprinting), + (override)); + MOCK_METHOD(void, OnSessionServiceBlockingUpdated, + (const int32_t& sessionId, + const std::vector& service_blocking), + (override)); }; typedef StrictMock StrictMockEventListener; @@ -125,19 +133,9 @@ class MockFileSystem : public wvutil::FileSystem { typedef NiceMock NiceMockFileSystem; class MockWidevineSession : public wvcas::WidevineCasSession { - class EcmParser : public wvcas::EcmParser { - public: - EcmParser() {} - ~EcmParser() override {} - }; - public: MockWidevineSession() {} ~MockWidevineSession() override {} - std::unique_ptr getEcmParser( - const wvcas::CasEcm& ecm) const override { - return make_unique(); - } MOCK_METHOD2(processEcm, wvcas::CasStatus(const wvcas::CasEcm& ecm, uint8_t parental_control_age)); MOCK_METHOD2(HandleProcessEcm, diff --git a/tests/src/widevine_cas_session_test.cpp b/tests/src/widevine_cas_session_test.cpp index babded9..9eb7aef 100644 --- a/tests/src/widevine_cas_session_test.cpp +++ b/tests/src/widevine_cas_session_test.cpp @@ -4,6 +4,7 @@ #include "widevine_cas_session.h" +#include #include #include @@ -11,10 +12,13 @@ #include #include +#include "cas_types.h" #include "cas_util.h" +#include "media_cas.pb.h" #include "mock_crypto_session.h" #include "string_conversions.h" +namespace { using ::testing::_; using ::testing::DoAll; using ::testing::Eq; @@ -109,20 +113,24 @@ MATCHER(IsValidKeyOddSlotData, "") { class MockEcmParser : public wvcas::EcmParser { public: - MOCK_CONST_METHOD0(sequence_count, uint8_t()); + MOCK_CONST_METHOD0(version, uint8_t()); MOCK_CONST_METHOD0(age_restriction, uint8_t()); MOCK_CONST_METHOD0(crypto_mode, wvcas::CryptoMode()); MOCK_CONST_METHOD0(rotation_enabled, bool()); + MOCK_CONST_METHOD0(content_iv_size, size_t()); MOCK_CONST_METHOD1(entitlement_key_id, - const std::vector(wvcas::KeySlotId id)); - MOCK_CONST_METHOD1(content_key_id, - const std::vector(wvcas::KeySlotId id)); + std::vector(wvcas::KeySlotId id)); + MOCK_CONST_METHOD1(content_key_id, std::vector(wvcas::KeySlotId id)); MOCK_CONST_METHOD1(wrapped_key_data, - const std::vector(wvcas::KeySlotId id)); - MOCK_CONST_METHOD1(wrapped_key_iv, - const std::vector(wvcas::KeySlotId id)); - MOCK_CONST_METHOD1(content_iv, - const std::vector(wvcas::KeySlotId id)); + 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_CONST_METHOD0(has_fingerprinting, bool()); + MOCK_CONST_METHOD0(fingerprinting, video_widevine::Fingerprinting()); + MOCK_CONST_METHOD0(has_service_blocking, bool()); + MOCK_CONST_METHOD0(service_blocking, video_widevine::ServiceBlocking()); + MOCK_CONST_METHOD0(ecm_serialized_payload, std::string()); + MOCK_CONST_METHOD0(signature, std::string()); }; class CasSessionTest : public ::testing::Test { @@ -138,7 +146,7 @@ class TestCasSession : public wvcas::WidevineCasSession { virtual ~TestCasSession() {} std::unique_ptr getEcmParser( - const wvcas::CasEcm& ecm) const; + const wvcas::CasEcm& ecm) const override; std::vector entitlement_key_id(wvcas::KeySlotId id) const { std::string key_id; @@ -194,15 +202,30 @@ class TestCasSession : public wvcas::WidevineCasSession { age_restriction_ = age_restriction; } + void set_fingerprinting_control(const std::string& control) { + fingerprinting_.clear_control(); + if (!control.empty()) { + fingerprinting_.set_control(control); + } + } + + void set_service_blocking_groups(const std::vector& groups) { + service_blocking_.clear_device_groups(); + for (auto const& group : groups) { + service_blocking_.add_device_groups(group); + } + } + private: uint8_t age_restriction_ = 0; + video_widevine::Fingerprinting fingerprinting_; + video_widevine::ServiceBlocking service_blocking_; }; std::unique_ptr TestCasSession::getEcmParser( const wvcas::CasEcm& ecm) const { std::unique_ptr> mock_ecm_parser( new NiceMock); - ON_CALL(*mock_ecm_parser, sequence_count()).WillByDefault(Return(0)); ON_CALL(*mock_ecm_parser, age_restriction()) .WillByDefault(Return(age_restriction_)); ON_CALL(*mock_ecm_parser, crypto_mode()) @@ -218,6 +241,14 @@ 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, has_fingerprinting()) + .WillByDefault(Return(fingerprinting_.has_control())); + ON_CALL(*mock_ecm_parser, fingerprinting()) + .WillByDefault(Return(fingerprinting_)); + ON_CALL(*mock_ecm_parser, has_service_blocking()) + .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()); } @@ -231,7 +262,8 @@ TEST_F(CasSessionTest, processEcm) { .WillOnce(DoAll(SetArgPointee<0>(kEntitledKeySessionId), Return(wvcas::CasStatusCode::kNoError))); ASSERT_EQ(wvcas::CasStatusCode::kNoError, - session.initialize(mock, &session_id).status_code()); + session.initialize(mock, /*event_listener=*/nullptr, &session_id) + .status_code()); EXPECT_EQ(session_id, kEntitledKeySessionId); wvcas::CasEcm ecm(184); @@ -250,7 +282,8 @@ TEST_F(CasSessionTest, parentalControl) { .WillOnce(DoAll(SetArgPointee<0>(kEntitledKeySessionId), Return(wvcas::CasStatusCode::kNoError))); ASSERT_EQ(wvcas::CasStatusCode::kNoError, - session.initialize(mock, &session_id).status_code()); + session.initialize(mock, /*event_listener=*/nullptr, &session_id) + .status_code()); EXPECT_EQ(session_id, kEntitledKeySessionId); EXPECT_CALL(*mock, LoadCasECMKeys(session_id, IsValidKeyEvenSlotData(), @@ -281,3 +314,141 @@ TEST_F(CasSessionTest, parentalControl) { session.processEcm(ecm, 3).status_code()); EXPECT_CALL(*mock, RemoveEntitledKeySession(session_id)); } + +class MockEventListener : public wvcas::CasEventListener { + public: + MockEventListener() {} + ~MockEventListener() override {} + MOCK_METHOD0(OnSessionRenewalNeeded, void()); + MOCK_METHOD2(OnSessionKeysChange, void(const wvcas::KeyStatusMap& keys_status, + bool has_new_usable_key)); + MOCK_METHOD1(OnExpirationUpdate, void(int64_t new_expiry_time_seconds)); + MOCK_METHOD1(OnNewRenewalServerUrl, + void(const std::string& renewal_server_url)); + MOCK_METHOD0(OnLicenseExpiration, void()); + MOCK_METHOD2(OnAgeRestrictionUpdated, + void(const wvcas::WvCasSessionId& sessionId, + uint8_t ecm_age_restriction)); + MOCK_METHOD(void, OnSessionFingerprintingUpdated, + (const int32_t& sessionId, + const std::vector& fingerprinting), + (override)); + MOCK_METHOD(void, OnSessionServiceBlockingUpdated, + (const int32_t& sessionId, + const std::vector& service_blocking), + (override)); +}; + +TEST_F(CasSessionTest, FingerprintingSuccess) { + TestCasSession session; + auto mock_crypto = std::make_shared(); + MockEventListener mock_listener; + uint32_t session_id; + ASSERT_EQ(session.initialize(mock_crypto, &mock_listener, &session_id) + .status_code(), + wvcas::CasStatusCode::kNoError); + session.set_fingerprinting_control("control"); + std::vector expected_message = {0x00, 0x00, 0x07, 'c', 'o', + 'n', 't', 'r', 'o', 'l'}; + EXPECT_CALL(mock_listener, + OnSessionFingerprintingUpdated(session_id, expected_message)) + .Times(1); + + session.processEcm(wvcas::CasEcm(184, '0'), 0); +} + +TEST_F(CasSessionTest, RepeatedFingerprintingNoEventSuccess) { + TestCasSession session; + auto mock_crypto = std::make_shared(); + MockEventListener mock_listener; + uint32_t session_id; + ASSERT_EQ(session.initialize(mock_crypto, &mock_listener, &session_id) + .status_code(), + wvcas::CasStatusCode::kNoError); + session.set_fingerprinting_control("control"); + EXPECT_CALL(mock_listener, OnSessionFingerprintingUpdated).Times(1); + + session.processEcm(wvcas::CasEcm(184, '0'), 0); + // Same fingerprinting will not trigger event. + EXPECT_CALL(mock_listener, OnSessionFingerprintingUpdated).Times(0); + session.processEcm(wvcas::CasEcm(184, '1'), 0); +} + +TEST_F(CasSessionTest, DifferentFingerprintingTriggerEventSuccess) { + TestCasSession session; + auto mock_crypto = std::make_shared(); + MockEventListener mock_listener; + uint32_t session_id; + ASSERT_EQ(session.initialize(mock_crypto, &mock_listener, &session_id) + .status_code(), + wvcas::CasStatusCode::kNoError); + session.set_fingerprinting_control("control"); + EXPECT_CALL(mock_listener, OnSessionFingerprintingUpdated).Times(1); + + session.processEcm(wvcas::CasEcm(184, '0'), 0); + // Different fingerprinting will trigger event. + session.set_fingerprinting_control("control2"); + EXPECT_CALL(mock_listener, OnSessionFingerprintingUpdated).Times(1); + session.processEcm(wvcas::CasEcm(184, '1'), 0); + // 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); +} + +TEST_F(CasSessionTest, ServiceBlockingSuccess) { + TestCasSession session; + auto mock_crypto = std::make_shared(); + MockEventListener mock_listener; + uint32_t session_id; + ASSERT_EQ(session.initialize(mock_crypto, &mock_listener, &session_id) + .status_code(), + wvcas::CasStatusCode::kNoError); + session.set_service_blocking_groups({"Group1", "g2"}); + std::vector expected_message = {0x00, 0x00, 0x06, 'G', 'r', + 'o', 'u', 'p', '1', 0x00, + 0x00, 0x02, 'g', '2'}; + EXPECT_CALL(mock_listener, + OnSessionServiceBlockingUpdated(session_id, expected_message)) + .Times(1); + + session.processEcm(wvcas::CasEcm(184, '0'), 0); +} + +TEST_F(CasSessionTest, RepeatedServiceBlockingNoEventSuccess) { + TestCasSession session; + auto mock_crypto = std::make_shared(); + MockEventListener mock_listener; + uint32_t session_id; + ASSERT_EQ(session.initialize(mock_crypto, &mock_listener, &session_id) + .status_code(), + wvcas::CasStatusCode::kNoError); + session.set_service_blocking_groups({"Group1", "g2"}); + EXPECT_CALL(mock_listener, OnSessionServiceBlockingUpdated).Times(1); + + session.processEcm(wvcas::CasEcm(184, '0'), 0); + EXPECT_CALL(mock_listener, OnSessionServiceBlockingUpdated).Times(0); + session.processEcm(wvcas::CasEcm(184, '1'), 0); +} + +TEST_F(CasSessionTest, DifferentServiceBlockingTriggerEventSuccess) { + TestCasSession session; + auto mock_crypto = std::make_shared(); + MockEventListener mock_listener; + uint32_t session_id; + ASSERT_EQ(session.initialize(mock_crypto, &mock_listener, &session_id) + .status_code(), + wvcas::CasStatusCode::kNoError); + session.set_service_blocking_groups({"Group1", "g2"}); + EXPECT_CALL(mock_listener, OnSessionServiceBlockingUpdated).Times(1); + + session.processEcm(wvcas::CasEcm(184, '0'), 0); + EXPECT_CALL(mock_listener, OnSessionServiceBlockingUpdated).Times(1); + session.set_service_blocking_groups({"Group1"}); + session.processEcm(wvcas::CasEcm(184, '1'), 0); + EXPECT_CALL(mock_listener, OnSessionServiceBlockingUpdated).Times(1); + session.set_service_blocking_groups({}); + session.processEcm(wvcas::CasEcm(184, '2'), 0); +} + +} // namespace