1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4# Copyright (c) 2021 Huawei Device Co., Ltd. 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16""" 17Description : Defining constants, common interface 18""" 19import argparse 20import json 21import os 22import shutil 23import tempfile 24import zipfile 25from collections import OrderedDict 26from copy import copy 27from ctypes import cdll 28from cryptography.hazmat.primitives import hashes 29from log_exception import UPDATE_LOGGER 30import xmltodict 31from build_pkcs7 import sign_ota_package 32 33operation_path = os.path.dirname(os.path.realpath(__file__)) 34PRODUCT = 'hi3516' 35BUILD_TOOLS_FILE_NAME = 'build_tools.zip' 36UPDATE_BIN_FILE_NAME = "update.bin" 37UPDATE_EXE_FILE_NAME = "updater_binary" 38 39SCRIPT_KEY_LIST = ['prelude', 'verse', 'refrain', 'ending'] 40TOTAL_SCRIPT_FILE_NAME = "loadScript.us" 41REGISTER_SCRIPT_FILE_NAME = "registerCmd.us" 42SCRIPT_FILE_NAME = '-script.us' 43 44UPDATER_CONFIG = "updater_config" 45XML_FILE_PATH = "updater_specified_config.xml" 46SO_PATH = os.path.join(operation_path, 'lib/libpackage.so') 47SO_PATH_L1 = os.path.join(operation_path, 'lib/libpackageL1.so') 48DIFF_EXE_PATH = os.path.join(operation_path, 'lib/diff') 49E2FSDROID_PATH = os.path.join(operation_path, 'lib/e2fsdroid') 50MISC_INFO_PATH = "misc_info.txt" 51VERSION_MBN_PATH = "VERSION.mbn" 52BOARD_LIST_PATH = "BOARD.list" 53EXTEND_COMPONENT_LIST = ["version_list", "board_list"] 54EXTEND_OPTIONAL_COMPONENT_LIST = ["partitions_file"] 55PARTITION_FILE = "partitions_file" 56IGNORED_PARTITION_LIST = ['fastboot', 'boot', 'kernel', 'misc', 57 'updater', 'userdata'] 58 59HASH_ALGORITHM_DICT = {'sha256': hashes.SHA256, 'sha384': hashes.SHA384} 60LINUX_HASH_ALGORITHM_DICT = {'sha256': 'sha256sum', 'sha384': 'sha384sum'} 61HASH_CONTENT_LEN_DICT = {'sha256': 64, 'sha384': 96} 62 63COMPONENT_INFO_INNIT = ['', '000', '00', '0', '0o00'] 64 65ON_SERVER = "ON_SERVER" 66 67EXTEND_VALUE = 512 68 69FILE_MAP_ZERO_KEY = "__ZERO" 70FILE_MAP_NONZERO_KEY = "__NONZERO" 71FILE_MAP_COPY_KEY = "__COPY" 72 73MAX_BLOCKS_PER_GROUP = BLOCK_LIMIT = 1024 74PER_BLOCK_SIZE = 4096 75 76VERSE_SCRIPT_EVENT = 0 77INC_IMAGE_EVENT = 1 78SIGN_PACKAGE_EVENT = 2 79GENERATE_SIGNED_DATA_EVENT = 6 # sign build tools files to get hash_signed_data 80 81# 1000000: max number of function recursion depth 82MAXIMUM_RECURSION_DEPTH = 1000000 83 84 85def singleton(cls): 86 _instance = {} 87 88 def _singleton(*args, **kargs): 89 if cls not in _instance: 90 _instance[cls] = cls(*args, **kargs) 91 return _instance[cls] 92 93 return _singleton 94 95 96class ExtInit: 97 """ 98 Init event for ext 99 """ 100 101 def __init__(self): 102 self.funs = [] 103 104 def reg_event(self, evevt_id, funs): 105 self.funs.append([evevt_id, funs]) 106 UPDATE_LOGGER.print_log( 107 'register event %s: %s' % (evevt_id, funs.__name__)) 108 109 def invoke_event(self, evevt_id): 110 UPDATE_LOGGER.print_log(self.funs) 111 for event in self.funs: 112 funs = event[1] 113 if event[0] == evevt_id and funs is not None: 114 UPDATE_LOGGER.print_log( 115 'invoke event %s: %s' % (evevt_id, funs.__name__)) 116 return funs 117 return False 118 119 120@singleton 121class OptionsManager: 122 """ 123 Options management class 124 """ 125 126 def __init__(self): 127 self.init = ExtInit() 128 self.parser = argparse.ArgumentParser() 129 130 # Own parameters 131 self.product = None 132 133 # Entry parameters 134 self.source_package = None 135 self.target_package = None 136 self.update_package = None 137 self.unpack_package_path = None 138 self.no_zip = False 139 self.partition_file = None 140 self.signing_algorithm = None 141 self.hash_algorithm = None 142 self.private_key = None 143 self.not_l2 = False 144 self.signing_length = 256 145 self.xml_path = None 146 self.sd_card = False 147 148 self.make_dir_path = None 149 150 # Parsed package parameters 151 self.target_package_dir = None 152 self.target_package_config_dir = None 153 self.target_package_temp_obj = None 154 self.misc_info_dict = {} 155 self.version_mbn_file_path = None 156 self.version_mbn_content = None 157 self.board_list_file_path = None 158 self.board_list_content = None 159 160 self.source_package_dir = None 161 self.source_package_temp_obj = None 162 163 # XML parsing parameters 164 self.head_info_list = [] 165 self.component_info_dict = {} 166 self.full_img_list = [] 167 self.full_img_name_list = [] 168 self.incremental_img_list = [] 169 self.incremental_img_name_list = [] 170 self.target_package_version = None 171 self.source_package_version = None 172 self.full_image_path_list = [] 173 174 self.partition_file_obj = None 175 176 # Full processing parameters 177 self.full_image_content_len_list = [] 178 self.full_image_file_obj_list = [] 179 180 # Incremental processing parameters 181 self.incremental_content_len_list = [] 182 self.incremental_image_file_obj_dict = {} 183 self.incremental_block_file_obj_dict = {} 184 self.incremental_temp_file_obj_list = [] 185 self.max_stash_size = 0 186 self.src_image = None 187 self.tgt_image = None 188 189 # Script parameters 190 self.opera_script_file_name_dict = {} 191 for each in SCRIPT_KEY_LIST: 192 self.opera_script_file_name_dict[each] = [] 193 self.total_script_file_obj = None 194 195 self.register_script_file_obj = None 196 197 # Update package parameters 198 self.update_bin_obj = None 199 self.build_tools_zip_obj = None 200 self.update_package_file_path = None 201 self.signed_package = None 202 203 204OPTIONS_MANAGER = OptionsManager() 205 206 207def unzip_package(package_path, origin='target'): 208 """ 209 Decompress the zip package. 210 :param package_path: zip package path 211 :param origin: package origin, which indicates 212 whether the zip package is a source package or target package 213 :return: Temporary directory (tmp_dir) and zip_data package; 214 false if an exception occurs. 215 """ 216 try: 217 tmp_dir_obj = tempfile.TemporaryDirectory(prefix="%sfiles-" % origin) 218 tmp_dir = tmp_dir_obj.name 219 220 zf_obj = zipfile.ZipFile(package_path) 221 for name in zf_obj.namelist(): 222 if name.endswith('/'): 223 os.mkdir(os.path.join(tmp_dir, name)) 224 else: 225 ext_filename = os.path.join( 226 tmp_dir, name) 227 with open(ext_filename, 'wb') as f_w: 228 f_w.write(zf_obj.read(name)) 229 except OSError: 230 UPDATE_LOGGER.print_log( 231 "Unzip package failed! path: %s" % package_path, 232 log_type=UPDATE_LOGGER.ERROR_LOG) 233 return False, False 234 tmp_dir_list = os.listdir(tmp_dir) 235 if len(tmp_dir_list) == 1: 236 unzip_dir = os.path.join(tmp_dir, tmp_dir_list[0]) 237 if UPDATER_CONFIG not in \ 238 os.listdir(unzip_dir): 239 UPDATE_LOGGER.print_log( 240 'Unsupported zip package structure!', UPDATE_LOGGER.ERROR_LOG) 241 return False, False 242 elif UPDATER_CONFIG in tmp_dir_list: 243 unzip_dir = tmp_dir 244 else: 245 UPDATE_LOGGER.print_log( 246 'Unsupported zip package structure!', UPDATE_LOGGER.ERROR_LOG) 247 return False, False 248 UPDATE_LOGGER.print_log( 249 '%s package unzip complete! path: %s' % (origin.title(), unzip_dir)) 250 251 return tmp_dir_obj, unzip_dir 252 253 254def split_img_name(image_path): 255 """ 256 Split the image name by image path 257 :return image name 258 """ 259 tmp_path = image_path 260 str_list = tmp_path.split("/") 261 262 return str_list[-1] 263 264 265def get_update_config_softversion(mbn_dir, head_info_dict): 266 soft_version_file = head_info_dict.get('softVersionFile') 267 if soft_version_file is not None: 268 mbn_path = os.path.join(mbn_dir, soft_version_file) 269 if os.path.exists(mbn_path): 270 with open(mbn_path, 'r') as mbn_file: 271 head_info_dict['info']["@softVersion"] = mbn_file.read() 272 273 274def parse_update_config(xml_path): 275 """ 276 Parse the XML configuration file. 277 :param xml_path: XML configuration file path 278 :return head_info_dict: header information dict of the update package 279 component_info_dict: component information dict 280 full_img_list: full image list 281 incremental_img_list: incremental image list 282 """ 283 if os.path.exists(xml_path): 284 with open(xml_path, 'r') as xml_file: 285 xml_str = xml_file.read() 286 else: 287 UPDATE_LOGGER.print_log("XML file does not exist! xml path: %s" % 288 xml_path, UPDATE_LOGGER.ERROR_LOG) 289 ret_params = [False, False, False, False, False, False, False] 290 return ret_params 291 xml_content_dict = xmltodict.parse(xml_str, encoding='utf-8') 292 package_dict = xml_content_dict.get('package', {}) 293 get_update_config_softversion(OPTIONS_MANAGER.target_package_dir, package_dict.get('head', {})) 294 head_dict = package_dict.get('head', {}).get('info') 295 package_version = head_dict.get("@softVersion") 296 # component 297 component_info = package_dict.get('group', {}).get('component') 298 head_list = list(head_dict.values()) 299 head_list.pop() 300 whole_list = [] 301 difference_list = [] 302 component_dict = {} 303 full_image_path_list = [] 304 305 if not OPTIONS_MANAGER.not_l2: 306 expand_component(component_dict) 307 if isinstance(component_info, OrderedDict) or isinstance(component_info, dict): 308 component_info = [component_info] 309 if component_info is None: 310 ret_params = [[], {}, [], [], '', [], False] 311 return ret_params 312 for component in component_info: 313 component_list = list(component.values()) 314 component_list.pop() 315 316 if component['@compAddr'] in (whole_list + difference_list): 317 UPDATE_LOGGER.print_log("This component %s repeats!" % 318 component['@compAddr'], 319 UPDATE_LOGGER.ERROR_LOG) 320 ret_params = [False, False, False, False, False, False, False] 321 return ret_params 322 323 if component['@compType'] == '0': 324 whole_list.append(component['@compAddr']) 325 OPTIONS_MANAGER.full_img_name_list.\ 326 append(split_img_name(component['#text'])) 327 tem_path = os.path.join(OPTIONS_MANAGER.target_package_dir, 328 component.get("#text", None)) 329 full_image_path_list.append(tem_path) 330 component_dict[component['@compAddr']] = component_list 331 elif component['@compType'] == '1': 332 difference_list.append(component['@compAddr']) 333 OPTIONS_MANAGER.incremental_img_name_list.\ 334 append(split_img_name(component['#text'])) 335 336 UPDATE_LOGGER.print_log('XML file parsing completed!') 337 ret_params = [head_list, component_dict, 338 whole_list, difference_list, package_version, 339 full_image_path_list] 340 return ret_params 341 342 343def partitions_conversion(data): 344 """ 345 Convert the start or length data in the partition table through 346 multiply 1024 * 1024 and return the data. 347 :param data: start or length data 348 :return : 349 """ 350 if data == '0': 351 return 0 352 elif data.endswith('M'): 353 return int(data[0:-1]) * 1024 * 1024 // 512 354 else: 355 return False 356 357 358def parse_partition_file_xml(xml_path): 359 """ 360 Parse the XML configuration file. 361 :param xml_path: XML configuration file path 362 :return part_json: partition table information in JSON format 363 """ 364 if os.path.exists(xml_path): 365 with open(xml_path, 'r') as xml_file: 366 xml_str = xml_file.read() 367 else: 368 UPDATE_LOGGER.print_log("XML file does not exist! xml path: %s" % 369 xml_path, UPDATE_LOGGER.ERROR_LOG) 370 return False, False, False 371 partitions_list = [] 372 partitions_file_path_list = [] 373 xml_content_dict = xmltodict.parse(xml_str, encoding='utf-8') 374 part_list = xml_content_dict['Partition_Info']['Part'] 375 new_part_list = [] 376 for i, part in enumerate(part_list): 377 start_value = partitions_conversion(part.get('@Start')) 378 length_value = partitions_conversion(part.get('@Length')) 379 if start_value is False or length_value is False: 380 UPDATE_LOGGER.print_log( 381 "Partition file parsing failed! part_name: %s, xml_path: %s" % 382 (part.get('@PartitionName'), xml_path), 383 UPDATE_LOGGER.ERROR_LOG) 384 return False, False, False 385 386 if part.get('@PartitionName') not in IGNORED_PARTITION_LIST: 387 partitions_list.append(part.get('@PartitionName')) 388 partitions_file_path_list.append( 389 os.path.join(OPTIONS_MANAGER.target_package_dir, 390 "%s.img" % part.get('@PartitionName'))) 391 part_dict = {'start': start_value, 392 'length': length_value, 393 'partName': part.get('@PartitionName'), 394 'fsType': part.get('@FlashType')} 395 new_part_list.append(part_dict) 396 part_json = json.dumps(new_part_list) 397 part_json = '{"Partition": %s}' % part_json 398 file_obj = tempfile.NamedTemporaryFile( 399 dir=OPTIONS_MANAGER.target_package_dir, prefix="partition_file-", mode='wb') 400 file_obj.write(part_json.encode()) 401 file_obj.seek(0) 402 return file_obj, partitions_list, partitions_file_path_list 403 404 405def expand_component(component_dict): 406 """ 407 Append components such as VERSION.mbn and board list. 408 :param component_dict: component information dict 409 :return: 410 """ 411 if OPTIONS_MANAGER.partition_file is not None: 412 extend_component_list = \ 413 EXTEND_COMPONENT_LIST + EXTEND_OPTIONAL_COMPONENT_LIST 414 else: 415 extend_component_list = EXTEND_COMPONENT_LIST 416 for each in extend_component_list: 417 tmp_info_list = copy(COMPONENT_INFO_INNIT) 418 tmp_info_list[0] = each 419 component_dict[each] = tmp_info_list 420 421 422def clear_options(): 423 """ 424 Clear OPTIONS_MANAGER. 425 """ 426 OPTIONS_MANAGER.product = None 427 428 # Entry parameters 429 OPTIONS_MANAGER.source_package = None 430 OPTIONS_MANAGER.target_package = None 431 OPTIONS_MANAGER.update_package = None 432 OPTIONS_MANAGER.no_zip = False 433 OPTIONS_MANAGER.partition_file = None 434 OPTIONS_MANAGER.signing_algorithm = None 435 OPTIONS_MANAGER.hash_algorithm = None 436 OPTIONS_MANAGER.private_key = None 437 OPTIONS_MANAGER.not_l2 = False 438 OPTIONS_MANAGER.signing_length = 256 439 OPTIONS_MANAGER.xml_path = None 440 OPTIONS_MANAGER.sd_card = False 441 442 OPTIONS_MANAGER.full_image_path_list = [] 443 444 OPTIONS_MANAGER.make_dir_path = None 445 446 # Parsed package parameters 447 OPTIONS_MANAGER.target_package_dir = None 448 OPTIONS_MANAGER.target_package_config_dir = None 449 OPTIONS_MANAGER.target_package_temp_obj = None 450 OPTIONS_MANAGER.misc_info_dict = {} 451 OPTIONS_MANAGER.version_mbn_file_path = None 452 OPTIONS_MANAGER.version_mbn_content = None 453 OPTIONS_MANAGER.board_list_file_path = None 454 OPTIONS_MANAGER.board_list_content = None 455 456 OPTIONS_MANAGER.source_package_dir = None 457 OPTIONS_MANAGER.source_package_temp_obj = None 458 459 # XML parsing parameters 460 OPTIONS_MANAGER.head_info_list = [] 461 OPTIONS_MANAGER.component_info_dict = {} 462 OPTIONS_MANAGER.full_img_list = [] 463 OPTIONS_MANAGER.incremental_img_list = [] 464 OPTIONS_MANAGER.target_package_version = None 465 OPTIONS_MANAGER.source_package_version = None 466 OPTIONS_MANAGER.partition_file_obj = None 467 468 # Global processing parameters 469 OPTIONS_MANAGER.full_image_content_len_list = [] 470 OPTIONS_MANAGER.full_image_file_obj_list = [] 471 472 # Incremental processing parameters 473 OPTIONS_MANAGER.incremental_content_len_list = [] 474 OPTIONS_MANAGER.incremental_temp_file_obj_list = [] 475 476 # Script parameters 477 OPTIONS_MANAGER.opera_script_file_name_dict = {} 478 for each in SCRIPT_KEY_LIST: 479 OPTIONS_MANAGER.opera_script_file_name_dict[each] = [] 480 OPTIONS_MANAGER.total_script_file_obj = None 481 482 OPTIONS_MANAGER.register_script_file_obj = None 483 484 # Update package parameters 485 OPTIONS_MANAGER.update_bin_obj = None 486 OPTIONS_MANAGER.build_tools_zip_obj = None 487 OPTIONS_MANAGER.update_package_file_path = None 488 489 490def clear_resource(err_clear=False): 491 """ 492 Clear resources, close temporary files, and clear temporary paths. 493 :param err_clear: whether to clear errors 494 :return: 495 """ 496 target_package_temp_obj = OPTIONS_MANAGER.target_package_temp_obj 497 if target_package_temp_obj is not None: 498 target_package_temp_obj.cleanup() 499 source_package_temp_obj = OPTIONS_MANAGER.source_package_temp_obj 500 if source_package_temp_obj is not None: 501 source_package_temp_obj.cleanup() 502 503 partition_file_obj = OPTIONS_MANAGER.partition_file_obj 504 if partition_file_obj is not None: 505 partition_file_obj.close() 506 507 build_tools_zip_obj = OPTIONS_MANAGER.build_tools_zip_obj 508 if build_tools_zip_obj is not None: 509 build_tools_zip_obj.close() 510 update_bin_obj = OPTIONS_MANAGER.update_bin_obj 511 if update_bin_obj is not None: 512 update_bin_obj.close() 513 total_script_file_obj = OPTIONS_MANAGER.total_script_file_obj 514 if total_script_file_obj is not None: 515 total_script_file_obj.close() 516 517 register_script_file_obj = OPTIONS_MANAGER.register_script_file_obj 518 if register_script_file_obj is not None: 519 register_script_file_obj.close() 520 521 full_image_file_obj_list = OPTIONS_MANAGER.full_image_file_obj_list 522 if len(full_image_file_obj_list) != 0: 523 for each_full_obj in full_image_file_obj_list: 524 each_full_obj.close() 525 526 clear_file_obj(err_clear) 527 clear_options() 528 529 530def clear_file_obj(err_clear): 531 """ 532 Clear resources and temporary file objects. 533 :param err_clear: whether to clear errors 534 :return: 535 """ 536 incremental_temp_file_obj_list = \ 537 OPTIONS_MANAGER.incremental_temp_file_obj_list 538 if len(incremental_temp_file_obj_list) != 0: 539 for each_incremental_temp_obj in incremental_temp_file_obj_list: 540 if each_incremental_temp_obj is not None: 541 each_incremental_temp_obj.close() 542 opera_script_file_name_dict = OPTIONS_MANAGER.opera_script_file_name_dict 543 for each_value in opera_script_file_name_dict.values(): 544 for each in each_value: 545 each[1].close() 546 if err_clear: 547 make_dir_path = OPTIONS_MANAGER.make_dir_path 548 if make_dir_path is not None and os.path.exists(make_dir_path): 549 shutil.rmtree(make_dir_path) 550 update_package_file_path = OPTIONS_MANAGER.update_package_file_path 551 if update_package_file_path is not None and \ 552 os.path.exists(update_package_file_path): 553 os.remove(update_package_file_path) 554 UPDATE_LOGGER.print_log( 555 'Exception occurred, Resource cleaning completed!') 556 else: 557 UPDATE_LOGGER.print_log('Resource cleaning completed!') 558 559 560def get_file_content(file_path, file_name=None): 561 """ 562 Read the file content. 563 :param file_path: file path 564 :param file_name: file name 565 :return: file content 566 """ 567 if not os.path.exists(file_path): 568 UPDATE_LOGGER.print_log( 569 "%s is not exist! path: %s" % (file_name, file_path), 570 log_type=UPDATE_LOGGER.ERROR_LOG) 571 return False 572 with open(file_path, 'r') as r_f: 573 file_content = r_f.read() 574 UPDATE_LOGGER.print_log( 575 "%s file parsing complete! path: %s" % (file_name, file_path)) 576 return file_content 577 578 579def get_update_info(): 580 """ 581 Parse the configuration file to obtain the update information. 582 :return: update information if any; false otherwise. 583 """ 584 if not OPTIONS_MANAGER.not_l2: 585 OPTIONS_MANAGER.version_mbn_file_path = os.path.join( 586 OPTIONS_MANAGER.target_package_config_dir, VERSION_MBN_PATH) 587 version_mbn_content = \ 588 get_file_content( 589 OPTIONS_MANAGER.version_mbn_file_path, os.path.basename( 590 os.path.join(OPTIONS_MANAGER.target_package_config_dir, 591 VERSION_MBN_PATH))) 592 if version_mbn_content is False: 593 UPDATE_LOGGER.print_log( 594 "Get version mbn content failed!", 595 log_type=UPDATE_LOGGER.ERROR_LOG) 596 return False 597 OPTIONS_MANAGER.version_mbn_content = version_mbn_content 598 OPTIONS_MANAGER.board_list_file_path = os.path.join( 599 OPTIONS_MANAGER.target_package_config_dir, BOARD_LIST_PATH) 600 board_list_content = \ 601 get_file_content( 602 OPTIONS_MANAGER.board_list_file_path, os.path.basename( 603 os.path.join(OPTIONS_MANAGER.target_package_config_dir, 604 BOARD_LIST_PATH))) 605 if board_list_content is False: 606 UPDATE_LOGGER.print_log( 607 "Get board list content failed!", 608 log_type=UPDATE_LOGGER.ERROR_LOG) 609 return False 610 OPTIONS_MANAGER.board_list_content = board_list_content 611 612 if OPTIONS_MANAGER.xml_path is None: 613 xml_file_path = os.path.join( 614 OPTIONS_MANAGER.target_package_config_dir, XML_FILE_PATH) 615 else: 616 xml_file_path = OPTIONS_MANAGER.xml_path 617 618 # Parse the XML configuration file. 619 head_info_list, component_info_dict, \ 620 full_img_list, incremental_img_list, \ 621 OPTIONS_MANAGER.target_package_version, \ 622 OPTIONS_MANAGER.full_image_path_list = \ 623 parse_update_config(xml_file_path) 624 if head_info_list is False or component_info_dict is False or \ 625 full_img_list is False or incremental_img_list is False: 626 UPDATE_LOGGER.print_log( 627 "Get parse update config xml failed!", 628 log_type=UPDATE_LOGGER.ERROR_LOG) 629 return False 630 OPTIONS_MANAGER.head_info_list, OPTIONS_MANAGER.component_info_dict, \ 631 OPTIONS_MANAGER.full_img_list, OPTIONS_MANAGER.incremental_img_list = \ 632 head_info_list, component_info_dict, \ 633 full_img_list, incremental_img_list 634 return True 635 636 637def sign_package(): 638 return sign_ota_package( 639 OPTIONS_MANAGER.update_package_file_path, 640 OPTIONS_MANAGER.signed_package, 641 OPTIONS_MANAGER.private_key)