From c1d4133f794754e22c83921e95ba865345e0ab38 Mon Sep 17 00:00:00 2001 From: cfig Date: Wed, 1 Jan 2020 22:07:11 +0800 Subject: [PATCH] first update in 2020 update gradle managed modules update gradle to 6 refactor directory for aosp modules --- README.md | 5 +- .../avb}/avb_test_data/testkey_atx_pik.pem | 0 .../avb}/avb_test_data/testkey_atx_prk.pem | 0 .../avb}/avb_test_data/testkey_atx_psk.pem | 0 .../avb}/avb_test_data/testkey_rsa2048.pem | 0 .../avb}/avb_test_data/testkey_rsa2048.pk8 | Bin .../avb}/avb_test_data/testkey_rsa4096.pem | 0 .../avb}/avb_test_data/testkey_rsa4096.pk8 | Bin .../avb}/avb_test_data/testkey_rsa8192.pem | 0 .../avb}/avb_test_data/testkey_rsa8192.pk8 | Bin {avb => aosp/avb}/avbtool | 1680 ++++++++++++++--- {avb => aosp/avb}/avbtool.diff | 0 .../src/main/java/BootSignature.java | 22 +- .../build/tools}/extract_kernel.py | 4 +- {security => aosp/security}/README | 0 {security => aosp/security}/media.pk8 | Bin {security => aosp/security}/media.x509.pem | 0 {security => aosp/security}/platform.pk8 | Bin {security => aosp/security}/platform.x509.pem | 0 {security => aosp/security}/shared.pk8 | Bin {security => aosp/security}/shared.x509.pem | 0 {security => aosp/security}/testkey.pk8 | Bin {security => aosp/security}/testkey.x509.pem | 0 {security => aosp/security}/verity.pk8 | Bin {security => aosp/security}/verity.x509.pem | 0 {security => aosp/security}/verity_key | Bin .../system/libufdt/utils/src}/mkdtboimg.py | 0 .../system/tools/mkbootimg/mkbootimg.py | 146 +- bbootimg/build.gradle.kts | 10 +- bbootimg/src/main/kotlin/Helper.kt | 14 +- bbootimg/src/main/kotlin/ParamConfig.kt | 3 +- .../src/main/kotlin/avb/alg/Algorithms.kt | 12 +- .../avb/desc/ChainPartitionDescriptor.kt | 2 +- .../kotlin/avb/desc/PropertyDescriptor.kt | 2 +- bbootimg/src/main/kotlin/bootimg/ImgInfo.kt | 4 +- bbootimg/src/main/kotlin/cfig/io/Struct3.kt | 661 +++---- .../kotlin/kernel_util/KernelExtractor.kt | 2 +- .../src/main/kotlin/packable/BootImgParser.kt | 4 +- .../src/main/kotlin/packable/IPackable.kt | 1 + .../src/test/kotlin/cfig/io/Struct3Test.kt | 7 +- gradle/wrapper/gradle-wrapper.jar | Bin 55741 -> 58702 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 51 +- gradlew.bat | 18 +- settings.gradle.kts | 2 +- 45 files changed, 2024 insertions(+), 628 deletions(-) rename {avb => aosp/avb}/avb_test_data/testkey_atx_pik.pem (100%) rename {avb => aosp/avb}/avb_test_data/testkey_atx_prk.pem (100%) rename {avb => aosp/avb}/avb_test_data/testkey_atx_psk.pem (100%) rename {avb => aosp/avb}/avb_test_data/testkey_rsa2048.pem (100%) rename {avb => aosp/avb}/avb_test_data/testkey_rsa2048.pk8 (100%) rename {avb => aosp/avb}/avb_test_data/testkey_rsa4096.pem (100%) rename {avb => aosp/avb}/avb_test_data/testkey_rsa4096.pk8 (100%) rename {avb => aosp/avb}/avb_test_data/testkey_rsa8192.pem (100%) rename {avb => aosp/avb}/avb_test_data/testkey_rsa8192.pk8 (100%) rename {avb => aosp/avb}/avbtool (74%) rename {avb => aosp/avb}/avbtool.diff (100%) rename {external => aosp/build/tools}/extract_kernel.py (97%) rename {security => aosp/security}/README (100%) rename {security => aosp/security}/media.pk8 (100%) rename {security => aosp/security}/media.x509.pem (100%) rename {security => aosp/security}/platform.pk8 (100%) rename {security => aosp/security}/platform.x509.pem (100%) rename {security => aosp/security}/shared.pk8 (100%) rename {security => aosp/security}/shared.x509.pem (100%) rename {security => aosp/security}/testkey.pk8 (100%) rename {security => aosp/security}/testkey.x509.pem (100%) rename {security => aosp/security}/verity.pk8 (100%) rename {security => aosp/security}/verity.x509.pem (100%) rename {security => aosp/security}/verity_key (100%) rename {external => aosp/system/libufdt/utils/src}/mkdtboimg.py (100%) rename external/mkbootimg => aosp/system/tools/mkbootimg/mkbootimg.py (63%) diff --git a/README.md b/README.md index 29556b6..2e3db0d 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,6 @@ We now support both VB 1.0 and AVB 2.0 layouts. boot\_signer https://android.googlesource.com/platform/system/extras -bouncycastle -https://android.googlesource.com/platform/external/bouncycastle - cpio / fs\_config https://android.googlesource.com/platform/system/core @@ -105,7 +102,7 @@ AVB https://android.googlesource.com/platform/external/avb/ mkbootimg -https://android.googlesource.com/platform/system/core/+/master/mkbootimg/ +https://android.googlesource.com/platform/system/tools/mkbootimg/+/refs/heads/master/ Android version list https://source.android.com/source/build-numbers.html diff --git a/avb/avb_test_data/testkey_atx_pik.pem b/aosp/avb/avb_test_data/testkey_atx_pik.pem similarity index 100% rename from avb/avb_test_data/testkey_atx_pik.pem rename to aosp/avb/avb_test_data/testkey_atx_pik.pem diff --git a/avb/avb_test_data/testkey_atx_prk.pem b/aosp/avb/avb_test_data/testkey_atx_prk.pem similarity index 100% rename from avb/avb_test_data/testkey_atx_prk.pem rename to aosp/avb/avb_test_data/testkey_atx_prk.pem diff --git a/avb/avb_test_data/testkey_atx_psk.pem b/aosp/avb/avb_test_data/testkey_atx_psk.pem similarity index 100% rename from avb/avb_test_data/testkey_atx_psk.pem rename to aosp/avb/avb_test_data/testkey_atx_psk.pem diff --git a/avb/avb_test_data/testkey_rsa2048.pem b/aosp/avb/avb_test_data/testkey_rsa2048.pem similarity index 100% rename from avb/avb_test_data/testkey_rsa2048.pem rename to aosp/avb/avb_test_data/testkey_rsa2048.pem diff --git a/avb/avb_test_data/testkey_rsa2048.pk8 b/aosp/avb/avb_test_data/testkey_rsa2048.pk8 similarity index 100% rename from avb/avb_test_data/testkey_rsa2048.pk8 rename to aosp/avb/avb_test_data/testkey_rsa2048.pk8 diff --git a/avb/avb_test_data/testkey_rsa4096.pem b/aosp/avb/avb_test_data/testkey_rsa4096.pem similarity index 100% rename from avb/avb_test_data/testkey_rsa4096.pem rename to aosp/avb/avb_test_data/testkey_rsa4096.pem diff --git a/avb/avb_test_data/testkey_rsa4096.pk8 b/aosp/avb/avb_test_data/testkey_rsa4096.pk8 similarity index 100% rename from avb/avb_test_data/testkey_rsa4096.pk8 rename to aosp/avb/avb_test_data/testkey_rsa4096.pk8 diff --git a/avb/avb_test_data/testkey_rsa8192.pem b/aosp/avb/avb_test_data/testkey_rsa8192.pem similarity index 100% rename from avb/avb_test_data/testkey_rsa8192.pem rename to aosp/avb/avb_test_data/testkey_rsa8192.pem diff --git a/avb/avb_test_data/testkey_rsa8192.pk8 b/aosp/avb/avb_test_data/testkey_rsa8192.pk8 similarity index 100% rename from avb/avb_test_data/testkey_rsa8192.pk8 rename to aosp/avb/avb_test_data/testkey_rsa8192.pk8 diff --git a/avb/avbtool b/aosp/avb/avbtool similarity index 74% rename from avb/avbtool rename to aosp/avb/avbtool index 1762d38..023833a 100755 --- a/avb/avbtool +++ b/aosp/avb/avbtool @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python # Copyright 2016, The Android Open Source Project # @@ -24,6 +24,8 @@ # """Command-line tool for working with Android Verified Boot images.""" +from __future__ import print_function + import argparse import binascii import bisect @@ -106,7 +108,7 @@ ALGORITHMS = { hash_name='sha256', hash_num_bytes=32, signature_num_bytes=256, - public_key_num_bytes=8 + 2*2048/8, + public_key_num_bytes=8 + 2*2048//8, padding=[ # PKCS1-v1_5 padding 0x00, 0x01] + [0xff]*202 + [0x00] + [ @@ -120,7 +122,7 @@ ALGORITHMS = { hash_name='sha256', hash_num_bytes=32, signature_num_bytes=512, - public_key_num_bytes=8 + 2*4096/8, + public_key_num_bytes=8 + 2*4096//8, padding=[ # PKCS1-v1_5 padding 0x00, 0x01] + [0xff]*458 + [0x00] + [ @@ -134,7 +136,7 @@ ALGORITHMS = { hash_name='sha256', hash_num_bytes=32, signature_num_bytes=1024, - public_key_num_bytes=8 + 2*8192/8, + public_key_num_bytes=8 + 2*8192//8, padding=[ # PKCS1-v1_5 padding 0x00, 0x01] + [0xff]*970 + [0x00] + [ @@ -148,7 +150,7 @@ ALGORITHMS = { hash_name='sha512', hash_num_bytes=64, signature_num_bytes=256, - public_key_num_bytes=8 + 2*2048/8, + public_key_num_bytes=8 + 2*2048//8, padding=[ # PKCS1-v1_5 padding 0x00, 0x01] + [0xff]*170 + [0x00] + [ @@ -162,7 +164,7 @@ ALGORITHMS = { hash_name='sha512', hash_num_bytes=64, signature_num_bytes=512, - public_key_num_bytes=8 + 2*4096/8, + public_key_num_bytes=8 + 2*4096//8, padding=[ # PKCS1-v1_5 padding 0x00, 0x01] + [0xff]*426 + [0x00] + [ @@ -176,7 +178,7 @@ ALGORITHMS = { hash_name='sha512', hash_num_bytes=64, signature_num_bytes=1024, - public_key_num_bytes=8 + 2*8192/8, + public_key_num_bytes=8 + 2*8192//8, padding=[ # PKCS1-v1_5 padding 0x00, 0x01] + [0xff]*938 + [0x00] + [ @@ -199,7 +201,7 @@ def get_release_string(): def round_to_multiple(number, size): """Rounds a number up to nearest multiple of another number. - Args: + Arguments: number: The number to round up. size: The multiple to round up to. @@ -216,7 +218,7 @@ def round_to_multiple(number, size): def round_to_pow2(number): """Rounds a number up to the next power of 2. - Args: + Arguments: number: The number to round up. Returns: @@ -258,7 +260,7 @@ def decode_long(blob): This is the reverse of encode_long(). Arguments: - value: A bytearray() with the encoded long. + blob: A bytearray() with the encoded long. Returns: The decoded value. @@ -286,9 +288,8 @@ def egcd(a, b): """ if a == 0: return (b, 0, 1) - else: - g, y, x = egcd(b % a, a) - return (g, x - (b // a) * y, y) + g, y, x = egcd(b % a, a) + return (g, x - (b // a) * y, y) def modinv(a, m): @@ -309,8 +310,7 @@ def modinv(a, m): gcd, x, _ = egcd(a, m) if gcd != 1: return None # modular inverse does not exist - else: - return x % m + return x % m def parse_number(string): @@ -349,6 +349,9 @@ class RSAPublicKey(object): Arguments: key_path: The path to a key file. + + Raises: + AvbError: If RSA key parameters could not be read from file. """ # We used to have something as simple as this: # @@ -390,6 +393,32 @@ class RSAPublicKey(object): self.exponent = 65537 +# TODO(danielaustin): Should this be moved into the RSAPublicKey class? +def rsa_key_read_pem_bytes(key_path): + """Reads the bytes out of the passed in PEM file. + + Arguments: + key_path: A string containing the path to the PEM file. + + Returns: + A bytearray containing the bytes in the PEM file. + + Raises: + AvbError: If openssl cannot decode the PEM file. + """ + # Use openssl to decode the PEM file. + args = ['openssl', 'rsa', '-in', key_path, '-pubout', '-outform', 'DER'] + p = subprocess.Popen(args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (pout, perr) = p.communicate() + retcode = p.wait() + if retcode != 0: + raise AvbError('Error decoding: {}'.format(perr)) + return bytearray(pout) + + def encode_rsa_key(key_path): """Encodes a public RSA key in |AvbRSAPublicKeyHeader| format. @@ -401,16 +430,19 @@ def encode_rsa_key(key_path): Returns: A bytearray() with the |AvbRSAPublicKeyHeader|. + + Raises: + AvbError: If given RSA key exponent is not 65537. """ key = RSAPublicKey(key_path) if key.exponent != 65537: raise AvbError('Only RSA keys with exponent 65537 are supported.') ret = bytearray() # Calculate n0inv = -1/n[0] (mod 2^32) - b = 2L**32 + b = 2L**32 # pylint: disable=long-suffix n0inv = b - modinv(key.modulus, b) # Calculate rr = r^2 (mod N), where r = 2^(# of key bits) - r = 2L**key.modulus.bit_length() + r = 2L**key.modulus.bit_length() # pylint: disable=long-suffix rrmodn = r * r % key.modulus ret.extend(struct.pack('!II', key.num_bits, n0inv)) ret.extend(encode_long(key.num_bits, key.modulus)) @@ -437,6 +469,25 @@ def lookup_algorithm_by_type(alg_type): raise AvbError('Unknown algorithm type {}'.format(alg_type)) +def lookup_hash_size_by_type(alg_type): + """Looks up hash size by type. + + Arguments: + alg_type: The integer representing the type. + + Returns: + The corresponding hash size. + + Raises: + AvbError: If the algorithm cannot be found. + """ + for alg_name in ALGORITHMS: + alg_data = ALGORITHMS[alg_name] + if alg_data.algorithm_type == alg_type: + return alg_data.hash_num_bytes + raise AvbError('Unsupported algorithm type {}'.format(alg_type)) + + def raw_sign(signing_helper, signing_helper_with_files, algorithm_name, signature_num_bytes, key_path, raw_data_to_sign): @@ -461,8 +512,8 @@ def raw_sign(signing_helper, signing_helper_with_files, signing_file = tempfile.NamedTemporaryFile() signing_file.write(str(raw_data_to_sign)) signing_file.flush() - p = subprocess.Popen( - [signing_helper_with_files, algorithm_name, key_path, signing_file.name]) + p = subprocess.Popen([ + signing_helper_with_files, algorithm_name, key_path, signing_file.name]) retcode = p.wait() if retcode != 0: raise AvbError('Error signing') @@ -492,8 +543,7 @@ def raw_sign(signing_helper, signing_helper_with_files, def verify_vbmeta_signature(vbmeta_header, vbmeta_blob): - """Checks that the signature in a vbmeta blob was made by - the embedded public key. + """Checks that signature in a vbmeta blob was made by the embedded public key. Arguments: vbmeta_header: A AvbVBMetaHeader. @@ -502,9 +552,13 @@ def verify_vbmeta_signature(vbmeta_header, vbmeta_blob): Returns: True if the signature is valid and corresponds to the embedded public key. Also returns True if the vbmeta blob is not signed. + + Raises: + AvbError: If there errors calling out to openssl command during + signature verification. """ (_, alg) = lookup_algorithm_by_type(vbmeta_header.algorithm_type) - if alg.hash_name == '': + if not alg.hash_name: return True header_blob = vbmeta_blob[0:256] auth_offset = 256 @@ -540,7 +594,7 @@ def verify_vbmeta_signature(vbmeta_header, vbmeta_blob): padding_and_digest.extend(computed_digest) (num_bits,) = struct.unpack('!I', pubkey_blob[0:4]) - modulus_blob = pubkey_blob[8:8 + num_bits/8] + modulus_blob = pubkey_blob[8:8 + num_bits//8] modulus = decode_long(modulus_blob) exponent = 65537 @@ -567,19 +621,22 @@ def verify_vbmeta_signature(vbmeta_header, vbmeta_blob): '\n' '[rsapubkey]\n' 'n=INTEGER:%s\n' - 'e=INTEGER:%s\n' % (hex(modulus).rstrip('L'), hex(exponent).rstrip('L'))) + 'e=INTEGER:%s\n' % (hex(modulus).rstrip('L'), + hex(exponent).rstrip('L'))) asn1_tmpfile = tempfile.NamedTemporaryFile() asn1_tmpfile.write(asn1_str) asn1_tmpfile.flush() der_tmpfile = tempfile.NamedTemporaryFile() p = subprocess.Popen( - ['openssl', 'asn1parse', '-genconf', asn1_tmpfile.name, '-out', der_tmpfile.name, '-noout']) + ['openssl', 'asn1parse', '-genconf', asn1_tmpfile.name, '-out', + der_tmpfile.name, '-noout']) retcode = p.wait() if retcode != 0: raise AvbError('Error generating DER file') p = subprocess.Popen( - ['openssl', 'rsautl', '-verify', '-pubin', '-inkey', der_tmpfile.name, '-keyform', 'DER', '-raw'], + ['openssl', 'rsautl', '-verify', '-pubin', '-inkey', der_tmpfile.name, + '-keyform', 'DER', '-raw'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -694,6 +751,9 @@ class ImageHandler(object): ValueError: If data in the file is invalid. """ self.filename = image_filename + self._num_total_blocks = 0 + self._num_total_chunks = 0 + self._file_pos = 0 self._read_header() def _read_header(self): @@ -741,7 +801,7 @@ class ImageHandler(object): # image. offset = 0 output_offset = 0 - for _ in xrange(1, self._num_total_chunks + 1): + for _ in range(1, self._num_total_chunks + 1): chunk_offset = self._image.tell() header_bin = self._image.read(struct.calcsize(ImageChunk.FORMAT)) @@ -847,14 +907,14 @@ class ImageHandler(object): return self._num_total_chunks += 1 - self._num_total_blocks += num_bytes / self.block_size + self._num_total_blocks += num_bytes // self.block_size self._update_chunks_and_blocks() self._image.seek(self._sparse_end, os.SEEK_SET) self._image.write(struct.pack(ImageChunk.FORMAT, ImageChunk.TYPE_DONT_CARE, 0, # Reserved - num_bytes / self.block_size, + num_bytes // self.block_size, struct.calcsize(ImageChunk.FORMAT))) self._read_header() @@ -875,14 +935,14 @@ class ImageHandler(object): return self._num_total_chunks += 1 - self._num_total_blocks += len(data) / self.block_size + self._num_total_blocks += len(data) // self.block_size self._update_chunks_and_blocks() self._image.seek(self._sparse_end, os.SEEK_SET) self._image.write(struct.pack(ImageChunk.FORMAT, ImageChunk.TYPE_RAW, 0, # Reserved - len(data) / self.block_size, + len(data) // self.block_size, len(data) + struct.calcsize(ImageChunk.FORMAT))) self._image.write(data) @@ -903,19 +963,19 @@ class ImageHandler(object): if not self.is_sparse: self._image.seek(0, os.SEEK_END) - self._image.write(fill_data * (size/4)) + self._image.write(fill_data * (size//4)) self._read_header() return self._num_total_chunks += 1 - self._num_total_blocks += size / self.block_size + self._num_total_blocks += size // self.block_size self._update_chunks_and_blocks() self._image.seek(self._sparse_end, os.SEEK_SET) self._image.write(struct.pack(ImageChunk.FORMAT, ImageChunk.TYPE_FILL, 0, # Reserved - size / self.block_size, + size // self.block_size, 4 + struct.calcsize(ImageChunk.FORMAT))) self._image.write(fill_data) self._read_header() @@ -925,9 +985,12 @@ class ImageHandler(object): Arguments: offset: Offset to seek to from the beginning of the file. + + Raises: + RuntimeError: If the given offset is negative. """ if offset < 0: - raise RuntimeError("Seeking with negative offset: %d" % offset) + raise RuntimeError('Seeking with negative offset: %d' % offset) self._file_pos = offset def read(self, size): @@ -966,7 +1029,7 @@ class ImageHandler(object): self._image.seek(chunk.input_offset + chunk_pos_offset) data.extend(self._image.read(chunk_pos_to_go)) elif chunk.chunk_type == ImageChunk.TYPE_FILL: - all_data = chunk.fill_data*(chunk_pos_to_go/len(chunk.fill_data) + 2) + all_data = chunk.fill_data*(chunk_pos_to_go // len(chunk.fill_data) + 2) offset_mod = chunk_pos_offset % len(chunk.fill_data) data.extend(all_data[offset_mod:(offset_mod + chunk_pos_to_go)]) else: @@ -1032,7 +1095,7 @@ class ImageHandler(object): assert chunk.chunk_type == ImageChunk.TYPE_DONT_CARE truncate_at = chunk.chunk_offset + struct.calcsize(ImageChunk.FORMAT) data_sz = 0 - chunk_sz = num_to_keep/self.block_size + chunk_sz = num_to_keep // self.block_size total_sz = data_sz + struct.calcsize(ImageChunk.FORMAT) self._image.seek(chunk.chunk_offset) self._image.write(struct.pack(ImageChunk.FORMAT, @@ -1049,7 +1112,7 @@ class ImageHandler(object): self._num_total_chunks = chunk_idx_for_update self._num_total_blocks = 0 for i in range(0, chunk_idx_for_update): - self._num_total_blocks += self._chunks[i].output_size / self.block_size + self._num_total_blocks += self._chunks[i].output_size // self.block_size self._update_chunks_and_blocks() self._image.truncate(truncate_at) @@ -1128,16 +1191,22 @@ class AvbDescriptor(object): image_dir: The directory of the file being verified. image_ext: The extension of the file being verified (e.g. '.img'). expected_chain_partitions_map: A map from partition name to the - tuple (rollback_index_location, key_blob). + tuple (rollback_index_location, key_blob). image_containing_descriptor: The image the descriptor is in. - accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is zeroed out. + accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is + zeroed out. Returns: True if the descriptor verifies, False otherwise. """ + # Deletes unused parameters to prevent pylint warning unused-argument. + del image_dir, image_ext, expected_chain_partitions_map + del image_containing_descriptor, accept_zeroed_hashtree + # Nothing to do. return True + class AvbPropertyDescriptor(AvbDescriptor): """A class for property descriptors. @@ -1215,9 +1284,10 @@ class AvbPropertyDescriptor(AvbDescriptor): image_dir: The directory of the file being verified. image_ext: The extension of the file being verified (e.g. '.img'). expected_chain_partitions_map: A map from partition name to the - tuple (rollback_index_location, key_blob). + tuple (rollback_index_location, key_blob). image_containing_descriptor: The image the descriptor is in. - accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is zeroed out. + accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is + zeroed out. Returns: True if the descriptor verifies, False otherwise. @@ -1225,6 +1295,7 @@ class AvbPropertyDescriptor(AvbDescriptor): # Nothing to do. return True + class AvbHashtreeDescriptor(AvbDescriptor): """A class for hashtree descriptors. @@ -1378,14 +1449,16 @@ class AvbHashtreeDescriptor(AvbDescriptor): image_dir: The directory of the file being verified. image_ext: The extension of the file being verified (e.g. '.img'). expected_chain_partitions_map: A map from partition name to the - tuple (rollback_index_location, key_blob). + tuple (rollback_index_location, key_blob). image_containing_descriptor: The image the descriptor is in. - accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is zeroed out. + accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is + zeroed out. Returns: True if the descriptor verifies, False otherwise. """ - if self.partition_name == '': + if not self.partition_name: + image_filename = image_containing_descriptor.filename image = image_containing_descriptor else: image_filename = os.path.join(image_dir, self.partition_name + image_ext) @@ -1394,7 +1467,7 @@ class AvbHashtreeDescriptor(AvbDescriptor): digest_size = len(hashlib.new(name=self.hash_algorithm).digest()) digest_padding = round_to_pow2(digest_size) - digest_size (hash_level_offsets, tree_size) = calc_hash_level_offsets( - self.image_size, self.data_block_size, digest_size + digest_padding) + self.image_size, self.data_block_size, digest_size + digest_padding) root_digest, hash_tree = generate_hash_tree(image, self.image_size, self.data_block_size, self.hash_algorithm, self.salt, @@ -1402,29 +1475,30 @@ class AvbHashtreeDescriptor(AvbDescriptor): hash_level_offsets, tree_size) # The root digest must match unless it is not embedded in the descriptor. - if len(self.root_digest) != 0 and root_digest != self.root_digest: + if self.root_digest and root_digest != self.root_digest: sys.stderr.write('hashtree of {} does not match descriptor\n'. format(image_filename)) return False # ... also check that the on-disk hashtree matches image.seek(self.tree_offset) hash_tree_ondisk = image.read(self.tree_size) - is_zeroed = (hash_tree_ondisk[0:8] == 'ZeRoHaSH') + is_zeroed = (self.tree_size == 0) or (hash_tree_ondisk[0:8] == 'ZeRoHaSH') if is_zeroed and accept_zeroed_hashtree: - print ('{}: skipping verification since hashtree is zeroed and --accept_zeroed_hashtree was given' - .format(self.partition_name)) + print('{}: skipping verification since hashtree is zeroed and ' + '--accept_zeroed_hashtree was given' + .format(self.partition_name)) else: if hash_tree != hash_tree_ondisk: sys.stderr.write('hashtree of {} contains invalid data\n'. - format(image_filename)) + format(image_filename)) return False - print ('{}: Successfully verified {} hashtree of {} for image of {} bytes' - .format(self.partition_name, self.hash_algorithm, image.filename, - self.image_size)) - # TODO: we could also verify that the FEC stored in the image is - # correct but this a) currently requires the 'fec' binary; and b) - # takes a long time; and c) is not strictly needed for - # verification purposes as we've already verified the root hash. + print('{}: Successfully verified {} hashtree of {} for image of {} bytes' + .format(self.partition_name, self.hash_algorithm, image.filename, + self.image_size)) + # TODO(zeuthen): we could also verify that the FEC stored in the image is + # correct but this a) currently requires the 'fec' binary; and b) takes a + # long time; and c) is not strictly needed for verification purposes as + # we've already verified the root hash. return True @@ -1541,14 +1615,16 @@ class AvbHashDescriptor(AvbDescriptor): image_dir: The directory of the file being verified. image_ext: The extension of the file being verified (e.g. '.img'). expected_chain_partitions_map: A map from partition name to the - tuple (rollback_index_location, key_blob). + tuple (rollback_index_location, key_blob). image_containing_descriptor: The image the descriptor is in. - accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is zeroed out. + accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is + zeroed out. Returns: True if the descriptor verifies, False otherwise. """ - if self.partition_name == '': + if not self.partition_name: + image_filename = image_containing_descriptor.filename image = image_containing_descriptor else: image_filename = os.path.join(image_dir, self.partition_name + image_ext) @@ -1559,13 +1635,13 @@ class AvbHashDescriptor(AvbDescriptor): ha.update(data) digest = ha.digest() # The digest must match unless there is no digest in the descriptor. - if len(self.digest) != 0 and digest != self.digest: + if self.digest and digest != self.digest: sys.stderr.write('{} digest of {} does not match digest in descriptor\n'. format(self.hash_algorithm, image_filename)) return False - print ('{}: Successfully verified {} hash of {} for image of {} bytes' - .format(self.partition_name, self.hash_algorithm, image.filename, - self.image_size)) + print('{}: Successfully verified {} hash of {} for image of {} bytes' + .format(self.partition_name, self.hash_algorithm, image.filename, + self.image_size)) return True @@ -1652,9 +1728,10 @@ class AvbKernelCmdlineDescriptor(AvbDescriptor): image_dir: The directory of the file being verified. image_ext: The extension of the file being verified (e.g. '.img'). expected_chain_partitions_map: A map from partition name to the - tuple (rollback_index_location, key_blob). + tuple (rollback_index_location, key_blob). image_containing_descriptor: The image the descriptor is in. - accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is zeroed out. + accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is + zeroed out. Returns: True if the descriptor verifies, False otherwise. @@ -1662,6 +1739,7 @@ class AvbKernelCmdlineDescriptor(AvbDescriptor): # Nothing to verify. return True + class AvbChainPartitionDescriptor(AvbDescriptor): """A class for chained partition descriptors. @@ -1756,9 +1834,10 @@ class AvbChainPartitionDescriptor(AvbDescriptor): image_dir: The directory of the file being verified. image_ext: The extension of the file being verified (e.g. '.img'). expected_chain_partitions_map: A map from partition name to the - tuple (rollback_index_location, key_blob). + tuple (rollback_index_location, key_blob). image_containing_descriptor: The image the descriptor is in. - accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is zeroed out. + accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is + zeroed out. Returns: True if the descriptor verifies, False otherwise. @@ -1786,8 +1865,8 @@ class AvbChainPartitionDescriptor(AvbDescriptor): format(self.partition_name)) return False - print ('{}: Successfully verified chain partition descriptor matches ' - 'expected data'.format(self.partition_name)) + print('{}: Successfully verified chain partition descriptor matches ' + 'expected data'.format(self.partition_name)) return True @@ -1884,12 +1963,840 @@ class AvbFooter(object): self.vbmeta_offset, self.vbmeta_size) +# Android Firmware Transparency Log Data Structures + + +class AvbIcpHeader(object): + """A class for the transparency log inclusion proof header. + + Attributes: + magic: Magic for identifying the ICP header. + required_icp_version_major: The major version of AVB that wrote the entry. + required_icp_version_minor: The minor version of AVB that wrote the entry. + algorithm: Hash algorithm used. ID is defined in ALGORITHMS. + icp_count: Number of inclusion proofs represented in this structure. + """ + + SIZE = 18 # The size of the structure, in bytes + MAGIC = 'AFTL' + FORMAT_STRING = ('!4s2L' # magic, major & minor version + 'L' # algorithm type for transparency log + 'H') # number of inclusion proof entries + + def __init__(self, data=None): + """Initializes a new transparency header object. + + Arguments: + data: If not None, must be a bytearray of size == 18. + + Raises: + AvbError: If invalid structure for AvbIcpHeader. + """ + assert struct.calcsize(self.FORMAT_STRING) == self.SIZE + + if data: + (self.magic, self.required_icp_version_major, + self.required_icp_version_minor, self.algorithm, + self.icp_count) = struct.unpack(self.FORMAT_STRING, data) + else: + self.magic = self.MAGIC + self.required_icp_version_major = AVB_VERSION_MAJOR + self.required_icp_version_minor = AVB_VERSION_MINOR + self.algorithm = 0 + self.icp_count = 0 + if not self.is_valid(): + raise AvbError('Invalid structure for AvbIcpHeader') + + def save(self, output): + """Serializes the transparency header (18) to disk. + + Arguments: + output: The object to write the header to. + + Raises: + AvbError if invalid structure for AvbIcpHeader. + """ + output.write(self.encode()) + + def encode(self): + """Serializes the header (18) to a bytearray(). + + Returns: + A bytearray() with the encoded header. + + Raises: + AvbError: If invalid structure for AvbIcpHeader. + """ + if not self.is_valid(): + raise AvbError('Invalid structure for AvbIcpHeader') + return struct.pack(self.FORMAT_STRING, self.magic, + self.required_icp_version_major, + self.required_icp_version_minor, + self.algorithm, self.icp_count) + + def is_valid(self): + """Ensures that values in an AvbIcpHeader structure are sane. + + Returns: + True if the values in the AvbIcpHeader are sane, False otherwise. + """ + if self.magic != AvbIcpHeader.MAGIC: + sys.stderr.write( + 'ICP Header: magic value mismatch: {}\n'.format(self.magic)) + return False + + if self.required_icp_version_major > AVB_VERSION_MAJOR: + sys.stderr.write('ICP header: major version mismatch: {}\n'.format( + self.required_icp_version_major)) + return False + + if self.required_icp_version_minor > AVB_VERSION_MINOR: + sys.stderr.write('ICP header: minor version mismatch: {}\n'.format( + self.required_icp_version_minor)) + return False + + if self.algorithm < 0 or self.algorithm >= len(ALGORITHMS): + sys.stderr.write( + 'ICP header: algorithm identifier out of range: {}\n'.format( + self.algorithm)) + return False + + if self.icp_count < 0: + sys.stderr.write( + 'ICP header: ICP entry count out of range: {}\n'.format( + self.icp_count)) + return False + return True + + def print_desc(self, o): + """Print the descriptor. + + Arguments: + o: The object to write the output to. + """ + o.write(' Major version: {}\n'.format( + self.required_icp_version_major)) + o.write(' Minor version: {}\n'.format( + self.required_icp_version_minor)) + o.write(' Algorithm: {}\n'.format( + lookup_algorithm_by_type(self.algorithm)[0])) + o.write(' ICP entries count: {}\n'.format( + self.icp_count)) + + +def check_signature(log_root, log_root_sig, + transparency_log_pub_key): + """Validates the signature provided by the transparency log. + + Arguments: + log_root: The transparency log_root data structure. + log_root_sig: The signature of the transparency log_root data structure. + transparency_log_pub_key: The trusted public key of the transparency log. + + Returns: + True if the signature check passes, otherwise False. + """ + + logsig_tmp = tempfile.NamedTemporaryFile() + logsig_tmp.write(log_root_sig) + logsig_tmp.flush() + logroot_tmp = tempfile.NamedTemporaryFile() + logroot_tmp.write(log_root) + logroot_tmp.flush() + + p = subprocess.Popen(['openssl', 'dgst', '-sha256', '-verify', + transparency_log_pub_key, + '-signature', logsig_tmp.name, logroot_tmp.name], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + (_, openssl_err) = p.communicate() + retcode = p.wait() + if not retcode: + return True + sys.stderr.write('openssl status {}'.format(openssl_err)) + return False + + +class AvbIcpSignedRootBlob(object): + """A class for the components required to validate the incusion proof. + + This class contains the signed tree root components required to verify + an inclusion proof given a list of hashes. + + Attributes: + leaf_hash: The hash of the leaf corresponding with this log entry. + tree_size: The size of the Merkle tree. + log_root: The transparency log_root data structure. + root_hash: The calculated root hash of the Merkle tree. + log_root_sig: The signed root hash. Used to verify the ICP. + """ + # TODO(danielaustin): Match hash and signature size to algorithm value. + SIZE = 645 + FORMAT_STRING = ('!32s' # The leaf hash corresponding to this vbmeta. + 'Q' # The Merkle tree size + '61s' # The log_root structure that is signed + '32s' # The Merkle tree root hash. + '512s') # The log_root signed with the transparency log key. + + def __init__(self, data=None): + """Initializes a new signed_root_blob structure. + + Arguments: + data: If not None, must be a bytearray of size |SIZE|. + + Raises: + AvbError: If data does not represent a well-formed AvbIcpSignedRootBlob. + """ + assert struct.calcsize(self.FORMAT_STRING) == self.SIZE + + if data: + (self.leaf_hash, self.tree_size, self.log_root, + self.root_hash, self.log_root_sig) = struct.unpack( + self.FORMAT_STRING, data) + else: + self.leaf_hash = bytearray() + self.tree_size = 0 + self.log_root = bytearray() + self.root_hash = bytearray() + self.log_root_sig = '' + + if not self.is_valid(): + raise AvbError('Invalid structure for AvbIcpSignedBlob') + + def translate_afi_response(self, afi_response): + """Translates an AddFirmwareImageResponse message to AvbIcpSignedRootBlob. + + Arguments: + afi_response: An AddFirmwareImageResponse proto message. + + Raises: + AvbError: If unsupported hash size is detected. + """ + # Do the hash calculation + self.leaf_hash = rfc6962_hash_leaf(afi_response.vbmeta_leaf) + self.log_root = afi_response.vbmeta_proof.sth.log_root + self.log_root_sig = str(afi_response.vbmeta_proof.sth.log_root_signature) + # Partial format string to extract the tree_size and root_hash from + # the log_root. THis structure is defined: + # https://github.com/google/trillian/blob/master/trillian.proto#L255 + + # TODO(danielaustin): Make this into a class. + partial_log_format_string = ('!H' # Version + 'Q' # tree_size + 'B' # hash_size, verify this is 32 for now + '32s') # The root_hash + + (log_root_version, self.tree_size, root_hash_size, + self.root_hash) = struct.unpack(partial_log_format_string, + self.log_root[0:43]) + if log_root_version != 1: + raise AvbError('Unsupported log root version: {}'.format( + log_root_version)) + if len(self.root_hash) != root_hash_size: + raise AvbError('Unsupported hash size.') + + def encode(self): + """Serializes the AvbSignedRootBlob structure (584) to a bytearray. + + Returns: + A bytearray with the AvbSignedRootBlob. + + Raises: + AvbError: If data does not represent a well-formed AvbIcpSignedRootBlob. + """ + if not self.is_valid(): + raise AvbError('Invalid structure for AvbIcpSignedRootBlob') + + return struct.pack(self.FORMAT_STRING, + str(self.leaf_hash), + self.tree_size, + str(self.log_root), + str(self.root_hash), + str(self.log_root_sig)) + + def is_valid(self): + """Ensures that values in the AvbIcpSignedRootBlob are sane. + + Returns: + True if the values in the AvbIcpSignedRootBlob are sane, False otherwise. + """ + # TODO(danielaustin): match these up with algorithm instead of defaults. + # All structures being of size 0 is valid + if (not self.leaf_hash and self.tree_size == 0 and + not self.root_hash and not self.log_root_sig): + return True + if len(self.leaf_hash) != 32: + sys.stderr.write('AvbIcpSignedRootBlob: Bad leaf_hash size {}'.format( + len(self.leaf_hash))) + return False + if self.tree_size < 0: + sys.stderr.write('AvbIcpSignedRootBlob: Bad tree_size value {}'.format( + self.tree_size)) + return False + if len(self.root_hash) != 32: + sys.stderr.write('AvbIcpSignedRootBlob: Bad root_hash size {}'.format( + len(self.root_hash))) + return False + if len(self.log_root_sig) != 512: + sys.stderr.write('AvbIcpSignedRootBlob: Bad log_root_sig size {}'.format( + len(self.log_root_sig))) + return False + return True + + def print_desc(self, o): + """Print the descriptor. + + Arguments: + o: The object to write the output to. + """ + o.write(' Leaf hash: {}\n'.format( + binascii.hexlify(self.leaf_hash))) + o.write(' Tree size: {}\n'.format( + self.tree_size)) + o.write(' Log root: {}\n'.format( + binascii.hexlify(self.log_root))) + o.write(' Root hash: {}\n'.format( + binascii.hexlify(self.root_hash))) + + +class AvbIcpEntry(object): + """A class for the transparency log inclusion proof entries. + + The data that represents each of the components of the ICP entry are stored + immediately following the ICP entry header. The format is log_url, + SignedLogRoot, and inclusion proof hashes. + + Attributes: + log_url_size: Length of the string representing the transparency log URL. + leaf_index: Leaf index in the transparency log representing this entry. + signed_root_blob_size: Size of the SignedLogRoot for the transparency log; + treat as an opaque blob for now. + proof_hash_count: Number of hashes comprising the inclusion proof. + proof_size: The total size of the inclusion proof, in bytes. + next_entry: 1 if there is a next entry, 0 otherwise. + log_url: The URL for the transparency log that generated this inclusion + proof. + signed_root_blob: The data comprising the signed tree head structure. + proofs: The hashes comprising the inclusion proof. + + """ + SIZE = 22 # The size of the structure, in bytes + FORMAT_STRING = ('!L' # transparency log server url size + 'Q' # leaf index + 'L' # signed tree root blob size + 'B' # number of hashes in the inclusion proof + 'L' # size of the inclusion proof in bytes + 'B') # next entry marker + # These are used to capture the log_url, signed_root_blob, + # and the proofs elements for the encode & save function. + + def __init__(self, data=None): + """Initializes a new ICP entry object. + + Arguments: + data: If not None, must be a bytearray of size >= 22. + + Raises: + AvbError: If data does not represent a well-formed AvbIcpEntry. + """ + # Assert the header structure is of a sane size. + assert struct.calcsize(self.FORMAT_STRING) == self.SIZE + + if data: + # Deserialize the header from the data blob. + (self.log_url_size, self.leaf_index, self.signed_root_blob_size, + self.proof_hash_count, self.proof_size, self.next_entry) = struct.unpack( + self.FORMAT_STRING, data[0:self.SIZE]) + if len(data) >= self.SIZE: + # There's more data. Ensure the data entry size is valid. + if len(data) != self.get_expected_size(): + if self.next_entry == 0: + raise AvbError('ICP entry size is not valid {}/{}.' + .format(len(data), self.get_expected_size())) + # Deserialize ICP entry components from the data blob. + expected_format_string = '{}s{}s{}s'.format( + self.log_url_size, + AvbIcpSignedRootBlob.SIZE, + self.proof_size) + + (self.log_url, signed_root_blob_bytes, proof_bytes) = struct.unpack( + expected_format_string, data[self.SIZE:self.get_expected_size()]) + self.signed_root_blob = AvbIcpSignedRootBlob(signed_root_blob_bytes) + self.proofs = [] + if self.proof_hash_count > 0: + proof_idx = 0 + hash_size = self.proof_size // self.proof_hash_count + for _ in range(self.proof_hash_count): + proof = proof_bytes[proof_idx:(proof_idx+hash_size)] + self.proofs.append(proof) + proof_idx += hash_size + else: + self.log_url_size = 0 + self.leaf_index = 0 + self.signed_root_blob_size = 0 + self.proof_hash_count = 0 + self.proof_size = 0 + self.next_entry = 0 + self.log_url = '' + self.signed_root_blob = AvbIcpSignedRootBlob() + self.proofs = [] + if not self.is_valid(): + raise AvbError('Invalid structure for AvbIcpEntry') + + def set_log_url(self, log_url): + """Sets the log_url and log_url_size elements in the AvbIcpEntry. + + Arguments: + log_url: The string representing the transparency log URL. + """ + self.log_url = log_url + self.log_url_size = len(log_url) + + def set_signed_root_blob(self, signed_root_blob): + """Sets signed_root_blob and signed_root_blob_size. + + Arguments: + signed_root_blob: An AvbIcpSignedRootBlob containing the SignedLogRoot + for the transparency log. + """ + self.signed_root_blob = signed_root_blob + self.signed_root_blob_size = signed_root_blob.SIZE + + def set_proofs(self, proofs): + """Sets the proof_hash_count, proofs, and proof_size. + + Arguments: + proofs: A bytearray of concatenated hashes comprising the inclusion proof. + """ + self.proof_hash_count = 0 + self.proofs = proofs + proof_size = 0 + for proof in proofs: + proof_size += len(proof) + self.proof_hash_count += 1 + self.proof_size = proof_size + + def verify_icp(self, transparency_log_pub_key): + """Verifies the contained inclusion proof given the public log key. + + Arguments: + transparency_log_pub_key: The trusted public key for the log. + + Returns: + True if the calculated signature matches AvbIcpEntry's. False otherwise. + """ + calc_root = root_from_icp(self.leaf_index, self.signed_root_blob.tree_size, + self.proofs, self.signed_root_blob.leaf_hash) + if (calc_root == self.signed_root_blob.root_hash) and check_signature( + self.signed_root_blob.log_root, self.signed_root_blob.log_root_sig, + transparency_log_pub_key): + return True + return False + + def save(self, output): + """Serializes the transparency header (22) and data to disk. + + Arguments: + output: The object to write the header to. + + Raises: + AvbError: If invalid entry structure. + """ + output.write(self.encode()) + + def encode(self): + """Serializes the header (22) and data to a bytearray(). + + Returns: + A bytearray() with the encoded header. + + Raises: + AvbError: If invalid entry structure. + """ + proof_bytes = bytearray() + if not self.is_valid(): + raise AvbError('Invalid AvbIcpEntry structure') + expected_format_string = '{}{}s{}s{}s'.format( + self.FORMAT_STRING, self.log_url_size, + self.signed_root_blob.SIZE, + self.proof_size) + + for proof in self.proofs: + proof_bytes.extend(proof) + + return struct.pack(expected_format_string, + self.log_url_size, self.leaf_index, + self.signed_root_blob_size, self.proof_hash_count, + self.proof_size, self.next_entry, self.log_url, + self.signed_root_blob.encode(), + str(proof_bytes)) + + # TODO(danielaustin): Add unit test. + def translate_response(self, transparency_log, afi_response): + """Takes an AddFirmwareInfoResponse object and translates to an AvbIcpEntry. + + Arguments: + transparency_log: String representing the transparency log URL. + afi_response: The AddFirmwareResponse object to translate. + """ + self.set_log_url(transparency_log) + self.leaf_index = afi_response.vbmeta_proof.proof.leaf_index + self.signed_root_blob = AvbIcpSignedRootBlob() + self.signed_root_blob.translate_afi_response(afi_response) + self.signed_root_blob_size = self.signed_root_blob.SIZE + # Calculate the number of hashes. + proof_hashes = afi_response.vbmeta_proof.proof.hashes + self.set_proofs(proof_hashes) + + def get_expected_size(self): + """Gets the expected size of the full entry out of the header. + + Returns: + The expected size of the AvbIcpEntry from the header. + """ + return (self.SIZE + self.log_url_size + + self.signed_root_blob_size + self.proof_size) + + def is_valid(self): + """Ensures that values in an AvbIcpEntry structure are sane. + + Returns: + True if the values in the AvbIcpEntry are sane, False otherwise. + """ + if ((self.log_url and self.log_url_size != len(self.log_url)) + or (not self.log_url and self.log_url_size != 0)): + sys.stderr.write('ICP entry: invalid URL size: {}\n' + .format(self.log_url_size)) + return False + + if self.leaf_index < 0: + sys.stderr.write('ICP entry: leaf index out of range: ' + '{}\n'.format(self.leaf_index)) + return False + + if not self.signed_root_blob or not self.signed_root_blob.is_valid(): + sys.stderr.write('ICP entry: invalid AvbIcpSignedRootBlob\n') + return False + + if (self.signed_root_blob_size != 0) and ( + self.signed_root_blob_size != self.signed_root_blob.SIZE): + sys.stderr.write('ICP entry: invalid signed root blob size: ' + '{}, should be {}\n'.format( + self.signed_root_blob_size, + self.signed_root_blob.SIZE)) + return False + + if self.proof_hash_count < 0: + sys.stderr.write('ICP entry: invalid proof count: {}\n'.format( + self.proof_hash_count)) + return False + + proof_size = 0 + if self.proofs: + for proof in self.proofs: + proof_size += len(proof) + if self.proof_size != proof_size: + sys.stderr.write('ICP entry: invalid transparency log proof size: ') + sys.stderr.write('{}, calculated {}\n'.format(self.proof_size, + proof_size)) + return False + elif self.proof_size != 0: + sys.stderr.write('ICP entry: invalid transparency log proof size ' + '(should be 0): {}'.format(self.proof_size)) + return False + if self.next_entry != 0 and self.next_entry != 1: + sys.stderr.write('ICP entry: invalid next entry value: {}\n'.format( + self.next_entry)) + return False + return True + + def print_desc(self, o): + """Print the descriptor. + + Arguments: + o: The object to write the output to. + """ + o.write(' Transparency Log: {}\n'.format(self.log_url)) + o.write(' Leaf index: {}\n'.format(self.leaf_index)) + o.write(' Next entry: {}\n'.format(self.next_entry)) + o.write(' ICP hashes: ') + for i, proof_hash in enumerate(self.proofs): + if i != 0: + o.write(' ' * 24) + o.write('{}\n'.format(binascii.hexlify(proof_hash))) + + +class AvbIcpBlob(object): + """A class for the transparency log inclusion proof blob. + + This encapsulates an AFTL ICP section with all information required to + validate an inclusion proof. + + Attributes: + icp_header: A header for the section. + icp_entries: A list of AvbIcpEntry objects representing the inclusion + proofs. + """ + + def __init__(self, data=None): + """Initializes a new AvbIcpBlob section. + + Arguments: + data: If not None, must be a bytearray representing an AvbIcpBlob. + + Raises: + AvbError: If the data does not represent a well-formed AvbIcpBlob. + """ + if data: + icp_header_bytes = data[0:AvbIcpHeader.SIZE] + self.icp_header = AvbIcpHeader(icp_header_bytes) + if not self.icp_header.is_valid(): + raise AvbError('Invalid ICP header.') + icp_count = self.icp_header.icp_count + algorithm_id = self.icp_header.algorithm + # TODO(danielaustin): make use of proof_hash_size. + # pylint: disable=unused-variable + proof_hash_size = lookup_hash_size_by_type(algorithm_id) + + # Jump past the header for entry deserialization. + icp_index = AvbIcpHeader.SIZE + # Validate each entry. + self.icp_entries = [] + # Add_icp_entry updates entries and header, so set header count to + # compensate. + self.icp_header.icp_count = 0 + for i in range(icp_count): + # Get the entry header from the ICP blob. + cur_icp_entry = AvbIcpEntry(data[icp_index:]) + cur_icp_entry_size = cur_icp_entry.get_expected_size() + # Now validate the entry structure. + if not cur_icp_entry.is_valid(): + raise AvbError('Validation of ICP entry failed.') + self.add_icp_entry(cur_icp_entry) + # Check if there is a next entry. + if cur_icp_entry.next_entry == 0: + if i != icp_count - 1: + raise AvbError('ICP entry count mismatch') + break + icp_index += cur_icp_entry_size + else: + self.icp_header = AvbIcpHeader() + self.icp_entries = [] + if not self.is_valid(): + raise AvbError('Malformed ICP blob') + + def set_algorithm(self, algorithm_id): + """Sets algorithm to be used by the inclusion proofs in AvbIcpBlob.""" + self.icp_header.algorithm = algorithm_id + + def add_icp_entry(self, avb_icp_entry): + """Adds a new AvbIcpEntry to the AvbIcpBlob, updating fields as necessary. + + Arguments: + avb_icp_entry: An AvbIcpEntry structure. + """ + + # Set the next entry field to denote that a new ICP entry will follow. + if self.icp_entries: + self.icp_entries[-1].next_entry = 1 + self.icp_entries.append(avb_icp_entry) + self.icp_header.icp_count += 1 + + def save(self, output): + """Serializes the AvbIcpBlob to disk. + + Arguments: + output: The object to write the blob to. + + Raises: + AvbError: If invalid blob structure. + """ + output.write(self.encode()) + + def encode(self): + """Serialize the AvbIcpBlob to a bytearray(). + + Returns: + A bytearray() with the encoded header. + + Raises: + AvbError: If invalid blob structure. + """ + # The header and entries are guaranteed to be valid when encode is called. + # Check the entire structure as a whole. + if not self.is_valid(): + raise AvbError('Invalid AvbIcpBlob structure.') + + icp_blob = bytearray() + icp_blob.extend(self.icp_header.encode()) + for icp_entry in self.icp_entries: + icp_blob.extend(icp_entry.encode()) + return icp_blob + + def is_valid(self): + """Ensures that values in the AvbIcpBlob are sane. + + Returns: + True if the values in the AvbIcpBlob are sane, False otherwise. + """ + if not self.icp_header.is_valid(): + return False + + if self.icp_header.icp_count != len(self.icp_entries): + return False + + for icp_entry in self.icp_entries: + if not icp_entry.is_valid(): + return False + return True + + +# AFTL Merkle Tree Functionality +# TODO(danielaustin): Encapsulate this behavior in a class. +def rfc6962_hash_leaf(leaf): + """RFC6962 hashing function for hashing leaves of a Merkle tree. + + Arguments: + leaf: A bytearray containing the Merkle tree leaf to be hashed. + + Returns: + A bytearray containing the RFC6962 SHA256 hash of the leaf. + """ + hasher = hashlib.sha256() + # RFC6962 states a '0' byte should be prepended to the data. + # This is done in conjunction with the '1' byte for non-leaf + # nodes for 2nd preimage attack resistance. + hasher.update(b'\x00') + hasher.update(leaf) + return hasher.digest() + + +def rfc6962_hash_children(l, r): + """Calculates the inner Merkle tree node hash of child nodes l and r. + + Arguments: + l: A bytearray containing the left child node to be hashed. + r: A bytearray containing the right child node to be hashed. + + Returns: + A bytearray containing the RFC6962 SHA256 hash of 1|l|r. + """ + hasher = hashlib.sha256() + # RFC6962 states a '1' byte should be prepended to the concatenated data. + # This is done in conjunction with the '0' byte for leaf + # nodes for 2nd preimage attack resistance. + hasher.update(b'\x01') + hasher.update(l) + hasher.update(r) + return hasher.digest() + + +def chain_border_right(seed, proof): + """Computes a subtree hash along the left-side tree border. + + Arguments: + seed: A bytearray containing the starting hash. + proof: A list of bytearrays representing the hashes in the inclusion proof. + + Returns: + A bytearray containing the left-side subtree hash. + """ + for h in proof: + seed = rfc6962_hash_children(h, seed) + return seed + + +def chain_inner(seed, proof, leaf_index): + """Computes a subtree hash on or below the tree's right border. + + Arguments: + seed: A bytearray containing the starting hash. + proof: A list of bytearrays representing the hashes in the inclusion proof. + leaf_index: The current leaf index. + + Returns: + A bytearray containing the subtree hash. + """ + for i, h in enumerate(proof): + if leaf_index >> i & 1 == 0: + seed = rfc6962_hash_children(seed, h) + else: + seed = rfc6962_hash_children(h, seed) + return seed + + +def root_from_icp(leaf_index, tree_size, proof, leaf_hash): + """Calculates the expected Merkle tree root hash. + + Arguments: + leaf_index: The current leaf index. + tree_size: The number of nodes in the Merkle tree. + proof: A list of bytearrays containing the inclusion proof. + leaf_hash: A bytearray containing the initial leaf hash. + + Returns: + A bytearray containing the calculated Merkle tree root hash. + + Raises: + AvbError: If invalid parameters are passed in. + """ + if leaf_index < 0: + raise AvbError('Invalid leaf_index value: {}'.format(leaf_index)) + if tree_size < 0: + raise AvbError('Invalid tree_size value: {}'.format(tree_size)) + if leaf_index >= tree_size: + err_str = 'leaf_index cannot be equal or larger than tree_size: {}, {}' + raise AvbError(err_str.format(leaf_index, tree_size)) + + # Calculate the point to split the proof into two parts. + # The split is where the paths to leaves diverge. + inner = (leaf_index ^ (tree_size - 1)).bit_length() + result = chain_inner(leaf_hash, proof[:inner], leaf_index) + result = chain_border_right(result, proof[inner:]) + return result + + class AvbVBMetaHeader(object): """A class for parsing and writing AVB vbmeta images. + The attributes correspond to the |AvbVBMetaImageHeader| struct defined in + avb_vbmeta_image.h. + Attributes: - The attributes correspond to the |AvbVBMetaImageHeader| struct defined in - avb_vbmeta_image.h. + magic: Four bytes equal to "AVB0" (AVB_MAGIC). + required_libavb_version_major: The major version of libavb required for this + header. + required_libavb_version_minor: The minor version of libavb required for this + header. + authentication_data_block_size: The size of the signature block. + auxiliary_data_block_size: The size of the auxiliary data block. + algorithm_type: The verification algorithm used, see |AvbAlgorithmType| + enum. + hash_offset: Offset into the "Authentication data" block of hash data. + hash_size: Length of the hash data. + signature_offset: Offset into the "Authentication data" block of signature + data. + signature_size: Length of the signature data. + public_key_offset: Offset into the "Auxiliary data" block of public key + data. + public_key_size: Length of the public key data. + public_key_metadata_offset: Offset into the "Auxiliary data" block of public + key metadata. + public_key_metadata_size: Length of the public key metadata. Must be set to + zero if there is no public key metadata. + descriptors_offset: Offset into the "Auxiliary data" block of descriptor + data. + descriptors_size: Length of descriptor data. + rollback_index: The rollback index which can be used to prevent rollback to + older versions. + flags: Flags from the AvbVBMetaImageFlags enumeration. This must be set to + zero if the vbmeta image is not a top-level image. + release_string: The release string from avbtool, e.g. "avbtool 1.0.0" or + "avbtool 1.0.0 xyz_board Git-234abde89". Is guaranteed to be NUL + terminated. Applications must not make assumptions about how this + string is formatted. """ SIZE = 256 @@ -2134,7 +3041,8 @@ class Avb(object): raise AvbError('Hash-tree and FEC data must be adjacent.') zero_fec_start_offset = ht_desc.fec_offset zero_fec_num_bytes = ht_desc.fec_size - zero_end_offset = zero_ht_start_offset + zero_ht_num_bytes + zero_fec_num_bytes + zero_end_offset = (zero_ht_start_offset + zero_ht_num_bytes + + zero_fec_num_bytes) image.seek(zero_end_offset) data = image.read(image.image_size - zero_end_offset) @@ -2172,7 +3080,7 @@ class Avb(object): 'block size {}.'.format(partition_size, image.block_size)) - (footer, vbmeta_header, descriptors, _) = self._parse_image(image) + (footer, _, _, _) = self._parse_image(image) if not footer: raise AvbError('Given image does not have a footer.') @@ -2182,7 +3090,8 @@ class Avb(object): vbmeta_end_offset = footer.vbmeta_offset + footer.vbmeta_size if vbmeta_end_offset % image.block_size != 0: - vbmeta_end_offset += image.block_size - (vbmeta_end_offset % image.block_size) + vbmeta_end_offset += image.block_size - (vbmeta_end_offset + % image.block_size) if partition_size < vbmeta_end_offset + 1*image.block_size: raise AvbError('Requested size of {} is too small for an image ' @@ -2288,21 +3197,62 @@ class Avb(object): if num_printed == 0: o.write(' (none)\n') - def verify_image(self, image_filename, key_path, expected_chain_partitions, follow_chain_partitions, - accept_zeroed_hashtree): + def info_image_icp(self, image_filename, output): + """Implements the 'info_image_icp' command. + + Arguments: + image_filename: Image file to get information from. + output: Output file to write human-readable information to (file object). + """ + image = ImageHandler(image_filename) + o = output + (footer, header, _, _) = self._parse_image(image) + + offset = 0 + if footer: + offset = footer.vbmeta_offset + image.seek(offset + + header.SIZE + + header.authentication_data_block_size + + header.auxiliary_data_block_size) + + # TODO(jpm): Fix up AvbIcp* records so the length of data to be read + # can be determined more easily. + icp_bytes = image.read(100000) + if not icp_bytes or len(icp_bytes) < 4 or icp_bytes[0:4] != AvbIcpHeader.MAGIC: + sys.stderr.write('Image does not contain AFTL inclusion proofs.\n') + return + + icp_blob = AvbIcpBlob(icp_bytes) + o.write('Android Firmware Transparency Descriptor:\n') + o.write(' Header:\n') + icp_blob.icp_header.print_desc(o) + for i, icp_entry in enumerate(icp_blob.icp_entries): + o.write(' Entry #{}:\n'.format(i + 1)) + icp_entry.print_desc(o) + o.write(' Signed Root Blob:\n') + icp_entry.signed_root_blob.print_desc(o) + + def verify_image(self, image_filename, key_path, expected_chain_partitions, + follow_chain_partitions, accept_zeroed_hashtree): """Implements the 'verify_image' command. Arguments: image_filename: Image file to get information from (file object). - key_path: None or check that embedded public key matches key at given path. + key_path: None or check that embedded public key matches key at given + path. expected_chain_partitions: List of chain partitions to check or None. - follow_chain_partitions: If True, will follows chain partitions even when not - specified with the --expected_chain_partition option - accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is zeroed out. + follow_chain_partitions: + If True, will follows chain partitions even when not specified with + the --expected_chain_partition option + accept_zeroed_hashtree: If True, don't fail if hashtree or FEC data is + zeroed out. + + Raises: + AvbError: If verification of the image fails. """ expected_chain_partitions_map = {} if expected_chain_partitions: - used_locations = {} for cp in expected_chain_partitions: cp_tokens = cp.split(':') if len(cp_tokens) != 3: @@ -2311,27 +3261,32 @@ class Avb(object): rollback_index_location = int(cp_tokens[1]) file_path = cp_tokens[2] pk_blob = open(file_path).read() - expected_chain_partitions_map[partition_name] = (rollback_index_location, pk_blob) + expected_chain_partitions_map[partition_name] = ( + rollback_index_location, pk_blob) image_dir = os.path.dirname(image_filename) - image_ext = os.path.splitext(image_filename)[1] + #image_ext = os.path.splitext(image_filename)[1] + image_ext = image_filename[image_filename.index('.'):] key_blob = None if key_path: - print 'Verifying image {} using key at {}'.format(image_filename, key_path) + print('Verifying image {} using key at {}'.format(image_filename, + key_path)) key_blob = encode_rsa_key(key_path) else: - print 'Verifying image {} using embedded public key'.format(image_filename) + print('Verifying image {} using embedded public key'.format( + image_filename)) image = ImageHandler(image_filename) - (footer, header, descriptors, image_size) = self._parse_image(image) + (footer, header, descriptors, _) = self._parse_image(image) offset = 0 if footer: offset = footer.vbmeta_offset image.seek(offset) - vbmeta_blob = image.read(header.SIZE + header.authentication_data_block_size + - header.auxiliary_data_block_size) + vbmeta_blob = image.read(header.SIZE + + header.authentication_data_block_size + + header.auxiliary_data_block_size) alg_name, _ = lookup_algorithm_by_type(header.algorithm_type) if not verify_vbmeta_signature(header, vbmeta_blob): @@ -2343,36 +3298,41 @@ class Avb(object): key_offset = AvbVBMetaHeader.SIZE key_offset += header.authentication_data_block_size key_offset += header.public_key_offset - key_blob_in_vbmeta = vbmeta_blob[key_offset:key_offset + header.public_key_size] + key_blob_in_vbmeta = vbmeta_blob[key_offset:key_offset + + header.public_key_size] if key_blob != key_blob_in_vbmeta: raise AvbError('Embedded public key does not match given key.') if footer: - print ('vbmeta: Successfully verified footer and {} vbmeta struct in {}' - .format(alg_name, image.filename)) + print('vbmeta: Successfully verified footer and {} vbmeta struct in {}' + .format(alg_name, image.filename)) else: - print ('vbmeta: Successfully verified {} vbmeta struct in {}' - .format(alg_name, image.filename)) + print('vbmeta: Successfully verified {} vbmeta struct in {}' + .format(alg_name, image.filename)) for desc in descriptors: - if (isinstance(desc, AvbChainPartitionDescriptor) and follow_chain_partitions and - expected_chain_partitions_map.get(desc.partition_name) == None): + if (isinstance(desc, AvbChainPartitionDescriptor) + and follow_chain_partitions + and expected_chain_partitions_map.get(desc.partition_name) is None): # In this case we're processing a chain descriptor but don't have a # --expect_chain_partition ... however --follow_chain_partitions was # specified so we shouldn't error out in desc.verify(). - print ('{}: Chained but ROLLBACK_SLOT (which is {}) and KEY (which has sha1 {}) not specified' + print('{}: Chained but ROLLBACK_SLOT (which is {}) ' + 'and KEY (which has sha1 {}) not specified' .format(desc.partition_name, desc.rollback_index_location, hashlib.sha1(desc.public_key).hexdigest())) - else: - if not desc.verify(image_dir, image_ext, expected_chain_partitions_map, image, - accept_zeroed_hashtree): - raise AvbError('Error verifying descriptor.') - # Honor --follow_chain_partitions - add '--' to make the output more readable. - if isinstance(desc, AvbChainPartitionDescriptor) and follow_chain_partitions: - print '--' - chained_image_filename = os.path.join(image_dir, desc.partition_name + image_ext) - self.verify_image(chained_image_filename, key_path, None, False, accept_zeroed_hashtree) - + elif not desc.verify(image_dir, image_ext, expected_chain_partitions_map, + image, accept_zeroed_hashtree): + raise AvbError('Error verifying descriptor.') + # Honor --follow_chain_partitions - add '--' to make the output more + # readable. + if (isinstance(desc, AvbChainPartitionDescriptor) + and follow_chain_partitions): + print('--') + chained_image_filename = os.path.join(image_dir, + desc.partition_name + image_ext) + self.verify_image(chained_image_filename, key_path, None, False, + accept_zeroed_hashtree) def calculate_vbmeta_digest(self, image_filename, hash_algorithm, output): """Implements the 'calculate_vbmeta_digest' command. @@ -2387,7 +3347,7 @@ class Avb(object): image_ext = os.path.splitext(image_filename)[1] image = ImageHandler(image_filename) - (footer, header, descriptors, image_size) = self._parse_image(image) + (footer, header, descriptors, _) = self._parse_image(image) offset = 0 if footer: offset = footer.vbmeta_offset @@ -2401,9 +3361,10 @@ class Avb(object): for desc in descriptors: if isinstance(desc, AvbChainPartitionDescriptor): - ch_image_filename = os.path.join(image_dir, desc.partition_name + image_ext) + ch_image_filename = os.path.join(image_dir, + desc.partition_name + image_ext) ch_image = ImageHandler(ch_image_filename) - (ch_footer, ch_header, ch_descriptors, ch_image_size) = self._parse_image(ch_image) + (ch_footer, ch_header, _, _) = self._parse_image(ch_image) ch_offset = 0 ch_size = (ch_header.SIZE + ch_header.authentication_data_block_size + ch_header.auxiliary_data_block_size) @@ -2414,8 +3375,7 @@ class Avb(object): hasher.update(ch_vbmeta_blob) digest = hasher.digest() - output.write('{}\n'.format(digest.encode('hex'))) - + output.write('{}\n'.format(binascii.hexlify(digest))) def calculate_kernel_cmdline(self, image_filename, hashtree_disabled, output): """Implements the 'calculate_kernel_cmdline' command. @@ -2435,7 +3395,8 @@ class Avb(object): cmdline_descriptors = [] for desc in descriptors: if isinstance(desc, AvbChainPartitionDescriptor): - ch_image_filename = os.path.join(image_dir, desc.partition_name + image_ext) + ch_image_filename = os.path.join(image_dir, + desc.partition_name + image_ext) ch_image = ImageHandler(ch_image_filename) _, _, ch_descriptors, _ = self._parse_image(ch_image) for ch_desc in ch_descriptors: @@ -2447,17 +3408,19 @@ class Avb(object): kernel_cmdline_snippets = [] for desc in cmdline_descriptors: use_cmdline = True - if (desc.flags & AvbKernelCmdlineDescriptor.FLAGS_USE_ONLY_IF_HASHTREE_NOT_DISABLED) != 0: + if ((desc.flags & + AvbKernelCmdlineDescriptor.FLAGS_USE_ONLY_IF_HASHTREE_NOT_DISABLED) + != 0): if hashtree_disabled: use_cmdline = False - if (desc.flags & AvbKernelCmdlineDescriptor.FLAGS_USE_ONLY_IF_HASHTREE_DISABLED) != 0: + if (desc.flags & + AvbKernelCmdlineDescriptor.FLAGS_USE_ONLY_IF_HASHTREE_DISABLED) != 0: if not hashtree_disabled: use_cmdline = False if use_cmdline: kernel_cmdline_snippets.append(desc.kernel_cmdline) output.write(' '.join(kernel_cmdline_snippets)) - def _parse_image(self, image): """Gets information about an image. @@ -2545,14 +3508,14 @@ class Avb(object): c = 'dm="1 vroot none ro 1,' c += '0' # start - c += ' {}'.format((ht.image_size / 512)) # size (# sectors) + c += ' {}'.format((ht.image_size // 512)) # size (# sectors) c += ' verity {}'.format(ht.dm_verity_version) # type and version c += ' PARTUUID=$(ANDROID_SYSTEM_PARTUUID)' # data_dev c += ' PARTUUID=$(ANDROID_SYSTEM_PARTUUID)' # hash_dev c += ' {}'.format(ht.data_block_size) # data_block c += ' {}'.format(ht.hash_block_size) # hash_block - c += ' {}'.format(ht.image_size / ht.data_block_size) # #blocks - c += ' {}'.format(ht.image_size / ht.data_block_size) # hash_offset + c += ' {}'.format(ht.image_size // ht.data_block_size) # #blocks + c += ' {}'.format(ht.image_size // ht.data_block_size) # hash_offset c += ' {}'.format(ht.hash_algorithm) # hash_alg c += ' {}'.format(str(ht.root_digest).encode('hex')) # root_digest c += ' {}'.format(str(ht.salt).encode('hex')) # salt @@ -2565,8 +3528,8 @@ class Avb(object): # Note that fec_blocks is the size that FEC covers, *not* the # size of the FEC data. Since we use FEC for everything up until # the FEC data, it's the same as the offset. - c += ' fec_blocks {}'.format(ht.fec_offset/ht.data_block_size) - c += ' fec_start {}'.format(ht.fec_offset/ht.data_block_size) + c += ' fec_blocks {}'.format(ht.fec_offset // ht.data_block_size) + c += ' fec_start {}'.format(ht.fec_offset // ht.data_block_size) else: c += ' 2' # number of optional args c += ' $(ANDROID_VERITY_MODE)' @@ -2617,6 +3580,73 @@ class Avb(object): return self._get_cmdline_descriptors_for_hashtree_descriptor(ht) + # TODO(danielaustin): Add unit tests. + def request_inclusion_proof(self, transparency_log, vbmeta_blob, + version_inc, manufacturer_key_path): + """Packages and sends a request to the specified transparency log. + + Arguments: + transparency_log: String containing the URL of a transparency log server. + vbmeta_blob: A bytearray with the vbmeta blob. + version_inc: Subcomponent of the build fingerprint. + manufacturer_key_path: Path to key used to sign messages sent to the + transparency log servers. + + Returns: + An AvbIcpEntry with the inclusion proof for the log entry. + + Raises: + AvbError: If grpc or the proto modules cannot be loaded, if there is an + error communicating with the log or if the manufacturer_key_path + cannot be decoded. + """ + # Import grpc and proto.api_pb2_grpc now to avoid global dependencies. + try: + import grpc + import proto.api_pb2_grpc + except ImportError as e: + err_str = 'grpc can be installed with python pip install grpcio.\n' + raise AvbError('Failed to import module: ({}).\n{}'.format(e, err_str)) + + # Set up the gRPC channel with the transparency log. + sys.stdout.write('Preparing to request inclusion proof from {}. This could ' + 'take ~30 seconds for the process to complete.\n'.format( + transparency_log)) + channel = grpc.insecure_channel(transparency_log) + stub = proto.api_pb2_grpc.AFTLogStub(channel) + + # Calculate the hash of the vbmeta image. + hasher = hashlib.sha256() + hasher.update(vbmeta_blob) + vbmeta_hash = hasher.digest() + # Extract the key data from the PEM file. + manufacturer_key_data = rsa_key_read_pem_bytes(manufacturer_key_path) + # Calculate the hash of the manufacturer key data. + hasher = hashlib.sha256() + hasher.update(manufacturer_key_data) + m_key_hash = hasher.digest() + # Create an AddFirmwareInfoRequest protobuf for transmission to the + # transparency log. + fw_info = proto.aftl_pb2.FirmwareInfo(vbmeta_hash=vbmeta_hash, + version_incremental=version_inc, + manufacturer_key_hash=m_key_hash) + # TODO(danielaustin): Sign the message with the manufacturer key. + sfw_info = proto.aftl_pb2.SignedFirmwareInfo(info=fw_info) + request = proto.api_pb2.AddFirmwareInfoRequest(vbmeta=bytes( + str(vbmeta_blob)), fw_info=sfw_info) + # Attempt to transmit to the transparency log. + try: + # TODO(danielaustin): Set a reasonable timeout deadline here. + sys.stdout.write('ICP is about to be requested from transparency log ' + 'with domain {}.\n'.format(transparency_log)) + response = stub.AddFirmwareInfo(request) + except grpc.RpcError as e: + raise AvbError('Error: grpc failure ({})'.format(e)) + # Return an AvbIcpEntry representing this response. + icp_entry = AvbIcpEntry() + icp_entry.translate_response(transparency_log, response) + return icp_entry + def make_vbmeta_image(self, output, chain_partitions, algorithm_name, key_path, public_key_metadata_path, rollback_index, flags, props, props_from_file, kernel_cmdlines, @@ -2664,10 +3694,10 @@ class Avb(object): (_, image_header, _, _) = self._parse_image(ImageHandler(image.name)) tmp_header.bump_required_libavb_version_minor( image_header.required_libavb_version_minor) - print '1.{}'.format(tmp_header.required_libavb_version_minor) + print('1.{}'.format(tmp_header.required_libavb_version_minor)) else: # Descriptors aside, all vbmeta features are supported in 1.0. - print '1.0' + print('1.0') return if not output: @@ -2692,6 +3722,147 @@ class Avb(object): padding_needed = padded_size - len(vbmeta_blob) output.write('\0' * padding_needed) + def make_icp_from_vbmeta(self, vbmeta_image_path, output, algorithm, + signing_helper, signing_helper_with_files, + version_incremental, transparency_log_servers, + transparency_log_pub_keys, manufacturer_key, + padding_size): + """Generates a vbmeta image with inclusion proof given a vbmeta image. + + This blob (struct AvbIcpBlob) contains the information required to + validate an inclusion proof for a specific vbmeta image. It consists + of a header (struct AvbIcpHeader) and zero or more entry structures + (struct AvbIcpEntry) that contain the vbmeta leaf hash, tree size, + root hash, inclusion proof hashes, and the signature for the root hash. + + The vbmeta image, its hash, the version_incremental part of the build + fingerprint, and the hash of the manufacturer key are sent to the + transparency log, with the message signed by the manufacturer key. + An inclusion proof is calculated and returned. This inclusion proof is + then packaged in a AvbIcpBlob structure. The existing vbmeta data is + copied to a new file, appended with the AvbIcpBlob data, and written to + output. Validation of the inclusion proof does not require + communication with the transparency log. + + Arguments: + vbmeta_image_path: Path to a vbmeta image file. + output: File to write the results to. + algorithm: The algorithm ID for signing and hashing (see ALGORITHMS). This + will be used for hash and signature size calculation and padding. + signing_helper: Program which signs a hash and returns a signature. + signing_helper_with_files: Same as signing_helper but uses files instead. + version_incremental: A string representing the subcomponent of the + build fingerprint used to identify the vbmeta in the transparency log. + transparency_log_servers: List of strings containing URLs of transparency + log servers where inclusion proofs are requested from. + transparency_log_pub_keys: List of paths to PEM files containing trusted + public keys that correspond with the transparency_logs. There must be + the same number of keys as log servers and they must be in the same + order, that is, transparency_log_pub_keys[n] corresponds to + transparency_log_servers[n]. + manufacturer_key: Path to PEM file containting the key file used to sign + messages sent to the transparency log servers. + padding_size: If not 0, pads output so size is a multiple of the number. + + Returns: + True if the inclusion proofs could be fetched from the transparency log + servers and could be successfully validated, False otherwise. + + Raises: + AvbError: If any parameters are invalid, communication with the log + fails or the structures are malformed. + """ + # TODO(danielaustin): Determine the best way to handle chained vbmeta + # structures. Currently, we only put the main one in the transparency + # log. + + # Validates command line parameters. + if not vbmeta_image_path: + raise AvbError('No vbmeta image path found.') + if not transparency_log_servers: + raise AvbError('No transparency log servers given.') + if not transparency_log_pub_keys: + raise AvbError('No transparency log public keys given.') + if len(transparency_log_servers) != len(transparency_log_pub_keys): + raise AvbError('Transparency log count and public key count mismatch: ' + '{} servers and {} public keys'.format( + len(transparency_log_servers), + len(transparency_log_pub_keys))) + if not manufacturer_key: + raise AvbError('No manufacturer key path given.') + + # TODO(danielaustin): add support for signing_helper and + # signing_helper_with_files + if signing_helper is not None or signing_helper_with_files is not None: + raise AvbError('signing_helper support not yet implemented for ICP.') + + try: + algorithm_id = ALGORITHMS[algorithm].algorithm_type + except KeyError: + raise AvbError('Unknown algorithm with name {}'.format(algorithm)) + + # Retrieves vbmeta structure from given partition image. + image = ImageHandler(vbmeta_image_path) + (footer, header, _, _) = self._parse_image(image) + offset = 0 + if footer: + offset = footer.vbmeta_offset + image.seek(offset) + vbmeta_blob = image.read(header.SIZE + + header.authentication_data_block_size + + header.auxiliary_data_block_size) + + # Fetches inclusion proofs for vbmeta structure from all transparency logs. + icp_entries = [] + for i, transparency_log in enumerate(transparency_log_servers): + try: + icp_entry = self.request_inclusion_proof(transparency_log, vbmeta_blob, + version_incremental, + manufacturer_key) + if not icp_entry.verify_icp(transparency_log_pub_keys[i]): + sys.stderr.write('The ICP from {} could not be verified\n'.format( + transparency_log)) + icp_entries.append(icp_entry) + except AvbError as e: + sys.stderr.write('AvbError: {}'.format(e)) + # The inclusion proof request failed. + # Continue and see if another will succeed. + continue + if not icp_entries: + sys.stderr.write('No inclusion proofs could be validated from any log.\n') + return False + + # Prepares the inclusion proof blob to be appended to the vbmeta image. + icp_blob = AvbIcpBlob() + icp_blob.set_algorithm(algorithm_id) + for icp_entry in icp_entries: + icp_blob.add_icp_entry(icp_entry) + if not icp_blob.is_valid(): + sys.stderr.write('Resulting AvbIcpBlob structure is malformed\n.') + return False + + # Write the original vbmeta blob, followed by the AvbIcpBlob. + if footer: # Checks if it is a chained partition. + # TODO(danielaustin): Add support for chained partitions like system.img + # using similar functionality as implemented in append_vbmeta_image(). + sys.stderr.write('Image has a footer and ICP for this format is not ' + 'implemented.') + return False + + # Writes vbmeta image with inclusion proof into a new vbmeta image. + output.seek(0) + output.write(vbmeta_blob) + encoded_icp_blob = icp_blob.encode() + output.write(encoded_icp_blob) + + if padding_size > 0: + blob_size = len(vbmeta_blob) + len(encoded_icp_blob) + padded_size = round_to_multiple(blob_size, padding_size) + padding_needed = padded_size - blob_size + output.write('\0' * padding_needed) + + return True + def _generate_vbmeta_blob(self, algorithm_name, key_path, public_key_metadata_path, descriptors, chain_partitions, @@ -2791,6 +3962,7 @@ class Avb(object): idx = prop.find(':') if idx == -1: raise AvbError('Malformed property "{}".'.format(prop)) + # pylint: disable=redefined-variable-type desc = AvbPropertyDescriptor() desc.key = prop[0:idx] desc.value = prop[(idx + 1):] @@ -2850,7 +4022,7 @@ class Avb(object): descriptors_dict[key] = desc.encode() else: encoded_descriptors.extend(desc.encode()) - for key in sorted(descriptors_dict.keys()): + for key in sorted(descriptors_dict): encoded_descriptors.extend(descriptors_dict[key]) # Load public key metadata blob, if requested. @@ -2871,6 +4043,7 @@ class Avb(object): algorithm_name)) # Override release string, if requested. + # pylint: disable=unicode-builtin if isinstance(release_string, (str, unicode)): h.release_string = release_string @@ -3088,7 +4261,7 @@ class Avb(object): # If we're asked to calculate minimum required libavb version, we're done. if print_required_libavb_version: - print '1.{}'.format(required_libavb_version_minor) + print('1.{}'.format(required_libavb_version_minor)) return # First, calculate the maximum image size such that an image @@ -3103,7 +4276,7 @@ class Avb(object): # If we're asked to only calculate the maximum image size, we're done. if calc_max_image_size: - print '{}'.format(max_image_size) + print('{}'.format(max_image_size)) return image = ImageHandler(image_filename) @@ -3141,16 +4314,15 @@ class Avb(object): digest_size = len(hashlib.new(name=hash_algorithm).digest()) if salt: - salt = salt.decode('hex') + salt = binascii.unhexlify(salt) + elif salt is None and not use_persistent_digest: + # If salt is not explicitly specified, choose a hash that's the same + # size as the hash size. Don't populate a random salt if this + # descriptor is being created to use a persistent digest on device. + hash_size = digest_size + salt = open('/dev/urandom').read(hash_size) else: - if salt is None and not use_persistent_digest: - # If salt is not explicitly specified, choose a hash that's the same - # size as the hash size. Don't populate a random salt if this - # descriptor is being created to use a persistent digest on device. - hash_size = digest_size - salt = open('/dev/urandom').read(hash_size) - else: - salt = '' + salt = '' hasher = hashlib.new(name=hash_algorithm, string=salt) # TODO(zeuthen): might want to read this in chunks to avoid @@ -3244,7 +4416,8 @@ class Avb(object): release_string, append_to_release_string, output_vbmeta_image, do_not_append_vbmeta_image, print_required_libavb_version, - use_persistent_root_digest, do_not_use_ab): + use_persistent_root_digest, do_not_use_ab, + no_hashtree): """Implements the 'add_hashtree_footer' command. See https://gitlab.com/cryptsetup/cryptsetup/wikis/DMVerity for @@ -3286,6 +4459,7 @@ class Avb(object): print_required_libavb_version: True to only print required libavb version. use_persistent_root_digest: Use a persistent root digest on device. do_not_use_ab: The partition does not use A/B. + no_hashtree: Do not append hashtree. Set size in descriptor as zero. Raises: AvbError: If an argument is incorrect. @@ -3297,7 +4471,7 @@ class Avb(object): # If we're asked to calculate minimum required libavb version, we're done. if print_required_libavb_version: - print '1.{}'.format(required_libavb_version_minor) + print('1.{}'.format(required_libavb_version_minor)) return digest_size = len(hashlib.new(name=hash_algorithm).digest()) @@ -3308,11 +4482,13 @@ class Avb(object): # vbmeta struct) fits in |partition_size|. We use very conservative figures # for metadata. if partition_size > 0: - (_, max_tree_size) = calc_hash_level_offsets( - partition_size, block_size, digest_size + digest_padding) + max_tree_size = 0 max_fec_size = 0 - if generate_fec: - max_fec_size = calc_fec_data_size(partition_size, fec_num_roots) + if not no_hashtree: + (_, max_tree_size) = calc_hash_level_offsets( + partition_size, block_size, digest_size + digest_padding) + if generate_fec: + max_fec_size = calc_fec_data_size(partition_size, fec_num_roots) max_metadata_size = (max_fec_size + max_tree_size + self.MAX_VBMETA_SIZE + self.MAX_FOOTER_SIZE) @@ -3322,7 +4498,7 @@ class Avb(object): # If we're asked to only calculate the maximum image size, we're done. if calc_max_image_size: - print '{}'.format(max_image_size) + print('{}'.format(max_image_size)) return image = ImageHandler(image_filename) @@ -3332,11 +4508,10 @@ class Avb(object): raise AvbError('Partition size of {} is not a multiple of the image ' 'block size {}.'.format(partition_size, image.block_size)) - else: - if image.image_size % image.block_size != 0: - raise AvbError('File size of {} is not a multiple of the image ' - 'block size {}.'.format(image.image_size, - image.block_size)) + elif image.image_size % image.block_size != 0: + raise AvbError('File size of {} is not a multiple of the image ' + 'block size {}.'.format(image.image_size, + image.block_size)) # If there's already a footer, truncate the image to its original # size. This way 'avbtool add_hashtree_footer' is idempotent @@ -3371,16 +4546,15 @@ class Avb(object): partition_size)) if salt: - salt = salt.decode('hex') + salt = binascii.unhexlify(salt) + elif salt is None and not use_persistent_root_digest: + # If salt is not explicitly specified, choose a hash that's the same + # size as the hash size. Don't populate a random salt if this + # descriptor is being created to use a persistent digest on device. + hash_size = digest_size + salt = open('/dev/urandom').read(hash_size) else: - if salt is None and not use_persistent_root_digest: - # If salt is not explicitly specified, choose a hash that's the same - # size as the hash size. Don't populate a random salt if this - # descriptor is being created to use a persistent digest on device. - hash_size = digest_size - salt = open('/dev/urandom').read(hash_size) - else: - salt = '' + salt = '' # Hashes are stored upside down so we need to calculate hash # offsets in advance. @@ -3405,6 +4579,9 @@ class Avb(object): # Generate HashtreeDescriptor with details about the tree we # just generated. + if no_hashtree: + tree_size = 0 + hash_tree = bytearray() ht_desc = AvbHashtreeDescriptor() ht_desc.dm_verity_version = 1 ht_desc.image_size = image.image_size @@ -3429,7 +4606,10 @@ class Avb(object): # Generate FEC codes, if requested. if generate_fec: - fec_data = generate_fec_data(image_filename, fec_num_roots) + if no_hashtree: + fec_data = bytearray() + else: + fec_data = generate_fec_data(image_filename, fec_num_roots) padding_needed = (round_to_multiple(len(fec_data), image.block_size) - len(fec_data)) fec_data_with_padding = fec_data + '\0'*padding_needed @@ -3530,7 +4710,7 @@ class Avb(object): hasher = hashlib.sha256() hasher.update(usage) signed_data.extend(hasher.digest()) - if not subject_key_version: + if subject_key_version is None: subject_key_version = int(time.time()) signed_data.extend(struct.pack(' block_size: - num_blocks = (size + block_size - 1) / block_size + num_blocks = (size + block_size - 1) // block_size level_size = round_to_multiple(num_blocks * digest_size, block_size) level_sizes.append(level_size) @@ -3709,7 +4889,7 @@ FEC_MAGIC = 0xfecfecfe def calc_fec_data_size(image_size, num_roots): """Calculates how much space FEC data will take. - Args: + Arguments: image_size: The size of the image. num_roots: Number of roots. @@ -3735,7 +4915,7 @@ def calc_fec_data_size(image_size, num_roots): def generate_fec_data(image_filename, num_roots): """Generate FEC codes for an image. - Args: + Arguments: image_filename: The filename of the image. num_roots: Number of roots. @@ -3764,7 +4944,7 @@ def generate_hash_tree(image, image_size, block_size, hash_alg_name, salt, digest_padding, hash_level_offsets, tree_size): """Generates a Merkle-tree for a file. - Args: + Arguments: image: The image, as a file. image_size: The size of the image. block_size: The block size, e.g. 4096. @@ -3973,12 +5153,50 @@ class AvbTool(object): sub_parser.add_argument('--padding_size', metavar='NUMBER', help='If non-zero, pads output with NUL bytes so ' - 'its size is a multiple of NUMBER (default: 0)', + 'its size is a multiple of NUMBER ' + '(default: 0)', type=parse_number, default=0) self._add_common_args(sub_parser) sub_parser.set_defaults(func=self.make_vbmeta_image) + sub_parser = subparsers.add_parser('make_icp_from_vbmeta', + help='Makes an ICP enhanced vbmeta image' + ' from an existing vbmeta image.') + sub_parser.add_argument('--output', + help='Output file name.', + type=argparse.FileType('wb'), + default=sys.stdout) + sub_parser.add_argument('--vbmeta_image_path', + help='Path to a generate vbmeta image file.') + sub_parser.add_argument('--version_incremental', help='Current build ID.') + sub_parser.add_argument('--manufacturer_key', + help='Path to the PEM file containing the ' + 'manufacturer key for use with the log.') + sub_parser.add_argument('--transparency_log_servers', + help='List of transparency log servers in ' + 'host:port format. This must not be None and must ' + 'be the same size as transparency_log_pub_keys. ' + 'Also, transparency_log_servers[n] must correspond ' + 'to transparency_log_pub_keys[n] for all values n.', + nargs='*') + sub_parser.add_argument('--transparency_log_pub_keys', + help='Paths to PEM files containing transparency ' + 'log server key(s). This must not be None and must ' + 'be the same size as transparency_log_servers. ' + 'Also, transparency_log_pub_keys[n] must ' + 'correspond to transparency_log_servers[n] for all ' + 'values n.', nargs='*') + sub_parser.add_argument('--padding_size', + metavar='NUMBER', + help='If non-zero, pads output with NUL bytes so ' + 'its size is a multiple of NUMBER ' + '(default: 0)', + type=parse_number, + default=0) + self._add_common_args(sub_parser) + sub_parser.set_defaults(func=self.make_icp_from_vbmeta) + sub_parser = subparsers.add_parser('add_hash_footer', help='Add hashes and footer to image.') sub_parser.add_argument('--image', @@ -4026,8 +5244,9 @@ class AvbTool(object): type=argparse.FileType('rb')) sub_parser.set_defaults(func=self.append_vbmeta_image) - sub_parser = subparsers.add_parser('add_hashtree_footer', - help='Add hashtree and footer to image.') + sub_parser = subparsers.add_parser( + 'add_hashtree_footer', + help='Add hashtree and footer to image.') sub_parser.add_argument('--image', help='Image to add hashtree to', type=argparse.FileType('rab+')) @@ -4055,9 +5274,10 @@ class AvbTool(object): sub_parser.add_argument('--generate_fec', help=argparse.SUPPRESS, action='store_true') - sub_parser.add_argument('--do_not_generate_fec', - help='Do not generate forward-error-correction codes', - action='store_true') + sub_parser.add_argument( + '--do_not_generate_fec', + help='Do not generate forward-error-correction codes', + action='store_true') sub_parser.add_argument('--fec_num_roots', help='Number of roots for FEC (default: 2)', type=parse_number, @@ -4082,6 +5302,9 @@ class AvbTool(object): sub_parser.add_argument('--setup_as_rootfs_from_kernel', action='store_true', help='Adds kernel cmdline for setting up rootfs') + sub_parser.add_argument('--no_hashtree', + action='store_true', + help='Do not append hashtree') self._add_common_args(sub_parser) self._add_common_footer_args(sub_parser) sub_parser.set_defaults(func=self.add_hashtree_footer) @@ -4105,8 +5328,9 @@ class AvbTool(object): required=True) sub_parser.set_defaults(func=self.zero_hashtree) - sub_parser = subparsers.add_parser('extract_vbmeta_image', - help='Extracts vbmeta from an image with a footer.') + sub_parser = subparsers.add_parser( + 'extract_vbmeta_image', + help='Extracts vbmeta from an image with a footer.') sub_parser.add_argument('--image', help='Image with footer', type=argparse.FileType('rb'), @@ -4117,7 +5341,8 @@ class AvbTool(object): sub_parser.add_argument('--padding_size', metavar='NUMBER', help='If non-zero, pads output with NUL bytes so ' - 'its size is a multiple of NUMBER (default: 0)', + 'its size is a multiple of NUMBER ' + '(default: 0)', type=parse_number, default=0) sub_parser.set_defaults(func=self.extract_vbmeta_image) @@ -4146,6 +5371,19 @@ class AvbTool(object): default=sys.stdout) sub_parser.set_defaults(func=self.info_image) + sub_parser = subparsers.add_parser( + 'info_image_icp', + help='Show information about AFTL ICPs in vbmeta or footer.') + sub_parser.add_argument('--image', + help='Image to show information about', + type=argparse.FileType('rb'), + required=True) + sub_parser.add_argument('--output', + help='Write info to file', + type=argparse.FileType('wt'), + default=sys.stdout) + sub_parser.set_defaults(func=self.info_image_icp) + sub_parser = subparsers.add_parser( 'verify_image', help='Verify an image.') @@ -4161,13 +5399,15 @@ class AvbTool(object): help='Expected chain partition', metavar='PART_NAME:ROLLBACK_SLOT:KEY_PATH', action='append') - sub_parser.add_argument('--follow_chain_partitions', - help=('Follows chain partitions even when not ' - 'specified with the --expected_chain_partition option'), - action='store_true') - sub_parser.add_argument('--accept_zeroed_hashtree', - help=('Accept images where the hashtree or FEC data is zeroed out'), - action='store_true') + sub_parser.add_argument( + '--follow_chain_partitions', + help=('Follows chain partitions even when not ' + 'specified with the --expected_chain_partition option'), + action='store_true') + sub_parser.add_argument( + '--accept_zeroed_hashtree', + help=('Accept images where the hashtree or FEC data is zeroed out'), + action='store_true') sub_parser.set_defaults(func=self.verify_image) sub_parser = subparsers.add_parser( @@ -4335,12 +5575,12 @@ class AvbTool(object): try: args.func(args) except AvbError as e: - sys.stderr.write('{}: {}\n'.format(argv[0], e.message)) + sys.stderr.write('{}: {}\n'.format(argv[0], str(e))) sys.exit(1) def version(self, _): """Implements the 'version' sub-command.""" - print get_release_string() + print(get_release_string()) def extract_public_key(self, args): """Implements the 'extract_public_key' sub-command.""" @@ -4363,6 +5603,19 @@ class AvbTool(object): args.print_required_libavb_version, args.padding_size) + def make_icp_from_vbmeta(self, args): + """Implements the 'make_icp_from_vbmeta' sub-command.""" + args = self._fixup_common_args(args) + self.avb.make_icp_from_vbmeta(args.vbmeta_image_path, + args.output, args.algorithm, + args.signing_helper, + args.signing_helper_with_files, + args.version_incremental, + args.transparency_log_servers, + args.transparency_log_pub_keys, + args.manufacturer_key, + args.padding_size) + def append_vbmeta_image(self, args): """Implements the 'append_vbmeta_image' sub-command.""" self.avb.append_vbmeta_image(args.image.name, args.vbmeta_image.name, @@ -4401,29 +5654,31 @@ class AvbTool(object): sys.stderr.write('The --generate_fec option is deprecated since FEC ' 'is now generated by default. Use the option ' '--do_not_generate_fec to not generate FEC.\n') - self.avb.add_hashtree_footer(args.image.name if args.image else None, - args.partition_size, - args.partition_name, - not args.do_not_generate_fec, args.fec_num_roots, - args.hash_algorithm, args.block_size, - args.salt, args.chain_partition, args.algorithm, - args.key, args.public_key_metadata, - args.rollback_index, args.flags, args.prop, - args.prop_from_file, - args.kernel_cmdline, - args.setup_rootfs_from_kernel, - args.setup_as_rootfs_from_kernel, - args.include_descriptors_from_image, - args.calc_max_image_size, - args.signing_helper, - args.signing_helper_with_files, - args.internal_release_string, - args.append_to_release_string, - args.output_vbmeta_image, - args.do_not_append_vbmeta_image, - args.print_required_libavb_version, - args.use_persistent_digest, - args.do_not_use_ab) + self.avb.add_hashtree_footer( + args.image.name if args.image else None, + args.partition_size, + args.partition_name, + not args.do_not_generate_fec, args.fec_num_roots, + args.hash_algorithm, args.block_size, + args.salt, args.chain_partition, args.algorithm, + args.key, args.public_key_metadata, + args.rollback_index, args.flags, args.prop, + args.prop_from_file, + args.kernel_cmdline, + args.setup_rootfs_from_kernel, + args.setup_as_rootfs_from_kernel, + args.include_descriptors_from_image, + args.calc_max_image_size, + args.signing_helper, + args.signing_helper_with_files, + args.internal_release_string, + args.append_to_release_string, + args.output_vbmeta_image, + args.do_not_append_vbmeta_image, + args.print_required_libavb_version, + args.use_persistent_digest, + args.do_not_use_ab, + args.no_hashtree) def erase_footer(self, args): """Implements the 'erase_footer' sub-command.""" @@ -4450,6 +5705,10 @@ class AvbTool(object): """Implements the 'info_image' sub-command.""" self.avb.info_image(args.image.name, args.output) + def info_image_icp(self, args): + """Implements the 'info_image_icp' sub-command.""" + self.avb.info_image_icp(args.image.name, args.output) + def verify_image(self, args): """Implements the 'verify_image' sub-command.""" self.avb.verify_image(args.image.name, args.key, @@ -4464,7 +5723,8 @@ class AvbTool(object): def calculate_kernel_cmdline(self, args): """Implements the 'calculate_kernel_cmdline' sub-command.""" - self.avb.calculate_kernel_cmdline(args.image.name, args.hashtree_disabled, args.output) + self.avb.calculate_kernel_cmdline(args.image.name, args.hashtree_disabled, + args.output) def make_atx_certificate(self, args): """Implements the 'make_atx_certificate' sub-command.""" diff --git a/avb/avbtool.diff b/aosp/avb/avbtool.diff similarity index 100% rename from avb/avbtool.diff rename to aosp/avb/avbtool.diff diff --git a/aosp/boot_signer/src/main/java/BootSignature.java b/aosp/boot_signer/src/main/java/BootSignature.java index 10171c3..28864ce 100644 --- a/aosp/boot_signer/src/main/java/BootSignature.java +++ b/aosp/boot_signer/src/main/java/BootSignature.java @@ -77,6 +77,12 @@ public class BootSignature extends ASN1Object * or equal to 1. */ private static final int BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET = 1632; + /** + * Offset of DTB length in a boot image header of version greater than + * or equal to 2. + */ + private static final int BOOT_IMAGE_HEADER_V2_DTB_SIZE_OFFSET = 1648; + /** * Initializes the object for signing an image file @@ -221,12 +227,16 @@ public class BootSignature extends ASN1Object length += ((recoveryDtboLength + pageSize - 1) / pageSize) * pageSize; image.getLong(); // recovery_dtbo address - if (headerVersion == 1) { - int headerSize = image.getInt(); - if (image.position() != headerSize) { - throw new IllegalArgumentException( - "Invalid image header: invalid header length"); - } + int headerSize = image.getInt(); + if (headerVersion == 2) { + image.position(BOOT_IMAGE_HEADER_V2_DTB_SIZE_OFFSET); + int dtbLength = image.getInt(); + length += ((dtbLength + pageSize - 1) / pageSize) * pageSize; + image.getLong(); // dtb address + } + if (image.position() != headerSize) { + throw new IllegalArgumentException( + "Invalid image header: invalid header length"); } } diff --git a/external/extract_kernel.py b/aosp/build/tools/extract_kernel.py similarity index 97% rename from external/extract_kernel.py rename to aosp/build/tools/extract_kernel.py index 0250712..1770758 100755 --- a/external/extract_kernel.py +++ b/aosp/build/tools/extract_kernel.py @@ -187,7 +187,7 @@ def main(): args.output_configs.write(o) else: sys.stderr.write( - "Cannot extract kernel configs in {}\n".format(args.input.name)) + "Cannot extract kernel configs in {}".format(args.input.name)) ret = 1 if args.output_version is not None: o = decompress_dump(dump_version, input_bytes) @@ -195,7 +195,7 @@ def main(): args.output_version.write(o) else: sys.stderr.write( - "Cannot extract kernel versions in {}\n".format(args.input.name)) + "Cannot extract kernel versions in {}".format(args.input.name)) ret = 1 return ret diff --git a/security/README b/aosp/security/README similarity index 100% rename from security/README rename to aosp/security/README diff --git a/security/media.pk8 b/aosp/security/media.pk8 similarity index 100% rename from security/media.pk8 rename to aosp/security/media.pk8 diff --git a/security/media.x509.pem b/aosp/security/media.x509.pem similarity index 100% rename from security/media.x509.pem rename to aosp/security/media.x509.pem diff --git a/security/platform.pk8 b/aosp/security/platform.pk8 similarity index 100% rename from security/platform.pk8 rename to aosp/security/platform.pk8 diff --git a/security/platform.x509.pem b/aosp/security/platform.x509.pem similarity index 100% rename from security/platform.x509.pem rename to aosp/security/platform.x509.pem diff --git a/security/shared.pk8 b/aosp/security/shared.pk8 similarity index 100% rename from security/shared.pk8 rename to aosp/security/shared.pk8 diff --git a/security/shared.x509.pem b/aosp/security/shared.x509.pem similarity index 100% rename from security/shared.x509.pem rename to aosp/security/shared.x509.pem diff --git a/security/testkey.pk8 b/aosp/security/testkey.pk8 similarity index 100% rename from security/testkey.pk8 rename to aosp/security/testkey.pk8 diff --git a/security/testkey.x509.pem b/aosp/security/testkey.x509.pem similarity index 100% rename from security/testkey.x509.pem rename to aosp/security/testkey.x509.pem diff --git a/security/verity.pk8 b/aosp/security/verity.pk8 similarity index 100% rename from security/verity.pk8 rename to aosp/security/verity.pk8 diff --git a/security/verity.x509.pem b/aosp/security/verity.x509.pem similarity index 100% rename from security/verity.x509.pem rename to aosp/security/verity.x509.pem diff --git a/security/verity_key b/aosp/security/verity_key similarity index 100% rename from security/verity_key rename to aosp/security/verity_key diff --git a/external/mkdtboimg.py b/aosp/system/libufdt/utils/src/mkdtboimg.py similarity index 100% rename from external/mkdtboimg.py rename to aosp/system/libufdt/utils/src/mkdtboimg.py diff --git a/external/mkbootimg b/aosp/system/tools/mkbootimg/mkbootimg.py similarity index 63% rename from external/mkbootimg rename to aosp/system/tools/mkbootimg/mkbootimg.py index e38c106..4733107 100755 --- a/external/mkbootimg +++ b/aosp/system/tools/mkbootimg/mkbootimg.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python # Copyright 2015, The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,13 +14,15 @@ # limitations under the License. from __future__ import print_function -from sys import argv, exit, stderr + from argparse import ArgumentParser, FileType, Action -from os import fstat -from struct import pack from hashlib import sha1 -import sys +from os import fstat import re +from struct import pack + + +BOOT_IMAGE_HEADER_V3_PAGESIZE = 4096 def filesize(f): if f is None: @@ -61,18 +63,61 @@ def get_recovery_dtbo_offset(args): return dtbo_offset +def write_header_v3(args): + BOOT_IMAGE_HEADER_V3_SIZE = 1596 + BOOT_MAGIC = 'ANDROID!'.encode() + + args.output.write(pack('8s', BOOT_MAGIC)) + args.output.write(pack( + '4I', + filesize(args.kernel), # kernel size in bytes + filesize(args.ramdisk), # ramdisk size in bytes + (args.os_version << 11) | args.os_patch_level, # os version and patch level + BOOT_IMAGE_HEADER_V3_SIZE)) + + args.output.write(pack('4I', 0, 0, 0, 0)) # reserved + + args.output.write(pack('I', args.header_version)) # version of bootimage header + args.output.write(pack('1536s', args.cmdline.encode())) + pad_file(args.output, BOOT_IMAGE_HEADER_V3_PAGESIZE) + +def write_vendor_boot_header(args): + VENDOR_BOOT_IMAGE_HEADER_V3_SIZE = 2108 + BOOT_MAGIC = 'VNDRBOOT'.encode() + + args.vendor_boot.write(pack('8s', BOOT_MAGIC)) + args.vendor_boot.write(pack( + '5I', + args.header_version, # version of header + args.pagesize, # flash page size we assume + args.base + args.kernel_offset, # kernel physical load addr + args.base + args.ramdisk_offset, # ramdisk physical load addr + filesize(args.vendor_ramdisk))) # vendor ramdisk size in bytes + args.vendor_boot.write(pack('2048s', args.vendor_cmdline.encode())) + args.vendor_boot.write(pack('I', args.base + args.tags_offset)) # physical addr for kernel tags + args.vendor_boot.write(pack('16s', args.board.encode())) # asciiz product name + args.vendor_boot.write(pack('I', VENDOR_BOOT_IMAGE_HEADER_V3_SIZE)) # header size in bytes + if filesize(args.dtb) == 0: + raise ValueError("DTB image must not be empty.") + args.vendor_boot.write(pack('I', filesize(args.dtb))) # size in bytes + args.vendor_boot.write(pack('Q', args.base + args.dtb_offset)) # dtb physical load address + pad_file(args.vendor_boot, args.pagesize) + def write_header(args): BOOT_IMAGE_HEADER_V1_SIZE = 1648 BOOT_IMAGE_HEADER_V2_SIZE = 1660 BOOT_MAGIC = 'ANDROID!'.encode() - if (args.header_version > 2): + if args.header_version > 3: raise ValueError('Boot header version %d not supported' % args.header_version) + elif args.header_version == 3: + return write_header_v3(args) args.output.write(pack('8s', BOOT_MAGIC)) final_ramdisk_offset = (args.base + args.ramdisk_offset) if filesize(args.ramdisk) > 0 else 0 final_second_offset = (args.base + args.second_offset) if filesize(args.second) > 0 else 0 - args.output.write(pack('10I', + args.output.write(pack( + '10I', filesize(args.kernel), # size in bytes args.base + args.kernel_offset, # physical load addr filesize(args.ramdisk), # size in bytes @@ -135,8 +180,8 @@ class ValidateStrLenAction(Action): def __call__(self, parser, namespace, values, option_string=None): if len(values) > self.maxlen: - raise ValueError('String argument too long: max {0:d}, got {1:d}'. - format(self.maxlen, len(values))) + raise ValueError( + 'String argument too long: max {0:d}, got {1:d}'.format(self.maxlen, len(values))) setattr(namespace, self.dest, values) @@ -150,6 +195,7 @@ def write_padded_file(f_out, f_in, padding): def parse_int(x): return int(x, 0) + def parse_os_version(x): match = re.search(r'^(\d{1,3})(?:\.(\d{1,3})(?:\.(\d{1,3}))?)?', x) if match: @@ -166,33 +212,40 @@ def parse_os_version(x): return (a << 14) | (b << 7) | c return 0 + def parse_os_patch_level(x): match = re.search(r'^(\d{4})-(\d{2})-(\d{2})', x) if match: y = int(match.group(1)) - 2000 m = int(match.group(2)) # 7 bits allocated for the year, 4 bits for the month - assert y >= 0 and y < 128 - assert m > 0 and m <= 12 + assert 0 <= y < 128 + assert 0 < m <= 12 return (y << 4) | m return 0 + def parse_cmdline(): parser = ArgumentParser() - parser.add_argument('--kernel', help='path to the kernel', type=FileType('rb'), - required=True) + parser.add_argument('--kernel', help='path to the kernel', type=FileType('rb')) parser.add_argument('--ramdisk', help='path to the ramdisk', type=FileType('rb')) parser.add_argument('--second', help='path to the 2nd bootloader', type=FileType('rb')) parser.add_argument('--dtb', help='path to dtb', type=FileType('rb')) recovery_dtbo_group = parser.add_mutually_exclusive_group() - recovery_dtbo_group.add_argument('--recovery_dtbo', help='path to the recovery DTBO', type=FileType('rb')) + recovery_dtbo_group.add_argument('--recovery_dtbo', help='path to the recovery DTBO', + type=FileType('rb')) recovery_dtbo_group.add_argument('--recovery_acpio', help='path to the recovery ACPIO', - type=FileType('rb'), metavar='RECOVERY_ACPIO', dest='recovery_dtbo') + type=FileType('rb'), metavar='RECOVERY_ACPIO', + dest='recovery_dtbo') parser.add_argument('--cmdline', help='extra arguments to be passed on the ' 'kernel command line', default='', action=ValidateStrLenAction, maxlen=1536) + parser.add_argument('--vendor_cmdline', + help='kernel command line arguments contained in vendor boot', + default='', action=ValidateStrLenAction, maxlen=2048) parser.add_argument('--base', help='base address', type=parse_int, default=0x10000000) parser.add_argument('--kernel_offset', help='kernel offset', type=parse_int, default=0x00008000) - parser.add_argument('--ramdisk_offset', help='ramdisk offset', type=parse_int, default=0x01000000) + parser.add_argument('--ramdisk_offset', help='ramdisk offset', type=parse_int, + default=0x01000000) parser.add_argument('--second_offset', help='2nd bootloader offset', type=parse_int, default=0x00f00000) parser.add_argument('--dtb_offset', help='dtb offset', type=parse_int, default=0x01f00000) @@ -205,34 +258,59 @@ def parse_cmdline(): parser.add_argument('--board', help='board name', default='', action=ValidateStrLenAction, maxlen=16) parser.add_argument('--pagesize', help='page size', type=parse_int, - choices=[2**i for i in range(11,15)], default=2048) + choices=[2**i for i in range(11, 15)], default=2048) parser.add_argument('--id', help='print the image ID on standard output', action='store_true') - parser.add_argument('--header_version', help='boot image header version', type=parse_int, default=0) - parser.add_argument('-o', '--output', help='output file name', type=FileType('wb'), - required=True) + parser.add_argument('--header_version', help='boot image header version', type=parse_int, + default=0) + parser.add_argument('-o', '--output', help='output file name', type=FileType('wb')) + parser.add_argument('--vendor_boot', help='vendor boot output file name', type=FileType('wb')) + parser.add_argument('--vendor_ramdisk', help='path to the vendor ramdisk', type=FileType('rb')) + return parser.parse_args() -def write_data(args): - write_padded_file(args.output, args.kernel, args.pagesize) - write_padded_file(args.output, args.ramdisk, args.pagesize) - write_padded_file(args.output, args.second, args.pagesize) +def write_data(args, pagesize): + write_padded_file(args.output, args.kernel, pagesize) + write_padded_file(args.output, args.ramdisk, pagesize) + write_padded_file(args.output, args.second, pagesize) + + if args.header_version > 0 and args.header_version < 3: + write_padded_file(args.output, args.recovery_dtbo, pagesize) + if args.header_version == 2: + write_padded_file(args.output, args.dtb, pagesize) + + +def write_vendor_boot_data(args): + write_padded_file(args.vendor_boot, args.vendor_ramdisk, args.pagesize) + write_padded_file(args.vendor_boot, args.dtb, args.pagesize) - if args.header_version > 0: - write_padded_file(args.output, args.recovery_dtbo, args.pagesize) - if args.header_version > 1: - write_padded_file(args.output, args.dtb, args.pagesize) def main(): args = parse_cmdline() - img_id = write_header(args) - write_data(args) - if args.id: - if isinstance(img_id, str): + if args.vendor_boot is not None: + if args.header_version < 3: + raise ValueError('--vendor_boot not compatible with given header version') + if args.vendor_ramdisk is None: + raise ValueError('--vendor_ramdisk missing or invalid') + write_vendor_boot_header(args) + write_vendor_boot_data(args) + if args.output is not None: + if args.kernel is None: + raise ValueError('kernel must be supplied when creating a boot image') + if args.second is not None and args.header_version > 2: + raise ValueError('--second not compatible with given header version') + img_id = write_header(args) + if args.header_version > 2: + write_data(args, BOOT_IMAGE_HEADER_V3_PAGESIZE) + else: + write_data(args, args.pagesize) + if args.id and img_id is not None: # Python 2's struct.pack returns a string, but py3 returns bytes. - img_id = [ord(x) for x in img_id] - print('0x' + ''.join('{:02x}'.format(c) for c in img_id)) + if isinstance(img_id, str): + img_id = [ord(x) for x in img_id] + print('0x' + ''.join('{:02x}'.format(c) for c in img_id)) + if __name__ == '__main__': main() diff --git a/bbootimg/build.gradle.kts b/bbootimg/build.gradle.kts index fc1fa34..57f64ca 100644 --- a/bbootimg/build.gradle.kts +++ b/bbootimg/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.jetbrains.kotlin.jvm").version("1.3.41") + id("org.jetbrains.kotlin.jvm").version("1.3.61") application } @@ -11,10 +11,10 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.jetbrains.kotlin:kotlin-reflect") - implementation("org.slf4j:slf4j-simple:1.7.25") - implementation("org.slf4j:slf4j-api:1.7.25") - implementation("com.fasterxml.jackson.core:jackson-annotations:2.9.4") - implementation("com.fasterxml.jackson.core:jackson-databind:2.9.4") + implementation("org.slf4j:slf4j-simple:1.7.29") + implementation("org.slf4j:slf4j-api:1.7.29") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.10.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.10.1") implementation("com.google.guava:guava:18.0") implementation("org.apache.commons:commons-exec:1.3") implementation("org.apache.commons:commons-compress:1.16.1") diff --git a/bbootimg/src/main/kotlin/Helper.kt b/bbootimg/src/main/kotlin/Helper.kt index 60c190e..041a3c8 100644 --- a/bbootimg/src/main/kotlin/Helper.kt +++ b/bbootimg/src/main/kotlin/Helper.kt @@ -83,18 +83,6 @@ class Helper { return data } - //similar to this.toString(StandardCharsets.UTF_8).replace("${Character.MIN_VALUE}", "") - @Deprecated("by 1.3.41 experimental api: String.decodeToString()") - fun toCString(ba: ByteArray): String { - val str = ba.toString(StandardCharsets.UTF_8) - val nullPos = str.indexOf(Character.MIN_VALUE) - return if (nullPos >= 0) { - str.substring(0, nullPos) - } else { - str - } - } - @Throws(IOException::class) fun gnuZipFile(compressedFile: String, decompressedFile: String) { val buffer = ByteArray(1024) @@ -302,7 +290,7 @@ class Helper { } fun String.check_call(): Boolean { - var ret = false + val ret: Boolean try { val cmd = CommandLine.parse(this) log.info(cmd.toString()) diff --git a/bbootimg/src/main/kotlin/ParamConfig.kt b/bbootimg/src/main/kotlin/ParamConfig.kt index dc39581..a513a12 100644 --- a/bbootimg/src/main/kotlin/ParamConfig.kt +++ b/bbootimg/src/main/kotlin/ParamConfig.kt @@ -9,4 +9,5 @@ data class ParamConfig( var dtbo: String? = UnifiedConfig.workDir + "recoveryDtbo", var dtb: String? = UnifiedConfig.workDir + "dtb", var cfg: String = UnifiedConfig.workDir + "bootimg.json", - val mkbootimg: String = "./external/mkbootimg") + val mkbootimg: String = "./aosp/system/tools/mkbootimg/mkbootimg.py") + diff --git a/bbootimg/src/main/kotlin/avb/alg/Algorithms.kt b/bbootimg/src/main/kotlin/avb/alg/Algorithms.kt index 4248a57..efbb0e9 100644 --- a/bbootimg/src/main/kotlin/avb/alg/Algorithms.kt +++ b/bbootimg/src/main/kotlin/avb/alg/Algorithms.kt @@ -36,7 +36,7 @@ class Algorithms { intArrayOf(0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20)), - defaultKey = "avb/avb_test_data/testkey_rsa2048.pem") + defaultKey = "aosp/avb/avb_test_data/testkey_rsa2048.pem") val SHA256_RSA4096 = Algorithm( name = "SHA256_RSA4096", @@ -53,7 +53,7 @@ class Algorithms { 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20) ), - defaultKey = "avb/avb_test_data/testkey_rsa4096.pem" + defaultKey = "aosp/avb/avb_test_data/testkey_rsa4096.pem" ) val SHA256_RSA8192 = Algorithm( @@ -70,7 +70,7 @@ class Algorithms { intArrayOf(0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20)), - defaultKey = "avb/avb_test_data/testkey_rsa8192.pem") + defaultKey = "aosp/avb/avb_test_data/testkey_rsa8192.pem") val SHA512_RSA2048 = Algorithm( name = "SHA512_RSA2048", @@ -86,7 +86,7 @@ class Algorithms { intArrayOf(0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40)), - defaultKey = "avb/avb_test_data/testkey_rsa2048.pem") + defaultKey = "aosp/avb/avb_test_data/testkey_rsa2048.pem") val SHA512_RSA4096 = Algorithm( name = "SHA512_RSA4096", @@ -102,7 +102,7 @@ class Algorithms { intArrayOf(0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40)), - defaultKey = "avb/avb_test_data/testkey_rsa4096.pem") + defaultKey = "aosp/avb/avb_test_data/testkey_rsa4096.pem") val SHA512_RSA8192 = Algorithm( name = "SHA512_RSA8192", @@ -119,7 +119,7 @@ class Algorithms { intArrayOf(0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40)), - defaultKey = "avb/avb_test_data/testkey_rsa8192.pem") + defaultKey = "aosp/avb/avb_test_data/testkey_rsa8192.pem") algMap[NONE.name] = NONE diff --git a/bbootimg/src/main/kotlin/avb/desc/ChainPartitionDescriptor.kt b/bbootimg/src/main/kotlin/avb/desc/ChainPartitionDescriptor.kt index c807c50..af20188 100644 --- a/bbootimg/src/main/kotlin/avb/desc/ChainPartitionDescriptor.kt +++ b/bbootimg/src/main/kotlin/avb/desc/ChainPartitionDescriptor.kt @@ -40,7 +40,7 @@ class ChainPartitionDescriptor( } constructor(data: InputStream, seq: Int = 0) : this() { - if (SIZE - RESERVED != Struct3(FORMAT_STRING).calcSize()!!.toLong()) { + if (SIZE - RESERVED != Struct3(FORMAT_STRING).calcSize().toLong()) { throw RuntimeException() } this.sequence = seq diff --git a/bbootimg/src/main/kotlin/avb/desc/PropertyDescriptor.kt b/bbootimg/src/main/kotlin/avb/desc/PropertyDescriptor.kt index ec50111..3aa6754 100644 --- a/bbootimg/src/main/kotlin/avb/desc/PropertyDescriptor.kt +++ b/bbootimg/src/main/kotlin/avb/desc/PropertyDescriptor.kt @@ -9,7 +9,7 @@ class PropertyDescriptor( var key: String = "", var value: String = "") : Descriptor(TAG, 0U, 0) { override fun encode(): ByteArray { - if (SIZE != Struct3(FORMAT_STRING).calcSize()!!.toUInt()) { + if (SIZE != Struct3(FORMAT_STRING).calcSize().toUInt()) { throw RuntimeException() } this.num_bytes_following = (SIZE + this.key.length.toUInt() + this.value.length.toUInt() + 2U - 16U).toULong() diff --git a/bbootimg/src/main/kotlin/bootimg/ImgInfo.kt b/bbootimg/src/main/kotlin/bootimg/ImgInfo.kt index 5ca39f6..578bfd6 100644 --- a/bbootimg/src/main/kotlin/bootimg/ImgInfo.kt +++ b/bbootimg/src/main/kotlin/bootimg/ImgInfo.kt @@ -32,7 +32,7 @@ data class ImgInfo( data class VeritySignature( var type: String = "dm-verity", var path: String = "/boot", - var verity_pk8: String = "security/verity.pk8", - var verity_pem: String = "security/verity.x509.pem", + var verity_pk8: String = "aosp/security/verity.pk8", + var verity_pem: String = "aosp/security/verity.x509.pem", var jarPath: String = "aosp/boot_signer/build/libs/boot_signer.jar") } diff --git a/bbootimg/src/main/kotlin/cfig/io/Struct3.kt b/bbootimg/src/main/kotlin/cfig/io/Struct3.kt index 71dd6da..7e2e544 100644 --- a/bbootimg/src/main/kotlin/cfig/io/Struct3.kt +++ b/bbootimg/src/main/kotlin/cfig/io/Struct3.kt @@ -1,14 +1,36 @@ package cfig.io -import cfig.Helper +import cfig.io.Struct3.ByteArrayExt.Companion.toCString +import cfig.io.Struct3.ByteArrayExt.Companion.toInt +import cfig.io.Struct3.ByteArrayExt.Companion.toLong +import cfig.io.Struct3.ByteArrayExt.Companion.toShort +import cfig.io.Struct3.ByteArrayExt.Companion.toUInt +import cfig.io.Struct3.ByteArrayExt.Companion.toULong +import cfig.io.Struct3.ByteArrayExt.Companion.toUShort +import cfig.io.Struct3.ByteBufferExt.Companion.appendByteArray +import cfig.io.Struct3.ByteBufferExt.Companion.appendPadding +import cfig.io.Struct3.ByteBufferExt.Companion.appendUByteArray +import cfig.io.Struct3.InputStreamExt.Companion.getByteArray +import cfig.io.Struct3.InputStreamExt.Companion.getCString +import cfig.io.Struct3.InputStreamExt.Companion.getChar +import cfig.io.Struct3.InputStreamExt.Companion.getInt +import cfig.io.Struct3.InputStreamExt.Companion.getLong +import cfig.io.Struct3.InputStreamExt.Companion.getPadding +import cfig.io.Struct3.InputStreamExt.Companion.getShort +import cfig.io.Struct3.InputStreamExt.Companion.getUByteArray +import cfig.io.Struct3.InputStreamExt.Companion.getUInt +import cfig.io.Struct3.InputStreamExt.Companion.getULong +import cfig.io.Struct3.InputStreamExt.Companion.getUShort import org.junit.Assert import org.slf4j.LoggerFactory import java.io.IOException import java.io.InputStream import java.nio.ByteBuffer import java.nio.ByteOrder +import java.nio.charset.StandardCharsets import java.util.* import java.util.regex.Pattern +import kotlin.random.Random @ExperimentalUnsignedTypes class Struct3 { @@ -17,85 +39,44 @@ class Struct3 { private var byteOrder = ByteOrder.LITTLE_ENDIAN private val formats = ArrayList>() - enum class Type { - Padding, - } - constructor(inFormatString: String) { + Assert.assertTrue("FORMAT_STRING must not be empty", + inFormatString.isNotEmpty()) formatString = inFormatString val m = Pattern.compile("(\\d*)([a-zA-Z])").matcher(formatString) - - if (formatString.startsWith(">") || formatString.startsWith("!")) { - this.byteOrder = ByteOrder.BIG_ENDIAN - log.debug("Parsing BIG_ENDIAN format: $formatString") - } else if (formatString.startsWith("@") || formatString.startsWith("=")) { - this.byteOrder = ByteOrder.nativeOrder() - log.debug("Parsing native ENDIAN format: $formatString") - } else { - log.debug("Parsing LITTLE_ENDIAN format: $formatString") + when (formatString[0]) { + '>', '!' -> this.byteOrder = ByteOrder.BIG_ENDIAN + '@', '=' -> this.byteOrder = ByteOrder.nativeOrder() + else -> this.byteOrder = ByteOrder.LITTLE_ENDIAN } - while (m.find()) { - var bExploded = false - val multiple = if (m.group(1).isEmpty()) 1 else Integer.decode(m.group(1)) //item[0]: Type, item[1]: multiple // if need to expand format items, explode it - // eg: "4L" will be exploded to "1L 1L 1L 1L" - // eg: "10x" won't be exploded, it's still "10x" - val item = arrayOfNulls(2) - - when (m.group(2)) { - //exploded types - "x" -> {//byte 1 - item[0] = Type.Padding - bExploded = true - } - "b" -> {//byte 1 - item[0] = Byte - bExploded = true - } - "B" -> {//UByte 1 - item[0] = UByte - bExploded = true - } - "s" -> {//string - item[0] = String - bExploded = true - } - //combo types, which need to be exploded with multiple=1 - "c" -> {//char 1 - item[0] = Char - bExploded = false - } - "h" -> {//2 - item[0] = Short - } - "H" -> {//2 - item[0] = UShort - } - "i", "l" -> {//4 - item[0] = Int - } - "I", "L" -> {//4 - item[0] = UInt - } - "q" -> {//8 - item[0] = Long - } - "Q" -> {//8 - item[0] = ULong - } - else -> { - throw IllegalArgumentException("type [" + m.group(2) + "] not supported") - } + // eg: "4L" will be exploded to "1L 1L 1L 1L", so it's treated as primitive + // eg: "10x" won't be exploded, it's still "10x", so it's treated as non-primitive + val typeName: Any = when (m.group(2)) { + //primitive types + "x" -> Random //byte 1 (exploded) + "b" -> Byte //byte 1 (exploded) + "B" -> UByte //UByte 1 (exploded) + "s" -> String //string (exploded) + //zippable types, which need to be exploded with multiple=1 + "c" -> Char + "h" -> Short //2 + "H" -> UShort //2 + "i", "l" -> Int //4 + "I", "L" -> UInt //4 + "q" -> Long //8 + "Q" -> ULong //8 + else -> throw IllegalArgumentException("type [" + m.group(2) + "] not supported") } - if (bExploded) { - item[1] = multiple - formats.add(item) + val bPrimitive = m.group(2) in listOf("x", "b", "B", "s") + val multiple = if (m.group(1).isEmpty()) 1 else Integer.decode(m.group(1)) + if (bPrimitive) { + formats.add(arrayOf(typeName, multiple)) } else { - item[1] = 1 for (i in 0 until multiple) { - formats.add(item) + formats.add(arrayOf(typeName, 1)) } } } @@ -105,25 +86,39 @@ class Struct3 { return ("type=" + formats.get(inCursor)[0] + ", value=" + formats.get(inCursor)[1]) } - fun calcSize(): Int? { + override fun toString(): String { + val formatStr = mutableListOf() + formats.forEach { + val fs = StringBuilder() + when (it[0]) { + Random -> fs.append("x") + Byte -> fs.append("b") + UByte -> fs.append("B") + String -> fs.append("s") + Char -> fs.append("c") + Short -> fs.append("h") + UShort -> fs.append("H") + Int -> fs.append("i") + UInt -> fs.append("I") + Long -> fs.append("q") + ULong -> fs.append("Q") + else -> throw IllegalArgumentException("type [" + it[0] + "] not supported") + } + fs.append(":" + it[1]) + formatStr.add(fs.toString()) + } + return "Struct3(formatString='$formatString', byteOrder=$byteOrder, formats=$formatStr)" + } + + fun calcSize(): Int { var ret = 0 for (format in formats) { - when (val formatType = format[0]) { - Byte, UByte, Char, String, Type.Padding -> { - ret += format[1] as Int - } - Short, UShort -> { - ret += 2 * format[1] as Int - } - Int, UInt -> { - ret += 4 * format[1] as Int - } - Long, ULong -> { - ret += 8 * format[1] as Int - } - else -> { - throw IllegalArgumentException("Class [" + formatType + "] not supported") - } + ret += when (val formatType = format[0]) { + Random, Byte, UByte, Char, String -> format[1] as Int + Short, UShort -> 2 * format[1] as Int + Int, UInt -> 4 * format[1] as Int + Long, ULong -> 8 * format[1] as Int + else -> throw IllegalArgumentException("Class [$formatType] not supported") } } return ret @@ -133,129 +128,88 @@ class Struct3 { if (args.size != this.formats.size) { throw IllegalArgumentException("argument size " + args.size + " doesn't match format size " + this.formats.size) - } else { - log.debug("byte buffer size: " + this.calcSize()!!) } - val bf = ByteBuffer.allocate(this.calcSize()!!) + val bf = ByteBuffer.allocate(this.calcSize()) bf.order(this.byteOrder) - var formatCursor = -1 //which format item to handle for (i in args.indices) { - formatCursor++ val arg = args[i] - val format2 = formats[i][0] - val size = formats[i][1] as Int + val typeName = formats[i][0] + val multiple = formats[i][1] as Int + + if (typeName !in arrayOf(Random, Byte, String, UByte)) { + Assert.assertEquals(1, multiple) + } //x: padding: - // arg == null: - // arg is Byte.class - // arg is Integer.class - if (Type.Padding == format2) { - val b = ByteArray(size) + if (Random == typeName) { when (arg) { - null -> Arrays.fill(b, 0.toByte()) - is Byte -> Arrays.fill(b, arg) - is Int -> Arrays.fill(b, arg.toByte()) + null -> bf.appendPadding(0, multiple) + is Byte -> bf.appendPadding(arg, multiple) + is Int -> bf.appendPadding(arg.toByte(), multiple) else -> throw IllegalArgumentException("Index[" + i + "] Unsupported arg [" + arg + "] with type [" + formats[i][0] + "]") } - bf.put(b) continue } //c: character - if (Char == format2) { - Assert.assertEquals(1, size.toLong()) - Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT instance of Character.class", + if (Char == typeName) { + Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT Char", arg is Char) - bf.put(getLowerByte(arg as Char)) + if ((arg as Char) !in '\u0000'..'\u00ff') { + throw IllegalArgumentException("arg[${arg.toInt()}] exceeds 8-bit bound") + } + bf.put(arg.toByte()) continue } //b: byte array - if (Byte == format2) { - Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT instance of ByteArray/IntArray", - arg is ByteArray || arg is IntArray) - val argInternal = if (arg is IntArray) { - val arg2: MutableList = mutableListOf() - for (item in arg) { - Assert.assertTrue("$item is not valid Byte", - item in Byte.MIN_VALUE..Byte.MAX_VALUE) - arg2.add(item.toByte()) - } - arg2.toByteArray() - } else { - arg as ByteArray - } - - val paddingSize = size - argInternal.size - Assert.assertTrue("argument size overflow: " + argInternal.size + " > " + size, - paddingSize >= 0) - bf.put(argInternal) - if (paddingSize > 0) { - val padBytes = ByteArray(paddingSize) - Arrays.fill(padBytes, 0.toByte()) - bf.put(padBytes) - log.debug("paddingSize $paddingSize") - } else { - log.debug("paddingSize is zero, perfect match") + if (Byte == typeName) { + when (arg) { + is IntArray -> bf.appendByteArray(arg, multiple) + is ByteArray -> bf.appendByteArray(arg, multiple) + else -> throw IllegalArgumentException("[$arg](${arg!!::class.java}) is NOT ByteArray/IntArray") } continue } //B: UByte array - if (UByte == format2) { - Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT instance of ByteArray/IntArray", - arg is ByteArray || arg is IntArray || arg is UByteArray) - val argInternal = if (arg is IntArray) { - val arg2: MutableList = mutableListOf() - for (item in arg) { - Assert.assertTrue("$item is not valid UByte", - item in UByte.MIN_VALUE.toInt()..UByte.MAX_VALUE.toInt()) - arg2.add(item.toByte()) - } - arg2.toByteArray() - } else if (arg is UByteArray) { - arg as ByteArray - } else { - arg as ByteArray + if (UByte == typeName) { + when (arg) { + is ByteArray -> bf.appendByteArray(arg, multiple) + is UByteArray -> bf.appendUByteArray(arg, multiple) + is IntArray -> bf.appendUByteArray(arg, multiple) + else -> throw IllegalArgumentException("[$arg](${arg!!::class.java}) is NOT ByteArray/IntArray") } + continue + } - val paddingSize = size - argInternal.size - Assert.assertTrue("argument size overflow: " + argInternal.size + " > " + size, - paddingSize >= 0) - bf.put(argInternal) - if (paddingSize > 0) { - val padBytes = ByteArray(paddingSize) - Arrays.fill(padBytes, 0.toByte()) - bf.put(padBytes) - log.debug("paddingSize $paddingSize") - } else { - log.debug("paddingSize is zero, perfect match") - } + //s: String + if (String == typeName) { + Assert.assertNotNull("arg can not be NULL for String, formatString=$formatString, ${getFormatInfo(i)}", arg) + Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT String, ${getFormatInfo(i)}", + arg is String) + bf.appendByteArray((arg as String).toByteArray(), multiple) continue } //h: Short - if (Short == format2) { - Assert.assertEquals(1, size.toLong()) - Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT instance of Short/Int", - arg is Short || arg is Int) + if (Short == typeName) { when (arg) { is Int -> { Assert.assertTrue("[$arg] is truncated as type Short.class", - arg in java.lang.Short.MIN_VALUE..java.lang.Short.MAX_VALUE) + arg in Short.MIN_VALUE..Short.MAX_VALUE) bf.putShort(arg.toShort()) } - is Short -> //instance Short - bf.putShort(arg) + is Short -> bf.putShort(arg) //instance Short + else -> throw IllegalArgumentException("[$arg](${arg!!::class.java}) is NOT Short/Int") } continue } //H: UShort - if (UShort == format2) { - Assert.assertEquals(1, size.toLong()) - Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT instance of UShort/UInt/Int", + if (UShort == typeName) { + Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT UShort/UInt/Int", arg is UShort || arg is UInt || arg is Int) when (arg) { is Int -> { @@ -274,18 +228,14 @@ class Struct3 { } //i, l: Int - if (Int == format2) { - Assert.assertEquals(1, size.toLong()) - Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT instance of Int", arg is Int) + if (Int == typeName) { + Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT Int", arg is Int) bf.putInt(arg as Int) continue } //I, L: UInt - if (UInt == format2) { - Assert.assertEquals(1, size.toLong()) - Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT instance of UInt/Int/Long", - arg is UInt || arg is Int || arg is Long) + if (UInt == typeName) { when (arg) { is Int -> { Assert.assertTrue("[$arg] is invalid as type UInt", arg >= 0) @@ -296,30 +246,23 @@ class Struct3 { Assert.assertTrue("[$arg] is invalid as type UInt", arg >= 0) bf.putInt(arg.toInt()) } - else -> { - Assert.fail("program bug") - } + else -> throw IllegalArgumentException("[$arg](${arg!!::class.java}) is NOT UInt/Int/Long") } continue } //q: Long - if (Long == format2) { - Assert.assertEquals(1, size.toLong()) - Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT instance of Long/Int", - arg is Long || arg is Int) + if (Long == typeName) { when (arg) { is Long -> bf.putLong(arg) is Int -> bf.putLong(arg.toLong()) + else -> throw IllegalArgumentException("[$arg](${arg!!::class.java}) is NOT Long/Int") } continue } //Q: ULong - if (ULong == format2) { - Assert.assertEquals(1, size.toLong()) - Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT instance of Int/Long/ULong", - arg is Int || arg is Long || arg is ULong) + if (ULong == typeName) { when (arg) { is Int -> { Assert.assertTrue("[$arg] is invalid as type ULong", arg >= 0) @@ -329,36 +272,14 @@ class Struct3 { Assert.assertTrue("[$arg] is invalid as type ULong", arg >= 0) bf.putLong(arg) } - is ULong -> { - bf.putLong(arg.toLong()) - } - } - continue - } - - //s: String - if (String == format2) { - Assert.assertNotNull("arg can not be NULL for String, formatString=$formatString, ${getFormatInfo(formatCursor)}", arg) - Assert.assertTrue("[$arg](${arg!!::class.java}) is NOT instance of String.class, ${getFormatInfo(formatCursor)}", - arg is String) - val paddingSize = size - (arg as String).length - Assert.assertTrue("argument size overflow: " + arg.length + " > " + size, - paddingSize >= 0) - bf.put(arg.toByteArray()) - if (paddingSize > 0) { - val padBytes = ByteArray(paddingSize) - Arrays.fill(padBytes, 0.toByte()) - bf.put(padBytes) - log.debug("paddingSize $paddingSize") - } else { - log.debug("paddingSize is zero, perfect match") + is ULong -> bf.putLong(arg.toLong()) + else -> throw IllegalArgumentException("[$arg](${arg!!::class.java}) is NOT Int/Long/ULong") } continue } - throw java.lang.IllegalArgumentException("unrecognized format $format2") + throw IllegalArgumentException("unrecognized format $typeName") } - log.debug("Pack Result:" + Helper.toHexString(bf.array())) return bf.array() } @@ -366,143 +287,251 @@ class Struct3 { fun unpack(iS: InputStream): List<*> { val ret = ArrayList() for (format in this.formats) { - //x: padding - //return padding byte - if (format[0] === Type.Padding) { - val multip = format[1] as Int - val data = ByteArray(1) - iS.read(data)//sample the 1st byte - val skipped = iS.skip(multip.toLong() - 1)//skip remaining - Assert.assertEquals(multip.toLong() - 1, skipped) - ret.add(data[0]) - continue + when (format[0]) { + Random -> ret.add(iS.getPadding(format[1] as Int)) //return padding byte + Byte -> ret.add(iS.getByteArray(format[1] as Int)) //b: byte array + UByte -> ret.add(iS.getUByteArray(format[1] as Int)) //B: ubyte array + Char -> ret.add(iS.getChar()) //char: 1 + String -> ret.add(iS.getCString(format[1] as Int)) //c string + Short -> ret.add(iS.getShort(this.byteOrder)) //h: short + UShort -> ret.add(iS.getUShort(this.byteOrder)) //H: UShort + Int -> ret.add(iS.getInt(this.byteOrder)) //i, l: Int + UInt -> ret.add(iS.getUInt(this.byteOrder)) //I, L: UInt + Long -> ret.add(iS.getLong(this.byteOrder)) //q: Long + ULong -> ret.add(iS.getULong(this.byteOrder)) //Q: ULong + else -> throw IllegalArgumentException("Class [" + format[0] + "] not supported") + }//end-of-when + }//end-of-for + return ret + } + + class ByteBufferExt { + companion object { + private val log = LoggerFactory.getLogger(ByteBufferExt::class.java) + + fun ByteBuffer.appendPadding(b: Byte, bufSize: Int) { + when { + bufSize == 0 -> { + log.debug("paddingSize is zero, perfect match") + return + } + bufSize < 0 -> { + throw IllegalArgumentException("illegal padding size: $bufSize") + } + else -> { + log.debug("paddingSize $bufSize") + } + } + val padding = ByteArray(bufSize) + Arrays.fill(padding, b) + this.put(padding) } - //b: byte array - if (format[0] === Byte) { - val data = ByteArray(format[1] as Int) - Assert.assertEquals(format[1] as Int, iS.read(data)) - ret.add(data) - continue + fun ByteBuffer.appendByteArray(inIntArray: IntArray, bufSize: Int) { + val arg2 = mutableListOf() + inIntArray.toMutableList().mapTo(arg2, { + if (it in Byte.MIN_VALUE..Byte.MAX_VALUE) + it.toByte() + else + throw IllegalArgumentException("$it is not valid Byte") + }) + appendByteArray(arg2.toByteArray(), bufSize) } - //B: ubyte array - if (format[0] === UByte) { - val data = ByteArray(format[1] as Int) - Assert.assertEquals(format[1] as Int, iS.read(data)) - val innerData = UByteArray(data.size) - for (i in 0 until data.size) { - innerData[i] = data[i].toUByte() - } - ret.add(innerData) - continue + fun ByteBuffer.appendByteArray(inByteArray: ByteArray, bufSize: Int) { + val paddingSize = bufSize - inByteArray.size + if (paddingSize < 0) throw IllegalArgumentException("arg length [${inByteArray.size}] exceeds limit: $bufSize") + //data + this.put(inByteArray) + //padding + this.appendPadding(0.toByte(), paddingSize) + log.debug("paddingSize $paddingSize") } - //char: 1 - if (format[0] === Char) { - val data = ByteArray(format[1] as Int)//now its size is fixed at 1 - Assert.assertEquals(format[1] as Int, iS.read(data)) - ret.add(data[0].toChar()) - continue + fun ByteBuffer.appendUByteArray(inIntArray: IntArray, bufSize: Int) { + val arg2 = mutableListOf() + inIntArray.toMutableList().mapTo(arg2, { + if (it in UByte.MIN_VALUE.toInt()..UByte.MAX_VALUE.toInt()) + it.toUByte() + else + throw IllegalArgumentException("$it is not valid Byte") + }) + appendUByteArray(arg2.toUByteArray(), bufSize) } - //string - if (format[0] === String) { - val data = ByteArray(format[1] as Int) - Assert.assertEquals(format[1] as Int, iS.read(data)) - ret.add(Helper.toCString(data)) - continue + fun ByteBuffer.appendUByteArray(inUByteArray: UByteArray, bufSize: Int) { + val bl = mutableListOf() + inUByteArray.toMutableList().mapTo(bl, { it.toByte() }) + this.appendByteArray(bl.toByteArray(), bufSize) + } + } + } + + class InputStreamExt { + companion object { + fun InputStream.getChar(): Char { + val data = ByteArray(Byte.SIZE_BYTES) + Assert.assertEquals(Byte.SIZE_BYTES, this.read(data)) + return data[0].toChar() + } + + fun InputStream.getShort(inByteOrder: ByteOrder): Short { + val data = ByteArray(Short.SIZE_BYTES) + Assert.assertEquals(Short.SIZE_BYTES, this.read(data)) + return data.toShort(inByteOrder) + } + + fun InputStream.getInt(inByteOrder: ByteOrder): Int { + val data = ByteArray(Int.SIZE_BYTES) + Assert.assertEquals(Int.SIZE_BYTES, this.read(data)) + return data.toInt(inByteOrder) } - //h: short - if (format[0] === Short) { - val data = ByteArray(2) - Assert.assertEquals(2, iS.read(data).toLong()) - ByteBuffer.allocate(2).let { - it.order(this.byteOrder) - it.put(data) + fun InputStream.getLong(inByteOrder: ByteOrder): Long { + val data = ByteArray(Long.SIZE_BYTES) + Assert.assertEquals(Long.SIZE_BYTES, this.read(data)) + return data.toLong(inByteOrder) + } + + fun InputStream.getUShort(inByteOrder: ByteOrder): UShort { + val data = ByteArray(UShort.SIZE_BYTES) + Assert.assertEquals(UShort.SIZE_BYTES, this.read(data)) + return data.toUShort(inByteOrder) + } + + fun InputStream.getUInt(inByteOrder: ByteOrder): UInt { + val data = ByteArray(UInt.SIZE_BYTES) + Assert.assertEquals(UInt.SIZE_BYTES, this.read(data)) + return data.toUInt(inByteOrder) + } + + fun InputStream.getULong(inByteOrder: ByteOrder): ULong { + val data = ByteArray(ULong.SIZE_BYTES) + Assert.assertEquals(ULong.SIZE_BYTES, this.read(data)) + return data.toULong(inByteOrder) + } + + fun InputStream.getByteArray(inSize: Int): ByteArray { + val data = ByteArray(inSize) + Assert.assertEquals(inSize, this.read(data)) + return data + } + + fun InputStream.getUByteArray(inSize: Int): UByteArray { + val data = ByteArray(inSize) + Assert.assertEquals(inSize, this.read(data)) + val innerData2 = mutableListOf() + data.toMutableList().mapTo(innerData2, { it.toUByte() }) + return innerData2.toUByteArray() + } + + fun InputStream.getCString(inSize: Int): String { + val data = ByteArray(inSize) + Assert.assertEquals(inSize, this.read(data)) + return data.toCString() + } + + fun InputStream.getPadding(inSize: Int): Byte { + val data = ByteArray(Byte.SIZE_BYTES) + Assert.assertEquals(Byte.SIZE_BYTES, this.read(data)) //sample the 1st byte + val skipped = this.skip(inSize.toLong() - Byte.SIZE_BYTES)//skip remaining to save memory + Assert.assertEquals(inSize.toLong() - Byte.SIZE_BYTES, skipped) + return data[0] + } + } + } + + class ByteArrayExt { + companion object { + fun ByteArray.toShort(inByteOrder: ByteOrder): Short { + val typeSize = Short.SIZE_BYTES / Byte.SIZE_BYTES + Assert.assertEquals("Short must have $typeSize bytes", typeSize, this.size) + var ret: Short + ByteBuffer.allocate(typeSize).let { + it.order(inByteOrder) + it.put(this) it.flip() - ret.add(it.short) + ret = it.getShort() } - continue + return ret } - //H: UShort - if (format[0] === UShort) { - val data = ByteArray(2) - Assert.assertEquals(2, iS.read(data).toLong()) - ByteBuffer.allocate(2).let { - it.order(this.byteOrder) - it.put(data) + fun ByteArray.toInt(inByteOrder: ByteOrder): Int { + val typeSize = Int.SIZE_BYTES / Byte.SIZE_BYTES + Assert.assertEquals("Int must have $typeSize bytes", typeSize, this.size) + var ret: Int + ByteBuffer.allocate(typeSize).let { + it.order(inByteOrder) + it.put(this) it.flip() - ret.add(it.short.toUShort()) + ret = it.getInt() } - continue + return ret } - //i, l: Int - if (format[0] === Int) { - val data = ByteArray(4) - Assert.assertEquals(4, iS.read(data).toLong()) - ByteBuffer.allocate(4).let { - it.order(this.byteOrder) - it.put(data) + fun ByteArray.toLong(inByteOrder: ByteOrder): Long { + val typeSize = Long.SIZE_BYTES / Byte.SIZE_BYTES + Assert.assertEquals("Long must have $typeSize bytes", typeSize, this.size) + var ret: Long + ByteBuffer.allocate(typeSize).let { + it.order(inByteOrder) + it.put(this) it.flip() - ret.add(it.int) + ret = it.getLong() } - continue + return ret } - //I, L: UInt - if (format[0] === UInt) { - val data = ByteArray(4) - Assert.assertEquals(4, iS.read(data).toLong()) - ByteBuffer.allocate(4).let { - it.order(this.byteOrder) - it.put(data) + fun ByteArray.toUShort(inByteOrder: ByteOrder): UShort { + val typeSize = UShort.SIZE_BYTES / Byte.SIZE_BYTES + Assert.assertEquals("UShort must have $typeSize bytes", typeSize, this.size) + var ret: UShort + ByteBuffer.allocate(typeSize).let { + it.order(inByteOrder) + it.put(this) it.flip() - ret.add(it.int.toUInt()) + ret = it.getShort().toUShort() } - continue + return ret } - //q: Long - if (format[0] === Long) { - val data = ByteArray(8) - Assert.assertEquals(8, iS.read(data).toLong()) - ByteBuffer.allocate(8).let { - it.order(this.byteOrder) - it.put(data) + fun ByteArray.toUInt(inByteOrder: ByteOrder): UInt { + val typeSize = UInt.SIZE_BYTES / Byte.SIZE_BYTES + Assert.assertEquals("UInt must have $typeSize bytes", typeSize, this.size) + var ret: UInt + ByteBuffer.allocate(typeSize).let { + it.order(inByteOrder) + it.put(this) it.flip() - ret.add(it.long) + ret = it.getInt().toUInt() } - continue + return ret } - //Q: ULong - if (format[0] === ULong) { - val data = ByteArray(8) - Assert.assertEquals(8, iS.read(data).toLong()) - ByteBuffer.allocate(8).let { - it.order(this.byteOrder) - it.put(data) + fun ByteArray.toULong(inByteOrder: ByteOrder): ULong { + val typeSize = ULong.SIZE_BYTES / Byte.SIZE_BYTES + Assert.assertEquals("ULong must have $typeSize bytes", typeSize, this.size) + var ret: ULong + ByteBuffer.allocate(typeSize).let { + it.order(inByteOrder) + it.put(this) it.flip() - ret.add(it.long.toULong()) + ret = it.getLong().toULong() } - continue + return ret } - throw IllegalArgumentException("Class [" + format[0] + "] not supported") + //similar to this.toString(StandardCharsets.UTF_8).replace("${Character.MIN_VALUE}", "") + // not Deprecated for now, "1.3.41 experimental api: ByteArray.decodeToString()") is a little different + fun ByteArray.toCString(): String { + val str = this.toString(StandardCharsets.UTF_8) + val nullPos = str.indexOf(Character.MIN_VALUE) + return if (nullPos >= 0) { + str.substring(0, nullPos) + } else { + str + } + } } - return ret - } - - //get lower 1 byte - private fun getLowerByte(obj: Char?): Byte { - val bf2 = ByteBuffer.allocate(Character.SIZE / 8) //aka. 16/8 - bf2.putChar(obj!!) - bf2.flip() - bf2.get() - return bf2.get() } } diff --git a/bbootimg/src/main/kotlin/kernel_util/KernelExtractor.kt b/bbootimg/src/main/kotlin/kernel_util/KernelExtractor.kt index cad3254..6f8f531 100644 --- a/bbootimg/src/main/kotlin/kernel_util/KernelExtractor.kt +++ b/bbootimg/src/main/kotlin/kernel_util/KernelExtractor.kt @@ -20,7 +20,7 @@ class KernelExtractor { val baseDir = "build/unzip_boot" val kernelVersionFile = "$baseDir/kernel_version.txt" val kernelConfigFile = "$baseDir/kernel_configs.txt" - val cmd = CommandLine.parse("external/extract_kernel.py").let { + val cmd = CommandLine.parse("aosp/build/tools/extract_kernel.py").let { it.addArgument("--input") it.addArgument(fileName) it.addArgument("--output-configs") diff --git a/bbootimg/src/main/kotlin/packable/BootImgParser.kt b/bbootimg/src/main/kotlin/packable/BootImgParser.kt index 45e50b0..5f7fd53 100644 --- a/bbootimg/src/main/kotlin/packable/BootImgParser.kt +++ b/bbootimg/src/main/kotlin/packable/BootImgParser.kt @@ -33,7 +33,7 @@ class BootImgParser() : IPackable { if (File(UnifiedConfig.workDir).exists()) File(UnifiedConfig.workDir).deleteRecursively() File(UnifiedConfig.workDir).mkdirs() try { - val info = Parser().parseBootImgHeader(fileName, avbtool = "avb/avbtool") + val info = Parser().parseBootImgHeader(fileName, avbtool = "aosp/avb/avbtool") InfoTable.instance.addRule() InfoTable.instance.addRow("image info", ParamConfig().cfg) if (info.signatureType == BootImgInfo.VerifyType.AVB) { @@ -69,7 +69,7 @@ class BootImgParser() : IPackable { override fun pack(fileName: String) { Packer().pack(mkbootfsBin = "./aosp/mkbootfs/build/exe/mkbootfs/mkbootfs") - Signer.sign(avbtool = "avb/avbtool", bootSigner = "aosp/boot_signer/build/libs/boot_signer.jar") + Signer.sign(avbtool = "aosp/avb/avbtool", bootSigner = "aosp/boot_signer/build/libs/boot_signer.jar") if (File("vbmeta.img").exists()) { val partitionName = ObjectMapper().readValue(File(Avb.getJsonFileName(fileName)), AVBInfo::class.java).let { it.auxBlob!!.hashDescriptors.get(0).partition_name diff --git a/bbootimg/src/main/kotlin/packable/IPackable.kt b/bbootimg/src/main/kotlin/packable/IPackable.kt index 2c6ec67..2bfde8e 100644 --- a/bbootimg/src/main/kotlin/packable/IPackable.kt +++ b/bbootimg/src/main/kotlin/packable/IPackable.kt @@ -5,6 +5,7 @@ import cfig.Helper.Companion.check_output import org.slf4j.Logger import org.slf4j.LoggerFactory +@ExperimentalUnsignedTypes interface IPackable { val loopNo: Int fun capabilities(): List { diff --git a/bbootimg/src/test/kotlin/cfig/io/Struct3Test.kt b/bbootimg/src/test/kotlin/cfig/io/Struct3Test.kt index 96fbd46..0e61346 100644 --- a/bbootimg/src/test/kotlin/cfig/io/Struct3Test.kt +++ b/bbootimg/src/test/kotlin/cfig/io/Struct3Test.kt @@ -46,6 +46,11 @@ class Struct3Test { Assert.assertEquals(9, Struct3("9c").calcSize()) } + @Test + fun toStringTest() { + println(Struct3("!4s2L2QL11QL4x47sx80x")) + } + //x @Test fun paddingTest() { @@ -189,7 +194,7 @@ class Struct3Test { Struct3("3s").pack("abcd") Assert.fail("should throw exception here") } catch (e: Throwable) { - Assert.assertTrue(e is AssertionError || e is IllegalArgumentException) + Assert.assertTrue(e.toString(), e is AssertionError || e is IllegalArgumentException) } //unpack diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 457aad0d98108420a977756b7145c93c8910b076..cc4fdc293d0e50b0ad9b65c16e7ddd1db2f6025b 100644 GIT binary patch delta 25889 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

Vp_F!2 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 delta 23210 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 T8$G=9f_Op3$oWUH6=nB-yb}(9 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ef9a9e0..9492014 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index af6708f..2fe81a7 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -109,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -138,19 +154,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 0f8d593..24467a1 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/settings.gradle.kts b/settings.gradle.kts index 1485b37..0f7b612 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,4 +8,4 @@ include("aosp:libsparse:simg2img") include("aosp:libsparse:simg2simg") include("aosp:libsparse:append2simg") include("aosp:libavb") -include("avbImpl") +//include("avbImpl")