From 1f770855719fff0921970a8bf000a3a9c6de2508 Mon Sep 17 00:00:00 2001 From: conglin Date: Mon, 9 Jun 2025 23:25:13 +0000 Subject: [PATCH 1/3] Re-purpose internal factory upload tool for AOSP This tool was supposed to be used for internal debugging purpose on Android devices. It already supports RKP uploading CSR format. Extend this tool to support Widevine uploading format: JSON csr and make this format as default output for AOSP (non-GMS) partners. A later change will move it to its own aosp/ directory. Test: run "wv_factory_extraction_tool json_csr" on Pixel 9 Bug: 414642286 Change-Id: I9cf4e9696d32201cc1ad70b6bee7932f7126a4ba --- .../tools/factory_upload_tool/Android.bp | 3 + .../tools/factory_upload_tool/cli.cpp | 76 +++++++++++++-- .../include/WidevineProvisioner.h | 5 + .../src/WidevineOemcryptoInterface.cpp | 34 ------- .../src/WidevineProvisioner.cpp | 93 ++++++++++++++++++- 5 files changed, 168 insertions(+), 43 deletions(-) diff --git a/libwvdrmengine/tools/factory_upload_tool/Android.bp b/libwvdrmengine/tools/factory_upload_tool/Android.bp index f5db00dd..8790850b 100644 --- a/libwvdrmengine/tools/factory_upload_tool/Android.bp +++ b/libwvdrmengine/tools/factory_upload_tool/Android.bp @@ -26,6 +26,9 @@ cc_binary { "liblog", "libutils", ], + static_libs: [ + "libwv_cdm_utils", + ], srcs: [ "cli.cpp", "src/log.cpp", diff --git a/libwvdrmengine/tools/factory_upload_tool/cli.cpp b/libwvdrmengine/tools/factory_upload_tool/cli.cpp index 3648d59c..166c276b 100644 --- a/libwvdrmengine/tools/factory_upload_tool/cli.cpp +++ b/libwvdrmengine/tools/factory_upload_tool/cli.cpp @@ -160,29 +160,82 @@ std::unique_ptr getCsrV3( return composeCertificateRequestV3(csr); } +void printHelp(const char* tool_name) { + fprintf(stdout, "Widevine Factory Extraction Tool for AOSP\n\n"); + fprintf(stdout, "Usage: %s [command]\n\n", tool_name); + fprintf(stdout, + "This tool extracts BCC and device information and generates CSR " + "required for Widevine Provisioning 4.0 factory uploading.\n\n"); + fprintf(stdout, "Commands:\n"); + fprintf(stdout, " json_csr (default)\n"); + fprintf(stdout, + " Generates and prints a JSON-formatted " + "Certificate Signing\n"); + fprintf(stdout, + " Request (CSR) to be uploaded to the " + "Widevine provisioning server.\n\n"); + fprintf(stdout, " bcc\n"); + fprintf(stdout, + " Outputs the raw binary Bootloader " + "Certificate Chain (BCC).\n\n"); + fprintf(stdout, " bcc_str\n"); + fprintf(stdout, + " Outputs a human-readable, parsed string " + "representation of the BCC.\n\n"); + fprintf(stdout, " device_info\n"); + fprintf( + stdout, + " Outputs the raw binary device information blob.\n\n"); + fprintf(stdout, " csr\n"); + fprintf(stdout, + " Generates and outputs a legacy format Certificate " + "Signing Request (CSR) to be uploaded to RKP backend.\n\n"); + fprintf(stdout, " csr_v3\n"); + fprintf(stdout, + " Generates and outputs a V3 format Certificate " + "Signing Request (CSR) to be uploaded to RKP backend.\n\n"); + fprintf(stdout, " help\n"); + fprintf(stdout, " Displays this help message.\n"); +} + int main(int argc, char** argv) { - if (argc < 2) { - fprintf(stderr, "%s \n", argv[0]); - return 0; - } widevine::WidevineProvisioner provisioner; - if (!std::strcmp(argv[1], "bcc")) { + + // Default to Widevine uploading request format "json_csr" if no arguments are + // provided. + const char* command = (argc > 1) ? argv[1] : "json_csr"; + if (!std::strcmp(command, "json_csr")) { + std::string request; + if (provisioner.GenerateWidevineUploadRequest(request)) { + std::copy(request.begin(), request.end(), + std::ostream_iterator(std::cout)); + } else { + fprintf(stderr, + "Failed to generate Widevine uploading request json CSR.\n"); + return 1; + } + } else if (!std::strcmp(command, "help")) { + printHelp(argv[0]); + } else if (!std::strcmp(command, "bcc")) { auto bcc = provisioner.GetBcc(); fwrite(bcc.data(), 1, bcc.size(), stdout); fflush(stdout); - } else if (!std::strcmp(argv[1], "bcc_str")) { + } else if (!std::strcmp(command, "bcc_str")) { auto bcc = provisioner.GetBcc(); widevine::BccParser bcc_parser; std::string parsed_bcc = bcc_parser.Parse(bcc); std::copy(parsed_bcc.begin(), parsed_bcc.end(), std::ostream_iterator(std::cout)); - } else if (!std::strcmp(argv[1], "device_info")) { + } else if (!std::strcmp(command, "device_info")) { std::vector deviceInfo; if (provisioner.GetDeviceInfo(deviceInfo)) { fwrite(deviceInfo.data(), 1, deviceInfo.size(), stdout); fflush(stdout); + } else { + fprintf(stderr, "Failed to get device info.\n"); + return 1; } - } else if (!std::strcmp(argv[1], "csr")) { + } else if (!std::strcmp(command, "csr")) { auto csr = getCsr(provisioner); auto bytes = csr.encode(); std::copy(bytes.begin(), bytes.end(), @@ -193,7 +246,14 @@ int main(int argc, char** argv) { auto bytes = csr->encode(); std::copy(bytes.begin(), bytes.end(), std::ostream_iterator(std::cout)); + } else { + fprintf(stderr, "Failed to generate CSR V3.\n"); + return 1; } + } else { + fprintf(stderr, "Error: Unknown command '%s'\n\n", command); + printHelp(argv[0]); + return 1; } return 0; } diff --git a/libwvdrmengine/tools/factory_upload_tool/include/WidevineProvisioner.h b/libwvdrmengine/tools/factory_upload_tool/include/WidevineProvisioner.h index d3e81d9d..7f1db2ad 100644 --- a/libwvdrmengine/tools/factory_upload_tool/include/WidevineProvisioner.h +++ b/libwvdrmengine/tools/factory_upload_tool/include/WidevineProvisioner.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -32,6 +33,7 @@ class WidevineProvisioner { bool GenerateCertificateRequestV2(const std::vector& challenge, std::vector* csr); bool GetDeviceInfo(std::vector& device_info); + bool GenerateWidevineUploadRequest(std::string& request); private: bool GenerateProtectedData( @@ -48,6 +50,9 @@ class WidevineProvisioner { bool GetDeviceInfoCommon(cppbor::Map& device_info_map); bool TryAddVerifiedDeviceInfo(cppbor::Map& device_info_map); bool GetDeviceInfoV2(cppbor::Map& device_info_map); + void PopulateDeviceInfoFromCborMap( + const cppbor::Map& device_info_map, + std::map& request_map); std::unique_ptr crypto_interface_; }; diff --git a/libwvdrmengine/tools/factory_upload_tool/src/WidevineOemcryptoInterface.cpp b/libwvdrmengine/tools/factory_upload_tool/src/WidevineOemcryptoInterface.cpp index ccb105a3..8fc2b87c 100644 --- a/libwvdrmengine/tools/factory_upload_tool/src/WidevineOemcryptoInterface.cpp +++ b/libwvdrmengine/tools/factory_upload_tool/src/WidevineOemcryptoInterface.cpp @@ -7,8 +7,6 @@ #include #include "OEMCryptoCENC.h" -#include "clock.h" -#include "file_store.h" #include "log.h" // These macros lookup the obfuscated name used for OEMCrypto. @@ -24,38 +22,6 @@ #define LOAD_SYM_IF_EXIST(name) \ name = reinterpret_cast(LOOKUP(handle_, OEMCrypto_##name)); -// These are implementations required by OEMCrypto Reference Implementation -// and/or the Testbed, but not needed in this package. -namespace wvutil { -int64_t Clock::GetCurrentTime() { return 0; } - -class FileImpl final : public File { - public: - FileImpl() {} - ssize_t Read(char*, size_t) override { return 0; } - ssize_t Write(const char*, size_t) override { return 0; } -}; - -class FileSystem::Impl { - public: - Impl() {} -}; - -FileSystem::FileSystem() {} -FileSystem::FileSystem(const std::string&, void*) {} -FileSystem::~FileSystem() {} -std::unique_ptr FileSystem::Open(const std::string&, int) { - return std::unique_ptr(new FileImpl()); -} -bool FileSystem::Exists(const std::string&) { return false; } -bool FileSystem::Remove(const std::string&) { return false; } -ssize_t FileSystem::FileSize(const std::string&) { return false; } -bool FileSystem::List(const std::string&, std::vector*) { - return false; -} - -} // namespace wvutil - namespace widevine { OEMCryptoInterface::~OEMCryptoInterface() { diff --git a/libwvdrmengine/tools/factory_upload_tool/src/WidevineProvisioner.cpp b/libwvdrmengine/tools/factory_upload_tool/src/WidevineProvisioner.cpp index 6f9d1f00..b0181990 100644 --- a/libwvdrmengine/tools/factory_upload_tool/src/WidevineProvisioner.cpp +++ b/libwvdrmengine/tools/factory_upload_tool/src/WidevineProvisioner.cpp @@ -22,6 +22,7 @@ #include "WidevineOemcryptoInterface.h" #include "log.h" #include "properties.h" +#include "string_conversions.h" namespace widevine { @@ -31,6 +32,51 @@ const std::vector> kAuthorizedEekRoots = { 0x62, 0xDC, 0x3E, 0x61, 0xAB, 0x57, 0x48, 0x7D, 0x75, 0x37, 0x29, 0xAD, 0x76, 0x80, 0x32, 0xD2, 0xB3, 0xCB, 0x63, 0x58, 0xD9}, }; + +std::string EscapeJson(const std::string& input) { + std::string result; + for (std::string::const_iterator c = input.begin(); c != input.end(); ++c) { + switch (*c) { + case '\"': + result += "\\\""; + break; + case '\\': + result += "\\\\"; + break; + case '\b': + result += "\\b"; + break; + case '\f': + result += "\\f"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + default: + result += *c; + break; + } + } + return result; +} + +std::string StringMapToJson( + const std::map& string_map) { + std::string json = "{"; + for (const auto& value_pair : string_map) { + json.append("\"" + value_pair.first + "\": " + "\"" + value_pair.second + + "\","); + } + json.resize(json.size() - 1); // Remove the last comma. + json.append("}"); + return json; +} } // namespace WidevineProvisioner::WidevineProvisioner() { @@ -107,7 +153,8 @@ bool WidevineProvisioner::TryAddVerifiedDeviceInfo( } bool WidevineProvisioner::GetDeviceInfoCommon(cppbor::Map& device_info_map) { - if (!TryAddVerifiedDeviceInfo(device_info_map)) return false; + // Best effort to populate device info from TEE first + TryAddVerifiedDeviceInfo(device_info_map); // Add device information from OS properties if the verified device info is // not present if (device_info_map.get("brand") == nullptr || @@ -390,6 +437,50 @@ bool WidevineProvisioner::GenerateCertificateRequestV2( return true; } +// Caller ensures the validity of `device_info_map` as a `cppbor::Map&`. +void WidevineProvisioner::PopulateDeviceInfoFromCborMap( + const cppbor::Map& device_info_map, + std::map& request_map) { + if (device_info_map.get("manufacturer")) { + request_map["company"] = + device_info_map.get("manufacturer")->asTstr()->value(); + } + if (device_info_map.get("device")) { + request_map["name"] = device_info_map.get("device")->asTstr()->value(); + } + if (device_info_map.get("architecture")) { + request_map["architecture"] = + device_info_map.get("architecture")->asTstr()->value(); + } + if (device_info_map.get("model")) { + request_map["model"] = device_info_map.get("model")->asTstr()->value(); + } + if (device_info_map.get("product")) { + request_map["product"] = device_info_map.get("product")->asTstr()->value(); + } + if (device_info_map.get("fingerprint")) { + request_map["build_info"] = + device_info_map.get("fingerprint")->asTstr()->value(); + } + if (device_info_map.get("oemcrypto_build_info")) { + request_map["oemcrypto_build_info"] = EscapeJson( + device_info_map.get("oemcrypto_build_info")->asTstr()->value()); + } +} + +bool WidevineProvisioner::GenerateWidevineUploadRequest(std::string& request) { + std::map request_map; + auto bcc = GetBcc(); + request_map["bcc"] = wvutil::Base64Encode(bcc); + + auto device_info_map = cppbor::Map(); + if (!GetDeviceInfoCommon(device_info_map)) return false; + PopulateDeviceInfoFromCborMap(device_info_map, request_map); + + request = StringMapToJson(request_map); + return true; +} + void WidevineProvisioner::InitializeCryptoInterface() { std::string oemcrypto_path; if (!wvcdm::Properties::GetOEMCryptoPath(&oemcrypto_path)) { From 7496c1c84c65c076601c2224f4cda34e8cca6895 Mon Sep 17 00:00:00 2001 From: conglin Date: Tue, 10 Jun 2025 18:20:43 +0000 Subject: [PATCH 2/3] Move ASOP factory extraction tool to its own directory Moved some source to common folder. Added uploading script which is also shared by CE CDM partners. Added README. Test: m wv_factory_extraction_tool Bug: 414642286 Change-Id: I565027b75528ab28f9f1eb8d9086c0213de992d0 --- .../tools/factory_upload_tool/Android.bp | 15 +- .../tools/factory_upload_tool/aosp/README.md | 34 ++ .../factory_upload_tool/{ => aosp}/cli.cpp | 0 .../{ => aosp}/include/BccParser.h | 0 .../{ => aosp}/include/DiceCborConstants.h | 0 .../{ => aosp}/include/WidevineProvisioner.h | 0 .../{ => aosp}/src/BccParser.cpp | 0 .../{ => aosp}/src/WidevineProvisioner.cpp | 0 .../{ => aosp}/src/log.cpp | 0 .../{ => aosp}/src/properties_android.cpp | 0 .../include/WidevineOemcryptoInterface.h | 0 .../{ => common}/include/properties.h | 0 .../src/WidevineOemcryptoInterface.cpp | 0 .../common/wv_upload_tool.py | 513 ++++++++++++++++++ 14 files changed, 555 insertions(+), 7 deletions(-) create mode 100644 libwvdrmengine/tools/factory_upload_tool/aosp/README.md rename libwvdrmengine/tools/factory_upload_tool/{ => aosp}/cli.cpp (100%) rename libwvdrmengine/tools/factory_upload_tool/{ => aosp}/include/BccParser.h (100%) rename libwvdrmengine/tools/factory_upload_tool/{ => aosp}/include/DiceCborConstants.h (100%) rename libwvdrmengine/tools/factory_upload_tool/{ => aosp}/include/WidevineProvisioner.h (100%) rename libwvdrmengine/tools/factory_upload_tool/{ => aosp}/src/BccParser.cpp (100%) rename libwvdrmengine/tools/factory_upload_tool/{ => aosp}/src/WidevineProvisioner.cpp (100%) rename libwvdrmengine/tools/factory_upload_tool/{ => aosp}/src/log.cpp (100%) rename libwvdrmengine/tools/factory_upload_tool/{ => aosp}/src/properties_android.cpp (100%) rename libwvdrmengine/tools/factory_upload_tool/{ => common}/include/WidevineOemcryptoInterface.h (100%) rename libwvdrmengine/tools/factory_upload_tool/{ => common}/include/properties.h (100%) rename libwvdrmengine/tools/factory_upload_tool/{ => common}/src/WidevineOemcryptoInterface.cpp (100%) create mode 100644 libwvdrmengine/tools/factory_upload_tool/common/wv_upload_tool.py diff --git a/libwvdrmengine/tools/factory_upload_tool/Android.bp b/libwvdrmengine/tools/factory_upload_tool/Android.bp index 8790850b..ce8adbfa 100644 --- a/libwvdrmengine/tools/factory_upload_tool/Android.bp +++ b/libwvdrmengine/tools/factory_upload_tool/Android.bp @@ -30,19 +30,20 @@ cc_binary { "libwv_cdm_utils", ], srcs: [ - "cli.cpp", - "src/log.cpp", - "src/properties_android.cpp", - "src/BccParser.cpp", - "src/WidevineProvisioner.cpp", - "src/WidevineOemcryptoInterface.cpp", + "aosp/cli.cpp", + "aosp/src/log.cpp", + "aosp/src/properties_android.cpp", + "aosp/src/BccParser.cpp", + "aosp/src/WidevineProvisioner.cpp", + "common/src/WidevineOemcryptoInterface.cpp", ], include_dirs: [ "vendor/widevine/libwvdrmengine/oemcrypto/include", "vendor/widevine/libwvdrmengine/cdm/util/include", ], local_include_dirs: [ - "include", + "aosp/include", + "common/include", ], dist: { targets: [ diff --git a/libwvdrmengine/tools/factory_upload_tool/aosp/README.md b/libwvdrmengine/tools/factory_upload_tool/aosp/README.md new file mode 100644 index 00000000..c0153122 --- /dev/null +++ b/libwvdrmengine/tools/factory_upload_tool/aosp/README.md @@ -0,0 +1,34 @@ +# Widevine Factory Extraction Tool for AOSP partners + +This tool extracts the BCC and generates the Certificate Signing Request (CSR) +needed to be uploaded to Wideivine Provisioning server for Prov4 device registration. + +## CSR extraction instructions: + +1. Make `wv_factory_extraction_tool`: + - m wv_factory_extraction_tool + +2. Locate build output and adb push it to the target device, e.g.: + - adb push out/target/product/{product name}/vendor/bin/hw/wv_factory_extraction_tool /vendor/bin/hw/wv_factory_extraction_tool + +3. Restart Widevine service on the device to ensure a clean state + before running the tool: + - adb shell pkill -f -9 widevine + +4. Run the wv_factory_extractor tool on the target device. By default, + the tool prints the CSR in JSON format directly to the console. + - adb shell /vendor/bin/hw/wv_factory_extraction_tool json_csr + or just + - adb shell /vendor/bin/hw/wv_factory_extraction_tool + For additional options, run the tool with the `help` argument: + - adb shell /vendor/bin/hw/wv_factory_extraction_tool help + +## Uploading instructions: + +1. Save the extracted CSR to `csr.json`. + +2. Upload the `csr.json` file using `common/wv_upload_tool.py`: + python3 wv_upload_tool.py --credentials=cred.json --org-name={your organization name} --json-csr=csr.json + - Replace cred.json with the path to your OAuth 2.0 client credentials file. + You can obtain this file through the Google Cloud Platform. + - Replace {your organization name} with the name of your organization. diff --git a/libwvdrmengine/tools/factory_upload_tool/cli.cpp b/libwvdrmengine/tools/factory_upload_tool/aosp/cli.cpp similarity index 100% rename from libwvdrmengine/tools/factory_upload_tool/cli.cpp rename to libwvdrmengine/tools/factory_upload_tool/aosp/cli.cpp diff --git a/libwvdrmengine/tools/factory_upload_tool/include/BccParser.h b/libwvdrmengine/tools/factory_upload_tool/aosp/include/BccParser.h similarity index 100% rename from libwvdrmengine/tools/factory_upload_tool/include/BccParser.h rename to libwvdrmengine/tools/factory_upload_tool/aosp/include/BccParser.h diff --git a/libwvdrmengine/tools/factory_upload_tool/include/DiceCborConstants.h b/libwvdrmengine/tools/factory_upload_tool/aosp/include/DiceCborConstants.h similarity index 100% rename from libwvdrmengine/tools/factory_upload_tool/include/DiceCborConstants.h rename to libwvdrmengine/tools/factory_upload_tool/aosp/include/DiceCborConstants.h diff --git a/libwvdrmengine/tools/factory_upload_tool/include/WidevineProvisioner.h b/libwvdrmengine/tools/factory_upload_tool/aosp/include/WidevineProvisioner.h similarity index 100% rename from libwvdrmengine/tools/factory_upload_tool/include/WidevineProvisioner.h rename to libwvdrmengine/tools/factory_upload_tool/aosp/include/WidevineProvisioner.h diff --git a/libwvdrmengine/tools/factory_upload_tool/src/BccParser.cpp b/libwvdrmengine/tools/factory_upload_tool/aosp/src/BccParser.cpp similarity index 100% rename from libwvdrmengine/tools/factory_upload_tool/src/BccParser.cpp rename to libwvdrmengine/tools/factory_upload_tool/aosp/src/BccParser.cpp diff --git a/libwvdrmengine/tools/factory_upload_tool/src/WidevineProvisioner.cpp b/libwvdrmengine/tools/factory_upload_tool/aosp/src/WidevineProvisioner.cpp similarity index 100% rename from libwvdrmengine/tools/factory_upload_tool/src/WidevineProvisioner.cpp rename to libwvdrmengine/tools/factory_upload_tool/aosp/src/WidevineProvisioner.cpp diff --git a/libwvdrmengine/tools/factory_upload_tool/src/log.cpp b/libwvdrmengine/tools/factory_upload_tool/aosp/src/log.cpp similarity index 100% rename from libwvdrmengine/tools/factory_upload_tool/src/log.cpp rename to libwvdrmengine/tools/factory_upload_tool/aosp/src/log.cpp diff --git a/libwvdrmengine/tools/factory_upload_tool/src/properties_android.cpp b/libwvdrmengine/tools/factory_upload_tool/aosp/src/properties_android.cpp similarity index 100% rename from libwvdrmengine/tools/factory_upload_tool/src/properties_android.cpp rename to libwvdrmengine/tools/factory_upload_tool/aosp/src/properties_android.cpp diff --git a/libwvdrmengine/tools/factory_upload_tool/include/WidevineOemcryptoInterface.h b/libwvdrmengine/tools/factory_upload_tool/common/include/WidevineOemcryptoInterface.h similarity index 100% rename from libwvdrmengine/tools/factory_upload_tool/include/WidevineOemcryptoInterface.h rename to libwvdrmengine/tools/factory_upload_tool/common/include/WidevineOemcryptoInterface.h diff --git a/libwvdrmengine/tools/factory_upload_tool/include/properties.h b/libwvdrmengine/tools/factory_upload_tool/common/include/properties.h similarity index 100% rename from libwvdrmengine/tools/factory_upload_tool/include/properties.h rename to libwvdrmengine/tools/factory_upload_tool/common/include/properties.h diff --git a/libwvdrmengine/tools/factory_upload_tool/src/WidevineOemcryptoInterface.cpp b/libwvdrmengine/tools/factory_upload_tool/common/src/WidevineOemcryptoInterface.cpp similarity index 100% rename from libwvdrmengine/tools/factory_upload_tool/src/WidevineOemcryptoInterface.cpp rename to libwvdrmengine/tools/factory_upload_tool/common/src/WidevineOemcryptoInterface.cpp diff --git a/libwvdrmengine/tools/factory_upload_tool/common/wv_upload_tool.py b/libwvdrmengine/tools/factory_upload_tool/common/wv_upload_tool.py new file mode 100644 index 00000000..e1d34b66 --- /dev/null +++ b/libwvdrmengine/tools/factory_upload_tool/common/wv_upload_tool.py @@ -0,0 +1,513 @@ +#!/usr/bin/env python3 +# Copyright 2022 Google LLC. All rights reserved. +"""Uploader tool for sending device keys to Widevine remote provisioning server. + +This tool consumes an input file containing device info, which includes the +device's public key, and batch uploads it to Google. Once uploaded, the device +may use Widevine provisioning 4 to request OEM certificates from Google. + +This tool is designed to be used with the widevine factory extraction tool. +Therefore, the JSON output from widevine factory extraction tool is the expected +input, with one JSON string per line of input. +""" + +import argparse +from http import HTTPStatus +import http.server +import json +import os +import sys +import urllib.parse +import urllib.request +import uuid +import webbrowser +from google.auth.transport import requests +from google.oauth2 import service_account + +DEFAULT_BASE = 'https://widevine.googleapis.com/v1beta1' +UPLOAD_PATH = '/uniqueDeviceInfo:batchUpload' +BATCH_CHECK_PATH = '/uniqueDeviceInfo:batchCheck' +TOKEN_CACHE_FILE = os.path.join( + os.path.expanduser('~'), '.device_info_uploader.token' +) + +OAUTH_SERVICE_BASE = 'https://accounts.google.com/o/oauth2' +OAUTH_AUTHN_URL = OAUTH_SERVICE_BASE + '/auth' +OAUTH_TOKEN_URL = OAUTH_SERVICE_BASE + '/token' + +OAUTH_SCOPES = ['https://www.googleapis.com/auth/widevine/frontend'] + + +class OAuthHTTPRequestHandler(http.server.BaseHTTPRequestHandler): + """HTTP Handler used to accept the oauth response when the user logs in.""" + + def do_GET(self): # pylint: disable=invalid-name + """Handles GET, extracting the authorization code in the query params.""" + print(f'GET path: {self.path}') + parsed_path = urllib.parse.urlparse(self.path) + params = dict(urllib.parse.parse_qsl(parsed_path.query)) + if 'error' in params: + error = params['error'] + self.respond( + 400, error, f'Error received from the OAuth server: {error}.' + ) + sys.exit(-1) + elif 'code' not in params: + self.respond( + 400, + 'ERROR', + ( + 'Response from OAuth server is missing the authorization ' + f'code. Full response: "{self.path}"' + ), + ) + sys.exit(-1) + else: + self.respond( + 200, 'Success!', 'Success! You may close this browser window.' + ) + self.server.code = params['code'] + + def do_POST(self): # pylint: disable=invalid-name + print(f'POST path: {self.path}') + + def respond(self, code, title, message): + """Send a response to the HTTP client. + + Args: + code: The HTTP status code to send + title: The page title to display + message: The message to display to the user on the page + """ + if code != 200: + eprint(message) + self.send_response(code) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write( + ( + '' + f' {title}' + ' ' + f'

{message}

' + ' ' + '' + ).encode('utf-8') + ) + + +class LocalOAuthReceiver(http.server.HTTPServer): + """HTTP server that will wait for an OAuth authorization code.""" + + def __init__(self): + super(LocalOAuthReceiver, self).__init__( + ('127.0.0.1', 0), OAuthHTTPRequestHandler + ) + self.code = None + + def port(self): + return self.socket.getsockname()[1] + + def wait_for_code(self): + print('Waiting for a response from the Google OAuth service.') + print('If you receive an error in your browser, interrupt this script.') + self.handle_request() + return self.code + + +def eprint(message): + print(message, file=sys.stderr) + + +def die(message): + eprint(message) + sys.exit(-1) + + +def parse_args(): + """Parse and return the command line args. + + Returns: + An argparse.Namespace object populated with the arguments. + """ + parser = argparse.ArgumentParser( + description='Widevine BCC Batch Upload/Check Tool' + ) + + parser.add_argument( + '--version', action='version', version='20240822' + ) # yyyymmdd + + parser.add_argument( + '--json-csr', + nargs='+', + required=True, + help='list of files containing JSON output from factory extraction tool', + ) + parser.add_argument('--credentials', help='JSON credentials file') + + parser.add_argument( + '--endpoint', default=DEFAULT_BASE, help='destination server URL' + ) + + parser.add_argument('--org-name', required=True, help='orgnization name') + + parser.add_argument( + '--cache-token', + action='store_true', + help='Use a locally cached a refresh token', + ) + + parser.add_argument( + '--service-credentials', help='JSON credentials file for service account' + ) + + parser.add_argument( + '--die-on-error', + action='store_true', + help='exit on error and stop uploading more CSRs', + ) + + parser.add_argument( + '--dryrun', + action='store_true', + help=( + 'Do not upload anything. Instead print out what actions would have' + ' been taken if the --dryrun flag had not been specified.' + ), + ) + + parser.add_argument( + '--check', + action='store_true', + required=False, + help='Perform a batch check on the CSRs.', + ) + + parser.add_argument( + '--verbose', + action='store_true', + required=False, + help='Print request and response details.', + ) + + return parser.parse_args() + + +def parse_json_csrs(filename, batches): + """Parse the given file and insert it into batches. + + If the input is not a valid JSON CSR blob, exit the program. + + Args: + filename: The file that contains a JSON-formatted build and CSR + batches: Output dict containing a mapping from json dumped device metadata + to BCCs. + """ + base_filename = os.path.basename(filename) + line_count = 0 + for line in open(filename): + line_count = line_count + 1 + obj = {} + try: + obj = json.loads(line) + except json.JSONDecodeError as e: + die(f'{e.msg} {filename}:{line_count}, char {e.pos}') + + try: + bcc = { + 'boot_certificate_chain': obj['bcc'], + 'name': f'{base_filename}#{line_count}', + } + device_metadata = json.dumps({ + 'company': obj['company'], + 'architecture': obj['architecture'], + 'name': obj['name'], + 'model': obj['model'], + 'product': obj['product'], + 'build_info': obj['build_info'], + 'oemcrypto_build_info': obj['oemcrypto_build_info'], + }) + if device_metadata not in batches: + batches[device_metadata] = [] + batches[device_metadata].append(bcc) + except KeyError as e: + die(f'Invalid object at {filename}:{line_count}, missing {e}') + + if line_count == 0: + die('Empty BCC file!') + + +def format_request_body(args, device_metadata, bccs): + """Generate a formatted request buffer for the given build and CSRs.""" + request = { + 'parent': 'orgs/' + args.org_name, + 'request_id': uuid.uuid4().hex, + 'metadata': json.loads(device_metadata), + 'device_info': bccs, + } + return json.dumps(request).encode('utf-8') + + +def format_check_request_body(args, bccs): + """Generate a formatted BatchCheck request buffer for the given build and CSRs.""" + request = { + 'parent': 'orgs/' + args.org_name, + 'request_id': uuid.uuid4().hex, + 'device_info': bccs, + } + + return json.dumps(request).encode('utf-8') + + +def load_refresh_token(): + if not os.path.exists(TOKEN_CACHE_FILE): + return None + with open(TOKEN_CACHE_FILE) as f: + return f.readline() + + +def store_refresh_token(refresh_token): + with open(TOKEN_CACHE_FILE, 'w') as f: + f.write(refresh_token) + + +def fetch_access_token(creds, cache_token=False, code=None, redirect_uri=None): + """Fetch an oauth2 access token. + + If a code is passed, then it is used to get the token. If code + is None, then look for a persisted refresh token and use that to + get the access token instead. + + Args: + creds: The OAuth client credentials, including client secret and id. + cache_token: If True, then the refresh token is cached on disk so that the + user does not have to reauthenticate when the script is used again. + code: The OAuth authorization code, returned by Google's OAuth service. + redirect_uri: If an authorization code is supplied, then the redirect_uri + used to fetch the code must be passed here. + + Returns: + A base64-encode OAuth access token, suitable for including in a request. + """ + request = urllib.request.Request(OAUTH_TOKEN_URL) + request.add_header('Content-Type', 'application/x-www-form-urlencoded') + body = 'client_id=' + creds['client_id'] + body += '&client_secret=' + creds['client_secret'] + + if code is not None: + if redirect_uri is None: + raise ValueError('"code" was supplied, but "redirect_uri" is None') + body += '&grant_type=authorization_code' + body += '&code=' + code + body += '&redirect_uri=' + redirect_uri + else: + refresh_token = load_refresh_token() + if refresh_token is None: + return None + body += '&grant_type=refresh_token' + body += '&refresh_token=' + refresh_token + + try: + response = urllib.request.urlopen(request, body.encode('utf-8')) + parsed_response = json.load(response) + if cache_token: + store_refresh_token(parsed_response['refresh_token']) + return parsed_response['access_token'] + except urllib.error.HTTPError as e: + # Catch bogus/expired refresh tokens, but bubble up errors when + # an authorization code is used. + if code is None: + return None + die(f'Failed to receive access token: {e.code} {e.reason}') + + +def load_and_validate_creds(credfile): + """Loads the credentials from the given file and validates them. + + Args: + credfile: the name of the file containing the client credentials + + Returns: + A map containing the credentials for connecting to the APE backend. + """ + credmap = json.load(open(credfile)) + + not_local_app_creds_error = ( + 'ERROR: Invalid credential file.\n' + ' The given credentials do not appear to be for a locally installed\n' + ' application. Please navigate to the credentials dashboard and\n' + ' ensure that the "Type" of your client is "Desktop":\n' + ' https://console.cloud.google.com/apis/credentials' + ) + + if 'installed' not in credmap: + die(not_local_app_creds_error) + + creds = credmap['installed'] + + expected_keys = set(['client_id', 'client_secret', 'redirect_uris']) + if not expected_keys.issubset(creds.keys()): + die(( + 'ERROR: Invalid credential file.\n' + ' The given credentials do not appear to be valid. Please\n' + ' re-download the client credentials file from the dashboard:\n' + ' https://console.cloud.google.com/apis/credentials' + )) + + if 'http://localhost' not in creds['redirect_uris']: + die(not_local_app_creds_error) + + return creds + + +def authenticate_and_fetch_token(args): + """Authenticate and fetch an OAUTH2 access token.""" + # Auth for service account + if args.service_credentials: + if not os.path.exists(args.service_credentials): + die('Service account credentials file does not exist.') + svc_account = service_account.Credentials.from_service_account_file( + args.service_credentials, + scopes=OAUTH_SCOPES, + ) + svc_account.refresh(requests.Request()) + return svc_account.token + + # Auth for user account + if args.credentials is None: + die('User credentials is not provided.') + if not os.path.exists(args.credentials): + die('User credentials file does not exist.') + creds = load_and_validate_creds(args.credentials) + + access_type = 'online' + if args.cache_token: + token = fetch_access_token(creds) + if token is not None: + return token + access_type = 'offline' + + httpd = LocalOAuthReceiver() + redirect_uri = f'http://127.0.0.1:{httpd.port()}' + url = ( + OAUTH_AUTHN_URL + + '?response_type=code' + + '&client_id=' + + creds['client_id'] + + '&redirect_uri=' + + redirect_uri + + '&scope=https://www.googleapis.com/auth/widevine/frontend' + + '&access_type=' + + access_type + + '&prompt=select_account' + ) + print('Opening your web browser to authenticate...') + if not webbrowser.open(url, new=1, autoraise=True): + print('Error opening the browser. Please open this link in a browser') + print(f'that is running on this same system:\n {url}\n') + code = httpd.wait_for_code() + return fetch_access_token(creds, args.cache_token, code, redirect_uri) + + +def upload_batch(args, device_metadata, bccs): + """Batch upload all the CSRs associated build device_metadata. + + Args: + args: The parsed command-line arguments + device_metadata: The build for which we're uploading CSRs + bccs: a list of BCCs to be uploaded for the given build + """ + print('Uploading {} bcc(s)'.format(len(bccs))) + if args.verbose: + print("Build: '{}'".format(device_metadata)) + body = format_request_body(args, device_metadata, bccs) + return batch_action_single_attempt(args, UPLOAD_PATH, body) + + +def check_batch(args, device_metadata, bccs): + """Batch check all the CSRs. + + Args: + args: The parsed command-line arguments + device_metadata: The build for which we're checking CSRs + bccs: a list of BCCs to be checked for the given build + """ + print('Checking {} bcc(s)'.format(len(bccs))) + if args.verbose: + print("Build: '{}'".format(device_metadata)) + body = format_check_request_body(args, bccs) + return batch_action_single_attempt(args, BATCH_CHECK_PATH, body) + + +def batch_action_single_attempt(args, path, body): + """Batch action (upload or check existence) for all the CSRs in chunks. + + Args: + args: The parsed command-line arguments + csrs: a list of CSRs to be uploaded/checked for the given build + path: The endpoint url for the specific action + body: The formatted request body + """ + if args.verbose: + print('Request body:') + print(body) + print('Request target:') + print(args.endpoint + path) + request = urllib.request.Request(args.endpoint + path) + request.add_header('Content-Type', 'application/json') + request.add_header('X-GFE-SSL', 'yes') + request.add_header( + 'Authorization', 'Bearer ' + authenticate_and_fetch_token(args) + ) + if args.dryrun: + print('dry run: would have reached to ' + request.full_url) + return HTTPStatus.OK + + try: + response = urllib.request.urlopen(request, body) + except urllib.error.HTTPError as e: + eprint(f'Error uploading bccs. {e}') + for line in e: + eprint(line.decode('utf-8').rstrip()) + sys.exit(1) + + response_body = response.read().decode('utf-8') + if args.verbose: + print('Response body:') + print(response_body) + res = json.loads(response_body) + if 'failedDeviceInfo' in res: + eprint('Failed to upload/check some device info! Response body:') + eprint(response_body) + eprint('Failed: {} bcc(s)'.format(len(res['failedDeviceInfo']))) + if args.die_on_error: + sys.exit(1) + elif 'successDeviceInfo' in res: + print('Success: {} bcc(s)'.format(len(res['successDeviceInfo']))) + else: + eprint('Failed with unexpected response:') + eprint(response_body) + + +def main(): + args = parse_args() + if args.dryrun: + print('Dry run mode enabled. Service APIs will not be called.') + + batches = {} + for filename in args.json_csr: + parse_json_csrs(filename, batches) + + if len(batches) > 1: + print('WARNING: {} different device metadata'.format(len(batches))) + + for device_metadata, bccs in batches.items(): + if args.check: + check_batch(args, device_metadata, bccs) + else: + upload_batch(args, device_metadata, bccs) + + +if __name__ == '__main__': + main() From f6330825a306e2f87b0ec322ff798b17ad4fce5d Mon Sep 17 00:00:00 2001 From: conglin Date: Wed, 11 Jun 2025 21:15:49 +0000 Subject: [PATCH 3/3] Move json util code to common So that it can be shared by CE CDM extraction tool later. Test: m wv_factory_extraction_tool Bug: 414642286 Change-Id: I6dba70227ce2789dd3686ebbf6ed3a0dbf68dc00 --- .../tools/factory_upload_tool/Android.bp | 1 + .../aosp/src/WidevineProvisioner.cpp | 46 +------------- .../common/include/json_utils.h | 19 ++++++ .../common/src/json_utils.cpp | 63 +++++++++++++++++++ 4 files changed, 84 insertions(+), 45 deletions(-) create mode 100644 libwvdrmengine/tools/factory_upload_tool/common/include/json_utils.h create mode 100644 libwvdrmengine/tools/factory_upload_tool/common/src/json_utils.cpp diff --git a/libwvdrmengine/tools/factory_upload_tool/Android.bp b/libwvdrmengine/tools/factory_upload_tool/Android.bp index ce8adbfa..1bc86162 100644 --- a/libwvdrmengine/tools/factory_upload_tool/Android.bp +++ b/libwvdrmengine/tools/factory_upload_tool/Android.bp @@ -35,6 +35,7 @@ cc_binary { "aosp/src/properties_android.cpp", "aosp/src/BccParser.cpp", "aosp/src/WidevineProvisioner.cpp", + "common/src/json_utils.cpp", "common/src/WidevineOemcryptoInterface.cpp", ], include_dirs: [ diff --git a/libwvdrmengine/tools/factory_upload_tool/aosp/src/WidevineProvisioner.cpp b/libwvdrmengine/tools/factory_upload_tool/aosp/src/WidevineProvisioner.cpp index b0181990..f95be63d 100644 --- a/libwvdrmengine/tools/factory_upload_tool/aosp/src/WidevineProvisioner.cpp +++ b/libwvdrmengine/tools/factory_upload_tool/aosp/src/WidevineProvisioner.cpp @@ -20,6 +20,7 @@ #include #include "WidevineOemcryptoInterface.h" +#include "json_utils.h" #include "log.h" #include "properties.h" #include "string_conversions.h" @@ -32,51 +33,6 @@ const std::vector> kAuthorizedEekRoots = { 0x62, 0xDC, 0x3E, 0x61, 0xAB, 0x57, 0x48, 0x7D, 0x75, 0x37, 0x29, 0xAD, 0x76, 0x80, 0x32, 0xD2, 0xB3, 0xCB, 0x63, 0x58, 0xD9}, }; - -std::string EscapeJson(const std::string& input) { - std::string result; - for (std::string::const_iterator c = input.begin(); c != input.end(); ++c) { - switch (*c) { - case '\"': - result += "\\\""; - break; - case '\\': - result += "\\\\"; - break; - case '\b': - result += "\\b"; - break; - case '\f': - result += "\\f"; - break; - case '\n': - result += "\\n"; - break; - case '\r': - result += "\\r"; - break; - case '\t': - result += "\\t"; - break; - default: - result += *c; - break; - } - } - return result; -} - -std::string StringMapToJson( - const std::map& string_map) { - std::string json = "{"; - for (const auto& value_pair : string_map) { - json.append("\"" + value_pair.first + "\": " + "\"" + value_pair.second + - "\","); - } - json.resize(json.size() - 1); // Remove the last comma. - json.append("}"); - return json; -} } // namespace WidevineProvisioner::WidevineProvisioner() { diff --git a/libwvdrmengine/tools/factory_upload_tool/common/include/json_utils.h b/libwvdrmengine/tools/factory_upload_tool/common/include/json_utils.h new file mode 100644 index 00000000..1a473e07 --- /dev/null +++ b/libwvdrmengine/tools/factory_upload_tool/common/include/json_utils.h @@ -0,0 +1,19 @@ +// Copyright 2025 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine License +// Agreement. + +#ifndef WIDEVINE_JSON_UTILS_H_ +#define WIDEVINE_JSON_UTILS_H_ + +#include +#include + +namespace widevine { + +std::string EscapeJson(const std::string& input); +std::string StringMapToJson( + const std::map& string_map); + +} // namespace widevine + +#endif // WIDEVINE_JSON_UTILS_H_ diff --git a/libwvdrmengine/tools/factory_upload_tool/common/src/json_utils.cpp b/libwvdrmengine/tools/factory_upload_tool/common/src/json_utils.cpp new file mode 100644 index 00000000..7cb8a669 --- /dev/null +++ b/libwvdrmengine/tools/factory_upload_tool/common/src/json_utils.cpp @@ -0,0 +1,63 @@ +// Copyright 2025 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine License +// Agreement. + +#include "json_utils.h" + +#include + +namespace widevine { + +std::string EscapeJson(const std::string& input) { + std::string result; + for (const char& c : input) { + switch (c) { + case '\"': + result += "\\\""; + break; + case '\\': + result += "\\\\"; + break; + case '\b': + result += "\\b"; + break; + case '\f': + result += "\\f"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + default: + result += c; + break; + } + } + return result; +} + +std::string StringMapToJson( + const std::map& string_map) { + if (string_map.empty()) { + return "{}"; + } + std::ostringstream json_stream; + json_stream << "{"; + bool is_first_element = true; + for (const auto& pair : string_map) { + if (!is_first_element) { + json_stream << ","; + } + json_stream << "\"" << pair.first << "\": \"" << pair.second << "\""; + is_first_element = false; + } + json_stream << "}"; + return json_stream.str(); +} + +} // namespace widevine