From 601c24222fff271bf82bcc873a34f304fbd66af4 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Tue, 30 May 2023 22:11:33 -0400 Subject: [PATCH] Add support for supplying the private key passphrases non-interactively Fixes: #99 Signed-off-by: Andrew Gunnerson --- README.md | 34 ++++++++++ avbroot/main.py | 165 +++++++++++++++++++++++++++++++++------------ avbroot/openssl.py | 19 +++++- 3 files changed, 172 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 78bf743..7d82719 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,40 @@ ValueError: vbmeta flags disable AVB: 0x3 To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`. +### Non-interactive use + +avbroot prompts for the private key passphrases interactively by default. To run avbroot non-interactively, either: + +* Supply the passphrases via files: + + ```bash + avbroot patch \ + --passphrase-avb-file /path/to/avb.passphrase \ + --passphrase-ota-file /path/to/ota.passphrase \ + <...> + ``` + + On Unix-like systems, the "files" can be pipes. With shells that support process substituion (bash, zsh, etc.), the passphrase can be queried from a command (eg. querying a password manager). + + ```bash + avbroot patch \ + --passphrase-avb-file <(command to query AVB passphrase) \ + --passphrase-ota-file <(command to query OTA passphrase) \ + <...> + ``` + +* Supply the passphrases via environment variables. This is less secure since any process running as the same user can see the environment variable values. + + ```bash + export PASSPHRASE_AVB="the AVB passphrase" + export PASSPHRASE_OTA="the OTA passphrase" + + avbroot patch \ + --passphrase-avb-env-var PASSPHRASE_AVB \ + --passphrase-ota-env-var PASSPHRASE_OTA \ + <...> + ``` + ### Extracting the entire OTA To extract all images contained within the OTA's `payload.bin`, run: diff --git a/avbroot/main.py b/avbroot/main.py index 664cd4a..5521f49 100644 --- a/avbroot/main.py +++ b/avbroot/main.py @@ -326,8 +326,16 @@ def patch_subcommand(args): root_patch = boot.PrepatchedImage(args.prepatched) # Get passphrases for keys - passphrase_avb = openssl.prompt_passphrase(args.privkey_avb) - passphrase_ota = openssl.prompt_passphrase(args.privkey_ota) + passphrase_avb = openssl.prompt_passphrase( + args.privkey_avb, + args.passphrase_avb_env_var, + args.passphrase_avb_file, + ) + passphrase_ota = openssl.prompt_passphrase( + args.privkey_ota, + args.passphrase_ota_env_var, + args.passphrase_ota_file, + ) # Ensure that the certificate matches the private key if not openssl.cert_matches_key(args.cert_ota, args.privkey_ota, @@ -425,61 +433,132 @@ def uint64_arg(arg): def parse_args(argv=None): parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest='subcommand', required=True, - help='Subcommands') + subparsers = parser.add_subparsers( + dest='subcommand', + required=True, + help='Subcommands', + ) - patch = subparsers.add_parser('patch', help='Patch a full OTA zip') + patch = subparsers.add_parser( + 'patch', + help='Patch a full OTA zip', + ) - patch.add_argument('--input', required=True, - help='Path to original raw payload or OTA zip') - patch.add_argument('--output', - help='Path to new raw payload or OTA zip') - patch.add_argument('--privkey-avb', required=True, - help='Private key for signing root vbmeta image') - patch.add_argument('--privkey-ota', required=True, - help='Private key for signing OTA payload') - patch.add_argument('--cert-ota', required=True, - help='Certificate for OTA payload signing key') + patch.add_argument( + '--input', + required=True, + help='Path to original raw payload or OTA zip', + ) + patch.add_argument( + '--output', + help='Path to new raw payload or OTA zip', + ) + patch.add_argument( + '--privkey-avb', + required=True, + help='Private key for signing root vbmeta image', + ) + patch.add_argument( + '--privkey-ota', + required=True, + help='Private key for signing OTA payload', + ) + patch.add_argument( + '--cert-ota', + required=True, + help='Certificate for OTA payload signing key', + ) + + for arg in ('AVB', 'OTA'): + group = patch.add_mutually_exclusive_group() + group.add_argument( + f'--passphrase-{arg.lower()}-env-var', + help=f'Environment variable containing {arg} private key passphrase', + ) + group.add_argument( + f'--passphrase-{arg.lower()}-file', + help=f'File containing {arg} private key passphrase', + ) boot_group = patch.add_mutually_exclusive_group(required=True) - boot_group.add_argument('--magisk', - help='Path to Magisk APK') - boot_group.add_argument('--prepatched', - help='Path to prepatched boot image') - boot_group.add_argument('--rootless', action='store_true', - help='Skip applying root patch') + boot_group.add_argument( + '--magisk', + help='Path to Magisk APK', + ) + boot_group.add_argument( + '--prepatched', + help='Path to prepatched boot image', + ) + boot_group.add_argument( + '--rootless', + action='store_true', + help='Skip applying root patch', + ) - patch.add_argument('--magisk-preinit-device', - help='Magisk preinit device') - patch.add_argument('--magisk-random-seed', type=uint64_arg, - help='Magisk random seed') - patch.add_argument('--ignore-magisk-warnings', action='store_true', - help='Ignore Magisk compatibility/version warnings') + patch.add_argument( + '--magisk-preinit-device', + help='Magisk preinit device', + ) + patch.add_argument( + '--magisk-random-seed', + type=uint64_arg, + help='Magisk random seed', + ) + patch.add_argument( + '--ignore-magisk-warnings', + action='store_true', + help='Ignore Magisk compatibility/version warnings', + ) - patch.add_argument('--clear-vbmeta-flags', action='store_true', - help='Forcibly clear vbmeta flags if they disable AVB') + patch.add_argument( + '--clear-vbmeta-flags', + action='store_true', + help='Forcibly clear vbmeta flags if they disable AVB', + ) extract = subparsers.add_parser( - 'extract', help='Extract patched images from a patched OTA zip') + 'extract', + help='Extract patched images from a patched OTA zip', + ) - extract.add_argument('--input', required=True, - help='Path to patched OTA zip') - extract.add_argument('--directory', default='.', - help='Output directory for extracted images') + extract.add_argument( + '--input', + required=True, + help='Path to patched OTA zip', + ) + extract.add_argument( + '--directory', + default='.', + help='Output directory for extracted images', + ) extract_group = extract.add_mutually_exclusive_group() - extract_group.add_argument('--all', action='store_true', - help='Extract all images from the payload') - extract_group.add_argument('--boot-only', action='store_true', - help='Extract only the boot image') + extract_group.add_argument( + '--all', + action='store_true', + help='Extract all images from the payload', + ) + extract_group.add_argument( + '--boot-only', + action='store_true', + help='Extract only the boot image', + ) for subcmd in (patch, extract): - subcmd.add_argument('--boot-partition', default='@gki_ramdisk', - help='Boot partition name') + subcmd.add_argument( + '--boot-partition', + default='@gki_ramdisk', + help='Boot partition name', + ) magisk_info = subparsers.add_parser( - 'magisk-info', help='Print Magisk config from a patched boot image') - magisk_info.add_argument('--image', required=True, - help='Patch to Magisk-patched boot image') + 'magisk-info', + help='Print Magisk config from a patched boot image', + ) + magisk_info.add_argument( + '--image', + required=True, + help='Patch to Magisk-patched boot image', + ) args = parser.parse_args(args=argv) diff --git a/avbroot/openssl.py b/avbroot/openssl.py index 9d5525e..4e4f536 100644 --- a/avbroot/openssl.py +++ b/avbroot/openssl.py @@ -192,15 +192,28 @@ def _is_encrypted(pkey): return False -def prompt_passphrase(pkey): +def prompt_passphrase(pkey, passphrase_env_var=None, passphrase_file=None): ''' - Prompt and return passphrase if the private key is encrypted. + If the private key is encrypted: + + * try to read from the specified passphrase file (first line with trailing + line endings stripped) + * try to read from the passphrase environment variable + * prompt for the passphrase interactively + + There is no fallback behavior. ''' if not _is_encrypted(pkey): return None - passphrase = getpass.getpass(f'Passphrase for {pkey}: ') + if passphrase_file is not None: + with open(passphrase_file, 'r') as f: + passphrase = f.readline().rstrip('\r\n') + elif passphrase_env_var is not None: + passphrase = os.environ[passphrase_env_var] + else: + passphrase = getpass.getpass(f'Passphrase for {pkey}: ') # Verify that it is correct with inject_passphrase(passphrase):