diff --git a/common/certificate_type.h b/common/certificate_type.h index 1d07166..b63a639 100644 --- a/common/certificate_type.h +++ b/common/certificate_type.h @@ -1,9 +1,15 @@ -// Copyright 2017 Google LLC. All rights reserved. +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2017 Google LLC. +// +// This software is licensed under the terms defined in the Widevine Master +// License Agreement. For a copy of this agreement, please contact +// widevine-licensing@google.com. +//////////////////////////////////////////////////////////////////////////////// -#ifndef VIDEO_WIDEVINE_EXPORT_COMMON_CERTIFICATE_TYPE_H_ -#define VIDEO_WIDEVINE_EXPORT_COMMON_CERTIFICATE_TYPE_H_ +#ifndef COMMON_CERTIFICATE_TYPE_H_ +#define COMMON_CERTIFICATE_TYPE_H_ -namespace video_widevine { +namespace widevine { enum CertificateType { kCertificateTypeTesting, @@ -11,6 +17,6 @@ enum CertificateType { kCertificateTypeProduction, }; -} // namespace video_widevine +} // namespace widevine -#endif // VIDEO_WIDEVINE_EXPORT_COMMON_CERTIFICATE_TYPE_H_ +#endif // COMMON_CERTIFICATE_TYPE_H_ diff --git a/common/default_device_security_profile_list.h b/common/default_device_security_profile_list.h index e5dad24..33d96d9 100644 --- a/common/default_device_security_profile_list.h +++ b/common/default_device_security_profile_list.h @@ -1,14 +1,20 @@ -// Copyright 2020 Google LLC. All rights reserved. +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 Google LLC. +// +// This software is licensed under the terms defined in the Widevine Master +// License Agreement. For a copy of this agreement, please contact +// widevine-licensing@google.com. +//////////////////////////////////////////////////////////////////////////////// // // Description: // Container of Widevine default security profiless. -#ifndef VIDEO_WIDEVINE_EXPORT_COMMON_DEFAULT_DEVICE_SECURITY_PROFILE_LIST_H_ -#define VIDEO_WIDEVINE_EXPORT_COMMON_DEFAULT_DEVICE_SECURITY_PROFILE_LIST_H_ +#ifndef COMMON_DEFAULT_DEVICE_SECURITY_PROFILE_LIST_H_ +#define COMMON_DEFAULT_DEVICE_SECURITY_PROFILE_LIST_H_ -#include "video/widevine/export/common/security_profile_list.h" +#include "common/security_profile_list.h" -namespace video_widevine { +namespace widevine { class DefaultDeviceSecurityProfileList : public SecurityProfileList { public: @@ -28,6 +34,6 @@ class DefaultDeviceSecurityProfileList : public SecurityProfileList { std::vector* default_profile_strings) const; }; -} // namespace video_widevine +} // namespace widevine -#endif // VIDEO_WIDEVINE_EXPORT_COMMON_DEFAULT_DEVICE_SECURITY_PROFILE_LIST_H_ +#endif // COMMON_DEFAULT_DEVICE_SECURITY_PROFILE_LIST_H_ diff --git a/common/security_profile_list.h b/common/security_profile_list.h index 9217e58..b6cdb96 100644 --- a/common/security_profile_list.h +++ b/common/security_profile_list.h @@ -1,20 +1,26 @@ -// Copyright 2020 Google LLC. All rights reserved. +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 Google LLC. +// +// This software is licensed under the terms defined in the Widevine Master +// License Agreement. For a copy of this agreement, please contact +// widevine-licensing@google.com. +//////////////////////////////////////////////////////////////////////////////// // // Description: // Container of device security profiles. Security profiles indicate rules // to allow using the profile. The rules are based on DRM capabilities of a // device. -#ifndef VIDEO_WIDEVINE_EXPORT_COMMON_SECURITY_PROFILE_LIST_H_ -#define VIDEO_WIDEVINE_EXPORT_COMMON_SECURITY_PROFILE_LIST_H_ +#ifndef COMMON_SECURITY_PROFILE_LIST_H_ +#define COMMON_SECURITY_PROFILE_LIST_H_ -#include "third_party/absl/synchronization/mutex.h" -#include "video/widevine/protos/public/client_identification.proto.h" -#include "video/widevine/protos/public/device_security_profile_data.proto.h" -#include "video/widevine/protos/public/provisioned_device_info.proto.h" -#include "video/widevine/protos/public/security_profile.proto.h" +#include "absl/synchronization/mutex.h" +#include "protos/public/client_identification.pb.h" +#include "protos/public/device_security_profile_data.pb.h" +#include "protos/public/provisioned_device_info.pb.h" +#include "protos/public/security_profile.pb.h" -namespace video_widevine { +namespace widevine { using ClientCapabilities = ClientIdentification::ClientCapabilities; // The SecurityProfileList will hold all security profiles. During license @@ -76,10 +82,10 @@ class SecurityProfileList { const ClientIdentification& client_id, const ProvisionedDeviceInfo& device_info) const; - int64 GetCurrentTimeSeconds() const; + int64_t GetCurrentTimeSeconds() const; bool IsProfileActive(const SecurityProfile& profile, - int64 current_time_seconds) const; + int64_t current_time_seconds) const; mutable absl::Mutex mutex_; // Security profiles @@ -87,5 +93,5 @@ class SecurityProfileList { std::vector security_profiles_ ABSL_GUARDED_BY(mutex_); }; -} // namespace video_widevine -#endif // VIDEO_WIDEVINE_EXPORT_COMMON_SECURITY_PROFILE_LIST_H_ +} // namespace widevine +#endif // COMMON_SECURITY_PROFILE_LIST_H_ diff --git a/example/BUILD b/example/BUILD index 0384be8..faec702 100644 --- a/example/BUILD +++ b/example/BUILD @@ -79,3 +79,13 @@ cc_binary( "//media_cas_packager_sdk/public:wv_cas_emm", ], ) + +cc_binary( + name = "wv_ecmg_example", + srcs = ["wv_ecmg_example.cc"], + deps = [ + "//base", + "//media_cas_packager_sdk/public:wv_cas_ecmg_client_handler", + "//media_cas_packager_sdk/public:wv_cas_types", + ], +) diff --git a/example/wv_ecmg_example.cc b/example/wv_ecmg_example.cc new file mode 100644 index 0000000..f0cfd6f --- /dev/null +++ b/example/wv_ecmg_example.cc @@ -0,0 +1,201 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 Google LLC. +// +// This software is licensed under the terms defined in the Widevine Master +// License Agreement. For a copy of this agreement, please contact +// widevine-licensing@google.com. +//////////////////////////////////////////////////////////////////////////////// + +// Example of Simulcrypt ECMG with custom entitlement key fetcher. + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include "media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.h" +#include "media_cas_packager_sdk/public/wv_cas_types.h" + +namespace { + +using widevine::cas::EcmgConfig; +using widevine::cas::EntitlementKeyInfo; +using widevine::cas::WvCasEcmgClientHandler; + +constexpr int kServerPortNumber = 1234; +constexpr int kListenQueueSize = 20; +constexpr int kBufferSizeBytes = 2048; + +void BuildEcmgConfig(EcmgConfig* config) { + config->delay_start = 200; // in milliseconds. + config->delay_stop = 200; // in milliseconds. + config->ecm_rep_period = 100; // in milliseconds. + config->max_comp_time = 100; // in milliseconds. + config->access_criteria_transfer_mode = 0; + config->number_of_content_keys = 2; + config->crypto_mode = widevine::cas::CryptoMode::kAesCtr; +} + +void PrintMessage(const std::string& description, const char* const message, + size_t length) { + std::cout << description << std::endl; + for (size_t i = 0; i < length; i++) { + printf("'\\x%02x', ", static_cast(*(message + i))); + fflush(stdout); + } + printf("\n"); + fflush(stdout); +} + +void ServeClient(int socket_fd, WvCasEcmgClientHandler* ecmg) { + char request[kBufferSizeBytes]; + char response[kBufferSizeBytes]; + while (true) { + bzero(request, kBufferSizeBytes); + bzero(response, kBufferSizeBytes); + size_t response_length = 0; + size_t request_length = recv(socket_fd, request, kBufferSizeBytes, 0); + if (request_length == 0) { + std::cout << "No more request from client." << std::endl; + return; + } + if (request_length < 0) { + std::cerr << "Failed to receive request from client." << std::endl; + return; + } + PrintMessage("Request", request, request_length); + ecmg->HandleRequest(kBufferSizeBytes, request, kBufferSizeBytes, response, + &response_length); + PrintMessage("Response", response, response_length); + if (send(socket_fd, response, response_length, 0) < 0) { + std::cerr << "Failed to send response to client." << std::endl; + return; + } + } +} + +std::vector MyEntitlementKeyFetcherFun(uint16_t channel_id, + uint16_t stream_id, + uint16_t ecm_id) { + // Fill the function to get entitlement keys based on the input parameters. + // Potential step 1: Convert the input {channel_id, stream_id, ecm_id} to + // EntitlementRequestParams defined in wv_cas_key_fetcher.h, such as + // content_id, content provider, etc. + // Potential step 2: Make the key fetch request. Refer to wv_cas_key_fetcher + // and wv_cas_curl_key_fetcher for APIs that can fetch keys from Widevine + // server with EntitlementRequestParams. + + // The size of the returned vector must not exceed 2. When the vector size is + // 2, one of them must be specified as even key and the other must be odd key. + constexpr char track_types_hd[] = "HD"; + constexpr char entitlement_key_id_even[] = "fake_key_id1...."; + constexpr char entitlement_key_value_even[] = + "fakefakefakefakefakefakefake1..."; + constexpr char entitlement_key_id_odd[] = "fake_key_id2...."; + constexpr char entitlement_key_value_odd[] = + "fakefakefakefakefakefakefake2..."; + return {{track_types_hd, /*is_even_key=*/true, entitlement_key_id_even, + entitlement_key_value_even}, + {track_types_hd, /*is_even_key=*/false, entitlement_key_id_odd, + entitlement_key_value_odd}}; +} + +int create_listen_socket_fd() { + // Server address. + struct sockaddr_in server_address; + bzero(reinterpret_cast(&server_address), sizeof(server_address)); + server_address.sin_family = AF_INET; + server_address.sin_addr.s_addr = htonl(INADDR_ANY); + server_address.sin_port = htons(kServerPortNumber); + + // Create a listening socket. + int listen_socket_fd = socket(AF_INET, SOCK_STREAM, /* protocol= */ 0); + if (listen_socket_fd < 0) { + std::cerr << "Failed to open listening socket" << std::endl; + } + // Set SO_REUSEADDR on a socket to true (1). + int optval = 1; + setsockopt(listen_socket_fd, SOL_SOCKET, SO_REUSEADDR, &optval, + sizeof(optval)); + // Bind address. + if (bind(listen_socket_fd, + reinterpret_cast(&server_address), + sizeof(server_address)) < 0) { + std::cerr << "Failed to bind on server socket" << std::endl; + } + return listen_socket_fd; +} + +} // namespace + +int main(int argc, char** argv) { + EcmgConfig ecmg_config; + BuildEcmgConfig(&ecmg_config); + + int listen_socket_fd = create_listen_socket_fd(); + // Listen for connection from clients. + std::cout << "Server listening ..." << std::endl << std::flush; + int return_val = listen(listen_socket_fd, kListenQueueSize); + switch (return_val) { + case EADDRINUSE: + std::cerr << "Another socket is already listening on the same port." + << std::endl; + break; + case EBADF: + std::cerr << "The argument sockfd is not a valid descriptor." + << std::endl; + break; + case ENOTSOCK: + std::cerr << "The argument sockfd is not a socket." << std::endl; + break; + case EOPNOTSUPP: + std::cerr << "The socket is not of a type that supports the listen() " + "operation." + << std::endl; + break; + default: + break; + } + + // While loop to serve different client connections. + while (true) { + struct sockaddr_in client_address; + socklen_t client_address_size = sizeof(client_address); + int client_socket_fd = accept( + listen_socket_fd, reinterpret_cast(&client_address), + &client_address_size); + std::cout << "\nTCP connection " << client_socket_fd << " start" + << std::endl; + if (client_socket_fd < 0) { + std::cerr << "Failed to accept connection request from client" + << std::endl; + } else { + if (fork() == 0) { + // Reaching here, I am in the child process. + close(listen_socket_fd); + WvCasEcmgClientHandler client_handler(ecmg_config); + client_handler.SetCustomEntitlementKeyFetcherFunc( + MyEntitlementKeyFetcherFun); + ServeClient(client_socket_fd, &client_handler); + std::cout << "\nTCP connection " << client_socket_fd << " closed" + << std::endl; + close(client_socket_fd); + // Terminate child process. + exit(0); + } + // Reaching here, I am in parent process. + close(client_socket_fd); + } + usleep(1000); + } + return 0; +} diff --git a/media_cas_packager_sdk/internal/BUILD b/media_cas_packager_sdk/internal/BUILD index d504807..7c3bd3f 100644 --- a/media_cas_packager_sdk/internal/BUILD +++ b/media_cas_packager_sdk/internal/BUILD @@ -111,6 +111,7 @@ cc_test( "@abseil_repo//absl/memory", "@abseil_repo//absl/strings", "@abseil_repo//absl/strings:str_format", + "@abseil_repo//absl/types:span", "//example:test_ecmg_messages", ], ) diff --git a/media_cas_packager_sdk/internal/ecmg_client_handler.cc b/media_cas_packager_sdk/internal/ecmg_client_handler.cc index 8e87994..14c8ae9 100644 --- a/media_cas_packager_sdk/internal/ecmg_client_handler.cc +++ b/media_cas_packager_sdk/internal/ecmg_client_handler.cc @@ -420,10 +420,10 @@ void BuildEcmResponse(uint16_t channel_id, uint16_t stream_id, uint16_t cp_numbe } // namespace -EcmgClientHandler::EcmgClientHandler(EcmgConfig* ecmg_config) { +EcmgClientHandler::EcmgClientHandler(EcmgConfig* ecmg_config) + : channel_id_set_(false), custom_key_fetcher_(nullptr) { DCHECK(ecmg_config); ecmg_config_ = ecmg_config; - channel_id_set_ = false; } void EcmgClientHandler::HandleRequest(const char* const request, char* response, @@ -608,6 +608,36 @@ void EcmgClientHandler::HandleStreamSetup(const EcmgParameters& params, return; } + // Try to fetch entitlement keys using custom key fetcher if none is received. + std::vector& entitlements = + streams_info_[params.ecm_stream_id]->entitlement_comb; + if (entitlements.empty() && custom_key_fetcher_ != nullptr) { + const std::vector& fetched_entitlements = + custom_key_fetcher_(params.ecm_channel_id, params.ecm_stream_id, + params.ecm_id); + // Only process if the fetched size is expected (1 or 2 entries). + if (fetched_entitlements.empty()) { + LOG(WARNING) << "EntitlementKeyFetcherFunc returned empty result."; + } else if (fetched_entitlements.size() > 2) { + LOG(ERROR) + << "EntitlementKeyFetcherFunc should return at most 2 entries, but " + << fetched_entitlements.size() << " entries are returned."; + } else { + entitlements.reserve(fetched_entitlements.size()); + for (auto const& fetched_entitlement : fetched_entitlements) { + EntitlementIdKeyComb entitlement_comb; + entitlement_comb.key_id = fetched_entitlement.key_id; + entitlement_comb.key_value = fetched_entitlement.key_value; + // In the case of two entitlement keys, the first in |entitlements| must + // be even key followed by the odd key. + entitlements.insert(fetched_entitlement.is_even_key + ? entitlements.begin() + : entitlements.end(), + entitlement_comb); + } + } + } + BuildStreamStatus(params.ecm_channel_id, params.ecm_stream_id, params.ecm_id, ecmg_config_->access_criteria_transfer_mode, response, response_length); diff --git a/media_cas_packager_sdk/internal/ecmg_client_handler.h b/media_cas_packager_sdk/internal/ecmg_client_handler.h index 1965a3b..616fbab 100644 --- a/media_cas_packager_sdk/internal/ecmg_client_handler.h +++ b/media_cas_packager_sdk/internal/ecmg_client_handler.h @@ -24,17 +24,6 @@ namespace widevine { namespace cas { -// A struct that captures the Ecmg configs. -struct EcmgConfig { - int16_t delay_start; - int16_t delay_stop; - uint16_t ecm_rep_period; - uint16_t max_comp_time; - uint8_t access_criteria_transfer_mode; - uint8_t number_of_content_keys; - CryptoMode crypto_mode; -}; - // A struct that represent a CP_CW_Combination. struct EcmgCpCwCombination { uint16_t cp; // crypto period @@ -91,8 +80,17 @@ class EcmgClientHandler { // Handle a |request| from the client. // If any response is generated, it would returned via |response| // and |response_length|. - void HandleRequest(const char* const request, char* response, - size_t* response_length); + virtual void HandleRequest(const char* const request, char* response, + size_t* response_length); + + // Sets the custom entitlement key fetching function used by ECMG to fetch + // entitlement keys. If entitlement key information is not received in + // Simulcrypt ECMG from SCS, ECMG will try to fetch entitlement keys using + // this function. + virtual void SetCustomEntitlementKeyFetcherFunc( + EntitlementKeyFetcherFunc fetcher) { + custom_key_fetcher_ = fetcher; + } private: void HandleChannelSetup(const EcmgParameters& params, char* response, @@ -133,7 +131,7 @@ class EcmgClientHandler { EcmgConfig* ecmg_config_; // Per spec, "There is always one (and only one) channel per TCP connection". - bool channel_id_set_; + bool channel_id_set_ = false; uint16_t channel_id_; // Channel specific information. @@ -142,6 +140,8 @@ class EcmgClientHandler { // Map from ECM_stream_id to EcmgStreamInfo. absl::flat_hash_map> streams_info_; + // Used to fetch entitlement keys if none are received from SCS. + EntitlementKeyFetcherFunc custom_key_fetcher_; }; } // namespace cas diff --git a/media_cas_packager_sdk/internal/ecmg_client_handler_test.cc b/media_cas_packager_sdk/internal/ecmg_client_handler_test.cc index cb73337..900b531 100644 --- a/media_cas_packager_sdk/internal/ecmg_client_handler_test.cc +++ b/media_cas_packager_sdk/internal/ecmg_client_handler_test.cc @@ -12,11 +12,14 @@ #include #include +#include + #include "testing/gmock.h" #include "testing/gunit.h" #include "absl/memory/memory.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" +#include "absl/types/span.h" #include "example/test_ecmg_messages.h" #include "media_cas_packager_sdk/internal/ecmg_constants.h" #include "media_cas_packager_sdk/internal/mpeg2ts.h" @@ -32,6 +35,7 @@ using simulcrypt_util::AddUint16Param; using simulcrypt_util::AddUint32Param; using simulcrypt_util::AddUint8Param; using simulcrypt_util::BuildMessageHeader; +using ::testing::ElementsAreArray; constexpr size_t kBufferSize = 1024; constexpr uint16_t kWidevineSystemId = 0x4AD4; @@ -44,8 +48,10 @@ constexpr size_t kEcmId = 2; constexpr size_t kNominalCpDuration = 0x64; constexpr size_t kCpNumber = 0; constexpr char kContentKeyEven[] = "0123456701234567"; +constexpr char kContentKeyIvEven[] = "0123456701234567"; constexpr char kContentKeyEven8Bytes[] = "01234567"; constexpr char kContentKeyOdd[] = "abcdefghabcdefgh"; +constexpr char kContentKeyIvOdd[] = "abcdefghabcdefgh"; constexpr char kContentKeyOdd8Bytes[] = "abcdefgh"; constexpr char kEntitlementKeyIdEven[] = "0123456701234567"; constexpr char kEntitlementKeyValueEven[] = "01234567012345670123456701234567"; @@ -58,6 +64,14 @@ constexpr char kTrackTypesSD[] = "SD"; constexpr char kTrackTypesHD[] = "HD"; constexpr absl::string_view kWrappedKeyIv = "0123456701234567"; +std::vector FakeEntitlementKeyFetcherFunc( + uint16_t /*channel_id*/, uint16_t /*stream_id*/, uint16_t /*ecm_id*/) { + return {{kTrackTypesSD, /*is_even_key*/ false, kEntitlementKeyIdOdd, + kEntitlementKeyValueOdd}, + {kTrackTypesSD, /*is_even_key*/ true, kEntitlementKeyIdEven, + kEntitlementKeyValueEven}}; +} + class MockEcmgClientHandler : public EcmgClientHandler { public: explicit MockEcmgClientHandler(EcmgConfig* ecmg_config) @@ -339,6 +353,33 @@ TEST_F(EcmgClientHandlerTest, SuccessSequenceInjectedEntitlements) { EXPECT_EQ(sizeof(kTestEcmgEcmResponse), response_len_); } +TEST_F(EcmgClientHandlerTest, SuccessSequenceFetchedEntitlements) { + handler_->SetCustomEntitlementKeyFetcherFunc(FakeEntitlementKeyFetcherFunc); + BuildChannelSetupRequest(kChannelId, kSuperCasId, /*age_restriction=*/0, + /*crypto_mode=*/"", request_, &request_len_); + handler_->HandleRequest(request_, response_, &response_len_); + EXPECT_EQ(response_len_, sizeof(kTestEcmgChannelStatus)); + EXPECT_THAT(absl::MakeSpan(response_, response_len_), + ElementsAreArray(kTestEcmgChannelStatus)); + + // No entitlement keys are injected. + BuildStreamSetupRequest( + kChannelId, kStreamId, kEcmId, kNominalCpDuration, kTrackTypesSD, + /*entitlements=*/{}, {kContentKeyIvEven, kContentKeyIvOdd}, request_, + &request_len_); + handler_->HandleRequest(request_, response_, &response_len_); + EXPECT_EQ(response_len_, sizeof(kTestEcmgStreamStatus)); + + const std::vector cp_cw_combination = { + {kCpNumber, kContentKeyEven}, {kCpNumber + 1, kContentKeyOdd}}; + BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + request_, &request_len_); + EXPECT_THAT(absl::MakeSpan(response_, response_len_), + ElementsAreArray(kTestEcmgStreamStatus)); + handler_->HandleRequest(request_, response_, &response_len_); + EXPECT_EQ(response_len_, sizeof(kTestEcmgEcmResponse)); +} + TEST_F(EcmgClientHandlerTest, SuccessSequenceCsa2) { BuildChannelSetupRequest(kChannelId, kSuperCasId, kAgeRestriction, kCryptoModeCsa2, request_, &request_len_); diff --git a/media_cas_packager_sdk/public/BUILD b/media_cas_packager_sdk/public/BUILD index 310c238..0e18eee 100644 --- a/media_cas_packager_sdk/public/BUILD +++ b/media_cas_packager_sdk/public/BUILD @@ -189,6 +189,20 @@ cc_library( ], ) +cc_library( + name = "wv_cas_ecmg_client_handler", + srcs = ["wv_cas_ecmg_client_handler.cc"], + hdrs = ["wv_cas_ecmg_client_handler.h"], + deps = [ + ":wv_cas_types", + "//base", + "@abseil_repo//absl/memory", + "@abseil_repo//absl/strings", + "//common:status", + "//media_cas_packager_sdk/internal:ecmg_client_handler", + ], +) + cc_test( name = "wv_cas_types_test", size = "small", @@ -210,6 +224,19 @@ cc_test( ], ) +cc_test( + name = "wv_cas_ecmg_client_handler_test", + srcs = ["wv_cas_ecmg_client_handler_test.cc"], + deps = [ + ":wv_cas_ecmg_client_handler", + ":wv_cas_types", + "//testing:gunit_main", + "@abseil_repo//absl/memory", + "//common:status", + "//media_cas_packager_sdk/internal:ecmg_client_handler", + ], +) + cc_binary( name = "wv_ecmg", srcs = ["wv_ecmg.cc"], diff --git a/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.cc b/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.cc new file mode 100644 index 0000000..8eb05a7 --- /dev/null +++ b/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.cc @@ -0,0 +1,74 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 Google LLC. +// +// This software is licensed under the terms defined in the Widevine Master +// License Agreement. For a copy of this agreement, please contact +// widevine-licensing@google.com. +//////////////////////////////////////////////////////////////////////////////// + +#include "media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.h" + +#include "glog/logging.h" +#include "absl/memory/memory.h" +#include "absl/strings/str_cat.h" +#include "media_cas_packager_sdk/internal/ecmg_client_handler.h" + +namespace widevine { +namespace cas { + +namespace { +constexpr size_t kSimulcryptMessageHeaderSizeBytes = 5; +constexpr size_t kMinimumResponseBufferSizeBytes = 2048; +} // namespace + +WvCasEcmgClientHandler::WvCasEcmgClientHandler(const EcmgConfig& ecmg_config) + : ecmg_config_(ecmg_config), + inner_handler_(absl::make_unique(&ecmg_config_)) {} + +WvCasEcmgClientHandler::~WvCasEcmgClientHandler() = default; + +WvCasEcmgClientHandler::WvCasEcmgClientHandler( + std::unique_ptr inner_handler) + : inner_handler_(std::move(inner_handler)) {} + +void WvCasEcmgClientHandler::SetCustomEntitlementKeyFetcherFunc( + EntitlementKeyFetcherFunc fetcher) { + return inner_handler_->SetCustomEntitlementKeyFetcherFunc(fetcher); +} + +Status WvCasEcmgClientHandler::HandleRequest(size_t request_buffer_size, + const char* const request_buffer, + size_t response_buffer_size, + char* response_buffer, + size_t* response_length) { + if (request_buffer == nullptr || response_buffer == nullptr || + response_length == nullptr) { + std::string error_message = + "request_buffer, response_buffer and response_length must " + "not be null pointer."; + LOG(ERROR) << error_message; + return {error::INVALID_ARGUMENT, error_message}; + } + if (request_buffer_size < kSimulcryptMessageHeaderSizeBytes) { + std::string error_message = + absl::StrCat("request_buffer_size is too small. Minimum required is ", + kSimulcryptMessageHeaderSizeBytes, " bytes."); + LOG(ERROR) << error_message; + *response_length = 0; + return {error::INVALID_ARGUMENT, error_message}; + } + if (response_buffer_size < kMinimumResponseBufferSizeBytes) { + std::string error_message = + absl::StrCat("response_buffer_size is too small. Minimum required is ", + kMinimumResponseBufferSizeBytes, " bytes."); + LOG(ERROR) << error_message; + *response_length = 0; + return {error::INVALID_ARGUMENT, error_message}; + } + inner_handler_->HandleRequest(request_buffer, response_buffer, + response_length); + return OkStatus(); +} + +} // namespace cas +} // namespace widevine diff --git a/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.h b/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.h new file mode 100644 index 0000000..c326db8 --- /dev/null +++ b/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.h @@ -0,0 +1,67 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 Google LLC. +// +// This software is licensed under the terms defined in the Widevine Master +// License Agreement. For a copy of this agreement, please contact +// widevine-licensing@google.com. +//////////////////////////////////////////////////////////////////////////////// + +#ifndef MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_ECMG_CLIENT_HANDLER_H_ +#define MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_ECMG_CLIENT_HANDLER_H_ + +#include + +#include "common/status.h" +#include "media_cas_packager_sdk/public/wv_cas_types.h" + +namespace widevine { +namespace cas { + +// Forward declaration to avoid including internal headers. +class EcmgClientHandler; + +// Handles requests from one and only one SCS client. +// WvCasEcmgClientHandler is NOT thread-safe. +class WvCasEcmgClientHandler { + public: + explicit WvCasEcmgClientHandler(const EcmgConfig& ecmg_config); + WvCasEcmgClientHandler(const WvCasEcmgClientHandler&) = delete; + WvCasEcmgClientHandler& operator=(const WvCasEcmgClientHandler&) = delete; + virtual ~WvCasEcmgClientHandler(); + + // Sets the custom entitlement key fetching function used by ECMG to fetch + // entitlement keys. If entitlement key information is not received in + // Simulcrypt ECMG from SCS, ECMG will try to fetch entitlement keys using + // this function. + // Calling this function is optional. + void SetCustomEntitlementKeyFetcherFunc(EntitlementKeyFetcherFunc fetcher); + + // Handles a |request| from the SCS client. If any response is generated, it + // will return the response via |response_buffer| and |response_length|. + // Args: + // - |request_buffer_size| is the size of |request_buffer|. + // - |request_buffer| is the buffer that holds the request message. + // - |response_buffer_size| is the size of |response_buffer|. Must be at large + // enough to hold the max possible response message (at least 2048 bytes). + // - |response_buffer| is the buffer that holds the response message. + // - |response_length| is the actual length of the response message. + Status HandleRequest(size_t request_buffer_size, + const char* const request_buffer, + size_t response_buffer_size, char* response_buffer, + size_t* response_length); + + protected: + // For unit test only. + explicit WvCasEcmgClientHandler( + std::unique_ptr inner_handler); + + private: + // Serves as storage for the |inner_handler_| below. + EcmgConfig ecmg_config_; + std::unique_ptr inner_handler_; +}; + +} // namespace cas +} // namespace widevine + +#endif // MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_ECMG_CLIENT_HANDLER_H_ diff --git a/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler_test.cc b/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler_test.cc new file mode 100644 index 0000000..882b97c --- /dev/null +++ b/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler_test.cc @@ -0,0 +1,147 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 Google LLC. +// +// This software is licensed under the terms defined in the Widevine Master +// License Agreement. For a copy of this agreement, please contact +// widevine-licensing@google.com. +//////////////////////////////////////////////////////////////////////////////// + +#include "media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.h" + +#include "testing/gmock.h" +#include "testing/gunit.h" +#include "absl/memory/memory.h" +#include "common/status.h" +#include "media_cas_packager_sdk/internal/ecmg_client_handler.h" +#include "media_cas_packager_sdk/public/wv_cas_types.h" + +namespace widevine { +namespace cas { +namespace { + +constexpr size_t kBufferSize = 2048; + +class MockEcmgClientHandler : public EcmgClientHandler { + public: + MockEcmgClientHandler() : EcmgClientHandler(&config_) {} + MOCK_METHOD(void, HandleRequest, + (const char* const request, char* response, + size_t* response_length), + (override)); + + private: + EcmgConfig config_; +}; + +// Changes protected methods to public. +class TestWvCasEcmgClientHandler : public WvCasEcmgClientHandler { + public: + explicit TestWvCasEcmgClientHandler( + std::unique_ptr inner_handler) + : WvCasEcmgClientHandler(std::move(inner_handler)) {} +}; + +TEST(WvCasEcmgClientHandlerTest, HandleRequestSuccess) { + size_t request_buffer_size = kBufferSize; + char request_buffer[kBufferSize]; + size_t response_buffer_size = kBufferSize; + char response_buffer[kBufferSize]; + size_t response_length = 0; + auto mock_internal_handler = absl::make_unique(); + EXPECT_CALL(*mock_internal_handler, + HandleRequest(request_buffer, response_buffer, &response_length)); + + auto handler = absl::make_unique( + std::move(mock_internal_handler)); + EXPECT_OK(handler->HandleRequest(request_buffer_size, request_buffer, + response_buffer_size, response_buffer, + &response_length)); +} + +class WvCasEcmgClientHandlerInvalidInputTest : public ::testing::Test { + protected: + size_t request_buffer_size_ = kBufferSize; + char request_buffer_[kBufferSize]; + size_t response_buffer_size_ = kBufferSize; + char response_buffer_[kBufferSize]; + size_t response_length_ = 0; +}; + +TEST_F(WvCasEcmgClientHandlerInvalidInputTest, RequestBufferTooSmallFail) { + auto mock_internal_handler = absl::make_unique(); + EXPECT_CALL(*mock_internal_handler, HandleRequest).Times(0); + auto handler = absl::make_unique( + std::move(mock_internal_handler)); + + EXPECT_EQ(handler + ->HandleRequest(/*request_buffer_size=*/1, request_buffer_, + response_buffer_size_, response_buffer_, + &response_length_) + .error_code(), + error::INVALID_ARGUMENT); + EXPECT_EQ(response_length_, 0); +} + +TEST_F(WvCasEcmgClientHandlerInvalidInputTest, ResponseBufferTooSmallFail) { + auto mock_internal_handler = absl::make_unique(); + EXPECT_CALL(*mock_internal_handler, HandleRequest).Times(0); + auto handler = absl::make_unique( + std::move(mock_internal_handler)); + + EXPECT_EQ(handler + ->HandleRequest(request_buffer_size_, request_buffer_, + /*response_buffer_size=*/1, response_buffer_, + &response_length_) + .error_code(), + error::INVALID_ARGUMENT); + EXPECT_EQ(response_length_, 0); +} + +TEST_F(WvCasEcmgClientHandlerInvalidInputTest, RequestBufferNullFail) { + auto mock_internal_handler = absl::make_unique(); + EXPECT_CALL(*mock_internal_handler, HandleRequest).Times(0); + auto handler = absl::make_unique( + std::move(mock_internal_handler)); + + EXPECT_EQ( + handler + ->HandleRequest(request_buffer_size_, + /*request_buffer=*/nullptr, response_buffer_size_, + response_buffer_, &response_length_) + .error_code(), + error::INVALID_ARGUMENT); + EXPECT_EQ(response_length_, 0); +} + +TEST_F(WvCasEcmgClientHandlerInvalidInputTest, ResponseBufferNullFail) { + auto mock_internal_handler = absl::make_unique(); + EXPECT_CALL(*mock_internal_handler, HandleRequest).Times(0); + auto handler = absl::make_unique( + std::move(mock_internal_handler)); + + EXPECT_EQ(handler + ->HandleRequest(request_buffer_size_, request_buffer_, + response_buffer_size_, + /*response_buffer=*/nullptr, &response_length_) + .error_code(), + error::INVALID_ARGUMENT); + EXPECT_EQ(response_length_, 0); +} + +TEST_F(WvCasEcmgClientHandlerInvalidInputTest, ResponseLengthNullFail) { + auto mock_internal_handler = absl::make_unique(); + EXPECT_CALL(*mock_internal_handler, HandleRequest).Times(0); + auto handler = absl::make_unique( + std::move(mock_internal_handler)); + + EXPECT_EQ(handler + ->HandleRequest(request_buffer_size_, request_buffer_, + response_buffer_size_, response_buffer_, + /*response_length=*/nullptr) + .error_code(), + error::INVALID_ARGUMENT); +} + +} // namespace +} // namespace cas +} // namespace widevine diff --git a/media_cas_packager_sdk/public/wv_cas_types.h b/media_cas_packager_sdk/public/wv_cas_types.h index e8b4bfd..52e378a 100644 --- a/media_cas_packager_sdk/public/wv_cas_types.h +++ b/media_cas_packager_sdk/public/wv_cas_types.h @@ -65,6 +65,68 @@ struct EntitlementKeyInfo { std::string key_value; // must be 32 bytes. }; +// A struct that captures the Simulcrypt ECMG configurations. Most fields are +// Simulcrypt standard fields (See ETSI TS 103 197 V1.5.1 (2008-10) +// Section 5.3). +struct EcmgConfig { + // |delay_start| is a signed integer represents the amount of time between the + // start of a Crypto Period, and the start of the broadcasting of the ECM + // attached to this period. If it is positive, it means that the ECM shall + // be delayed with respect to the start of the Crypto Period. If negative, + // it means that the ECM shall be broadcast ahead of this time. This + // parameter is communicated by the ECMG to the SCS during the channel + // setup. + int16_t delay_start; + // |delay_stop| is a signed integer represents the amount of time between the + // end of a Crypto Period, and the end of the broadcasting of the ECM + // attached to this period. If it is positive, it means that the end of the + // ECM broadcast shall be delayed with respect to the end of the Crypto + // Period. If negative, it means that the ECM broadcast shall be ended ahead + // of time. This parameter is communicated by the ECMG to the SCS during the + // channel setup. + int16_t delay_stop; + // |ecm_rep_period| is an integer represents the period in milliseconds for + // the repetition of data (e.g. ECMs). + uint16_t ecm_rep_period; + // |max_comp_time| this parameter is communicated by the ECMG to the SCS + // during channel setup. It is the worst case time needed by an ECMG to + // compute an ECM when all the streams in a channel are being used. This + // time is typically used by the SCS to decide when to time-out on the + // ECM_response message. This value shall be lower than the min_CP_duration + // parameter of the same channel_status message. + uint16_t max_comp_time; + // |access_criteria_transfer_mode| this 1-byte parameter is a flag. If it + // equals 0, it indicates that the access_criteria parameter is required in + // the CW_provision message only when the contents of this parameter change. + // If it equals 1, it indicates that the ECMG requires the access_criteria + // parameter be present in each CW_provision message. + uint8_t access_criteria_transfer_mode; + // |number_of_content_keys| is the number of content keys in an ECM. If + // content key rotation is enabled, number_of_content_keys should be set to 2; + // otherwise it should be set to 1. + uint8_t number_of_content_keys; + // |crypto_mode| indicates the crypto mode used to encrypt the content. It + // will be included in the ECM. If new crypto_mode is received from SCS, ECM + // will use the new one. + CryptoMode crypto_mode; +}; + +// A custom entitlement key fetching function used by ECMG to fetch entitlement +// keys. If entitlement key information is not received in Simulcrypt ECMG from +// SCS, ECMG will try to fetch entitlement keys using this function. +// Args: +// |channel_id| is the channel id received at ECMG from SCS when setting up +// the channel. +// |stream_id| is the stream id received at ECMG from SCS when setting up +// the stream. +// |ecm_id| is the ecm id received at ECMG from SCS when setting up the +// stream. +// Returns a vector of EntitlementKeyInfo. The size of the returned vector +// must not exceed 2. When the vector size is 2, one of them must be specified +// as even key and the other must be odd key (sequence does not matter). +using EntitlementKeyFetcherFunc = std::vector (*)( + uint16_t channel_id, uint16_t stream_id, uint16_t ecm_id); + struct WvCasEncryptionRequest { std::string content_id; std::string provider;