diff --git a/libwvdrmengine/tools/factory_upload_tool/Android.bp b/libwvdrmengine/tools/factory_upload_tool/Android.bp index f5db00dd..1bc86162 100644 --- a/libwvdrmengine/tools/factory_upload_tool/Android.bp +++ b/libwvdrmengine/tools/factory_upload_tool/Android.bp @@ -26,20 +26,25 @@ cc_binary { "liblog", "libutils", ], + static_libs: [ + "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/json_utils.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 72% rename from libwvdrmengine/tools/factory_upload_tool/cli.cpp rename to libwvdrmengine/tools/factory_upload_tool/aosp/cli.cpp index 3648d59c..166c276b 100644 --- a/libwvdrmengine/tools/factory_upload_tool/cli.cpp +++ b/libwvdrmengine/tools/factory_upload_tool/aosp/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/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 90% rename from libwvdrmengine/tools/factory_upload_tool/include/WidevineProvisioner.h rename to libwvdrmengine/tools/factory_upload_tool/aosp/include/WidevineProvisioner.h index d3e81d9d..7f1db2ad 100644 --- a/libwvdrmengine/tools/factory_upload_tool/include/WidevineProvisioner.h +++ b/libwvdrmengine/tools/factory_upload_tool/aosp/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/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 89% rename from libwvdrmengine/tools/factory_upload_tool/src/WidevineProvisioner.cpp rename to libwvdrmengine/tools/factory_upload_tool/aosp/src/WidevineProvisioner.cpp index 6f9d1f00..f95be63d 100644 --- a/libwvdrmengine/tools/factory_upload_tool/src/WidevineProvisioner.cpp +++ b/libwvdrmengine/tools/factory_upload_tool/aosp/src/WidevineProvisioner.cpp @@ -20,8 +20,10 @@ #include #include "WidevineOemcryptoInterface.h" +#include "json_utils.h" #include "log.h" #include "properties.h" +#include "string_conversions.h" namespace widevine { @@ -107,7 +109,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 +393,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)) { 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/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/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 85% rename from libwvdrmengine/tools/factory_upload_tool/src/WidevineOemcryptoInterface.cpp rename to libwvdrmengine/tools/factory_upload_tool/common/src/WidevineOemcryptoInterface.cpp index ccb105a3..8fc2b87c 100644 --- a/libwvdrmengine/tools/factory_upload_tool/src/WidevineOemcryptoInterface.cpp +++ b/libwvdrmengine/tools/factory_upload_tool/common/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/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 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() diff --git a/libwvdrmengine/version.txt b/libwvdrmengine/version.txt index b531b653..0cb3515c 100644 --- a/libwvdrmengine/version.txt +++ b/libwvdrmengine/version.txt @@ -1 +1 @@ -AV1A.250514.001 +AV1A.250618.001