diff --git a/example/test_ecmg_messages.h b/example/test_ecmg_messages.h index 36e4a77..b4c7ae5 100644 --- a/example/test_ecmg_messages.h +++ b/example/test_ecmg_messages.h @@ -14,7 +14,7 @@ namespace widevine { namespace cas { -const char kTestChannelSetup[] = { +const char kTestEcmgChannelSetup[] = { '\x03', // protocol_version '\x00', '\x01', // message_type - Channel_setup '\x00', '\x0e', // message_length @@ -26,7 +26,7 @@ const char kTestChannelSetup[] = { '\x4a', '\xd4', '\x00', '\x00' // parameter_value }; -const char kTestChannelStatus[] = { +const char kTestEcmgChannelStatus[] = { '\x03', // protocol_version '\x00', '\x03', // message_type - Channel_status '\x00', '\x39', // message_length @@ -62,7 +62,7 @@ const char kTestChannelStatus[] = { '\x00', '\x64' // parameter_value }; -const char kTestStreamSetup[] = { +const char kTestEcmgStreamSetup[] = { '\x03', // protocol_version '\x01', '\x01', // message_type - Stream_setup '\x00', '\x18', // message_length @@ -80,7 +80,7 @@ const char kTestStreamSetup[] = { '\x00', '\x64' // parameter_value }; -const char kTestStreamStatus[] = { +const char kTestEcmgStreamStatus[] = { '\x03', // protocol_version '\x01', '\x03', // message_type - Stream_status '\x00', '\x17', // message_length @@ -98,7 +98,7 @@ const char kTestStreamStatus[] = { '\x01' // parameter_value }; -const char kTestCwProvision[] = { +const char kTestEcmgCwProvision[] = { '\x03', // protocol_version '\x02', '\x01', // message_type - CW_provision '\x00', '\x44', // message_length @@ -126,7 +126,7 @@ const char kTestCwProvision[] = { '\x18', '\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f'}; // CW is encrypted using hardcoded fixed entitlement key. -const char kTestEcmResponse[] = { +const char kTestEcmgEcmResponse[] = { '\x03', // protocol_version '\x02', '\x02', // message_type - ECM_response '\x00', '\xd2', // message_length @@ -164,7 +164,7 @@ const char kTestEcmResponse[] = { '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff', '\xff'}; -const char kTestStreamCloseRequest[] = { +const char kTestEcmgStreamCloseRequest[] = { '\x03', // protocol_version '\x01', '\x04', // message_type - Stream_close_request '\x00', '\x0c', // message_length @@ -176,7 +176,7 @@ const char kTestStreamCloseRequest[] = { '\x00', '\x01' // parameter_value }; -const char kTestStreamCloseResponse[] = { +const char kTestEcmgStreamCloseResponse[] = { '\x03', // protocol_version '\x01', '\x05', // message_type - Stream_close_response '\x00', '\x0c', // message_length @@ -188,7 +188,7 @@ const char kTestStreamCloseResponse[] = { '\x00', '\x01' // parameter_value }; -const char kTestChannelClose[] = { +const char kTestEcmgChannelClose[] = { '\x03', // protocol_version '\x00', '\x04', // message_type - Channel_close '\x00', '\x06', // message_length diff --git a/example/wv_cas_ecm_example b/example/wv_cas_ecm_example index 05ef129..b1fbff7 100644 Binary files a/example/wv_cas_ecm_example and b/example/wv_cas_ecm_example differ diff --git a/example/wv_cas_ecm_example.cc b/example/wv_cas_ecm_example.cc index 4c5dab4..d59a4d9 100644 --- a/example/wv_cas_ecm_example.cc +++ b/example/wv_cas_ecm_example.cc @@ -8,12 +8,23 @@ // Example of how to use the wv_cas_ecm library. +#include +#include #include #include +#include "gflags/gflags.h" #include "media_cas_packager_sdk/public/wv_cas_ecm.h" #include "media_cas_packager_sdk/public/wv_cas_types.h" +DEFINE_int32(content_iv_size, 8, "Content IV size"); +DEFINE_bool(key_rotation, true, "Whether key rotation is enabled"); +DEFINE_string(crypto_mode, "CSA2", "Only CBC, CTR, or CSA2 is allowed"); +DEFINE_int32(ecm_pid, 149, "PID for the ECM packet"); +DEFINE_string(output_file, "", + "If specified, generated ECM TS packet will be written to the " + "specified output file path"); + const char kCsaEvenKey[] = "even_key"; // 8 bytes const char kEvenContentIv8Bytes[] = "even_iv."; // 8 bytes const char kEvenEntitlementKeyId[] = "fake_key_id1...."; // 16 bytes @@ -24,26 +35,45 @@ const char kOddContentIv8Bytes[] = "odd_iv.."; // 8 bytes const char kOddEntitlementKeyId[] = "fake_key_id2...."; // 16 bytes const char kOddEntitlementKey[] = "fakefakefakefakefakefakefake2..."; // 32 bytes +const size_t kTsPacketSize = 188; + +widevine::cas::CryptoMode GetCryptoMode() { + if (FLAGS_crypto_mode.compare("CBC") == 0) { + return widevine::cas::CryptoMode::kAesCbc; + } + if (FLAGS_crypto_mode.compare("CTR") == 0) { + return widevine::cas::CryptoMode::kAesCtr; + } + return widevine::cas::CryptoMode::kDvbCsa2; +} int main(int argc, char **argv) { + gflags::ParseCommandLineFlags(&argc, &argv, true); + // Generate ECM. widevine::cas::WvCasEcm wv_cas_ecm; widevine::cas::WvCasStatus status = wv_cas_ecm.Initialize( - /* content_iv_size= */ 8, /* key_rotation_enabled= */ true, - widevine::cas::CryptoMode::kDvbCsa2); + FLAGS_content_iv_size, FLAGS_key_rotation, GetCryptoMode()); if (status != widevine::cas::OK) { std::cerr << "Failed to initialize WV CAS ECM, error: " << widevine::cas::GetWvCasStatusMessage(status) << std::endl; } std::string ecm; - status = wv_cas_ecm.GenerateEcm( - kCsaEvenKey, kEvenContentIv8Bytes, kEvenEntitlementKeyId, - kEvenEntitlementKey, kCsaOddKey, kOddContentIv8Bytes, - kOddEntitlementKeyId, kOddEntitlementKey, &ecm); + if (FLAGS_key_rotation) { + status = wv_cas_ecm.GenerateEcm( + kCsaEvenKey, kEvenContentIv8Bytes, kEvenEntitlementKeyId, + kEvenEntitlementKey, kCsaOddKey, kOddContentIv8Bytes, + kOddEntitlementKeyId, kOddEntitlementKey, &ecm); + } else { + status = wv_cas_ecm.GenerateSingleKeyEcm(kCsaEvenKey, kEvenContentIv8Bytes, + kEvenEntitlementKeyId, + kEvenEntitlementKey, &ecm); + } if (status != widevine::cas::OK) { std::cerr << "Failed to generate WV CAS ECM, error: " << widevine::cas::GetWvCasStatusMessage(status) << std::endl; + return -1; } else { std::cout << "ECM size: " << ecm.size() << std::endl; std::cout << "ECM bytes: "; @@ -52,6 +82,30 @@ int main(int argc, char **argv) { } std::cout << std::endl; } + // Generate ECM TS Packet. + uint8_t packet[kTsPacketSize]; + uint8_t continuity_counter; // not used. + status = wv_cas_ecm.GenerateTsPacket(ecm, FLAGS_ecm_pid, + /* table_id= */ 0x80, + &continuity_counter, packet); + if (status != widevine::cas::OK) { + std::cerr << "Failed to create ECM TS packet" << std::endl; + return -1; + } else { + std::cout << "TS packet bytes: "; + for (size_t i = 0; i < kTsPacketSize; i++) { + printf("'\\x%02x', ", static_cast(packet[i])); + } + std::cout << std::endl; + } + // Write ECM TS Packet to a file. + if (!FLAGS_output_file.empty()) { + std::ofstream file; + file.open(FLAGS_output_file.c_str(), std::ios_base::binary); + assert(file.is_open()); + file.write(reinterpret_cast(packet), kTsPacketSize); + file.close(); + } return 0; } diff --git a/example/wv_cas_types_example b/example/wv_cas_types_example new file mode 100644 index 0000000..880f7c5 Binary files /dev/null and b/example/wv_cas_types_example differ diff --git a/example/wv_cas_types_example.cc b/example/wv_cas_types_example.cc new file mode 100644 index 0000000..d116d76 --- /dev/null +++ b/example/wv_cas_types_example.cc @@ -0,0 +1,37 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2019 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 how to use types/functions in wv_cas_types. + +#include + +#include "gflags/gflags.h" +#include "glog/logging.h" +#include "media_cas_packager_sdk/public/wv_cas_types.h" + +DEFINE_string(function_to_call, "CreateWvCasEncryptionRequestJson", + "Function in wv_cas_types to exercise"); + +void CallCreateWvCasEncryptionRequestJson() { + widevine::cas::WvCasEncryptionRequest request; + request.content_id = "cont_id cont_id "; + request.provider = "widevine_test"; + request.track_types = {"SD"}; + request.key_rotation = true; + std::string request_json; + widevine::cas::CreateWvCasEncryptionRequestJson(request, &request_json); + LOG(INFO) << FLAGS_function_to_call << " returns " << request_json; +} + +int main(int argc, char **argv) { + gflags::ParseCommandLineFlags(&argc, &argv, true); + if (FLAGS_function_to_call.compare("CreateWvCasEncryptionRequestJson") == 0) { + CallCreateWvCasEncryptionRequestJson(); + } + return 0; +} diff --git a/libmedia_cas_packager_sdk.so b/libmedia_cas_packager_sdk.so index ba29909..bf17dc3 100755 Binary files a/libmedia_cas_packager_sdk.so and b/libmedia_cas_packager_sdk.so differ diff --git a/media_cas_packager_sdk/public/wv_cas_key_fetcher.cc b/media_cas_packager_sdk/public/wv_cas_key_fetcher.cc new file mode 100644 index 0000000..d8efb49 --- /dev/null +++ b/media_cas_packager_sdk/public/wv_cas_key_fetcher.cc @@ -0,0 +1,154 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2018 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_key_fetcher.h" + +#include +#include +#include + +#include "gflags/gflags.h" +#include "glog/logging.h" +#include "google/protobuf/util/json_util.h" +#include "absl/strings/escaping.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "curl/curl.h" +#include "curl/easy.h" +#include "common/signature_util.h" +#include "protos/public/media_cas_encryption.pb.h" + +using google::protobuf::util::JsonPrintOptions; +using google::protobuf::util::JsonStringToMessage; +using google::protobuf::util::MessageToJsonString; + +DEFINE_string( + license_server, "", + "HTTP URL to the license server for making CAS encryption request"); +DEFINE_string(signing_provider, "", + "Name of the provider signing the CAS encryption request"); +DEFINE_string(signing_key, "", + "AES key (in hex) for signing the CAS encryption request"); +DEFINE_string(signing_iv, "", + "AES iv (in hex) for signing the CAS encryption request"); + +namespace widevine { +namespace cas { + +Status WvCasKeyFetcher::RequestEntitlementKey(const std::string& request_string, + std::string* signed_response_string) { + if (FLAGS_signing_provider.empty() || FLAGS_signing_key.empty() || + FLAGS_signing_iv.empty()) { + return Status( + error::INVALID_ARGUMENT, + "Flag 'signing_provider', 'signing_key' or 'signing_iv' is empty"); + } + + // Processes request. + CasEncryptionRequest request; + request.ParseFromString(request_string); + std::string request_json; + JsonPrintOptions print_options; + // Set this option so that the json output is + // {"content_id":"MjExNDA4NDQ=", ... + // instead of + // {"contentId":"MjExNDA4NDQ=", ... + print_options.preserve_proto_field_names = true; + // NOTE: MessageToJsonString will automatically converts 'bytes' type fields + // to base64. For example content ID '21140844' becomes 'MjExNDA4NDQ='. + if (!MessageToJsonString(request, &request_json, print_options).ok()) { + return Status(error::INTERNAL, + "Failed to convert request message to json."); + } + LOG(INFO) << "Json CasEncryptionRequest: " << request_json; + + // Creates signed request. + SignedCasEncryptionRequest signed_request; + signed_request.set_request(request_json); + std::string signature; + if (!signature_util::GenerateAesSignature( + request_json, absl::HexStringToBytes(FLAGS_signing_key), + absl::HexStringToBytes(FLAGS_signing_iv), &signature) + .ok()) { + return Status(error::INTERNAL, "Failed to sign the request."); + } + signed_request.set_signature(signature); + signed_request.set_signer(FLAGS_signing_provider); + std::string signed_request_json; + // NOTE: MessageToJsonString will automatically converts the 'request' and + // 'signature' fields in SignedCasEncryptionRequest to base64, because they + // are of type 'bytes'. + if (!MessageToJsonString(signed_request, &signed_request_json).ok()) { + return Status(error::INTERNAL, + "Failed to convert signed request message to json."); + } + LOG(INFO) << "Json SignedCasEncryptionRequest: " << signed_request_json; + + // Makes HTTP request against License Server. + std::string http_response_json; + Status status = MakeHttpRequest(signed_request_json, &http_response_json); + if (!status.ok()) { + return status; + } + LOG(INFO) << "Json HTTP response: " << http_response_json; + HttpResponse http_response; + if (!JsonStringToMessage(http_response_json, &http_response).ok()) { + return Status(error::INTERNAL, + "Failed to convert http response json to message."); + } + + // Processes signed response. + LOG(INFO) << "Json CasEncryptionResponse: " << http_response.response(); + CasEncryptionResponse response; + if (!JsonStringToMessage(http_response.response(), &response).ok()) { + return Status(error::INTERNAL, + "Failed to convert response json to message."); + } + SignedCasEncryptionResponse signed_response; + signed_response.set_response(response.SerializeAsString()); + signed_response.SerializeToString(signed_response_string); + return OkStatus(); +} + +size_t AppendToString(void* ptr, size_t size, size_t count, std::string* output) { + const absl::string_view data(static_cast(ptr), size * count); + absl::StrAppend(output, data); + return data.size(); +} + +Status WvCasKeyFetcher::MakeHttpRequest(const std::string& signed_request_json, + std::string* http_response_json) const { + CHECK(http_response_json); + if (FLAGS_license_server.empty()) { + return Status(error::INVALID_ARGUMENT, "Flag 'license_server' is empty"); + } + CURL* curl; + CURLcode curl_code; + curl = curl_easy_init(); + if (curl) { + curl_easy_setopt(curl, CURLOPT_URL, FLAGS_license_server.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, signed_request_json.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, http_response_json); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &AppendToString); + // If we don't provide POSTFIELDSIZE, libcurl will strlen() by itself. + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, + (int64_t)strlen(signed_request_json.c_str())); + curl_code = curl_easy_perform(curl); + if (curl_code != CURLE_OK) { + return Status(error::INTERNAL, "curl_easy_perform() failed: " + + std::string(curl_easy_strerror(curl_code))); + } + curl_easy_cleanup(curl); + } else { + return Status(error::INTERNAL, "curl_easy_init() failed"); + } + return OkStatus(); +} + +} // namespace cas +} // namespace widevine diff --git a/media_cas_packager_sdk/public/wv_cas_types.cc b/media_cas_packager_sdk/public/wv_cas_types.cc new file mode 100644 index 0000000..02d2da8 --- /dev/null +++ b/media_cas_packager_sdk/public/wv_cas_types.cc @@ -0,0 +1,140 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2018 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_types.h" + +#include "glog/logging.h" +#include "base/macros.h" +#include "google/protobuf/util/json_util.h" +#include "common/status.h" +#include "protos/public/media_cas_encryption.pb.h" + +using google::protobuf::util::JsonPrintOptions; +using google::protobuf::util::JsonStringToMessage; +using google::protobuf::util::MessageToJsonString; + +namespace widevine { +namespace cas { + +static const char* kWvCasStatusMessage[] = { + "OK", // OK = 0, + "", + "", + "Invalid argument", // INVALID_ARGUMENT = 3, + "", + "Not found", // NOT_FOUND = 5, + "Already exists", // ALREADY_EXISTS = 6, + "Permission denied", // PERMISSION_DENIED = 7, + "", + "", + "", + "", + "Unimplemented", // UNIMPLEMENTED = 12, + "Internal", // INTERNAL = 13, + "Unavailable", // UNAVAILABLE = 14, +}; + +std::string GetWvCasStatusMessage(WvCasStatus status) { + static_assert(arraysize(kWvCasStatusMessage) == NUM_WV_CAS_STATUS, + "mismatching status message and status."); + return kWvCasStatusMessage[status]; +} + +// Numeric value of crypto mode is the index into strings array. +static const char* kCrypoModeStrings[] = { + "AesCbc", + "AesCtr", + "DvbCsa2", +}; + +bool CryptoModeToString(CryptoMode mode, std::string* str) { + if (str == nullptr) { + return false; + } + int mode_idx = static_cast(mode); + if (mode_idx >= 0 && mode_idx < arraysize(kCrypoModeStrings)) { + *str = kCrypoModeStrings[mode_idx]; + return true; + } + LOG(ERROR) << "Invalid crypto mode: " << mode_idx; + return false; +} + +bool StringToCryptoMode(const std::string& str, CryptoMode* mode) { + if (mode == nullptr) { + return false; + } + for (int i = 0; i < arraysize(kCrypoModeStrings); ++i) { + if (str.compare(kCrypoModeStrings[i]) == 0) { + *mode = static_cast(i); + return true; + } + } + LOG(ERROR) << "Invalid crypto mode: " << str; + return false; +} + +WvCasStatus CreateWvCasEncryptionRequestJson( + const WvCasEncryptionRequest& request, std::string* request_json) { + CHECK(request_json); + + CasEncryptionRequest request_proto; + request_proto.set_content_id(request.content_id); + request_proto.set_provider(request.provider); + for (const std::string& track_type : request.track_types) { + request_proto.add_track_types(track_type); + } + request_proto.set_key_rotation(request.key_rotation); + + JsonPrintOptions print_options; + // Set this option so that the json output is + // {"content_id":"MjExNDA4NDQ=", ... + // instead of + // {"contentId":"MjExNDA4NDQ=", ... + print_options.preserve_proto_field_names = true; + // NOTE: MessageToJsonString will automatically converts 'bytes' type fields + // to base64. For example content ID '21140844' becomes 'MjExNDA4NDQ='. + if (!MessageToJsonString(request_proto, request_json, print_options).ok()) { + LOG(ERROR) << "Failed to convert request message to json."; + return INTERNAL; + } + + return OK; +} + +WvCasStatus ParseWvCasEncryptionResponseJson( + const std::string& response_json, WvCasEncryptionResponse* response) { + CHECK(response); + + CasEncryptionResponse response_proto; + // NOTE: JsonStringToMessage will automatically perform base64 decode for + // 'bytes' type fields. + if (!JsonStringToMessage(response_json, &response_proto).ok()) { + LOG(ERROR) << "Failed to convert response json to message."; + return INTERNAL; + } + + response->status = + static_cast(response_proto.status()); + response->status_message = response_proto.status_message(); + response->content_id = response_proto.content_id(); + for (const auto& key_info_proto : response_proto.entitlement_keys()) { + WvCasEncryptionResponse::KeyInfo key_info; + key_info.key_id = key_info_proto.key_id(); + key_info.key = key_info_proto.key(); + key_info.track_type = key_info_proto.track_type(); + key_info.key_slot = static_cast( + key_info_proto.key_slot()); + response->entitlement_keys.push_back(key_info); + } + + return OK; +} + +} // 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 2d42894..c269a62 100644 --- a/media_cas_packager_sdk/public/wv_cas_types.h +++ b/media_cas_packager_sdk/public/wv_cas_types.h @@ -114,15 +114,15 @@ struct WvCasEncryptionResponse { // request JSON message. // And that signed JSON message can be sent to Widevine license server for // aquiring entitlement keys. -Status CreateWvCasEncryptionRequestJson(const WvCasEncryptionRequest& request, - std::string* request_json); +WvCasStatus CreateWvCasEncryptionRequestJson( + const WvCasEncryptionRequest& request, std::string* request_json); // Parses a WvCasEncryptionResponse in JSON format, returns a // WvCasEncryptionResponse. // |response_json| is supposed to be the 'response' field in the signed // response from Widevine license server. -Status ParseWvCasEncryptionResponseJson(const std::string& response_json, - WvCasEncryptionResponse* response); +WvCasStatus ParseWvCasEncryptionResponseJson(const std::string& response_json, + WvCasEncryptionResponse* response); } // namespace cas } // namespace widevine diff --git a/media_cas_packager_sdk/public/wv_ecmg b/media_cas_packager_sdk/public/wv_ecmg index 3995c47..43a0afc 100644 Binary files a/media_cas_packager_sdk/public/wv_ecmg and b/media_cas_packager_sdk/public/wv_ecmg differ