Add support for supplying the private key passphrases non-interactively

Fixes: #99

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This commit is contained in:
Andrew Gunnerson
2023-05-30 22:11:33 -04:00
parent 669ea9c471
commit 601c24222f
3 changed files with 172 additions and 46 deletions
+34
View File
@@ -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:
+122 -43
View File
@@ -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)
+16 -3
View File
@@ -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):