diff --git a/1.kts b/1.kts
new file mode 100644
index 0000000..e54fb24
--- /dev/null
+++ b/1.kts
@@ -0,0 +1,71 @@
+import java.io.*
+import org.apache.commons.exec.CommandLine
+import org.apache.commons.exec.DefaultExecutor
+import org.apache.commons.exec.PumpStreamHandler
+import com.fasterxml.jackson.databind.ObjectMapper
+
+fun adbCmd(cmd: String): String {
+    val outputStream = ByteArrayOutputStream()
+    val exec = DefaultExecutor()
+    exec.streamHandler = PumpStreamHandler(outputStream)
+    val cmdline = "adb shell $cmd"
+    //println(cmdline)
+    exec.execute(CommandLine.parse(cmdline))
+    //println(outputStream)
+    return outputStream.toString().trim()
+}
+
+val cpufreqDir = "/sys/devices/system/cpu/cpufreq/policy0"
+val interactGov = "/sys/devices/system/cpu/cpufreq/interactive"
+
+val scaling_governor = adbCmd("cat $cpufreqDir/scaling_governor")
+val avail_governer = adbCmd("cat $cpufreqDir/scaling_available_governors")
+val avail_freq = adbCmd("cat $cpufreqDir/scaling_available_frequencies")
+println("Available governers: " + avail_governer)
+println("Available frequency: " + avail_freq)
+
+val scaleMax = adbCmd("cat $cpufreqDir/scaling_max_freq")
+val scaleMin = adbCmd("cat $cpufreqDir/scaling_min_freq")
+println("scaling_X_freq: [$scaleMin, $scaleMax]")
+println("Current governer: $scaling_governor")
+
+fun getInteractValue(k: String): String {
+    return adbCmd("cat $interactGov/$k")
+}
+fun getInteractInt(k: String): Int {
+    return Integer.decode(adbCmd("cat $interactGov/$k"))
+}
+
+data class Boost(
+    var boost: Int,
+    var boostpulse_duration_ms: Int)
+val boostInfo = Boost(getInteractInt("boost"), getInteractInt("boostpulse_duration") / 1000)
+
+data class HiSpeed(
+    var load: Int,
+    var above_delay_Ms: Int,
+    var freq_GHz: Double)
+val hiSpeedInfo = HiSpeed(
+    getInteractInt("go_hispeed_load"),
+    getInteractInt("above_hispeed_delay") / 1000,
+    getInteractInt("hispeed_freq") / 1000000.0)
+
+data class InteractiveGov(
+    var target_loads: Int,
+    var boost: Boost,
+    var hiSpeed: HiSpeed,
+    var minSampleTimeMs: Int,
+    var timerRateMs: Int,
+    var timerSlackMs: Int,
+    var io_is_busy: Int)
+
+val info = InteractiveGov(
+    getInteractInt("target_loads"), 
+    boostInfo,
+    hiSpeedInfo,
+    getInteractInt("min_sample_time") / 1000,
+    getInteractInt("timer_rate") / 1000,
+    getInteractInt("timer_slack") / 1000,
+    getInteractInt("io_is_busy"))
+
+println(ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(info))
diff --git a/README.expert.md b/README.expert.md
index e5e2199..71a850d 100644
--- a/README.expert.md
+++ b/README.expert.md
@@ -1,5 +1,17 @@
 # layout of boot.img
 
+### Image Content Index
+
+[1 header part](#1-header-part)
+
+[2 data part](#2-data-part)
+
+[3 signature part](#3-signature-part)
+
+ - [3.1 Boot Image Signature](#31-boot-image-signature-vboot-10)
+
+ - [3.2 AVB Footer](#32-avb-footer-vboot-20)
+
 ### 1. header part
 
               item                        size in bytes             position
@@ -81,65 +93,65 @@
 
 #### 3.2 AVB Footer (VBoot 2.0)
 
-              item                        size in bytes             position
-    +--------------------------------+-------------------------+    --> end of data part (say locaton A)
-    | VBMeta Header                  | total 256               |
-    |                                |                         |
-    |   - Header Magic "AVB0"        |     4                   |
-    |   - avb_version Major          |     4                   |
-    |   - avb_version Minor          |     4                   |
-    |   - authentication blob size   |     8                   |
-    |   - auxiliary blob size        |     8                   |
-    |   - algorithm type             |     4                   |
-    |   - hash offset                |     8                   |
-    |   - hash size                  |     8                   |
-    |   - signature offset           |     8                   |
-    |   - signature size             |     8                   |
-    |   - pub key offset             |     8                   |
-    |   - pub key size               |     8                   |
-    |   - pub key metadata offset    |     8                   |
-    |   - pub key metadata size      |     8                   |
-    |   - descriptors offset         |     8                   |
-    |   - descriptors size           |     8                   |
-    |   - rollback index             |     8                   |
-    |   - flags                      |     4                   |
-    |   - RESERVED                   |     4                   |
-    |   - release string             |     47                  |
-    |   - NULL                       |     1                   |
-    |   - RESERVED                   |     80                  |
-    |--------------------------------+-------------------------+    --> (location A) + 256
-    | Authentication Blob            |                         |
-    |   - Hash of Header & Aux Blob  | alg.hash_num_bytes      |
-    |   - Signature of Hash          | alg.signature_num_bytes |
-    |   - Padding                    | align by 64             |
-    +--------------------------------+-------------------------+
-    | Auxiliary Blob                 |                         |
-    |   - descriptors                |                         |
-    |   - pub key                    |                         |
-    |   - pub key meta data          |                         |
-    |   - padding                    | align by 64             |
-    +--------------------------------+-------------------------+
-    | Padding                        | align by block_size     |
-    +--------------------------------+-------------------------+    --> (location A) + (block_size * n)
-
-    +--------------------------------+-------------------------+
-    |                                |                         |
-    |                                |                         |
-    | DONOT CARE CHUNK               |                         |
-    |                                |                         |
-    |                                |                         |
-    +--------------------------------+-------------------------+
-
-    +--------------------------------+-------------------------+    --> partition_size - block_size
-    | Padding                        | block_size - 64         |
-    +--------------------------------+-------------------------+    --> partition_size - 64
-    | AVB Footer                     | total 64                |
-    |                                |                         |
-    |   - Footer Magic "AVBf"        |     4                   |
-    |   - Footer Major Version       |     4                   |
-    |   - Footer Minor Version       |     4                   |
-    |   - original image size        |     8                   |
-    |   - VBMeta offset              |     8                   |
-    |   - VBMeta size                |     8                   |
-    |   - Padding                    |     28                  |
-    +--------------------------------+-------------------------+    --> partition_size
+                         item                        size in bytes             position
+    +------+--------------------------------+-------------------------+ --> end of data part (say locaton +0)
+    |      | VBMeta Header                  | total 256               |
+    |      |                                |                         |
+    |      |   - Header Magic "AVB0"        |     4                   |
+    |      |   - avb_version Major          |     4                   |
+    |      |   - avb_version Minor          |     4                   |
+    |      |   - authentication blob size   |     8                   |
+    |      |   - auxiliary blob size        |     8                   |
+    |      |   - algorithm type             |     4                   |
+    |      |   - hash_offset                |     8                   |
+    |      |   - hash_size                  |     8                   |
+    |      |   - signature_offset           |     8                   |
+    |      |   - signature_size             |     8                   |
+    |      |   - pub_key_offset             |     8                   |
+    |VBMeta|   - pub_key_size               |     8                   |
+    | Blob |   - pub_key_metadata_offset    |     8                   |
+    |      |   - pub_key_metadata_size      |     8                   |
+    |      |   - descriptors_offset         |     8                   |
+    |      |   - descriptors_size           |     8                   |
+    |      |   - rollback_index             |     8                   |
+    |      |   - flags                      |     4                   |
+    |      |   - RESERVED                   |     4                   |
+    |      |   - release string             |     47                  |
+    |      |   - NULL                       |     1                   |
+    |      |   - RESERVED                   |     80                  |
+    |      |--------------------------------+-------------------------+ --> + 256
+    |      | Authentication Blob            |                         |
+    |      |   - Hash of Header & Aux Blob  | alg.hash_num_bytes      |
+    |      |   - Signature of Hash          | alg.signature_num_bytes |
+    |      |   - Padding                    | align by 64             |
+    |      +--------------------------------+-------------------------+
+    |      | Auxiliary Blob                 |                         |
+    |      |   - descriptors                |                         | --> + 256 + descriptors_offset
+    |      |   - pub key                    |                         | --> + 256 + pub_key_offset
+    |      |   - pub key meta data          |                         | --> + 256 + pub_key_metadata_offset
+    |      |   - padding                    | align by 64             |
+    |      +--------------------------------+-------------------------+
+    |      | Padding                        | align by block_size     |
+    +------+--------------------------------+-------------------------+ --> + (block_size * n)
+   
+    +---------------------------------------+-------------------------+
+    |                                       |                         |
+    |                                       |                         |
+    | DONOT CARE CHUNK                      |                         |
+    |                                       |                         |
+    |                                       |                         |
+    +--------------------------------------- -------------------------+
+                                             
+    +---------------------------------------+-------------------------+ --> partition_size - block_size
+    | Padding                               | block_size - 64         |
+    +---------------------------------------+-------------------------+ --> partition_size - 64
+    | AVB Footer                            | total 64                |
+    |                                       |                         |
+    |   - Footer Magic "AVBf"               |     4                   |
+    |   - Footer Major Version              |     4                   |
+    |   - Footer Minor Version              |     4                   |
+    |   - Original image size               |     8                   |
+    |   - VBMeta offset                     |     8                   |
+    |   - VBMeta size                       |     8                   |
+    |   - Padding                           |     28                  |
+    +---------------------------------------+-------------------------+ --> partition_size
diff --git a/README.md b/README.md
index 2bd26f2..83fb5fd 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,7 @@ An example boot.img has been placed at **src/test/resources/boot.img**, which is
 
 ## boot.img layout
 Read [layout](README.expert.md) of Android boot.img.
-We now support **os\_version** ,**os\_patch\_level**, **header_version** and **dtbo**
+We now support both VB 1.0 and AVB 2.0 layouts.
 
 ## References
 
diff --git a/README.other.md b/README.other.md
new file mode 100644
index 0000000..1e909e7
--- /dev/null
+++ b/README.other.md
@@ -0,0 +1,21 @@
+
+              item                        size in bytes             position
+    +--------------------------------+-------------------------+
+    | Hash Descriptor                |  total 132              |
+    |                                |                         |
+    |  - tag                         |     8                   |    --> +0
+    |  - num_bytes_following         |     8                   |    --> +8
+    |  - hash algorithm              |     8                   |    --> +16
+    |  - partition name              |     32                  |   
+    |  - salt length                 |     4                   |   
+    |  - digest length               |     4                   |   
+    |  - reserved                    |     60                  |    
+    +--------------------------------+-------------------------+
+    | Partition name                 |                         |
+    +--------------------------------+-------------------------+
+    | salt                           |                         |
+    +--------------------------------+-------------------------+
+    | digest                         |                         |
+    +--------------------------------+-------------------------+
+    | Padding                        |  align by 8             |
+    +--------------------------------+-------------------------+    --> +16 + num_bytes_following
diff --git a/avb/avbtool b/avb/avbtool
index e340abe..5f62948 100755
--- a/avb/avbtool
+++ b/avb/avbtool
@@ -2205,6 +2205,49 @@ class Avb(object):
         raise AvbError('Error verifying descriptor.')
 
 
+  def calculate_vbmeta_digest(self, image_filename, hash_algorithm, output):
+    """Implements the 'calculate_vbmeta_digest' command.
+
+    Arguments:
+      image_filename: Image file to get information from (file object).
+      hash_algorithm: Hash algorithm used.
+      output: Output file to write human-readable information to (file object).
+    """
+
+    image_dir = os.path.dirname(image_filename)
+    image_ext = os.path.splitext(image_filename)[1]
+
+    image = ImageHandler(image_filename)
+    (footer, header, descriptors, image_size) = self._parse_image(image)
+    offset = 0
+    if footer:
+      offset = footer.vbmeta_offset
+    size = (header.SIZE + header.authentication_data_block_size +
+            header.auxiliary_data_block_size)
+    image.seek(offset)
+    vbmeta_blob = image.read(size)
+
+    hasher = hashlib.new(name=hash_algorithm)
+    hasher.update(vbmeta_blob)
+
+    for desc in descriptors:
+      if isinstance(desc, AvbChainPartitionDescriptor):
+        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_offset = 0
+        if ch_footer:
+          ch_offset = ch_footer.vbmeta_offset
+          ch_size = (ch_header.SIZE + ch_header.authentication_data_block_size +
+                     ch_header.auxiliary_data_block_size)
+        ch_image.seek(ch_offset)
+        ch_vbmeta_blob = ch_image.read(ch_size)
+        hasher.update(ch_vbmeta_blob)
+
+    digest = hasher.digest()
+    output.write('{}\n'.format(digest.encode('hex')))
+
+
   def _parse_image(self, image):
     """Gets information about an image.
 
@@ -3871,6 +3914,22 @@ class AvbTool(object):
                             action='append')
     sub_parser.set_defaults(func=self.verify_image)
 
+    sub_parser = subparsers.add_parser(
+        'calculate_vbmeta_digest',
+        help='Calculate vbmeta digest.')
+    sub_parser.add_argument('--image',
+                            help='Image to calculate digest for',
+                            type=argparse.FileType('rb'),
+                            required=True)
+    sub_parser.add_argument('--hash_algorithm',
+                            help='Hash algorithm to use (default: sha256)',
+                            default='sha256')
+    sub_parser.add_argument('--output',
+                            help='Write hex digest to file (default: stdout)',
+                            type=argparse.FileType('wt'),
+                            default=sys.stdout)
+    sub_parser.set_defaults(func=self.calculate_vbmeta_digest)
+
     sub_parser = subparsers.add_parser('set_ab_metadata',
                                        help='Set A/B metadata.')
     sub_parser.add_argument('--misc_image',
@@ -4115,6 +4174,11 @@ class AvbTool(object):
     self.avb.verify_image(args.image.name, args.key,
                           args.expected_chain_partition)
 
+  def calculate_vbmeta_digest(self, args):
+    """Implements the 'calculate_vbmeta_digest' sub-command."""
+    self.avb.calculate_vbmeta_digest(args.image.name, args.hash_algorithm,
+                                     args.output)
+
   def make_atx_certificate(self, args):
     """Implements the 'make_atx_certificate' sub-command."""
     self.avb.make_atx_certificate(args.output, args.authority_key,
diff --git a/avb/avbtool.diff b/avb/avbtool.diff
index d88d459..faaa663 100644
--- a/avb/avbtool.diff
+++ b/avb/avbtool.diff
@@ -1,8 +1,14 @@
 diff --git a/avb/avbtool b/avb/avbtool
-index b742466..2830e20 100755
+index 8732024..5f62948 100755
 --- a/avb/avbtool
 +++ b/avb/avbtool
-@@ -2142,7 +2142,8 @@ class Avb(object):
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python2.7
+ 
+ # Copyright 2016, The Android Open Source Project
+ #
+@@ -2159,7 +2159,8 @@ class Avb(object):
          expected_chain_partitions_map[partition_name] = (rollback_index_location, pk_blob)
  
      image_dir = os.path.dirname(image_filename)
diff --git a/avb/avbtool.diff.2 b/avb/avbtool.diff.2
deleted file mode 100644
index b225281..0000000
--- a/avb/avbtool.diff.2
+++ /dev/null
@@ -1,10 +0,0 @@
-diff --git a/avb/avbtool b/avb/avbtool
-index 2830e20..647d344 100755
---- a/avb/avbtool
-+++ b/avb/avbtool
-@@ -1,4 +1,4 @@
--#!/usr/bin/env python
-+#!/usr/bin/env python2.7
- 
- # Copyright 2016, The Android Open Source Project
- #
diff --git a/bbootimg/build.gradle b/bbootimg/build.gradle
index 1a84fe1..df3659a 100644
--- a/bbootimg/build.gradle
+++ b/bbootimg/build.gradle
@@ -11,8 +11,9 @@ buildscript {
     }
 }
 
-apply plugin: 'kotlin'
-apply plugin: 'application'
+apply plugin: "java"
+apply plugin: "kotlin"
+apply plugin: "application"
 
 sourceCompatibility = 1.8
 compileKotlin {
@@ -43,6 +44,11 @@ dependencies {
     compile("org.apache.commons:commons-exec:1.3")
     compile("org.apache.commons:commons-compress:1.16.1")
     compile("junit:junit:4.12")
+    //compile("org.bouncycastle:bcprov-jdk15on:1.59")
+    compile("org.nd4j:nd4j-api:0.9.1")
+
+    compile project(':bouncycastle:bcpkix')
+    compile project(':bouncycastle:bcprov')
 }
 
 mainClassName = "cfig.RKt"
@@ -55,3 +61,9 @@ jar {
         attributes "Main-Class": "cfig.RKt"
     }
 }
+
+test {
+    testLogging {
+        showStandardStreams = true
+    }
+}
diff --git a/bbootimg/src/main/java/cfig/io/Struct.java b/bbootimg/src/main/java/cfig/io/Struct.java
new file mode 100644
index 0000000..cee99a8
--- /dev/null
+++ b/bbootimg/src/main/java/cfig/io/Struct.java
@@ -0,0 +1,359 @@
+package cfig.io;
+
+import cfig.Helper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.junit.Assert.assertEquals;
+
+public class Struct {
+    private static Logger log = LoggerFactory.getLogger(Struct.class);
+
+    public ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN;
+    public List<Object[]> formats = new ArrayList<>();
+
+    public Struct(String formatString) {
+        Matcher 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 {
+            log.debug("Parsing LITTLE_ENDIAN format: " + formatString);
+        }
+
+        while (m.find()) {
+            boolean bExpand = true;
+            int mul = 1;
+            if (!m.group(1).isEmpty()) {
+                mul = Integer.decode(m.group(1));
+            }
+            Object item[] = new Object[2];
+            switch (m.group(2)) {
+                case "x": {//byte 1
+                    item[0] = PadByte.class;
+                    bExpand = false;
+                    break;
+                }
+                case "b": {//byte 1
+                    item[0] = Byte.class;
+                    bExpand = false;
+                    break;
+                }
+                case "s": {//python: char 1
+                    item[0] = Character.class;
+                    bExpand = false;
+                    break;
+                }
+                case "h": {//2
+                    item[0] = Short.class;
+                    break;
+                }
+                case "H": {//2
+                    item[0] = UnsignedShort.class;
+                    break;
+                }
+                case "i":
+                case "l": {//4
+                    item[0] = Integer.class;
+                    break;
+                }
+                case "I":
+                case "L": {//4
+                    item[0] = UnsignedInt.class;
+                    break;
+                }
+                case "q": {//8
+                    item[0] = Long.class;
+                    break;
+                }
+                case "Q": {//8
+                    item[0] = UnsignedLong.class;
+                    break;
+                }
+                default: {
+                    throw new IllegalArgumentException("type [" + m.group(2) + "] not supported");
+                }
+            }
+            if (bExpand) {
+                item[1] = 1;
+                for (int i = 0; i < mul; i++) {
+                    formats.add(item);
+                }
+            } else {
+                item[1] = mul;
+                formats.add(item);
+            }
+        }
+    }
+
+    public Integer calcsize() {
+        Integer ret = 0;
+        for (Object[] format : formats) {
+            if (format[0] == Byte.class || format[0] == Character.class || format[0] == PadByte.class) {
+                ret += 1 * (int) format[1];
+                continue;
+            }
+            if (format[0] == Short.class) {
+                ret += 2 * (int) format[1];
+                continue;
+            }
+            if (format[0] == UnsignedShort.class) {
+                ret += 2 * (int) format[1];
+                continue;
+            }
+            if (format[0] == Integer.class) {
+                ret += 4 * (int) format[1];
+                continue;
+            }
+            if (format[0] == UnsignedInt.class) {
+                ret += 4 * (int) format[1];
+                continue;
+            }
+            if (format[0] == Long.class || format[0] == UnsignedLong.class) {
+                ret += 8 * (int) format[1];
+                continue;
+            }
+            throw new IllegalArgumentException("Class [" + format[0] + "] not supported");
+        }
+        return ret;
+    }
+
+    public void dump() {
+        log.info("--- Format ---");
+        log.info("Endian: " + this.byteOrder);
+        for (Object[] formatItem : formats) {
+            log.info(formatItem[0] + ":" + formatItem[1]);
+        }
+        log.info("--- Format ---");
+    }
+
+    public List unpack(InputStream iS) throws IOException {
+        List<Object> ret = new ArrayList<>();
+        ByteBuffer bf = ByteBuffer.allocate(32);
+        bf.order(this.byteOrder);
+        for (Object[] format : this.formats) {
+            //return 'null' for padding bytes
+            if (format[0] == PadByte.class) {
+                iS.skip((Integer) format[1]);
+                ret.add(null);
+                continue;
+            }
+
+            if (format[0] == Byte.class || format[0] == Character.class || format[0] == PadByte.class) {
+                byte[] data = new byte[(Integer) format[1]];
+                assertEquals((int) format[1], iS.read(data));
+                ret.add(data);
+                continue;
+            }
+
+            if (format[0] == Short.class) {
+                byte[] data = new byte[2];
+                assertEquals(2, iS.read(data));
+                bf.clear();
+                bf.put(data);
+                bf.flip();
+                ret.add(bf.getShort());
+                continue;
+            }
+
+            if (format[0] == UnsignedShort.class) {
+                byte[] data = new byte[2];
+                assertEquals(2, iS.read(data));
+                log.debug("UnsignedShort: " + Helper.Companion.toHexString(data));
+                bf.clear();
+                if (this.byteOrder == ByteOrder.LITTLE_ENDIAN) {
+                    bf.put(data);
+                    bf.put(new byte[2]); //complete high bits with 0
+                } else {
+                    bf.put(new byte[2]); //complete high bits with 0
+                    bf.put(data);
+                }
+                bf.flip();
+                ret.add(bf.getInt());
+                continue;
+            }
+
+            if (format[0] == Integer.class) {
+                byte[] data = new byte[4];
+                assertEquals(4, iS.read(data));
+                log.debug("Integer: " + Helper.Companion.toHexString(data));
+                bf.clear();
+                bf.put(data);
+                bf.flip();
+                ret.add(bf.getInt());
+                continue;
+            }
+
+            if (format[0] == UnsignedInt.class) {
+                byte[] data = new byte[4];
+                assertEquals(4, iS.read(data));
+                bf.clear();
+                log.debug("UnsignedInt: " + Helper.Companion.toHexString(data));
+                if (this.byteOrder == ByteOrder.LITTLE_ENDIAN) {
+                    bf.put(data);
+                    bf.put(new byte[4]); //complete high bits with 0
+                } else {
+                    bf.put(new byte[4]); //complete high bits with 0
+                    bf.put(data);
+                }
+                bf.flip();
+                ret.add(bf.getLong());
+                continue;
+            }
+
+            //TODO: maybe exceeds limits of Long.class ?
+            if (format[0] == Long.class || format[0] == UnsignedLong.class) {
+                byte[] data = new byte[8];
+                assertEquals(8, iS.read(data));
+                bf.clear();
+                bf.put(data);
+                bf.flip();
+                ret.add(bf.getLong());
+                continue;
+            }
+
+            throw new IllegalArgumentException("Class [" + format[0] + "] not supported");
+        }
+        return ret;
+    }
+
+    public byte[] pack(Object... args) {
+        if (args.length != this.formats.size()) {
+            throw new IllegalArgumentException("argument size " + args.length +
+                    " doesn't match format size " + this.formats.size());
+        }
+        ByteBuffer bf = ByteBuffer.allocate(this.calcsize());
+        bf.order(this.byteOrder);
+        for (int i = 0; i < args.length; i++) {
+            Object arg = args[i];
+            Class<?> format = (Class<?>) formats.get(i)[0];
+            Integer size = (int) formats.get(i)[1];
+            log.debug("Index[" + i + "], fmt = " + format + ", arg = " + arg + ", multi = " + size);
+
+            //padding
+            if (format == PadByte.class) {
+                byte b[] = new byte[size];
+                if (arg == null) {
+                    Arrays.fill(b, (byte) 0);
+                } else if (arg instanceof Byte) {
+                    Arrays.fill(b, (byte) arg);
+                } else if (arg instanceof Integer) {
+                    Arrays.fill(b, ((Integer) arg).byteValue());
+                } else {
+                    throw new IllegalArgumentException("Index[" + i + "] Unsupported arg [" + arg + "] with type [" + format + "]");
+                }
+                bf.put(b);
+                continue;
+            }
+
+            //signed byte
+            if (arg instanceof byte[]) {
+                bf.put((byte[]) arg);
+                int paddingSize = size - ((byte[]) arg).length;
+                if (0 < paddingSize) {
+                    byte padBytes[] = new byte[size - ((byte[]) arg).length];
+                    Arrays.fill(padBytes, (byte) 0);
+                    bf.put(padBytes);
+                } else if (0 > paddingSize) {
+                    log.error("container size " + size + ", value size " + ((byte[]) arg).length);
+                    throw new IllegalArgumentException("Index[" + i + "] arg [" + arg + "] with type [" + format + "] size overflow");
+                } else {
+                    //perfect match
+                }
+                continue;
+            }
+
+            //unsigned byte
+            if (arg instanceof int[] && format == Byte.class) {
+                for (int v : (int[]) arg) {
+                    if (v > 255 || v < 0) {
+                        throw new IllegalArgumentException("Index[" + i + "] Unsupported [int array] arg [" + arg + "] with type [" + format + "]");
+                    }
+                    bf.put((byte) v);
+                }
+                continue;
+            }
+
+            if (arg instanceof Short) {
+                bf.putShort((short) arg);
+                continue;
+            }
+
+            if (arg instanceof Integer) {
+                if (format == Integer.class) {
+                    bf.putInt((int) arg);
+                } else if (format == UnsignedShort.class) {
+                    ByteBuffer bf2 = ByteBuffer.allocate(4);
+                    bf2.order(this.byteOrder);
+                    bf2.putInt((int) arg);
+                    bf2.flip();
+                    if (this.byteOrder == ByteOrder.LITTLE_ENDIAN) {//LE
+                        bf.putShort(bf2.getShort());
+                        bf2.getShort();//discard
+                    } else {//BE
+                        bf2.getShort();//discard
+                        bf.putShort(bf2.getShort());
+                    }
+                } else if (format == UnsignedInt.class) {
+                    if ((Integer) arg < 0) {
+                        throw new IllegalArgumentException("Index[" + i + "] Unsupported [Integer] arg [" + arg + "] with type [" + format + "]");
+                    }
+                    bf.putInt((int) arg);
+                } else {
+                    throw new IllegalArgumentException("Index[" + i + "] Unsupported [Integer] arg [" + arg + "] with type [" + format + "]");
+                }
+                continue;
+            }
+
+            if (arg instanceof Long) {
+                //XXX: maybe run into issue if we meet REAL Unsigned Long
+                if (format == Long.class || format == UnsignedLong.class) {
+                    bf.putLong((long) arg);
+                } else if (format == UnsignedInt.class) {
+                    if ((Long) arg < 0L || (Long) arg > (Integer.MAX_VALUE * 2L + 1)) {
+                        throw new IllegalArgumentException("Index[" + i + "] Unsupported [Long] arg [" + arg + "] with type [" + format + "]");
+                    }
+                    ByteBuffer bf2 = ByteBuffer.allocate(8);
+                    bf2.order(this.byteOrder);
+                    bf2.putLong((long) arg);
+                    bf2.flip();
+                    if (this.byteOrder == ByteOrder.LITTLE_ENDIAN) {//LE
+                        bf.putInt(bf2.getInt());
+                        bf2.getInt();//discard
+                    } else {//BE
+                        bf2.getInt();//discard
+                        bf.putInt(bf2.getInt());
+                    }
+                } else {
+                    throw new IllegalArgumentException("Index[" + i + "] Unsupported arg [" + arg + "] with type [" + format + "]");
+                }
+                continue;
+            }
+        }
+        log.debug("Pack Result:" + Helper.Companion.toHexString(bf.array()));
+        return bf.array();
+    }
+
+    public static class UnsignedInt {
+    }
+
+    public static class UnsignedLong {
+    }
+
+    public static class UnsignedShort {
+    }
+
+    public static class PadByte {
+    }
+}
diff --git a/bbootimg/src/main/kotlin/AVBInfo.kt b/bbootimg/src/main/kotlin/AVBInfo.kt
new file mode 100755
index 0000000..783d75b
--- /dev/null
+++ b/bbootimg/src/main/kotlin/AVBInfo.kt
@@ -0,0 +1,51 @@
+package cfig
+
+import avb.*
+import avb.desc.*
+import org.bouncycastle.util.encoders.Hex
+
+/*
+    a wonderfaul base64 encoder/decoder: https://cryptii.com/base64-to-hex
+ */
+class AVBInfo(var header: Header? = null,
+              var authBlob: AuthBlob? = null,
+              var auxBlob: AuxBlob? = null,
+              var footer: Footer? = null) {
+    data class AuthBlob(
+            var offset: Long = 0L,
+            var size: Long = 0L,
+            var hash: String? = null,
+            var signature: String? = null)
+
+    data class AuxBlob(
+            var pubkey: PubKeyInfo? = null,
+            var pubkeyMeta: PubKeyMetadataInfo? = null,
+            var hashTreeDescriptor: MutableList<HashTreeDescriptor> = mutableListOf(),
+            var hashDescriptors: MutableList<HashDescriptor> = mutableListOf(),
+            var kernelCmdlineDescriptor: MutableList<KernelCmdlineDescriptor> = mutableListOf(),
+            var unknownDescriptors: MutableList<UnknownDescriptor> = mutableListOf()
+    ) {
+        data class PubKeyInfo(
+                var offset: Long = 0L,
+                var size: Long = 0L,
+                var pubkey: ByteArray = byteArrayOf()
+        )
+
+        data class PubKeyMetadataInfo(
+                var offset: Long = 0L,
+                var size: Long = 0L
+        )
+
+        fun encodeDescriptors(): ByteArray {
+            var descList: MutableList<Descriptor> = mutableListOf()
+            this.hashTreeDescriptor.forEach { descList.add(it) }
+            this.hashDescriptors.forEach { descList.add(it) }
+            this.kernelCmdlineDescriptor.forEach { descList.add(it) }
+            this.unknownDescriptors.forEach { descList.add(it) }
+            descList.sortBy { it.sequence }
+            var ret = byteArrayOf()
+            descList.forEach { ret = Helper.join(ret, it.encode()) }
+            return ret
+        }
+    }
+}
diff --git a/bbootimg/src/main/kotlin/Avb.kt b/bbootimg/src/main/kotlin/Avb.kt
new file mode 100755
index 0000000..fb940fc
--- /dev/null
+++ b/bbootimg/src/main/kotlin/Avb.kt
@@ -0,0 +1,405 @@
+package cfig
+
+import avb.*
+import avb.alg.Algorithms
+import avb.desc.*
+import cfig.io.Struct
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.bouncycastle.util.encoders.Hex
+import org.slf4j.LoggerFactory
+import java.io.*
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.nio.file.StandardOpenOption
+import java.security.MessageDigest
+
+class Avb {
+    val MAX_VBMETA_SIZE = 64 * 1024
+    val MAX_FOOTER_SIZE = 4096
+    val BLOCK_SIZE = 4096
+
+    private var required_libavb_version_minor = 0
+
+    fun add_hash_footer(image_file: String,
+                        partition_size: Long,
+                        use_persistent_digest: Boolean,
+                        do_not_use_ab: Boolean,
+                        salt: String,
+                        hash_algorithm: String,
+                        partition_name: String,
+                        rollback_index: Long,
+                        common_algorithm: String,
+                        common_key_path: String) {
+        var original_image_size = 0L
+        //required libavb version
+        if (use_persistent_digest || do_not_use_ab) {
+            required_libavb_version_minor = 1
+        }
+        log.info("Required_libavb_version: 1.$required_libavb_version_minor")
+
+        // SIZE + metadata (footer + vbmeta struct)
+        val max_metadata_size = MAX_VBMETA_SIZE + MAX_FOOTER_SIZE
+        if (partition_size < max_metadata_size) {
+            throw IllegalArgumentException("Parition SIZE of $partition_size is too small. " +
+                    "Needs to be at least $max_metadata_size")
+        }
+        val max_image_size = partition_size - max_metadata_size
+        log.info("max_image_size: $max_image_size")
+
+        if (partition_size % 4096L != 0L) {
+            throw IllegalArgumentException("Partition SIZE of $partition_size is not a multiple of the image block SIZE 4096")
+        }
+
+        val fis = FileInputStream(image_file)
+        fis.skip(File(image_file).length() - 64)
+        try {
+            val footer = Footer(fis)
+            original_image_size = footer.originalImageSize
+            FileOutputStream(File(image_file), true).channel.use {
+                log.info("truncate $image_file to its original SIZE ${footer.originalImageSize}")
+                it.truncate(footer.originalImageSize)
+            }
+        } catch (e: IllegalArgumentException) {
+            log.info("original image doesn't have footer")
+            original_image_size = File(image_file).length()
+        }
+
+        val saltByteArray = Helper.fromHexString(salt)
+        val digest = MessageDigest.getInstance(Helper.pyAlg2java(hash_algorithm)).apply {
+            update(saltByteArray)
+            update(File(image_file).readBytes())
+        }.digest()
+        log.info("Digest: " + Helper.toHexString(digest))
+
+        val hd = HashDescriptor()
+        hd.image_size = File(image_file).length()
+        hd.hash_algorithm = hash_algorithm.toByteArray()
+        hd.partition_name = partition_name
+        hd.salt = saltByteArray
+        hd.flags = 0
+        if (do_not_use_ab) hd.flags = hd.flags or 1
+        if (!use_persistent_digest) hd.digest = digest
+        log.info("encoded hash descriptor:" + String(Hex.encode(hd.encode())))
+        val vbmeta_blob = generateVbMetaBlob(common_algorithm,
+                common_key_path,
+                null,
+                arrayOf(hd as Descriptor),
+                null,
+                rollback_index,
+                0,
+                null,
+                null,
+                null,
+                false,
+                null,
+                false,
+                null,
+                null,
+                null,
+                false,
+                0)
+        log.debug("vbmeta_blob: " + Helper.toHexString(vbmeta_blob))
+
+        if (hd.image_size % BLOCK_SIZE != 0L) {
+            val padding_needed = BLOCK_SIZE - (hd.image_size % BLOCK_SIZE)
+            FileOutputStream(image_file, true).use { fos ->
+                fos.write(ByteArray(padding_needed.toInt()))
+            }
+            log.info("$image_file padded: ${hd.image_size} -> ${File(image_file).length()}")
+        } else {
+            log.info("$image_file doesn't need padding")
+        }
+        val vbmeta_offset = hd.image_size
+        val padding_needed = Helper.round_to_multiple(vbmeta_blob.size.toLong(), BLOCK_SIZE) - vbmeta_blob.size
+        val vbmeta_blob_with_padding = Helper.join(vbmeta_blob, Struct("${padding_needed}x").pack(null))
+        FileOutputStream(image_file, true).use { fos ->
+            fos.write(vbmeta_blob_with_padding)
+        }
+        val vbmeta_end_offset = vbmeta_offset + vbmeta_blob_with_padding.size
+        FileOutputStream(image_file, true).use { fos ->
+            fos.write(Struct("${partition_size - vbmeta_end_offset - 1 * BLOCK_SIZE}x").pack(null))
+        }
+
+        val footer = Footer()
+        footer.originalImageSize = original_image_size
+        footer.vbMetaOffset = vbmeta_offset
+        footer.vbMetaSize = vbmeta_blob.size.toLong()
+        val footer_blob = footer.encode()
+        val footer_blob_with_padding = Helper.join(
+                Struct("${BLOCK_SIZE - Footer.SIZE}x").pack(null), footer_blob)
+        log.info("footer:" + Helper.toHexString(footer_blob))
+        log.info(footer.toString())
+        FileOutputStream(image_file, true).use { fos ->
+            fos.write(footer_blob_with_padding)
+        }
+    }
+
+    fun generateVbMetaBlob(algorithm_name: String,
+                           key_path: String?,
+                           public_key_metadata_path: String?,
+                           descriptors: Array<Descriptor>,
+                           chain_partitions: String?,
+                           inRollbackIndex: Long,
+                           inFlags: Long,
+                           props: String?,
+                           props_from_file: String?,
+                           kernel_cmdlines: String?,
+                           setup_rootfs_from_kernel: Boolean,
+                           ht_desc_to_setup: String?,
+                           include_descriptors_from_image: Boolean,
+                           signing_helper: String?,
+                           signing_helper_with_files: String?,
+                           release_string: String?,
+                           append_to_release_string: Boolean,
+                           required_libavb_version_minor: Int): ByteArray {
+        //encoded descriptors
+        var encodedDesc: ByteArray = byteArrayOf()
+        descriptors.forEach { encodedDesc = Helper.join(encodedDesc, it.encode()) }
+        //algorithm
+        val alg = Algorithms.get(algorithm_name)!!
+        //encoded pubkey
+        val encodedKey = Blob.encodePubKey(alg, Files.readAllBytes(Paths.get(key_path)))
+
+        //3 - whole aux blob
+        val auxBlob = Blob.getAuxDataBlob(encodedDesc, encodedKey)
+
+        //1 - whole header blob
+        val headerBlob = Header().apply {
+            bump_required_libavb_version_minor(required_libavb_version_minor)
+            auxiliary_data_block_size = auxBlob.size.toLong()
+
+            authentication_data_block_size = Helper.round_to_multiple(
+                    (alg.hash_num_bytes + alg.signature_num_bytes).toLong(), 64)
+
+            algorithm_type = alg.algorithm_type.toLong()
+
+            hash_offset = 0
+            hash_size = alg.hash_num_bytes.toLong()
+
+            signature_offset = alg.hash_num_bytes.toLong()
+            signature_size = alg.signature_num_bytes.toLong()
+
+            public_key_offset = descriptors_size
+            public_key_size = encodedKey.size.toLong()
+
+            //TODO: support pubkey metadata
+            public_key_metadata_size = 0
+            public_key_metadata_offset = public_key_offset + public_key_size
+
+            descriptors_offset = 0
+            descriptors_size = encodedDesc.size.toLong()
+
+            rollback_index = inRollbackIndex
+            flags = inFlags
+        }.encode()
+
+        //2 - auth blob
+        var authBlob = Blob.getAuthBlob(headerBlob, auxBlob, algorithm_name, key_path)
+
+        return Helper.join(headerBlob, authBlob, auxBlob)
+    }
+
+    fun parseVbMeta(image_file: String): AVBInfo {
+        log.info("parsing $image_file ...")
+        val jsonFile = getJsonFileName(image_file)
+        var footer: Footer? = null
+        var vbMetaOffset = 0L
+        FileInputStream(image_file).use { fis ->
+            fis.skip(File(image_file).length() - Footer.SIZE)
+            try {
+                footer = Footer(fis)
+                vbMetaOffset = footer!!.vbMetaOffset
+                log.info("$image_file: $footer")
+            } catch (e: IllegalArgumentException) {
+                log.info("image $image_file has no AVB Footer")
+            }
+        }
+
+        var vbMetaHeader = Header()
+        FileInputStream(image_file).use { fis ->
+            fis.skip(vbMetaOffset)
+            vbMetaHeader = Header(fis)
+        }
+        log.info(vbMetaHeader.toString())
+        log.debug(ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(vbMetaHeader))
+
+        val authBlockOffset = vbMetaOffset + Header.SIZE
+        val auxBlockOffset = authBlockOffset + vbMetaHeader.authentication_data_block_size
+        val descStartOffset = auxBlockOffset + vbMetaHeader.descriptors_offset
+
+        val ai = AVBInfo()
+        ai.footer = footer
+        ai.auxBlob = AVBInfo.AuxBlob()
+        ai.header = vbMetaHeader
+        if (vbMetaHeader.public_key_size > 0L) {
+            ai.auxBlob!!.pubkey = AVBInfo.AuxBlob.PubKeyInfo()
+            ai.auxBlob!!.pubkey!!.offset = vbMetaHeader.public_key_offset
+            ai.auxBlob!!.pubkey!!.size = vbMetaHeader.public_key_size
+        }
+        if (vbMetaHeader.public_key_metadata_size > 0L) {
+            ai.auxBlob!!.pubkeyMeta = AVBInfo.AuxBlob.PubKeyMetadataInfo()
+            ai.auxBlob!!.pubkeyMeta!!.offset = vbMetaHeader.public_key_metadata_offset
+            ai.auxBlob!!.pubkeyMeta!!.size = vbMetaHeader.public_key_metadata_size
+        }
+
+        var descriptors: List<Any> = mutableListOf()
+        if (vbMetaHeader.descriptors_size > 0) {
+            FileInputStream(image_file).use { fis ->
+                fis.skip(descStartOffset)
+                descriptors = UnknownDescriptor.parseDescriptors2(fis, vbMetaHeader.descriptors_size)
+            }
+
+            descriptors.forEach {
+                log.debug(it.toString())
+            }
+        }
+
+        if (vbMetaHeader.public_key_size > 0) {
+            FileInputStream(image_file).use { fis ->
+                fis.skip(auxBlockOffset)
+                fis.skip(vbMetaHeader.public_key_offset)
+                ai.auxBlob!!.pubkey!!.pubkey = ByteArray(vbMetaHeader.public_key_size.toInt())
+                fis.read(ai.auxBlob!!.pubkey!!.pubkey)
+                log.debug("Parsed Pub Key: " + String(Hex.encode(ai.auxBlob!!.pubkey!!.pubkey)))
+            }
+        }
+
+        if (vbMetaHeader.public_key_metadata_size > 0) {
+            FileInputStream(image_file).use { fis ->
+                fis.skip(vbMetaOffset)
+                fis.skip(Header.SIZE.toLong())
+                fis.skip(vbMetaHeader.public_key_metadata_offset)
+                val ba = ByteArray(vbMetaHeader.public_key_metadata_size.toInt())
+                fis.read(ba)
+                log.debug("Parsed Pub Key Metadata: " + String(Hex.encode(ba)))
+            }
+        }
+
+        if (vbMetaHeader.authentication_data_block_size > 0) {
+            FileInputStream(image_file).use { fis ->
+                fis.skip(vbMetaOffset)
+                fis.skip(Header.SIZE.toLong())
+                fis.skip(vbMetaHeader.hash_offset)
+                val ba = ByteArray(vbMetaHeader.hash_size.toInt())
+                fis.read(ba)
+                log.debug("Parsed Auth Hash (Header & Aux Blob): " + Hex.encode(ba))
+                val bb = ByteArray(vbMetaHeader.signature_size.toInt())
+                fis.read(bb)
+                log.debug("Parsed Auth Signature (of hash): " + String(Hex.encode(bb)))
+
+                ai.authBlob = AVBInfo.AuthBlob()
+                ai.authBlob!!.offset = authBlockOffset
+                ai.authBlob!!.size = vbMetaHeader.authentication_data_block_size
+                ai.authBlob!!.hash = String(Hex.encode(ba))
+                ai.authBlob!!.signature = String(Hex.encode(bb))
+            }
+        }
+
+        descriptors.forEach {
+            when (it) {
+                is HashDescriptor -> {
+                    ai.auxBlob!!.hashDescriptors.add(it)
+                }
+                is KernelCmdlineDescriptor -> {
+                    ai.auxBlob!!.kernelCmdlineDescriptor.add(it)
+                }
+                is HashTreeDescriptor -> {
+                    ai.auxBlob!!.hashTreeDescriptor.add(it)
+                }
+                is UnknownDescriptor -> {
+                    ai.auxBlob!!.unknownDescriptors.add(it)
+                }
+                else -> {
+                    throw IllegalArgumentException("invalid descriptor: $it")
+                }
+            }
+        }
+        val aiStr = ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(ai)
+        log.debug(aiStr)
+        ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(File(jsonFile), ai)
+        log.info("vbmeta info written to $jsonFile")
+
+        return ai
+    }
+
+    fun packVbMeta(key_path: String, info: AVBInfo? = null): ByteArray {
+        val ai = info ?: ObjectMapper().readValue(File(getJsonFileName("vbmeta.img")), AVBInfo::class.java)
+        val alg = Algorithms.get(ai.header!!.algorithm_type.toInt())!!
+        val encodedDesc = ai.auxBlob!!.encodeDescriptors()
+        //encoded pubkey
+        val encodedKey = Blob.encodePubKey(alg, Files.readAllBytes(Paths.get(key_path)))
+
+        //3 - whole aux blob
+        var auxBlob = byteArrayOf()
+        if (ai.header!!.auxiliary_data_block_size > 0) {
+            if (encodedKey.contentEquals(ai.auxBlob!!.pubkey!!.pubkey)) {
+                log.info("Using the same key as original vbmeta")
+            } else {
+                log.warn("Using different key from original vbmeta")
+            }
+            auxBlob = Blob.getAuxDataBlob(encodedDesc, encodedKey)
+        } else {
+            log.info("No aux blob")
+        }
+
+        //1 - whole header blob
+        val headerBlob = ai.header!!.apply {
+            auxiliary_data_block_size = auxBlob.size.toLong()
+            authentication_data_block_size = Helper.round_to_multiple(
+                    (alg.hash_num_bytes + alg.signature_num_bytes).toLong(), 64)
+
+            hash_offset = 0
+            hash_size = alg.hash_num_bytes.toLong()
+
+            signature_offset = alg.hash_num_bytes.toLong()
+            signature_size = alg.signature_num_bytes.toLong()
+
+            public_key_offset = descriptors_size
+            public_key_size = encodedKey.size.toLong()
+
+            //TODO: support pubkey metadata
+            public_key_metadata_size = 0
+            public_key_metadata_offset = public_key_offset + public_key_size
+
+            descriptors_offset = 0
+            descriptors_size = encodedDesc.size.toLong()
+        }.encode()
+
+        //2 - auth blob
+        var authBlob = byteArrayOf()
+        if (ai.authBlob != null) {
+            authBlob = Blob.getAuthBlob(headerBlob, auxBlob, alg.name, key_path)
+        } else {
+            log.info("No auth blob")
+        }
+
+        return Helper.join(headerBlob, authBlob, auxBlob)
+    }
+
+    fun packVbMetaWithPadding(key_path: String, info: AVBInfo? = null) {
+        val rawBlob = packVbMeta(key_path, info)
+        val paddingSize = Helper.round_to_multiple(rawBlob.size.toLong(), BLOCK_SIZE) - rawBlob.size
+        val paddedBlob = Helper.join(rawBlob, Struct("${paddingSize}x").pack(null))
+        log.info("raw vbmeta size ${rawBlob.size}, padding size $paddingSize, total blob size ${paddedBlob.size}")
+        log.info("Writing padded vbmeta to file: vbmeta.img.signed")
+        Files.write(Paths.get("vbmeta.img.signed"), paddedBlob, StandardOpenOption.CREATE)
+    }
+
+    companion object {
+        private val log = LoggerFactory.getLogger(Avb::class.java)
+        val AVB_VERSION_MAJOR = 1
+        val AVB_VERSION_MINOR = 1
+        val AVB_VERSION_SUB = 0
+
+        //Keep in sync with libavb/avb_footer.h.
+        val AVB_FOOTER_VERSION_MAJOR = 1
+        val AVB_FOOTER_VERSION_MINOR = 0
+
+        fun getJsonFileName(image_file: String): String {
+            val fileName = File(image_file).name
+//        val jsonFile = fileName.substring(0, fileName.lastIndexOf(".")) + ".json"
+            val jsonFile = "$fileName.avb.json"
+            return UnifiedConfig.workDir + jsonFile
+        }
+    }
+}
diff --git a/bbootimg/src/main/kotlin/Helper.kt b/bbootimg/src/main/kotlin/Helper.kt
index 34a3a51..571c6e9 100644
--- a/bbootimg/src/main/kotlin/Helper.kt
+++ b/bbootimg/src/main/kotlin/Helper.kt
@@ -1,16 +1,50 @@
 package cfig
 
+import cfig.io.Struct
+import com.google.common.math.BigIntegerMath
 import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
 import org.apache.commons.compress.compressors.gzip.GzipParameters
+import org.apache.commons.exec.CommandLine
+import org.apache.commons.exec.DefaultExecutor
+import org.apache.commons.exec.ExecuteException
+import org.apache.commons.exec.PumpStreamHandler
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
+import org.bouncycastle.asn1.pkcs.RSAPrivateKey
+import org.bouncycastle.util.encoders.Hex
+import org.bouncycastle.util.io.pem.PemReader
+import org.junit.Assert
+import org.junit.Assert.assertTrue
 import org.slf4j.LoggerFactory
+import java.io.*
+import java.math.BigInteger
+import java.math.RoundingMode
 import java.nio.charset.StandardCharsets
 import java.util.zip.GZIPInputStream
 import java.util.zip.GZIPOutputStream
-import org.junit.Assert.*
-import java.io.*
+import javax.crypto.EncryptedPrivateKeyInfo
+import java.security.spec.InvalidKeySpecException
+import javax.crypto.Cipher
+import javax.crypto.spec.PBEKeySpec
+import javax.crypto.SecretKeyFactory
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.security.GeneralSecurityException
+import java.security.PrivateKey
+import java.security.spec.PKCS8EncodedKeySpec
+import java.util.*
+
 
 class Helper {
     companion object {
+        fun join(vararg source: ByteArray): ByteArray {
+            val baos = ByteArrayOutputStream()
+            for (src in source) {
+                if (source.isNotEmpty()) baos.write(src)
+            }
+            return baos.toByteArray()
+        }
+
         fun toHexString(inData: ByteArray): String {
             val sb = StringBuilder()
             for (i in inData.indices) {
@@ -31,7 +65,7 @@ class Helper {
         }
 
         //similar to this.toString(StandardCharsets.UTF_8).replace("${Character.MIN_VALUE}", "")
-        fun byteArray2CString(ba: ByteArray): String {
+        fun toCString(ba: ByteArray): String {
             val str = ba.toString(StandardCharsets.UTF_8)
             val nullPos = str.indexOf(Character.MIN_VALUE)
             return if (nullPos >= 0) {
@@ -94,8 +128,8 @@ class Helper {
         @Throws(IOException::class)
         fun gnuZipFile(compressedFile: String, fis: InputStream) {
             val buffer = ByteArray(1024)
-            FileOutputStream(compressedFile).use {fos ->
-                GZIPOutputStream(fos).use {gos ->
+            FileOutputStream(compressedFile).use { fos ->
+                GZIPOutputStream(fos).use { gos ->
                     var bytesRead: Int
                     while (true) {
                         bytesRead = fis.read(buffer)
@@ -111,8 +145,8 @@ class Helper {
             val buffer = ByteArray(1024)
             val p = GzipParameters()
             p.operatingSystem = 3
-            FileOutputStream(compressedFile).use {fos ->
-                GzipCompressorOutputStream(fos, p).use {gos ->
+            FileOutputStream(compressedFile).use { fos ->
+                GzipCompressorOutputStream(fos, p).use { gos ->
                     var bytesRead: Int
                     while (true) {
                         bytesRead = fis.read(buffer)
@@ -138,6 +172,86 @@ class Helper {
             }
         }
 
+        fun round_to_multiple(size: Long, page: Int): Long {
+            val remainder = size % page
+            return if (remainder == 0L) {
+                size
+            } else {
+                size + page - remainder
+            }
+        }
+
+
+        /*
+            read RSA private key
+            assert exp == 65537
+            num_bits = log2(modulus)
+
+            @return: AvbRSAPublicKeyHeader formatted bytearray
+                    https://android.googlesource.com/platform/external/avb/+/master/libavb/avb_crypto.h#158
+         */
+        fun encodeRSAkey(key: ByteArray): ByteArray {
+            val p2 = PemReader(InputStreamReader(ByteArrayInputStream(key))).readPemObject()
+            Assert.assertEquals("RSA PRIVATE KEY", p2.type)
+
+            val rsa = RSAPrivateKey.getInstance(p2.content)
+            Assert.assertEquals(65537.toBigInteger(), rsa.publicExponent)
+            val numBits: Int = BigIntegerMath.log2(rsa.modulus, RoundingMode.CEILING)
+            log.debug("modulus: " + rsa.modulus)
+            log.debug("numBits: " + numBits)
+            val b = BigInteger.valueOf(2).pow(32)
+            val n0inv = (b - rsa.modulus.modInverse(b)).toLong()
+            log.debug("n0inv = " + n0inv)
+            val r = BigInteger.valueOf(2).pow(numBits)
+            val rrModn = (r * r).mod(rsa.modulus)
+            log.debug("BB: " + numBits / 8 + ", mod_len: " + rsa.modulus.toByteArray().size + ", rrmodn = " + rrModn.toByteArray().size)
+            val unsignedModulo = rsa.modulus.toByteArray().sliceArray(1..numBits/8) //remove sign byte
+            log.debug("unsigned modulo: " + String(Hex.encode(unsignedModulo)))
+            val ret = Struct("!II${numBits / 8}b${numBits / 8}b").pack(
+                    numBits,
+                    n0inv,
+                    unsignedModulo,
+                    rrModn.toByteArray())
+            log.debug("rrmodn: " + String(Hex.encode(rrModn.toByteArray())))
+            log.debug("RSA: " + String(Hex.encode(ret)))
+            return ret
+        }
+
+        fun rawSign(keyPath: String, data: ByteArray): ByteArray {
+//            openssl rsautl -sign -inkey /Users/yu/work/boot/avb/avb_test_data/testkey_rsa4096.pem -raw
+            log.debug("Raw sign data: SIZE = " + data.size)
+            var ret = byteArrayOf()
+            val exe = DefaultExecutor()
+            val stdin = ByteArrayInputStream(data)
+            val stdout = ByteArrayOutputStream()
+            val stderr = ByteArrayOutputStream()
+            exe.streamHandler = PumpStreamHandler(stdout, stderr, stdin)
+            try {
+                exe.execute(CommandLine.parse("openssl rsautl -sign -inkey $keyPath -raw"))
+                ret = stdout.toByteArray()
+            } catch (e: ExecuteException) {
+                log.error("Execute error")
+            } finally {
+                log.debug("OUT: " + String(Hex.encode(stdout.toByteArray())))
+                log.debug("ERR: " + String(stderr.toByteArray()))
+            }
+
+            if (ret.isEmpty()) throw RuntimeException("raw sign failed")
+
+            return ret
+        }
+
+        fun pyAlg2java(alg: String): String {
+            return when (alg) {
+                "sha1" -> "sha-1"
+                "sha224" -> "sha-224"
+                "sha256" -> "sha-256"
+                "sha384" -> "sha-384"
+                "sha512" -> "sha-512"
+                else -> throw IllegalArgumentException("unknown algorithm: $alg")
+            }
+        }
+
         private val log = LoggerFactory.getLogger("Helper")
     }
 }
diff --git a/bbootimg/src/main/kotlin/ImgInfo.kt b/bbootimg/src/main/kotlin/ImgInfo.kt
index 1391ad0..05dd6c1 100644
--- a/bbootimg/src/main/kotlin/ImgInfo.kt
+++ b/bbootimg/src/main/kotlin/ImgInfo.kt
@@ -25,7 +25,9 @@ data class ImgInfo(
             var originalImageSize: Int? = null,
             var imageSize: Int? = null,
             var partName: String? = null,
-            var salt: String = "")
+            var salt: String = "",
+            var hashAlgorithm: String? = null,
+            var algorithm: String? = null)
 
     data class VeritySignature(
             var type: String = "dm-verity",
diff --git a/bbootimg/src/main/kotlin/Packer.kt b/bbootimg/src/main/kotlin/Packer.kt
index ec2bf3e..0d1fcab 100644
--- a/bbootimg/src/main/kotlin/Packer.kt
+++ b/bbootimg/src/main/kotlin/Packer.kt
@@ -41,7 +41,7 @@ class Packer {
                     md.update(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN)
                             .putInt(currentFile.length().toInt())
                             .array())
-                    log.debug("update size $item: " + Helper.toHexString((md.clone() as MessageDigest).digest()))
+                    log.debug("update SIZE $item: " + Helper.toHexString((md.clone() as MessageDigest).digest()))
                 }
             }
         }
@@ -71,7 +71,7 @@ class Packer {
 
     private fun writeData(inArgs: ImgArgs) {
         log.info("Writing data ...")
-        val bf = ByteBuffer.allocate(1024 * 1024 * 64)//assume total size small than 64MB
+        val bf = ByteBuffer.allocate(1024 * 1024 * 64)//assume total SIZE small than 64MB
         bf.order(ByteOrder.LITTLE_ENDIAN)
 
         writePaddedFile(bf, inArgs.kernel, inArgs.pageSize)
@@ -246,6 +246,7 @@ class Packer {
         File(args.output + ".google").deleleIfExists()
         File(args.output + ".clear").deleleIfExists()
         File(args.output + ".signed").deleleIfExists()
+        File(args.output + ".signed2").deleleIfExists()
         File("${UnifiedConfig.workDir}ramdisk.img").deleleIfExists()
 
         args.ramdisk?.let {
@@ -273,47 +274,6 @@ class Packer {
         }
     }
 
-    fun sign(avbtool: String, bootSigner: String) {
-        log.info("Loading config from ${workDir}bootimg.json")
-        val cfg = ObjectMapper().readValue(File(workDir + "bootimg.json"), UnifiedConfig::class.java)
-        val readBack = cfg.toArgs()
-        val args = readBack[0] as ImgArgs
-        val info = readBack[1] as ImgInfo
-
-        when (args.verifyType) {
-            ImgArgs.VerifyType.VERIFY -> {
-                log.info("Signing with verified-boot 1.0 style")
-                val sig = ObjectMapper().readValue(
-                        mapToJson(info.signature as LinkedHashMap<*, *>), ImgInfo.VeritySignature::class.java)
-                DefaultExecutor().execute(CommandLine.parse("java -jar $bootSigner " +
-                        "${sig.path} ${args.output}.clear ${sig.verity_pk8} ${sig.verity_pem} ${args.output}.signed"))
-
-            }
-            ImgArgs.VerifyType.AVB -> {
-                log.info("Adding hash_footer with verified-boot 2.0 style")
-                val sig = ObjectMapper().readValue(
-                        mapToJson(info.signature as LinkedHashMap<*, *>), ImgInfo.AvbSignature::class.java)
-                File(args.output + ".clear").copyTo(File(args.output + ".signed"))
-                DefaultExecutor().execute(CommandLine.parse(
-                        "$avbtool add_hash_footer " +
-                                "--image ${args.output}.signed " +
-                                "--partition_size ${sig.imageSize} " +
-                                "--salt ${sig.salt} " +
-                                "--partition_name ${sig.partName}"))
-                verifyAVBIntegrity(args, avbtool)
-            }
-        }
-    }
-
-    private fun mapToJson(m: LinkedHashMap<*, *>): String {
-        val sb = StringBuilder()
-        m.forEach { k, v ->
-            if (sb.isNotEmpty()) sb.append(", ")
-            sb.append("\"$k\": \"$v\"")
-        }
-        return "{ $sb }"
-    }
-
     private fun runCmdList(inCmd: List<String>, inWorkdir: String? = null) {
         log.info("CMD:$inCmd")
         val pb = ProcessBuilder(inCmd)
@@ -327,11 +287,4 @@ class Packer {
         p.waitFor()
         assertTrue(0 == p.exitValue())
     }
-
-    private fun verifyAVBIntegrity(args: ImgArgs, avbtool: String) {
-        val tgt = args.output + ".signed"
-        log.info("Verifying AVB: $tgt")
-        DefaultExecutor().execute(CommandLine.parse("$avbtool verify_image --image $tgt"))
-        log.info("Verifying image passed: $tgt")
-    }
 }
diff --git a/bbootimg/src/main/kotlin/Parser.kt b/bbootimg/src/main/kotlin/Parser.kt
index e7baaf8..c361b57 100644
--- a/bbootimg/src/main/kotlin/Parser.kt
+++ b/bbootimg/src/main/kotlin/Parser.kt
@@ -15,48 +15,8 @@ import org.apache.commons.exec.PumpStreamHandler
 import java.io.ByteArrayOutputStream
 import java.util.regex.Pattern
 
-
 class Parser {
     private val workDir = UnifiedConfig.workDir
-    private fun readInt(iS: InputStream): Int {
-        val bf = ByteBuffer.allocate(128)
-        bf.order(ByteOrder.LITTLE_ENDIAN)
-        val data4 = ByteArray(4)
-        assertTrue(4 == iS.read(data4))
-        bf.clear()
-        bf.put(data4)
-        bf.flip()
-        return bf.int
-    }
-
-    private fun readUnsignedAsLong(iS: InputStream): Long {
-        val bf = ByteBuffer.allocate(128)
-        bf.order(ByteOrder.LITTLE_ENDIAN)
-        val data4 = ByteArray(4)
-        assertTrue(4 == iS.read(data4))
-        bf.clear()
-        bf.put(data4)
-        bf.put(ByteArray(4)) //complete high bits with 0
-        bf.flip()
-        return bf.long
-    }
-
-    private fun readLong(iS: InputStream): Long {
-        val bf = ByteBuffer.allocate(128)
-        bf.order(ByteOrder.LITTLE_ENDIAN)
-        val data4 = ByteArray(8)
-        assertTrue(8 == iS.read(data4))
-        bf.clear()
-        bf.put(data4)
-        bf.flip()
-        return bf.long
-    }
-
-    private fun readBytes(iS: InputStream, len: Int): ByteArray {
-        val data4 = ByteArray(len)
-        assertTrue(len == iS.read(data4))
-        return data4
-    }
 
     private fun parseOsVersion(x: Int): String {
         val a = x shr 14
@@ -102,14 +62,14 @@ class Parser {
                 args.osPatchLevel = parseOsPatchLevel(osNPatch and 0x7ff)
             }
 
-            args.board = Helper.byteArray2CString(readBytes(iS, 16))
+            args.board = Helper.toCString(readBytes(iS, 16))
             if (args.board.isBlank()) {
                 args.board = ""
             }
 
-            val cmd1 = Helper.byteArray2CString(readBytes(iS, 512))
+            val cmd1 = Helper.toCString(readBytes(iS, 512))
             info.hash = readBytes(iS, 32) //hash
-            val cmd2 = Helper.byteArray2CString(readBytes(iS, 1024))
+            val cmd2 = Helper.toCString(readBytes(iS, 1024))
             args.cmdline = cmd1 + cmd2
 
             info.recoveryDtboLength = readInt(iS)
@@ -189,6 +149,16 @@ class Parser {
 
             }
 
+            val m5 = Pattern.compile("^\\s*Algorithm:\\s+(\\S+)$").matcher(it)
+            if (m5.find()) {
+                (info.signature as ImgInfo.AvbSignature).algorithm = m5.group(1)
+            }
+
+            val m6 = Pattern.compile("^\\s*Hash Algorithm:\\s+(\\S+)$").matcher(it)
+            if (m6.find()) {
+                (info.signature as ImgInfo.AvbSignature).hashAlgorithm = m6.group(1)
+            }
+
             log.debug("[" + it + "]")
         }
         assertNotNull((info.signature as ImgInfo.AvbSignature).imageSize)
@@ -210,8 +180,6 @@ class Parser {
     fun parseAndExtract(fileName: String?, avbtool: String) {
         val imgArgs = ImgArgs(output = fileName ?: "boot.img")
         val imgInfo = ImgInfo()
-        if (File(workDir).exists()) File(workDir).deleteRecursively()
-        File(workDir).mkdirs()
         if (!fileName.isNullOrBlank()) {
             imgArgs.output = fileName!!
         }
@@ -257,5 +225,60 @@ class Parser {
 
     companion object {
         private val log = LoggerFactory.getLogger("Parser")!!
+
+        fun readValues(iS: InputStream, vararg key: Any) {
+
+        }
+
+        fun readShort(iS: InputStream): Short {
+            val bf = ByteBuffer.allocate(128)
+            bf.order(ByteOrder.LITTLE_ENDIAN)
+            val data2 = ByteArray(2)
+            assertTrue(2 == iS.read(data2))
+            bf.clear()
+            bf.put(data2)
+            bf.flip()
+            return bf.short
+        }
+
+        fun readInt(iS: InputStream): Int {
+            val bf = ByteBuffer.allocate(128)
+            bf.order(ByteOrder.LITTLE_ENDIAN)
+            val data4 = ByteArray(4)
+            assertTrue(4 == iS.read(data4))
+            bf.clear()
+            bf.put(data4)
+            bf.flip()
+            return bf.int
+        }
+
+        fun readUnsignedAsLong(iS: InputStream): Long {
+            val bf = ByteBuffer.allocate(128)
+            bf.order(ByteOrder.LITTLE_ENDIAN)
+            val data4 = ByteArray(4)
+            assertTrue(4 == iS.read(data4))
+            bf.clear()
+            bf.put(data4)
+            bf.put(ByteArray(4)) //complete high bits with 0
+            bf.flip()
+            return bf.long
+        }
+
+        fun readLong(iS: InputStream): Long {
+            val bf = ByteBuffer.allocate(128)
+            bf.order(ByteOrder.LITTLE_ENDIAN)
+            val data4 = ByteArray(8)
+            assertTrue(8 == iS.read(data4))
+            bf.clear()
+            bf.put(data4)
+            bf.flip()
+            return bf.long
+        }
+
+        fun readBytes(iS: InputStream, len: Int): ByteArray {
+            val data4 = ByteArray(len)
+            assertTrue(len == iS.read(data4))
+            return data4
+        }
     }
 }
diff --git a/bbootimg/src/main/kotlin/R.kt b/bbootimg/src/main/kotlin/R.kt
old mode 100644
new mode 100755
index 9bd1038..767f40b
--- a/bbootimg/src/main/kotlin/R.kt
+++ b/bbootimg/src/main/kotlin/R.kt
@@ -1,16 +1,71 @@
 package cfig
 
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.slf4j.LoggerFactory
+import java.io.File
+
 fun main(args: Array<String>) {
+    val log = LoggerFactory.getLogger("Launcher")
     if ((args.size == 6) && args[0] in setOf("pack", "unpack", "sign")) {
-        when (args[0]) {
-            "unpack" -> {
-                Parser().parseAndExtract(fileName = args[1], avbtool = args[3])
-            }
-            "pack" -> {
-                Packer().pack(mkbootimgBin = args[2], mkbootfsBin = args[5])
+        if (args[1] == "vbmeta.img") {
+            when (args[0]) {
+                "unpack" -> {
+                    if (File(UnifiedConfig.workDir).exists()) File(UnifiedConfig.workDir).deleteRecursively()
+                    File(UnifiedConfig.workDir).mkdirs()
+                    Avb().parseVbMeta(args[1])
+                }
+                "pack" -> {
+                    Avb().packVbMetaWithPadding("/Users/yu/work/boot/avb/avb_test_data/testkey_rsa4096.pem")
+                }
+                "sign" -> {
+                    log.info("vbmeta is already signed")
+                }
             }
-            "sign" -> {
-                Packer().sign(avbtool = args[3], bootSigner = args[4])
+        } else {
+            when (args[0]) {
+                "unpack" -> {
+                    if (File(UnifiedConfig.workDir).exists()) File(UnifiedConfig.workDir).deleteRecursively()
+                    File(UnifiedConfig.workDir).mkdirs()
+                    Parser().parseAndExtract(fileName = args[1], avbtool = args[3])
+                    Avb().parseVbMeta(args[1])
+
+                    if (File("vbmeta.img").exists()) {
+                        Avb().parseVbMeta("vbmeta.img")
+                    }
+                }
+                "pack" -> {
+                    Packer().pack(mkbootimgBin = args[2], mkbootfsBin = args[5])
+                }
+                "sign" -> {
+                    Signer.sign(avbtool = args[3], bootSigner = args[4])
+
+                    val readBack = ObjectMapper().readValue(File(UnifiedConfig.workDir + "bootimg.json"),
+                            UnifiedConfig::class.java).toArgs()
+                    val imgArgs = readBack[0] as ImgArgs
+                    val info = readBack[1] as ImgInfo
+                    if (imgArgs.verifyType == ImgArgs.VerifyType.AVB) {
+                        if (File("vbmeta.img").exists()) {
+                            val sig = ObjectMapper().readValue(
+                                    Signer.mapToJson(info.signature as LinkedHashMap<*, *>), ImgInfo.AvbSignature::class.java)
+                            val newBootImgInfo = Avb().parseVbMeta(args[1] + ".signed")
+                            val hashDesc = newBootImgInfo.auxBlob!!.hashDescriptors[0]
+                            val origVbMeta = ObjectMapper().readValue(File(Avb.getJsonFileName("vbmeta.img")),
+                                    AVBInfo::class.java)
+                            for (i in 0..(origVbMeta.auxBlob!!.hashDescriptors.size - 1)) {
+                                if (origVbMeta.auxBlob!!.hashDescriptors[i].partition_name == sig.partName) {
+                                    val seq = origVbMeta.auxBlob!!.hashDescriptors[i].sequence
+                                    origVbMeta.auxBlob!!.hashDescriptors[i] = hashDesc
+                                    origVbMeta.auxBlob!!.hashDescriptors[i].sequence = seq
+                                }
+                            }
+                            ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(File(Avb.getJsonFileName("vbmeta.img")), origVbMeta)
+                            log.info("vbmeta info updated")
+                            Avb().packVbMetaWithPadding("/Users/yu/work/boot/avb/avb_test_data/testkey_rsa4096.pem")
+                        } else {
+                            //no vbmeta provided
+                        }
+                    }//end-of-avb
+                }//end-of-sign
             }
         }
     } else {
@@ -20,3 +75,16 @@ fun main(args: Array<String>) {
         System.exit(1)
     }
 }
+
+/*
+    (a * x) mod m == 1
+ */
+//    fun modInv(a: Int, m: Int): Int {
+//        for (x in 0 until m) {
+//            if (a * x % m == 1) {
+//                return x
+//            }
+//        }
+//        throw IllegalArgumentException("modular multiplicative inverse of [$a] under modulo [$m] doesn't exist")
+//    }
+//
diff --git a/bbootimg/src/main/kotlin/Signer.kt b/bbootimg/src/main/kotlin/Signer.kt
new file mode 100644
index 0000000..77b4a1d
--- /dev/null
+++ b/bbootimg/src/main/kotlin/Signer.kt
@@ -0,0 +1,76 @@
+package cfig
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.apache.commons.exec.CommandLine
+import org.apache.commons.exec.DefaultExecutor
+import org.slf4j.LoggerFactory
+import java.io.File
+
+class Signer {
+    companion object {
+        private val log = LoggerFactory.getLogger(Signer::class.java)
+        private val workDir = UnifiedConfig.workDir
+
+        fun sign(avbtool: String, bootSigner: String) {
+            log.info("Loading config from ${workDir}bootimg.json")
+            val cfg = ObjectMapper().readValue(File(workDir + "bootimg.json"), UnifiedConfig::class.java)
+            val readBack = cfg.toArgs()
+            val args = readBack[0] as ImgArgs
+            val info = readBack[1] as ImgInfo
+
+            when (args.verifyType) {
+                ImgArgs.VerifyType.VERIFY -> {
+                    log.info("Signing with verified-boot 1.0 style")
+                    val sig = ObjectMapper().readValue(
+                            mapToJson(info.signature as LinkedHashMap<*, *>), ImgInfo.VeritySignature::class.java)
+                    DefaultExecutor().execute(CommandLine.parse("java -jar $bootSigner " +
+                            "${sig.path} ${args.output}.clear ${sig.verity_pk8} ${sig.verity_pem} ${args.output}.signed"))
+                }
+                ImgArgs.VerifyType.AVB -> {
+                    log.info("Adding hash_footer with verified-boot 2.0 style")
+                    val sig = ObjectMapper().readValue(
+                            mapToJson(info.signature as LinkedHashMap<*, *>), ImgInfo.AvbSignature::class.java)
+                    File(args.output + ".clear").copyTo(File(args.output + ".signed"))
+                    val cmdlineStr = "$avbtool add_hash_footer " +
+                            "--image ${args.output}.signed " +
+                            "--partition_size ${sig.imageSize} " +
+                            "--salt ${sig.salt} " +
+                            "--partition_name ${sig.partName} " +
+                            "--hash_algorithm ${sig.hashAlgorithm} " +
+                            "--algorithm ${sig.algorithm} " +
+                            "--key avb/avb_test_data/testkey_rsa4096.pem"
+                    log.warn(cmdlineStr)
+                    DefaultExecutor().execute(CommandLine.parse(cmdlineStr))
+                    verifyAVBIntegrity(args, avbtool)
+
+                    File(args.output + ".clear").copyTo(File(args.output + ".signed2"))
+                    Avb().add_hash_footer(args.output + ".signed2",
+                            sig.imageSize!!.toLong(),
+                            false, false,
+                            salt = sig.salt,
+                            hash_algorithm = sig.hashAlgorithm!!,
+                            partition_name = sig.partName!!,
+                            rollback_index = 0,
+                            common_algorithm = sig.algorithm!!,
+                            common_key_path = "avb/avb_test_data/testkey_rsa4096.pem")
+                }
+            }
+        }
+
+        private fun verifyAVBIntegrity(args: ImgArgs, avbtool: String) {
+            val tgt = args.output + ".signed"
+            log.info("Verifying AVB: $tgt")
+            DefaultExecutor().execute(CommandLine.parse("$avbtool verify_image --image $tgt"))
+            log.info("Verifying image passed: $tgt")
+        }
+
+        fun mapToJson(m: LinkedHashMap<*, *>): String {
+            val sb = StringBuilder()
+            m.forEach { k, v ->
+                if (sb.isNotEmpty()) sb.append(", ")
+                sb.append("\"$k\": \"$v\"")
+            }
+            return "{ $sb }"
+        }
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/main/kotlin/avb/Blob.kt b/bbootimg/src/main/kotlin/avb/Blob.kt
new file mode 100644
index 0000000..88a9f25
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/Blob.kt
@@ -0,0 +1,64 @@
+package avb
+
+import avb.alg.Algorithm
+import avb.alg.Algorithms
+import cfig.Helper
+import cfig.io.Struct
+import org.junit.Assert
+import org.slf4j.LoggerFactory
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.security.MessageDigest
+
+class Blob {
+    companion object {
+        fun encodePubKey(alg: Algorithm, key: ByteArray): ByteArray {
+            var encodedKey = byteArrayOf()
+            if (alg.public_key_num_bytes > 0) {
+                encodedKey = Helper.encodeRSAkey(key)
+                log.info("encodePubKey(): size = ${alg.public_key_num_bytes}, algorithm key size: ${encodedKey.size}")
+                Assert.assertEquals(alg.public_key_num_bytes, encodedKey.size)
+            } else {
+                log.info("encodePubKey(): No key to use")
+            }
+            return encodedKey
+        }
+
+        //TODO: support pkmd_blob
+        //encoded_descriptors + encoded_key + pkmd_blob + (padding)
+        fun getAuxDataBlob(encodedDesc: ByteArray, encodedKey: ByteArray): ByteArray {
+            val auxSize = Helper.round_to_multiple(
+                    encodedDesc.size + encodedKey.size /* encoded key */ + 0L /* pkmd_blob */,
+                    64)
+            return Struct("${auxSize}b").pack(Helper.join(encodedDesc, encodedKey))
+        }
+
+        fun getAuthBlob(header_data_blob: ByteArray,
+                        aux_data_blob: ByteArray,
+                        algorithm_name: String,
+                        key_path: String?): ByteArray {
+            val alg = Algorithms.get(algorithm_name)!!
+            val authBlockSize = Helper.round_to_multiple((alg.hash_num_bytes + alg.signature_num_bytes).toLong(), 64)
+            if (authBlockSize == 0L) {
+                log.info("No auth blob")
+                return byteArrayOf()
+            }
+
+            //hash & signature
+            var binaryHash: ByteArray = byteArrayOf()
+            var binarySignature: ByteArray = byteArrayOf()
+            if (algorithm_name != "NONE") {
+                val hasher = MessageDigest.getInstance(Helper.pyAlg2java(alg.hash_name))
+                binaryHash = hasher.apply {
+                    update(header_data_blob)
+                    update(aux_data_blob)
+                }.digest()
+                binarySignature = Helper.rawSign(key_path!!, Helper.join(alg.padding, binaryHash))
+            }
+            val authData = Helper.join(binaryHash, binarySignature)
+            return Helper.join(authData, Struct("${authBlockSize - authData.size}x").pack(0))
+        }
+
+        private val log = LoggerFactory.getLogger(Blob::class.java)
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/main/kotlin/avb/ByteArraySerializer.kt b/bbootimg/src/main/kotlin/avb/ByteArraySerializer.kt
new file mode 100755
index 0000000..5711e20
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/ByteArraySerializer.kt
@@ -0,0 +1,16 @@
+package avb
+
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.databind.JsonSerializer
+import com.fasterxml.jackson.databind.SerializerProvider
+import org.bouncycastle.util.encoders.Hex
+
+class ByteArraySerializer: JsonSerializer<ByteArray>() {
+    override fun serialize(value: ByteArray?, gen: JsonGenerator?, serializers: SerializerProvider?) {
+        if (value != null) {
+            gen!!.writeString(String(Hex.encode(value!!)))
+        } else {
+            gen!!.writeString("")
+        }
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/main/kotlin/avb/Footer.kt b/bbootimg/src/main/kotlin/avb/Footer.kt
new file mode 100644
index 0000000..45f0a75
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/Footer.kt
@@ -0,0 +1,50 @@
+package avb
+
+import cfig.io.Struct
+import org.junit.Assert
+import java.io.InputStream
+
+data class Footer constructor(
+        var versionMajor: Long = FOOTER_VERSION_MAJOR,
+        var versionMinor: Long = FOOTER_VERSION_MINOR,
+        var originalImageSize: Long = 0L,
+        var vbMetaOffset: Long = 0L,
+        var vbMetaSize: Long = 0L
+) {
+    companion object {
+        const val MAGIC = "AVBf"
+        const val SIZE = 64
+        const val RESERVED = 28
+        const val FOOTER_VERSION_MAJOR = 1L
+        const val FOOTER_VERSION_MINOR = 0L
+        private const val FORMAT_STRING = "!4s2L3Q${RESERVED}x"
+
+        init {
+            Assert.assertEquals(SIZE, Struct(FORMAT_STRING).calcsize())
+        }
+    }
+
+    @Throws(IllegalArgumentException::class)
+    constructor(iS: InputStream) : this() {
+        val info = Struct(FORMAT_STRING).unpack(iS)
+        Assert.assertEquals(7, info.size)
+        if (!MAGIC.toByteArray().contentEquals(info[0] as ByteArray)) {
+            throw IllegalArgumentException("stream doesn't look like valid AVB Footer")
+        }
+        versionMajor = info[1] as Long
+        versionMinor = info[2] as Long
+        originalImageSize = info[3] as Long
+        vbMetaOffset = info[4] as Long
+        vbMetaSize = info[5] as Long
+    }
+
+    fun encode(): ByteArray {
+        return Struct(FORMAT_STRING).pack(Footer.MAGIC.toByteArray(),
+                this.versionMajor,
+                this.versionMinor,
+                this.originalImageSize,
+                this.vbMetaOffset,
+                this.vbMetaSize,
+                null)
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/main/kotlin/avb/Header.kt b/bbootimg/src/main/kotlin/avb/Header.kt
new file mode 100644
index 0000000..d5ba7c7
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/Header.kt
@@ -0,0 +1,90 @@
+package avb
+
+import cfig.Avb
+import cfig.Helper
+import cfig.io.Struct
+import org.junit.Assert
+import java.io.InputStream
+
+data class Header(
+        var required_libavb_version_major: Int = Avb.AVB_VERSION_MAJOR,
+        var required_libavb_version_minor: Int = 0,
+        var authentication_data_block_size: Long = 0L,
+        var auxiliary_data_block_size: Long = 0L,
+        var algorithm_type: Long = 0L,
+        var hash_offset: Long = 0L,
+        var hash_size: Long = 0L,
+        var signature_offset: Long = 0L,
+        var signature_size: Long = 0L,
+        var public_key_offset: Long = 0L,
+        var public_key_size: Long = 0L,
+        var public_key_metadata_offset: Long = 0L,
+        var public_key_metadata_size: Long = 0L,
+        var descriptors_offset: Long = 0L,
+        var descriptors_size: Long = 0L,
+        var rollback_index: Long = 0L,
+        var flags: Long = 0,
+        var release_string: String = "avbtool ${Avb.AVB_VERSION_MAJOR}.${Avb.AVB_VERSION_MINOR}.${Avb.AVB_VERSION_SUB}") {
+    fun bump_required_libavb_version_minor(minor: Int) {
+        this.required_libavb_version_minor = maxOf(required_libavb_version_minor, minor)
+    }
+
+    @Throws(IllegalArgumentException::class)
+    constructor(iS: InputStream) : this() {
+        val info = Struct(FORMAT_STRING).unpack(iS)
+        Assert.assertEquals(22, info.size)
+        if (!(info[0] as ByteArray).contentEquals(magic.toByteArray())) {
+            throw IllegalArgumentException("stream doesn't look like valid VBMeta Header")
+        }
+        this.required_libavb_version_major = (info[1] as Long).toInt()
+        this.required_libavb_version_minor = (info[2] as Long).toInt()
+        this.authentication_data_block_size = info[3] as Long
+        this.auxiliary_data_block_size = info[4] as Long
+        this.algorithm_type = info[5] as Long
+        this.hash_offset = info[6] as Long
+        this.hash_size = info[7] as Long
+        this.signature_offset = info[8] as Long
+        this.signature_size = info[9] as Long
+        this.public_key_offset = info[10] as Long
+        this.public_key_size = info[11] as Long
+        this.public_key_metadata_offset = info[12] as Long
+        this.public_key_metadata_size = info[13] as Long
+        this.descriptors_offset = info[14] as Long
+        this.descriptors_size = info[15] as Long
+        this.rollback_index = info[16] as Long
+        this.flags = info[17] as Long
+        //padding
+        this.release_string = Helper.toCString(info[19] as ByteArray)
+    }
+
+    fun encode(): ByteArray {
+        return Struct(FORMAT_STRING).pack(
+                magic.toByteArray(),
+                this.required_libavb_version_major, this.required_libavb_version_minor,
+                this.authentication_data_block_size, this.auxiliary_data_block_size,
+                this.algorithm_type,
+                this.hash_offset, this.hash_size,
+                this.signature_offset, this.signature_size,
+                this.public_key_offset, this.public_key_size,
+                this.public_key_metadata_offset, this.public_key_metadata_size,
+                this.descriptors_offset, this.descriptors_size,
+                this.rollback_index,
+                this.flags,
+                null,
+                this.release_string.toByteArray(),
+                null,
+                null)
+    }
+
+    companion object {
+        const val magic: String = "AVB0"
+        const val SIZE = 256
+        const val REVERSED0 = 4
+        const val REVERSED = 80
+        const val FORMAT_STRING = ("!4s2L2QL11QL${REVERSED0}x47sx" + "${REVERSED}x")
+
+        init {
+            Assert.assertEquals(SIZE, Struct(FORMAT_STRING).calcsize())
+        }
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/main/kotlin/avb/alg/Algorithm.kt b/bbootimg/src/main/kotlin/avb/alg/Algorithm.kt
new file mode 100644
index 0000000..0d9ff77
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/alg/Algorithm.kt
@@ -0,0 +1,10 @@
+package avb.alg
+
+data class Algorithm(
+        val name: String = "NONE",
+        val algorithm_type: Int = 0,
+        val hash_name: String = "",
+        val hash_num_bytes: Int = 0,
+        val signature_num_bytes: Int = 0,
+        val public_key_num_bytes: Int = 0,
+        val padding: ByteArray = byteArrayOf())
\ No newline at end of file
diff --git a/bbootimg/src/main/kotlin/avb/alg/Algorithms.kt b/bbootimg/src/main/kotlin/avb/alg/Algorithms.kt
new file mode 100644
index 0000000..8a22d37
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/alg/Algorithms.kt
@@ -0,0 +1,128 @@
+package avb.alg
+
+import cfig.io.Struct
+
+class Algorithms {
+    companion object {
+        private val algMap = mutableMapOf<String, Algorithm>()
+        fun get(name: String): Algorithm? {
+            return algMap[name]
+        }
+
+        fun get(algorithm_type: Int): Algorithm? {
+            for (item in algMap) {
+                if (item.value.algorithm_type == algorithm_type) {
+                    return item.value
+                }
+            }
+            return null
+        }
+
+        init {
+            val NONE = Algorithm(name = "NONE")
+
+            val SHA256_RSA2048 = Algorithm(
+                    algorithm_type = 1,
+                    name = "SHA256_RSA2048",
+                    hash_name = "sha256",
+                    hash_num_bytes = 32,
+                    signature_num_bytes = 256,
+                    public_key_num_bytes = 8 + 2 * 2048 / 8,
+                    padding = Struct("2b202x1b19b").pack(
+                            byteArrayOf(0x00, 0x01),
+                            0xff,
+                            byteArrayOf(0x00),
+                            intArrayOf(0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86,
+                                    0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05,
+                                    0x00, 0x04, 0x20)))
+
+            val SHA256_RSA4096 = Algorithm(
+                    name = "SHA256_RSA4096",
+                    algorithm_type = 2,
+                    hash_name = "sha256",
+                    hash_num_bytes = 32,
+                    signature_num_bytes = 512,
+                    public_key_num_bytes = 8 + 2 * 4096 / 8,
+                    padding = Struct("2b458x1x19b").pack(
+                            byteArrayOf(0x00, 0x01),
+                            0xff,
+                            0x00,
+                            intArrayOf(0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86,
+                                    0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05,
+                                    0x00, 0x04, 0x20)
+                    )
+            )
+
+            val SHA256_RSA8192 = Algorithm(
+                    name = "SHA256_RSA8192",
+                    algorithm_type = 3,
+                    hash_name = "sha256",
+                    hash_num_bytes = 32,
+                    signature_num_bytes = 1024,
+                    public_key_num_bytes = 8 + 2 * 8192 / 8,
+                    padding = Struct("2b970x1x19b").pack(
+                            intArrayOf(0x00, 0x01),
+                            0xff,
+                            0x00,
+                            intArrayOf(0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86,
+                                    0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05,
+                                    0x00, 0x04, 0x20)))
+
+            val SHA512_RSA2048 = Algorithm(
+                    name = "SHA512_RSA2048",
+                    algorithm_type = 4,
+                    hash_name = "sha512",
+                    hash_num_bytes = 64,
+                    signature_num_bytes = 256,
+                    public_key_num_bytes = 8 + 2 * 2048 / 8,
+                    padding = Struct("2b170x1x19b").pack(
+                            intArrayOf(0x00, 0x01),
+                            0xff,
+                            0x00,
+                            intArrayOf(0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86,
+                                    0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05,
+                                    0x00, 0x04, 0x40)))
+
+            val SHA512_RSA4096 = Algorithm(
+                    name = "SHA512_RSA4096",
+                    algorithm_type = 5,
+                    hash_name = "sha512",
+                    hash_num_bytes = 64,
+                    signature_num_bytes = 512,
+                    public_key_num_bytes = 8 + 2 * 4096 / 8,
+                    padding = Struct("2b426x1x19b").pack(
+                            intArrayOf(0x00, 0x01),
+                            0xff,
+                            0x00,
+                            intArrayOf(0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86,
+                                    0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05,
+                                    0x00, 0x04, 0x40)))
+
+            val SHA512_RSA8192 = Algorithm(
+                    name = "SHA512_RSA8192",
+                    algorithm_type = 6,
+                    hash_name = "sha512",
+                    hash_num_bytes = 64,
+                    signature_num_bytes = 1024,
+                    public_key_num_bytes = 8 + 2 * 8192 / 8,
+
+                    padding = Struct("2b938x1x19b").pack(
+                            intArrayOf(0x00, 0x01),
+                            0xff,
+                            0x00,
+                            intArrayOf(0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86,
+                                    0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05,
+                                    0x00, 0x04, 0x40)))
+
+            algMap[NONE.name] = NONE
+
+            algMap[SHA256_RSA2048.name] = SHA256_RSA2048
+            algMap[SHA256_RSA4096.name] = SHA256_RSA4096
+            algMap[SHA256_RSA8192.name] = SHA256_RSA8192
+
+            algMap[SHA512_RSA2048.name] = SHA512_RSA2048
+            algMap[SHA512_RSA4096.name] = SHA512_RSA4096
+            algMap[SHA512_RSA8192.name] = SHA512_RSA8192
+        }
+    }
+}
diff --git a/bbootimg/src/main/kotlin/avb/desc/Descriptor.kt b/bbootimg/src/main/kotlin/avb/desc/Descriptor.kt
new file mode 100755
index 0000000..1cfba41
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/desc/Descriptor.kt
@@ -0,0 +1,5 @@
+package avb.desc
+
+abstract class Descriptor(var tag: Long, var num_bytes_following: Long, var sequence: Int = 0) {
+    abstract fun encode(): ByteArray
+}
\ No newline at end of file
diff --git a/bbootimg/src/main/kotlin/avb/desc/HashDescriptor.kt b/bbootimg/src/main/kotlin/avb/desc/HashDescriptor.kt
new file mode 100755
index 0000000..3999074
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/desc/HashDescriptor.kt
@@ -0,0 +1,76 @@
+package avb.desc
+
+import cfig.Helper
+import cfig.io.Struct
+import org.junit.Assert
+import java.io.File
+import java.io.InputStream
+import java.security.MessageDigest
+
+class HashDescriptor(var image_size: Long = 0L,
+                     var hash_algorithm: ByteArray = byteArrayOf(),
+                     var partition_name_len: Long = 0L,
+                     var salt_len: Long = 0L,
+                     var digest_len: Long = 0L,
+                     var flags: Long = 0L,
+                     var partition_name: String = "",
+                     var salt: ByteArray = byteArrayOf(),
+                     var digest: ByteArray = byteArrayOf()) : Descriptor(TAG, 0, 0) {
+    constructor(data: InputStream, seq: Int = 0) : this() {
+        val info = Struct(FORMAT_STRING).unpack(data)
+        this.tag = info[0] as Long
+        this.num_bytes_following = info[1] as Long
+        this.image_size = info[2] as Long
+        this.hash_algorithm = info[3] as ByteArray
+        this.partition_name_len = info[4] as Long
+        this.salt_len = info[5] as Long
+        this.digest_len = info[6] as Long
+        this.flags = info[7] as Long
+        this.sequence = seq
+        val expectedSize = Helper.round_to_multiple(SIZE - 16 + partition_name_len + salt_len + digest_len, 8)
+        if (this.tag != TAG || expectedSize != this.num_bytes_following) {
+            throw IllegalArgumentException("Given data does not look like a |hash| descriptor")
+        }
+        val payload = Struct("${this.partition_name_len}s${this.salt_len}b${this.digest_len}b").unpack(data)
+        Assert.assertEquals(3, payload.size)
+        this.partition_name = Helper.toCString(payload[0] as ByteArray)
+        this.salt = payload[1] as ByteArray
+        this.digest = payload[2] as ByteArray
+    }
+
+    override fun encode(): ByteArray {
+        val payload_bytes_following = SIZE + this.partition_name.length + this.salt.size + this.digest.size - 16L
+        this.num_bytes_following = Helper.round_to_multiple(payload_bytes_following, 8)
+        val padding_size = num_bytes_following - payload_bytes_following
+        val desc = Struct(FORMAT_STRING).pack(
+                TAG,
+                this.num_bytes_following,
+                this.image_size,
+                this.hash_algorithm,
+                this.partition_name.length,
+                this.salt.size,
+                this.digest.size,
+                this.flags,
+                null)
+        val padding = Struct("${padding_size}x").pack(null)
+        return Helper.join(desc, partition_name.toByteArray(), this.salt, this.digest, padding)
+    }
+
+    fun verify(image_file: String) {
+        val hasher = MessageDigest.getInstance(Helper.pyAlg2java(hash_algorithm.toString()))
+        hasher.update(this.salt)
+        hasher.update(File(image_file).readBytes())
+        val digest = hasher.digest()
+    }
+
+    companion object {
+        const val TAG = 2L
+        private const val RESERVED = 60
+        private const val SIZE = 72 + RESERVED
+        private const val FORMAT_STRING = "!3Q32s4L${RESERVED}s"
+    }
+
+    override fun toString(): String {
+        return "HashDescriptor(TAG=$TAG, image_size=$image_size, hash_algorithm=${Helper.toCString(hash_algorithm)}, flags=$flags, partition_name='$partition_name', salt=${Helper.toHexString(salt)}, digest=${Helper.toHexString(digest)})"
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/main/kotlin/avb/desc/HashTreeDescriptor.kt b/bbootimg/src/main/kotlin/avb/desc/HashTreeDescriptor.kt
new file mode 100755
index 0000000..4c635b9
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/desc/HashTreeDescriptor.kt
@@ -0,0 +1,91 @@
+package avb.desc
+
+import cfig.Helper
+import cfig.io.Struct
+import org.slf4j.LoggerFactory
+import java.io.InputStream
+import java.util.*
+
+class HashTreeDescriptor(
+        var dm_verity_version: Long = 0L,
+        var image_size: Long = 0L,
+        var tree_offset: Long = 0L,
+        var tree_size: Long = 0L,
+        var data_block_size: Long = 0L,
+        var hash_block_size: Long = 0L,
+        var fec_num_roots: Long = 0L,
+        var fec_offset: Long = 0L,
+        var fec_size: Long = 0L,
+        var hash_algorithm: String = "",
+        var partition_name: String = "",
+        var salt: ByteArray = byteArrayOf(),
+        var root_digest: ByteArray = byteArrayOf(),
+        var flags: Long = 0L) : Descriptor(TAG, 0, 0) {
+    constructor(data: InputStream, seq: Int = 0) : this() {
+        this.sequence = seq
+        val info = Struct(FORMAT_STRING).unpack(data)
+        this.tag = info[0] as Long
+        this.num_bytes_following = info[1] as Long
+        this.dm_verity_version = info[2] as Long
+        this.image_size = info[3] as Long
+        this.tree_offset = info[4] as Long
+        this.tree_size = info[5] as Long
+        this.data_block_size = info[6] as Long
+        this.hash_block_size = info[7] as Long
+        this.fec_num_roots = info[8] as Long
+        this.fec_offset = info[9] as Long
+        this.fec_size = info[10] as Long
+        this.hash_algorithm = Helper.toCString(info[11] as ByteArray)
+        val partition_name_len = info[12] as Long
+        val salt_len = info[13] as Long
+        val root_digest_len = info[14] as Long
+        this.flags = info[15] as Long
+        val expectedSize = Helper.round_to_multiple(SIZE - 16 + partition_name_len + salt_len + root_digest_len, 8)
+        if (this.tag != TAG || this.num_bytes_following != expectedSize) {
+            throw IllegalArgumentException("Given data does not look like a hashtree descriptor")
+        }
+
+        val info2 = Struct("${partition_name_len}s${salt_len}s${root_digest_len}s").unpack(data)
+        this.partition_name = Helper.toCString(info2[0] as ByteArray)
+        this.salt = info2[1] as ByteArray
+        this.root_digest = info2[2] as ByteArray
+    }
+
+    override fun encode(): ByteArray {
+        this.num_bytes_following = SIZE + this.partition_name.length + this.salt.size + this.root_digest.size - 16
+        val nbf_with_padding = Helper.round_to_multiple(this.num_bytes_following, 8)
+        val padding_size = nbf_with_padding - this.num_bytes_following
+        val desc = Struct(FORMAT_STRING).pack(
+                TAG,
+                nbf_with_padding,
+                this.dm_verity_version,
+                this.image_size,
+                this.tree_offset,
+                this.tree_size,
+                this.data_block_size,
+                this.hash_block_size,
+                this.fec_num_roots,
+                this.fec_offset,
+                this.fec_size,
+                this.hash_algorithm.toByteArray(),
+                this.partition_name.length,
+                this.salt.size,
+                this.root_digest.size,
+                this.flags,
+                null)
+        val padding = Struct("${padding_size}x").pack(null)
+        return Helper.join(desc, this.partition_name.toByteArray(), this.salt, this.root_digest, padding)
+    }
+
+    override fun toString(): String {
+        return "HashTreeDescriptor(dm_verity_version=$dm_verity_version, image_size=$image_size, tree_offset=$tree_offset, tree_size=$tree_size, data_block_size=$data_block_size, hash_block_size=$hash_block_size, fec_num_roots=$fec_num_roots, fec_offset=$fec_offset, fec_size=$fec_size, hash_algorithm='$hash_algorithm', partition_name='$partition_name', salt=${Arrays.toString(salt)}, root_digest=${Arrays.toString(root_digest)}, flags=$flags)"
+    }
+
+    companion object {
+        const val TAG = 1L
+        private const val RESERVED = 60L
+        private const val SIZE = 120 + RESERVED
+        private const val FORMAT_STRING = "!2QL3Q3L2Q32s4L${RESERVED}s"
+        private val log = LoggerFactory.getLogger(HashTreeDescriptor::class.java)
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/main/kotlin/avb/desc/KernelCmdlineDescriptor.kt b/bbootimg/src/main/kotlin/avb/desc/KernelCmdlineDescriptor.kt
new file mode 100755
index 0000000..fe21340
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/desc/KernelCmdlineDescriptor.kt
@@ -0,0 +1,51 @@
+package avb.desc
+
+import cfig.Helper
+import cfig.io.Struct
+import org.junit.Assert
+import java.io.InputStream
+
+class KernelCmdlineDescriptor(
+        var flags: Long = 0,
+        var cmdlineLength: Long = 0,
+        var cmdline: String = "") : Descriptor(TAG, 0, 0) {
+    @Throws(IllegalArgumentException::class)
+    constructor(data: InputStream, seq: Int = 0) : this() {
+        val info = Struct(FORMAT_STRING).unpack(data)
+        this.tag = info[0] as Long
+        this.num_bytes_following = info[1] as Long
+        this.flags = info[2] as Long
+        this.cmdlineLength = info[3] as Long
+        this.sequence = seq
+        val expectedSize = Helper.round_to_multiple(SIZE - 16 + this.cmdlineLength, 8)
+        if ((this.tag != TAG) || (this.num_bytes_following != expectedSize)) {
+            throw IllegalArgumentException("Given data does not look like a kernel cmdline descriptor")
+        }
+        this.cmdline = Helper.toCString(Struct("${this.cmdlineLength}s").unpack(data)[0] as ByteArray)
+    }
+
+    override fun encode(): ByteArray {
+        val num_bytes_following = SIZE - 16 + cmdline.toByteArray().size
+        val nbf_with_padding = Helper.round_to_multiple(num_bytes_following.toLong(), 8)
+        val padding_size = nbf_with_padding - num_bytes_following
+        val desc = Struct(FORMAT_STRING).pack(
+                TAG,
+                nbf_with_padding,
+                this.flags,
+                cmdline.toByteArray().size)
+        val padding = Struct("${padding_size}x").pack(null)
+        return Helper.join(desc, cmdline.toByteArray(), padding)
+    }
+
+    companion object {
+        const val TAG = 3L
+        const val SIZE = 24
+        const val FORMAT_STRING = "!2Q2L" //# tag, num_bytes_following (descriptor header), flags, cmdline length (bytes)
+        const val flagHashTreeEnabled = 1
+        const val flagHashTreeDisabled = 2
+
+        init {
+            Assert.assertEquals(SIZE, Struct(FORMAT_STRING).calcsize())
+        }
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/main/kotlin/avb/desc/PropertyDescriptor.kt b/bbootimg/src/main/kotlin/avb/desc/PropertyDescriptor.kt
new file mode 100755
index 0000000..524a1a3
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/desc/PropertyDescriptor.kt
@@ -0,0 +1,30 @@
+package avb.desc
+
+import cfig.Helper
+import cfig.io.Struct
+
+class PropertyDescriptor(
+        var key: String = "",
+        var value: String = "") : Descriptor(TAG, 0, 0) {
+    override fun encode(): ByteArray {
+        this.num_bytes_following = SIZE + this.key.length + this.value.length + 2 - 16
+        val nbf_with_padding = Helper.round_to_multiple(this.num_bytes_following, 8)
+        val padding_size = nbf_with_padding - num_bytes_following
+        val padding = Struct("${padding_size}x").pack(0)
+        val desc = Struct(FORMAT_STRING).pack(
+                TAG,
+                nbf_with_padding,
+                this.key.length,
+                this.value.length)
+        return Helper.join(desc,
+                this.key.toByteArray(), ByteArray(1),
+                this.value.toByteArray(), ByteArray(1),
+                padding)
+    }
+
+    companion object {
+        val TAG = 0L
+        val SIZE = 32L
+        val FORMAT_STRING = "!4Q"
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/main/kotlin/avb/desc/UnknownDescriptor.kt b/bbootimg/src/main/kotlin/avb/desc/UnknownDescriptor.kt
new file mode 100755
index 0000000..ecb06f6
--- /dev/null
+++ b/bbootimg/src/main/kotlin/avb/desc/UnknownDescriptor.kt
@@ -0,0 +1,107 @@
+package avb.desc
+
+import cfig.Helper
+import cfig.io.Struct
+import org.bouncycastle.util.encoders.Hex
+import org.junit.Assert
+import org.slf4j.LoggerFactory
+import java.io.ByteArrayInputStream
+import java.io.InputStream
+
+class UnknownDescriptor(var data: ByteArray = byteArrayOf()) : Descriptor(0, 0, 0) {
+    @Throws(IllegalArgumentException::class)
+    constructor(stream: InputStream, seq: Int = 0) : this() {
+        this.sequence = seq
+        val info = Struct(FORMAT).unpack(stream)
+        this.tag = info[0] as Long
+        this.num_bytes_following = info[1] as Long
+        log.debug("UnknownDescriptor: tag = $tag, len = ${this.num_bytes_following}")
+        this.data = ByteArray(this.num_bytes_following.toInt())
+        if (this.num_bytes_following.toInt() != stream.read(data)) {
+            throw IllegalArgumentException("descriptor SIZE mismatch")
+        }
+    }
+
+    override fun encode(): ByteArray {
+        return Helper.join(Struct(FORMAT).pack(this.tag, this.data.size.toLong()), data)
+    }
+
+    override fun toString(): String {
+        return "UnknownDescriptor(tag=$tag, SIZE=${data.size}, data=${Hex.toHexString(data)})"
+    }
+
+    fun analyze(): Any {
+        return when (this.tag) {
+            1L -> {
+                HashTreeDescriptor(ByteArrayInputStream(this.encode()), this.sequence)
+            }
+            2L -> {
+                HashDescriptor(ByteArrayInputStream(this.encode()), this.sequence)
+            }
+            3L -> {
+                KernelCmdlineDescriptor(ByteArrayInputStream(this.encode()), this.sequence)
+            }
+            else -> {
+                this
+            }
+        }
+    }
+
+    companion object {
+        private const val SIZE = 16
+        private const val FORMAT = "!QQ"
+        private val log = LoggerFactory.getLogger(UnknownDescriptor::class.java)
+
+        fun parseDescriptors(stream: InputStream, totalSize: Long): List<UnknownDescriptor> {
+            log.debug("Parse descriptors stream, SIZE = $totalSize")
+            val ret: MutableList<UnknownDescriptor> = mutableListOf()
+            var currentSize = 0L
+            while (true) {
+                val desc = UnknownDescriptor(stream)
+                currentSize += desc.data.size + SIZE
+                log.debug("current SIZE = $currentSize")
+                ret.add(desc)
+                if (currentSize == totalSize) {
+                    log.debug("parse descriptor done")
+                    break
+                } else if (currentSize > totalSize) {
+                    log.error("Read more than expected")
+                    throw IllegalStateException("Read more than expected")
+                } else {
+                    log.debug(desc.toString())
+                    log.debug("read another descriptor")
+                }
+            }
+            return ret
+        }
+
+        fun parseDescriptors2(stream: InputStream, totalSize: Long): List<Any> {
+            log.info("Parse descriptors stream, SIZE = $totalSize")
+            val ret: MutableList<Any> = mutableListOf()
+            var currentSize = 0L
+            var seq = 0
+            while (true) {
+                val desc = UnknownDescriptor(stream, ++seq)
+                currentSize += desc.data.size + SIZE
+                log.debug("current SIZE = $currentSize")
+                log.debug(desc.toString())
+                ret.add(desc.analyze())
+                if (currentSize == totalSize) {
+                    log.debug("parse descriptor done")
+                    break
+                } else if (currentSize > totalSize) {
+                    log.error("Read more than expected")
+                    throw IllegalStateException("Read more than expected")
+                } else {
+                    log.debug(desc.toString())
+                    log.debug("read another descriptor")
+                }
+            }
+            return ret
+        }
+
+        init {
+            Assert.assertEquals(SIZE, Struct(FORMAT).calcsize())
+        }
+    }
+}
diff --git a/bbootimg/src/test/kotlin/AvbTest.kt b/bbootimg/src/test/kotlin/AvbTest.kt
new file mode 100644
index 0000000..1eb3a1f
--- /dev/null
+++ b/bbootimg/src/test/kotlin/AvbTest.kt
@@ -0,0 +1,30 @@
+import avb.desc.UnknownDescriptor
+import avb.desc.HashDescriptor
+import org.bouncycastle.util.encoders.Hex
+import org.junit.Test
+
+import org.junit.Assert.*
+import org.slf4j.LoggerFactory
+import java.io.ByteArrayInputStream
+
+class AvbTest {
+    private val log = LoggerFactory.getLogger(AvbTest::class.java)
+
+    @Test
+    fun readDescriptors() {
+        //output by "xxd -p <file>"
+        val descStr = "000000000000000200000000000000b800000000017b9000736861323536" +
+                "000000000000000000000000000000000000000000000000000000000004" +
+                "000000200000002000000000000000000000000000000000000000000000" +
+                "000000000000000000000000000000000000000000000000000000000000" +
+                "000000000000000000000000626f6f7428f6d60b554d9532bd45874ab0cd" +
+                "cb2219c4f437c9350f484fa189a881878ab6156408cd763ff119635ec9db" +
+                "2a9656e220fa1dc27e26e59bd3d85025b412ffc3"
+        val desc = UnknownDescriptor(ByteArrayInputStream(Hex.decode(descStr)))
+        val hashdDesc = HashDescriptor(ByteArrayInputStream(Hex.decode(descStr)))
+        log.info(desc.toString())
+        log.info(hashdDesc.toString())
+        val descAnalyzed = desc.analyze()
+        assertTrue(descAnalyzed is HashDescriptor)
+    }
+}
diff --git a/bbootimg/src/test/kotlin/avb/BlobTest.kt b/bbootimg/src/test/kotlin/avb/BlobTest.kt
new file mode 100644
index 0000000..f913f4a
--- /dev/null
+++ b/bbootimg/src/test/kotlin/avb/BlobTest.kt
@@ -0,0 +1,32 @@
+package avb
+
+import avb.alg.Algorithms
+import org.bouncycastle.util.encoders.Hex
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class BlobTest {
+    @Test
+    fun testEncodedKey2048() {
+        val keyStr = "2d2d2d2d2d424547494e205253412050524956415445204b45592d2d2d2d2d0a4d4949456f77494241414b4341514541786c56523354496b6f75414f7648373976614a54674668706676564b514965566b46525a5056584b2f7a5930477672680a344a4171476a4a6f572f50667251763573644433367174484833612b4735684c5a364e692b742f6d74666a7563785a66754c4743336b6d4a3154335871454b5a0a67585849324952377656536f496d5245764451474544794a7774487a4c414e6c6b624767306367685668575a5343416e644f3842656e616c43327639342f72740a44666b50656b48366467553353663430543073425365535939346d4f7a5461714f52327066563172576c4c5264576d6f33337a654842763532526c627430644d0a755841757265585769487a746b6d35474342433164674d2b4361784e74697a4e45674339314b63443078755243434d325778482b72316c70737a79494a4463740a596272466d5645596c2f6b6a517061666879374e736b316671535479526472695a53596d5451494441514142416f49424151432b6b4a6761437558387759416e0a5358575130666d645a6c586e4d4e5270634630613070443053417a47623152645942584d615869717479686977633533505078734344644e65636a6179494d640a6a4a56585054774c685472754f674d532f6270336763675777563334554856344c4a58474f4741452b6a625330686244424d6975644f596d6a36526d567368700a7a3947317a5a4353514e4d584861577345596b58353958707a7a6f423338346e52756c3251674574777a554e5239586c707a67744a424c6b335341436b76734e0a6d512f445738495748584c6738764c6e314c7a564a32653342313648344d6f45325443487871664d67723033494452524a6f676b656e517551734668657659540a6f2f6d4a79485357617656677a4d48473949356d2b65657046345779686a31593457794b41754d492b39644841582f68374c74385846435143683544626b56470a7a47723334735742416f4742414f73376e37595a714e616167756f7666496452527378785a7231794a41794473723677337947496d445a596a753463345759390a3565734f326b50334641347030633746685146356f4f623172427548455070333663704c346147654b38376361715466713633575a41756a6f545a7072394c700a4252626b4c37772f7847376a70512f636c70413873487a484751732f6e656c786f4f744337453131384669526776442f6a64686c4d794c39416f4742414e66580a76796f4e3170706c665432785238514f6a535a2b513335532f2b5341744d75426e4878336c307148326262426a63764d314d4e44576a6e5244796159686952750a692b4b4137747166696230392b58704233673544364f76376c732f4c647830532f56636d565774696132484b387938694c47746f6b6f425a4b513541614658320a695155382b744334683639476e4a59514b714e776743557a68382b674858355934366f4469546d52416f474159704f78386c582b637a42382f4461364d4e72570a6d495a4e543861745a4c4573447332414e455652784453496354435a4a4964372b6d31572b6e526f6179634c54574e6f775a312b3245724c765231302b4147590a62375973373957673969645961593979476e396c6e5a734d7a4169754c6579497658635371676a76414b6c565772684f51464f756768764e5776466c383559790a6f5753434d6c5069544c747437434373434b73674b7545436759426764497036475a7349666b67636c4b653068716776526f65553454523367636a4a6c4d39410a6c42546f2b704b686142656374706c783952785238416e73506f626271776361486e496641754b447a6a6b356d45764b5a6a436c6e46584634484148627941460a6e527a5a457939586b57466863383054357252705a4f3743377164786d753261694b69784d3356334c332f3055353871554c4544627562484d773962456841540a5075644938514b4267484545694d6d2f687239543431686251692f4c59616e576e6c46773175652b6f734b75463862585175786e6e484e7546542f632b392f410a76576867714736624f4548752b702f495072596d3474424d596c77737968346e5843794767444a4c624c49667a4b774b4157437448394c776e794456684f6f770a474839736864522b7357334577393778656630324b414834566c4e414e456d42563473514e7157577673597263466d32724f644c0a2d2d2d2d2d454e44205253412050524956415445204b45592d2d2d2d2d0a"
+        val encodedKey = Blob.encodePubKey(Algorithms.get("SHA512_RSA2048")!!, Hex.decode(keyStr))
+        val expectedKeyEnc = "00000800c9d87d7bc65551dd3224a2e00ebc7efdbda2538058697ef54a4087959054593d55caff36341afae1e0902a1a32685bf3dfad0bf9b1d0f7eaab471f76be1b984b67a362fadfe6b5f8ee73165fb8b182de4989d53dd7a842998175c8d8847bbd54a8226444bc3406103c89c2d1f32c036591b1a0d1c82156159948202774ef017a76a50b6bfde3faed0df90f7a41fa76053749fe344f4b0149e498f7898ecd36aa391da97d5d6b5a52d17569a8df7cde1c1bf9d9195bb7474cb9702eade5d6887ced926e460810b576033e09ac4db62ccd1200bdd4a703d31b910823365b11feaf5969b33c8824372d61bac599511897f92342969f872ecdb24d5fa924f245dae26526264d1efa3b53abc5d379532f66b82194669a936f3526438c8f96b9ffaccdf4006ebeb673548450854653d5dd43feb26a784079569f86f3d3813b3d409035329a517ff8c34bc7d6a1ca30fb1bfd270ab8644134c117dea1769aebcf0c50d913f50d0b2c9924cbb5b4f8c60ad026b15bfd4d44669db076aa799dc05c3b9436c18ffec9d25a6aa046e1a28b2f516151a3369183b4fbcda9403446988a1a91dfd92c3abf573a464620f2f0bc315e29fe5890325c6697999a8e1523eba947d363c618bb8ed2209f78af71b32e0889a1f17db130a0e61bbd6ec8f633e1dab0bd1641fe076f6e978b6a33d2d780036a4de70582281fef6aa9757ee14ee2955b4fe6dc03b981"
+        assertEquals(expectedKeyEnc, Hex.toHexString(encodedKey))
+    }
+
+    @Test
+    fun testEncodeKey4096() {
+        val keyStr = "2d2d2d2d2d424547494e205253412050524956415445204b45592d2d2d2d2d0a4d49494a4b51494241414b43416745413241537634394f456248344e695433436a4e4d5356656c697966455058737757637174456643786c53705331466973410a757762764577645454506c6b7553683647345359694e686e704350357030766353672f334f686975564b67562f724374724458614f36306e764b2f6f307938330a4e4e5a524b3278614a396557427139727549444b2b6a43307359577a546171717778593047726a6e782f7235435865726c355072524b3750494c7a77674248620a4977784863626c74316e74675234635756704f337769716173457742444444596b3466773757364c766a42623971617633594238525636506b5a4e65525036340a676766756563712f4d584e69574f504e784c7a434552326853722f2b4a333268396a576a587372635679382b384d6c64686d72347232616e37633234376146660a757075464774554a7270524f4f382f4c584d6c356750664d706b716f61746a544d52483539674a6a4b686f743052706d47785a42766233335463424b3553644a0a583339593479637435636c6d446c4934466a6a3746757454502b623936614a654a566e596555582f4130776d6f6742616a734a526f525835652f5263675a73590a527a58594c515870725138316442576a6a6f764d4a39703858655436424e4d4643376f36736b6c464c3066484455452f6c34424e5038473175334266707a65760a534349535253373144346553346f51422b5249504642556b7a6f6d5a37726e45463342774665712b786d7766597250304c5261482b315965526175754d7552650a6b6531545a6c36393761336d456a6b4e67386e6f6132777470653745576d61756a4a66584457784a782f58456b6a474c4365347a32716b33746b6b592b4135670a5263677a6b6538675678432b654332444a74624b59666b76344c38464d464a61456877417031334d664337466c59756a4f2f42444c6c3764414e7343417745410a41514b43416741576f4c38502f57736b746a755377623573592f764b74677a634848314172393432477379737554585044793638364c7046335238542f6a4e790a6e376b3255424169613878536f5743523642625275486556356f412b504c47654f704537516153666f6e422b79632b63793078334f7233737366714573752f710a746f47487037352f38445853365745304b303478393475317264433962397350727247426c57434c477a714d306b62754a667948586464336e32536f6641554f0a62355152536778442b327448557045726f4871486e574a436166344a30516567583435796b746c664f594e4b2f50484c44515856386c792f656a6333324d34590a5476376855744f4f4a54757138564367394f575a6d325a6f3151754d3958454a54504370356c332b6f35767a4f3679686b32676f7444764433324364412b336b0a744c4a525035344d31536e2b4958623167474b4e39724b4174474a62656e5749506c4e4f626851676b627747383951642b3572664d587369507631486c31744b0a2b7471776a4438322f48332f456c61614d6e77484370656f47537039354f626c416f426a7a6a4d50324b7362764b53644c384f2f7266316333754f77392b44460a6374683053413879335a7a493131674a746232514d475572436e79356e34735047476263337833384e644c6877626b504b5a7936304f69543467326b4e7064590a644969746d414d4c326f747474694634414a4d36417261506b3859567a6b504c546b736f4c33617a50427961356c496f444932483351765474537670586b58500a794b6368734453575962647166706c71432f5830446a70322f5a64386a704e3549362b3161536d70546d6277782f4a546c6c59314e383946525a4c4964786f680a326b38314c5069586845367552626a696f4a556c626e45574970593279324e32436c6d78706a68302f496358643158496d514b4341514541375a61692b796a6a0a387869743234614f395466336d5a4258426a5361446f646a43324b533179436341495870365337614830775a6970795a70516a7973337a614251794d525946470a625171496656416136696e5779446f6f6662414a484d7535425663484642505a765353355968446a6338585a3564715343787a497a396f70497141626d2b62340a6145562f3341334a6b6935447938792f356a323147414b3459346d71514f597a6e653762444769334879753034314d474d3471664963496b53354e31654857340a73445a4a68362b4b357475784e355458336e445a53706d396c754e48386d4c47674b415a313562314c715841744d3579636f4259394876303832737550506f6d0a4f2b72307962645258366e445348382b313179324b6950326b645649554843476b776c716772757835595a796a435a50774f764550687a536f4f532b764269460a555658413869646e784e4c6b31514b4341514541364d496968445358782b33353066577168512f3351633667412f74324331354a774a392b754657412b676a640a632f686e3548636d6e6d424a4e345230346e4c472f61553953517572383761346d6e432f4d70394a4941526a486c5a2f574e54345530734a79504556526735550a5a3956616a417563577769304a794a59434f31454d4d7936384a7038716c5472694b2f4c376e624438364a4a354153786a6f6a694e2f3070734b2f506b3630460a52722b73684b5069336a52513142446a447441784f666f346374662f6e4662554d34625930464e50514d50375765736f534b55304e42435252366430643274710a59666c4d6a495148782b4e373450356a4564534348545647516d2b646a3437705574336c4c504c576330625831472f47656b775850344e5573522f37304873690a6277786b4e6e4b325453477a6b743272634f6e757450313235724a753657705637534e727139726d37774b43415141664d524f636e625776694b48716e4450510a6864522f324b39554a54764568496e41534f5332555a5770692b733172657a394275536a69674f7834776261415a3474343450573743337579743834644866550a486b495162334935626738454e4d724a704b394e4e3333796b77757a6b44774d537746635a2b4763693937685375627a6f4d6c2f496b6569694e314d61704c340a47684c556773442b33554d564c2b593953796d4b383633374967796f434764694e44362f535873613853774c4a6f3356546a717834654b70583763766c53424c0a52725278633530546d775573416873643443446c39596e5341544c6a56764a4265596c664d32746246506159776c31615238762b50576b666e4b3065666d36300a66486b69333348456e47746542504b7547713476775659706e3662594777517a2b66363333352f4132444d665a484653706a56555248506352634862434d6c610a30635578416f4942415143323565594e6b4f3437386d6f2b62426245584a6c6b6f714c6d766a417947724e466f343846396c705648365930764e75576b584a4e0a5055674c556841753652596f746a47454e71473137727a387a742f505059394f6b325033734f783874303079316d496e2f686c445a58733535464d30664f4d750a505a616973634150733748447a76794f6d4461682b667a692b5a443848324d33445332572b5945306961654a6132765a4a5332743032573042475869444933330a495a44714d794c59767777506a4f6e53684a7964457a58494434784c6c30744e6a7a4c786f3347534e41376a59716c6d627456384358496337724d534c3657560a6b7449444b4b4a636e6d706e3354634b6558364d456a615349543832704e4f5333665933506d58754c2b434d7a6677382b75373745656371373866486154694c0a50354a474d393346366d7a693139455930746d496e55424d435774514c63454e416f494241514367304b614f6b6238543336717a5072746762666f75304532440a756664704c3175676d443465644f464b51423566444651684c6e534556534a71334b5567346b57735861705164734264366b4c6478532b4b364d51724c427a720a34746630633755434631417a576b3677584d45785a386d526232526b475a595142324464796846423354506d6e71394357384a43712b366b78672f776b5534730a764d344a587a676371566f53663432514a6c2b4239776165576867304254577830316c616c34647338384876454b6d4530696b354777694462723745764444770a453655625a745163496f535449495a4467597156466652324441686f3377584a52734f58683433336c454a3858376343447a726e674662516e6c4b7270774d4c0a58676d30534955632b4e6635706f4d4d3372664c464b3737742f6f6234772b355077524b636f536e69794178724864366277796b59413856757964760a2d2d2d2d2d454e44205253412050524956415445204b45592d2d2d2d2d0a"
+        val encodedKey = Blob.encodePubKey(Algorithms.get("SHA256_RSA4096")!!, Hex.decode(keyStr))
+        val expectedKeyEnc = "0000100055d904add804afe3d3846c7e0d893dc28cd31255e962c9f10f5ecc1672ab447c2c654a94b5162b00bb06ef1307534cf964b9287a1b849888d867a423f9a74bdc4a0ff73a18ae54a815feb0adac35da3bad27bcafe8d32f3734d6512b6c5a27d79606af6bb880cafa30b4b185b34daaaac316341ab8e7c7faf90977ab9793eb44aecf20bcf08011db230c4771b96dd67b604787165693b7c22a9ab04c010c30d89387f0ed6e8bbe305bf6a6afdd807c455e8f91935e44feb88207ee79cabf31736258e3cdc4bcc2111da14abffe277da1f635a35ecadc572f3ef0c95d866af8af66a7edcdb8eda15fba9b851ad509ae944e3bcfcb5cc97980f7cca64aa86ad8d33111f9f602632a1a2dd11a661b1641bdbdf74dc04ae527495f7f58e3272de5c9660e52381638fb16eb533fe6fde9a25e2559d87945ff034c26a2005a8ec251a115f97bf45c819b184735d82d05e9ad0f357415a38e8bcc27da7c5de4fa04d3050bba3ab249452f47c70d413f97804d3fc1b5bb705fa737af482212452ef50f8792e28401f9120f141524ce8999eeb9c417707015eabec66c1f62b3f42d1687fb561e45abae32e45e91ed53665ebdedade612390d83c9e86b6c2da5eec45a66ae8c97d70d6c49c7f5c492318b09ee33daa937b64918f80e6045c83391ef205710be782d8326d6ca61f92fe0bf0530525a121c00a75dcc7c2ec5958ba33bf0432e5edd00db0db33799a9cd9cb743f7354421c28271ab8daab44111ec1e8dfc1482924e836a0a6b355e5de95ccc8cde39d14a5b5f63a964e00acb0bb85a7cc30be6befe8b0f7d348e026674016cca76ac7c67082f3f1aa62c60b3ffda8db8120c007fcc50a15c64a1e25f3265c99cbed60a13873c2a45470cca4282fa8965e789b48ff71ee623a5d059377992d7ce3dfde3a10bcf6c85a065f35cc64a635f6e3a3a2a8b6ab62fbbf8b24b62bc1a912566e369ca60490bf68abe3e7653c27aa8041775f1f303621b85b2b0ef8015b6d44edf71acdb2a04d4b421ba655657e8fa84a27d130eafd79a582aa381848d09a06ac1bbd9f586acbd756109e68c3d77b2ed3020e4001d97e8bfc7001b21b116e741672eec38bce51bb4062331711c49cd764a76368da3898b4a7af487c8150f3739f66d8019ef5ca866ce1b167921dfd73130c421dd345bd21a2b3e5df7eaca058eb7cb492ea0e3f4a74819109c04a7f42874c86f63202b462426191dd12c316d5a29a206a6b241cc0a27960996ac476578685198d6d8a62da0cfece274f282e397d97ed4f80b70433db17b9780d6cbd719bc630bfd4d88fe67acb8cc50b768b35bd61e25fc5f3c8db1337cb349013f71550e51ba6126faeae5b5e8aacfcd969fd6c15f5391ad05de20e751da5b9567edf4ee426570130b70141cc9e019ca5ff51d704b6c0674ecb52e77e174a1a399a0859ef1acd87e"
+        assertEquals(expectedKeyEnc, Hex.toHexString(encodedKey))
+    }
+
+    @Test
+    fun testEncodeKey8192() {
+        val keyStr = "2d2d2d2d2d424547494e205253412050524956415445204b45592d2d2d2d2d0a4d4949534b67494241414b4342414541304433542b644953736d43486d37393777735830765666715557444a2f336d7644596f7a6c43616244686e474c6c53450a7041516266315a3854732b4f4d34705652484f4a554a4c305765624e646d5050476a737957517a367a5a4539366c515a4c3361764345587159565152363656350a3377644b2f6f68614d53526e4779454d4272716b56566246336743722b2f6972784433594b2b566f774f32574b732f3647724d647154413859354354462f4a650a707477735367354d4d6a723655614b3471446372656a33686b6742564776525633636a31736e4b3642723848755964466e7047475453306437554a6c4846676c0a74724748552f43424f393233686b48674a6157456a4330676953476a684b4b744c7a72566370445632792f6c57515039542f5434646a454149614871512b2b500a53644f535236707349475236685667536967743748436e45376e573731312f7266563555723945695670423034306d44496d4b5a6379382f2f544d6e5879644e0a314b595456642f33346664707a4d7053773569626c457262774f4c585654556d4f7a74596e706c343166654853762f6a506573487374506c666b6c494632766f0a475a456f6866397363517663754d3777454266432f615441394b33397a4d6d6b42626376535a6a4c79686d63535a574d50504f5a7949636c337a5935335168570a51432f61626d49634266493153342b72376d433469324a6e2b2b6f457675474e564772325359325a305a5a7858474c3148492f3038442f332b5463756d72636e0a34596a504b2f444d466930462b652b317834316c697075662b63782f3271524e51582f6d30325354724c59644d3665306733334b766c6e4664693262373532790a2f4f49614d777844614a76756e4d6836454d44574b4d31414862592f696f416f4b376553323648654a4c45446c6c714f342b535750333763386c4d76534557790a314769457252304863734f6a2f51775747504673656f56726f4d6941327355513049632f7467566a43546c58672b31325870556e6f754977654369384b634c2f0a6164327a4a6b6a75396842684a4c42512f32476e69764a69336c4667463447642f2f54534a36726757755846664d4b742f397a32537a33356f684558347941300a666c716c43654c496e46456f6576627a2b585439615266446536354d5a373979773354665039437256373468663152527a766544347a706933462b68635932690a4a5773483767524f5a65436d3666415835547265636433684f784a4f6641344e34727653534371364277437665625438465932355a2f564637635172485944530a696a3577366c71684d7a5848655545593930476139414b34587a6157774767657a712b52375a73303059534b7146763971594e4b645237747a33636a696a57660a39712f33523175683645514b544d5a4b6f345345436c4a6947796a4f42766d504b30396a4d465a544a763030684478616744505a426c3758704c444a352f4c6e0a31757070764c434e575759317a654a6661456c4d7971332f50714b5a4c6964463972566f41315349776b326c70645576506f7465326f466977435a6f586c775a0a4a326e636a6d5867514e7337362f38756e444a4130726a344a5071636377344d35477851376f6b62676d334634726d7a72694375763842654d53436b723272790a306d59335568706f68583477434d713047347835734555417a39465656505a4b6a786e59426d4c447a724a41522b342b4737675a736374303158444a596744640a4a5659496e465032322f63497265385672465759744862674f46644e7155695671353864653650645a472f452b7561576d455468536c527267456a54787570690a4f586667644b572f32306a317141746a4f6c714677735930393451357271554c513677507851494441514142416f4945415143686d6b6d6c68725242763432640a6659556979784b353262386174683073614a64447a36746c586d785944674a784d392f586c4f5274396f547a65446b6e6f454f356f6c752b72727834424267510a747a59696169775256585252455654575137746a7a5276614e4c2f47466b4c7439335854636370754b7779724e452f6269744c56616752627763492b485a46610a4d6b6e434f6968484d486f52746f386833464b41593934787a5341674f444d656b315747386a6867704358586d564e6e4250742b64346f44444944414741667a0a71676630334a356e6849622b38304b675a4f7a504f4b6e62764a614c36456d6c4c4862674233633432647a417737684874566d6f6659475763764c62324d49590a44564b4f3433352f73517831552f384e4448364a6a566441435a6a4c674f625848394b332f54743436445750456372504c6d443878686f633667464d2b5172300a41686b7a4b6f4259444e6b30436c6a6268644942586a6b74585536775251465a343575503265344a5a347a727a47424c722f74346c5461765a305351744c6c640a41366b4f7347682b6443574644746e736878596e6c2f7861642f79522b3361357a6d444a626f2f664a544258726c663142347266516b46744b323065744f50510a422b2b46432f726a68334d6d2f4b622f7039477a2f3275705a6441724839375a7644324c4246666a37376c466d4168714169337743526c4e2b656b755978615a0a743170425639795869673844796c64673164375838704f6e326b7972463372515544446634706137783976706e626b556c455569666f5639676e59736d646e690a71447a59427454763267364d4b7177517953586149555730594f4250624f656c6c574577784a7147595137793449665648664d306979486e65686b32745a63720a2b58617a4c6e7747652b427a347663677546684a584c7949752f2f6c414f685a74626b367231514a45557578614f4f515833777a79636545366e6b4473676d720a5035646a335a7064376653325656327679474849466e424a38384c52787265567667723651323855543237534238327a4d62376d525a545645327a65757562540a354432443158625a3077426f3657694b366552527244513248616565746b6a2f756f527936505758776e4161546d6d49727258774c71616f4a682f5531652b440a746673444c57643649784c6a66587647676c7248737274417a306f70727069785554655668675472476b394951526435727678754755596846756a56615949360a2b5155662b3333414664746e636238793943396a5a6d677838414b624a6b2b653733534c6842354a566f732b57746555376238642f4d696d356d414c6a6e4f360a5a316e2f75696d735437397353447179335853796d744b57586f2f3232556c72764743706f4575454c504d6236645346575237767772737668466e6759342f4b0a556e69746e7678626f45666c516e614951344966524c527a5a73582b73433545737177395535744874346f492b39314476334b626462634552675637334b36420a5a516743346c6b415171754658695a354149436b786a694d795a77547455394b4a37787631375875366f7977462f33417462564745545731442b336d614873440a7933444153576f6a79715a644c6a2b57477a4b5152612b737767434441594b65656b32664941584653644636337a784a3252784f4a3447696a53616f682b6d720a3448567663704461546a2b413854312b516442794d347339386775344744376b5674565147425a64576a75747948766830685776316774566d6268512f3431330a67444d4646447a48496a4c5459475965733468484c32323136396a565239735a316551787776544967334e3470443563466d307252755a5a54532b6f4a546f460a4732376142466968416f494341514479564236325a446e62785174686b2b7a49544b497a5255724a624c6f58725563414e63534866614e37696e4638374f76610a7a6537656a5439444e5345686274665a464a31473664694f596f53772b324d7a4658763067456b4c4b593064455479644b67484575366e5671356569764d67760a4434686339596b4a4d4844536c6d763246446b704c33415843416d6e5739724b702b64647474425a45436e6d6c504570484c6f6a367867427733704e613158730a49634c56666475674838364865786a366f306f4b6759666371725838555548745549322f585171674672496a386b736a6631664656574a524a46576d425871700a6e4d45735961727a4154654d316b512f6b446554315a55706f4750517430322f5871585434423541334154694574704d32752b6c343878746f675757673252790a47396c3933385374416d68556957316d37476e4b453645494676515938355776627a784f52304a5956555372374d72617346366e6e516c6859784675494a6f4a0a32682f4b4a51616f354743547647342b4774624a4a6d3463326e795a67777968697a4d7364677364636c7337396158694d6b725a5a6b616d4c56555a574f74450a3370412f6f42757a32716e4f3948776a62483148474f636371305458666d70465363455633435147594a646e6f3646793763626d7570614c34553961675134650a772b79674c31386e713548562b2b4c5374466e567267733559696a6a736b665264453947554d56446835704373643959323346796d616164344f2f32535243430a596b53737948354f7679444f4c706f79554a366736512b343548716d2f336c4734596a4e707a4655694d636e70372b33785533357143304c4b387845666565690a4d73316d54564569484e49703678482f547152645837335744372b59754b5a534c496652473764677269725536772b6d686876784435317548514b43416745410a322f316d42435235716d332f304c742b2b52516265794533746977343055657951717563472f2b567659373773534c6b492f4c78386977526c797758634c426e0a2b4134547667756b6d4164577a4373386e64674b4e7850412b67666f687642734d4f474e394b4f4231556735767667324a326b6949363476775943777a68645a0a4e5455556d4c2b474d464855715373575967366937694246635a6d7a6e72345732543362427879544d5a6b69374a53744238366533354b58727a63322f572f620a2b2f70355532484353617a444849356d4d7975436c486336476d5553564a3766374c486a4c39346a76694e716f627030566a3630337453634849536d4e725a770a544261766b765a475958736f574b767161766b376a424239517a61424c2b756e614652736c67356a5461694b6e49536a3434557331666a464b7538347869664c0a6e4a61457a6a445074375042786b6f374c5067455937774633396e4d3956706f65744937627752364e77444c535838555539374d47642b48592b4d4f315769310a7064324c61707772782f454b374f787a33333556524b344a6530615a6e61346a32547951644d4a616339667347505876345a734c66444c6a2f7744366c316a2b0a6c4c4c62427633496d64536a33324c4262687367463469434765584f384870504f2b512f68395856736e593532556d3258644e4d6e30335043476d365a76744d0a37445869532b6c50463930486a6f6c4a56485a54424e74645652724c7235337a4c7557456671543446654b72446178647469586b784c6a72422b352f565975370a6e74796b30315a513633564e664577533169726d4b6c392b715a6b54486b33484856396a4e5635527a575669776d4a493757707231597a42776d634b4342314f0a6f4755414444733851706e6b437a30786b4d56745977486a39714b5a6c716662487a72464455556346386b43676749416459765563676a662f2f6a75386d41380a3556513341635045365476796350572b6b5232447657313256634473462f73633155413764487a7a695068476e3938536d4e786c426a62387375536246505a380a5168565430574242446b6354696c774947507839617837553353366c47573256645336467151483566526d67514b5a79724356584c4f457a384267594272534a0a78752f335451415778483051746962646247486738506469353867596c574652686e394238536c6831615259484750623141684e4c4264302f6464592b3547320a397853794458646d5a67316355412b42337a41774e5371627a46786870327a552b563175587362706b344b746e595636435a4d39516c7243526a546b39694e550a645658462f716169526a667a726d3453736d4570436b4562737270374632325931626b6f6f4f52676c4d4f734e41574e716656587734774e2b7379586a31726f0a36765a3850455259724679414f52316473514d4968796d6e6d54506a4370614a34656d4b7268575479323073593731746848616b5a574a633232596f4e70625a0a453674674956734a50546c78672f342b667943434b6a3577577239326e687342314b425a50474f2f7a4668764d6c4a70765130744838573270624e3261306d490a3578394671414c6d2f716a774348665a49745377504d2b5a6f7a53687433634f6b4748646344354b58415866636673444a633453485a4b56497a71344e75734e0a353034522f6a76443147503873676c7947376f6d703735636b677a416d616b4c64784f503248685176495839746358705369724e4a36536c3262774b75754d460a77786f33722f6f2f3959393765344c6c66704559703965714d6463472b4e705239393349774b30556841575339483577646e57425355486435653478744455740a69494c4e52754f34366737522f4149687a3163535372615757516b4367674942414d6868505035433979743950496d316230655477434263746e465351494b6f0a4b734139726c6c3261622b624d4c6b396a63384d364d4c737a7930437457736f30397348663459593974696676726b4548526574684568387a736377557559750a736d326e31665469786b30756c364c5356676c35347558624d4a6179454e6e3450494b526b65773863413874536d613433343937773337686d442b4d674362310a414c7a7163636f3968666d6b676b4936666f316738436533554545434b7932594b536d5245646759634b394a46514f36315736416b46574a634478416d667a490a4a6a466b4b7773623754537737397a57694564536f4d396a6d377343504b41546436426d2f5a41416b5555547545466b666f626e39417831724a4e2f587862320a4d4b754155745176304e595930674556644736326a4974754b4c4964366e6e63483850472b7273526a504c495970577159644a704b783570556e522b34416b510a5336437352415377634634506442764444424946473658706a466f347050645168447a4c32735446386238535753424c6c4a516262374736554e7167435361750a537573434670617a7655354e66446d554d7563746f623245595661535871396a47616a366254556d447758487757696c66496b3958664c786e59665859724a360a786864497058476d4868754c517441674b324f314a744c6f50633973397150382f536b665037786a6a4736784873502f57764c37514531705073395a4d2f55490a4330314a4e484669394c4b436e386f356d625a6a4e386a556f77693766664b2b3736775a5547314c377a4d35797457514f59776f3054514266633866706d46770a2b5242524a58326b4a79444f32374578637a6f474f4b6a77714544614f444942392b397a63434b304267536f526962536d345a42766f787a57574436354b6c730a786450685a55486346475735416f494341514338694732376144386152557439344f656b363667464f4a78383451565a6568575071745a6a577956656e4475630a543864696e6b386f656a476a634b32554a755144613833617a7639306f63567145306e30726f6e5979737a74394962316a6c59432b434b3141723954594746670a5755354f57454479437a437071572f772f61473638553871684b6d304d766b4c4a522b47366576616e3954774568464556416d3369576c6c4e587339783239730a42756377794d4d4332337a73696d78596c53376441344474797656412b7a4c316f6d4c7053574862552f71747549334856314e654a7a73792b6743346d7750680a6a353274646c3636396679574c7a487a42524c65713664564f65646a6e436f2b6a6c5533644c323044456b39536157303844314350755a656b56316a56504d770a4a6f614463495268344b4c74513042595a37554a6546555473783143532f2b55717a715953504f69353761356b76723059385977526e534238644856467474580a4a5476383377545158485046534267666e484e65376c7352546649516675496b723262706955376838355551374c7371634936594861433037555263734746460a46724c5747683931717a416431646953486c6132526e59336e385050754d6e436b67754e684c557259646d794d6f6c374666574661396c77706c7375547a42710a4236796a3869616945334c4c2b512f65756c4a375336515066414932625530554a4f323359346b6f656f4969624545444d534351364b595a324e436c525252540a67613566533159666b44464563485551312f4b496b64594847424b426a6f4b4745787a69382b436769537953565359445a6c3677494f684c6a48324f5a336f6c0a6c64504e37694e41486972727867397638514f364f516c704c556b354c68702f3164536c5a36737933556a4671766178337477365a6a724c3838595035673d3d0a2d2d2d2d2d454e44205253412050524956415445204b45592d2d2d2d2d0a"
+        val encodedKey = Blob.encodePubKey(Algorithms.get("SHA256_RSA8192")!!, Hex.decode(keyStr))
+        val expectedKeyEnc = "00002000ef8168f3d03dd3f9d212b260879bbf7bc2c5f4bd57ea5160c9ff79af0d8a3394269b0e19c62e5484a4041b7f567c4ecf8e338a554473895092f459e6cd7663cf1a3b32590cfacd913dea54192f76af0845ea615411eba579df074afe885a3124671b210c06baa45556c5de00abfbf8abc43dd82be568c0ed962acffa1ab31da9303c63909317f25ea6dc2c4a0e4c323afa51a2b8a8372b7a3de19200551af455ddc8f5b272ba06bf07b987459e91864d2d1ded42651c5825b6b18753f0813bddb78641e025a5848c2d208921a384a2ad2f3ad57290d5db2fe55903fd4ff4f876310021a1ea43ef8f49d39247aa6c20647a8558128a0b7b1c29c4ee75bbd75feb7d5e54afd122569074e34983226299732f3ffd33275f274dd4a61355dff7e1f769ccca52c3989b944adbc0e2d75535263b3b589e9978d5f7874affe33deb07b2d3e57e4948176be819912885ff6c710bdcb8cef01017c2fda4c0f4adfdccc9a405b72f4998cbca199c49958c3cf399c88725df3639dd0856402fda6e621c05f2354b8fabee60b88b6267fbea04bee18d546af6498d99d196715c62f51c8ff4f03ff7f9372e9ab727e188cf2bf0cc162d05f9efb5c78d658a9b9ff9cc7fdaa44d417fe6d36493acb61d33a7b4837dcabe59c5762d9bef9db2fce21a330c43689bee9cc87a10c0d628cd401db63f8a80282bb792dba1de24b103965a8ee3e4963f7edcf2532f4845b2d46884ad1d0772c3a3fd0c1618f16c7a856ba0c880dac510d0873fb6056309395783ed765e9527a2e2307828bc29c2ff69ddb32648eef6106124b050ff61a78af262de516017819dfff4d227aae05ae5c57cc2adffdcf64b3df9a21117e320347e5aa509e2c89c51287af6f3f974fd6917c37bae4c67bf72c374df3fd0ab57be217f5451cef783e33a62dc5fa1718da2256b07ee044e65e0a6e9f017e53ade71dde13b124e7c0e0de2bbd2482aba0700af79b4fc158db967f545edc42b1d80d28a3e70ea5aa13335c7794118f7419af402b85f3696c0681eceaf91ed9b34d1848aa85bfda9834a751eedcf77238a359ff6aff7475ba1e8440a4cc64aa384840a52621b28ce06f98f2b4f6330565326fd34843c5a8033d9065ed7a4b0c9e7f2e7d6ea69bcb08d596635cde25f68494ccaadff3ea2992e2745f6b568035488c24da5a5d52f3e8b5eda8162c026685e5c192769dc8e65e040db3bebff2e9c3240d2b8f824fa9c730e0ce46c50ee891b826dc5e2b9b3ae20aebfc05e3120a4af6af2d26637521a68857e3008cab41b8c79b04500cfd15554f64a8f19d80662c3ceb24047ee3e1bb819b1cb74d570c96200dd2556089c53f6dbf708adef15ac5598b476e038574da94895ab9f1d7ba3dd646fc4fae6969844e14a546b8048d3c6ea623977e074a5bfdb48f5a80b633a5a85c2c634f78439aea50b43ac0fc51af9112e97f272e5448cc3110fce7a4081ea0613cba5637de45982ff7144d57cb6401e5186d39755af273947e526d5bb5bd4390562cfb8f28039c14be94986fb4f79a49bb97a30d2f252a8569e121993af3c57a60e98cb07dfedbdeffc887ada8d098ac4deef25fee3b33ce90be9d27ae66602da89b4b931a08ae776e7ecac8ccb30c02b72208b87f05b03b9cf83f66e4be8344fd4c4ddd08194ad7a0a3a6d6ec21058e6de8bf9c8691cb7994748e71989387340a5ecd3cd0f506b6686522673e9d2770cd13db18217cd261f706efcc0f4b3842982e4c8d333b3e0ae9348b91e15843e17371426b8294964f2e74cd1c2e08a575b66a79494d98f1db480edc741f689a3678985c379609bac92b0c192dfe5ea44fc284cf67297220888210d9ebd2d100ffc8bcb13636aad6263b2a735e3d006f309100dc391297ce7253e6df1885eefee0e7035ef318053d7177cd4acc5ec92031a1fd66def3404c4adfff0f12194a093fad509a0a6c7422011d0b0c40a6da8601fc71e48ad2544ebc652499ed591e6eb89b074fca8a920b8bbb568abfcc0af73fa4f3256cb30609cdbd11307db751d0835e09cfa1c747a58ed2919d4bd6a9a8eb2c1fb92e003bb090de173dc704b387f41b7285f95294d1ce756b269e9e3814ccb3f80efe0d7eac6d0cfe2cc51524aa4c984476d74fd34f3e4ff126cf01523a1deda0e5a0772f0e61bed8dc3615d547d8113d23d8f0bfc0097ae88258e6478cee389fbb6bf4ea68d083a938d7f781ce370c03cbd10a053e7d6a37526f610220eb684805e62204f210a790e7009cf71fb036851f5fcdb86bf640c94e0c0a412a66abf5a7f205d515b6c85d65b7da2f7e40dcf3d0567d2d851f02942a0f9a67e7f9f4427460de297d17219b940ddcfaf8310d6e906831be39c2cf1d527f2ffabd95dfe145c8f0adecfc4b167f38ae03fd8d2fe4d42bd8f2ae20f59a3688153a8b2c75ad360bd9a900d810d5ce17a5be35436cd15d6b2b881b56fd97277e4b2cd82f25df35286ec724a125280a036bcb602bd0038c5ea3026547462a06e4a45729ca0784e31d1103b7f991eaa89d7121cfd67510fb18d4d5b03068884c592d6bad56815d4eb4cffab1feb7ea27dd4e28c62db6d18f8d838b48553cd737611657463349f70cdc260ab0ad9bf163658eacc78982a274f85aadb0becf92588ecd53fc5e229bb1fe870a5f18c5c66bd154d052b2e2663004c0d6beacfcb55094ffb1898b7dbe3c9653815da4c11d53ac018b98fbb36fa61197de15258dc4614807c83c02f15420527508e63f8327b4c9862291810ff453b9badb3d7620d8c1aab8b5d50d6af59fc181161a5b1039090062f0c8995822be2df158447253b8abf913225c1bdef3e9a54a0589c1f69580e2565b28c75e9c4c8d738504ee8e08de414c64d8cee4864ecf9a60948515cc07680"
+        assertEquals(expectedKeyEnc, Hex.toHexString(encodedKey))
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/test/kotlin/avb/FooterTest.kt b/bbootimg/src/test/kotlin/avb/FooterTest.kt
new file mode 100644
index 0000000..f23aa63
--- /dev/null
+++ b/bbootimg/src/test/kotlin/avb/FooterTest.kt
@@ -0,0 +1,37 @@
+package avb
+
+import org.bouncycastle.util.encoders.Hex
+import org.junit.Test
+
+import org.junit.Assert.*
+import java.io.ByteArrayInputStream
+
+class FooterTest {
+
+    @Test
+    fun readAVBfooter() {
+        val footerBytes = this.javaClass.classLoader.getResourceAsStream("taimen.avbfooter").readBytes()
+        ByteArrayInputStream(footerBytes).use {
+            it.skip(footerBytes.size - 64L)
+            val footer = Footer(it)
+            println(footer.toString())
+            assertEquals(1, footer.versionMajor)
+            assertEquals(0, footer.versionMinor)
+            assertEquals(512, footer.vbMetaSize)
+            assertEquals(28983296, footer.vbMetaOffset)
+            assertEquals(28983296, footer.originalImageSize)
+        }
+    }
+
+    @Test
+    fun readInvalidFooterShouldFail() {
+        val vbmetaHeaderStr = "4156423000000001000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c8000000000000000000000000000000c80000000000000000000000000000000000000000000000c800000000000000000000000000000000617662746f6f6c20312e312e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+        ByteArrayInputStream(Hex.decode(vbmetaHeaderStr)).use {
+            try {
+                Footer(it)
+                assertEquals("Should never reach here", true, false)
+            } catch (e: IllegalArgumentException) {
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/test/kotlin/avb/HeaderTest.kt b/bbootimg/src/test/kotlin/avb/HeaderTest.kt
new file mode 100644
index 0000000..2d00c4d
--- /dev/null
+++ b/bbootimg/src/test/kotlin/avb/HeaderTest.kt
@@ -0,0 +1,15 @@
+package avb
+
+import org.bouncycastle.util.encoders.Hex
+import org.junit.Test
+import java.io.ByteArrayInputStream
+
+class HeaderTest {
+
+    @Test
+    fun readHeader() {
+        val vbmetaHeaderStr = "4156423000000001000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c8000000000000000000000000000000c80000000000000000000000000000000000000000000000c800000000000000000000000000000000617662746f6f6c20312e312e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
+        val header = Header(ByteArrayInputStream(Hex.decode(vbmetaHeaderStr)))
+        println(header.toString())
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/test/kotlin/avb/alg/AlgorithmTest.kt b/bbootimg/src/test/kotlin/avb/alg/AlgorithmTest.kt
new file mode 100644
index 0000000..1e533ee
--- /dev/null
+++ b/bbootimg/src/test/kotlin/avb/alg/AlgorithmTest.kt
@@ -0,0 +1,14 @@
+package avb.alg
+
+import cfig.io.Struct
+import org.junit.Test
+
+import org.junit.Assert.*
+import java.nio.ByteBuffer
+
+class AlgorithmTest {
+
+    @Test
+    fun getName() {
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/test/kotlin/avb/alg/AlgorithmsTest.kt b/bbootimg/src/test/kotlin/avb/alg/AlgorithmsTest.kt
new file mode 100644
index 0000000..13735cd
--- /dev/null
+++ b/bbootimg/src/test/kotlin/avb/alg/AlgorithmsTest.kt
@@ -0,0 +1,16 @@
+package avb.alg
+
+import avb.alg.Algorithms
+import cfig.Helper
+import org.junit.Assert
+import org.junit.Test
+
+class AlgorithmsTest {
+    @Test
+    fun test() {
+       val alg = Algorithms.get("NONE")!!
+
+        Assert.assertEquals(Helper.toHexString(Algorithms.get("SHA256_RSA4096")!!.padding),
+                "0001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d060960864801650304020105000420")
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/test/kotlin/avb/desc/HashDescriptorTest.kt b/bbootimg/src/test/kotlin/avb/desc/HashDescriptorTest.kt
new file mode 100644
index 0000000..15d2648
--- /dev/null
+++ b/bbootimg/src/test/kotlin/avb/desc/HashDescriptorTest.kt
@@ -0,0 +1,19 @@
+package avb.desc
+
+import org.bouncycastle.util.encoders.Hex
+import org.junit.Assert
+import org.junit.Test
+import org.slf4j.LoggerFactory
+import java.io.ByteArrayInputStream
+
+class HashDescriptorTest {
+    private val log = LoggerFactory.getLogger(HashDescriptorTest::class.java)
+
+    @Test
+    fun parseHashDescriptor() {
+        val descStr = "000000000000000200000000000000b80000000001ba4000736861323536000000000000000000000000000000000000000000000000000000000004000000200000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000626f6f74fbfb8e13c8082e0a16582163ad5075668903cc1237c6c007fed69de05957432103ae125531271eeeb83662cbe21543e3025f2d65268fb6b53c8718a90e3b03c7"
+        val desc = HashDescriptor(ByteArrayInputStream(Hex.decode(descStr)))
+        log.info(desc.toString())
+        Assert.assertEquals(descStr, String(Hex.encode(desc.encode())))
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/test/kotlin/avb/desc/HashTreeDescriptorTest.kt b/bbootimg/src/test/kotlin/avb/desc/HashTreeDescriptorTest.kt
new file mode 100644
index 0000000..055aa9f
--- /dev/null
+++ b/bbootimg/src/test/kotlin/avb/desc/HashTreeDescriptorTest.kt
@@ -0,0 +1,28 @@
+package avb.desc
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.bouncycastle.util.encoders.Hex
+import org.junit.Test
+
+import org.junit.Assert.*
+import java.io.ByteArrayInputStream
+
+class HashTreeDescriptorTest {
+
+    @Test
+    fun encode() {
+        val treeStr1 = "000000000000000100000000000000e000000001000000009d787000000000009d78700000000000013d9000000010000000100000000002000000009eb60000000000000141400073686131000000000000000000000000000000000000000000000000000000000000000600000020000000140000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000073797374656d28f6d60b554d9532bd45874ab0cdcb2219c4f437c9350f484fa189a881878ab609c2b0ad5852fc0f4a2d03ef9d2be5372e2bd1390000"
+        val treeStr2 = "000000000000000100000000000000e000000001000000001ec09000000000001ec0900000000000003e2000000010000000100000000002000000001efeb00000000000003ec00073686131000000000000000000000000000000000000000000000000000000000000000600000020000000140000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000076656e646f7228f6d60b554d9532bd45874ab0cdcb2219c4f437c9350f484fa189a881878ab698cea1ea79a3fa7277255355d42f19af3378b0110000"
+
+        val tree1 = HashTreeDescriptor(ByteArrayInputStream(Hex.decode(treeStr1)), 0)
+        println(ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(tree1))
+        assertEquals(treeStr1, String(Hex.encode(tree1.encode())))
+
+        val reDecoded = HashTreeDescriptor(ByteArrayInputStream(tree1.encode()), 0)
+        println(ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(reDecoded))
+
+        val tree2 = HashTreeDescriptor(ByteArrayInputStream(Hex.decode(treeStr2)), 0)
+        println(ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(tree2))
+        assertEquals(treeStr2, String(Hex.encode(tree2.encode())))
+    }
+}
diff --git a/bbootimg/src/test/kotlin/avb/desc/KernelCmdlineDescriptorTest.kt b/bbootimg/src/test/kotlin/avb/desc/KernelCmdlineDescriptorTest.kt
new file mode 100644
index 0000000..72c3cd0
--- /dev/null
+++ b/bbootimg/src/test/kotlin/avb/desc/KernelCmdlineDescriptorTest.kt
@@ -0,0 +1,22 @@
+package avb.desc
+
+import org.bouncycastle.util.encoders.Hex
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.slf4j.LoggerFactory
+import java.io.ByteArrayInputStream
+
+class KernelCmdlineDescriptorTest {
+    private val log = LoggerFactory.getLogger(KernelCmdlineDescriptorTest::class.java)
+
+    @Test
+    fun encode() {
+        val cmdStr1 = "000000000000000300000000000001a8000000010000019b646d3d22312076726f6f74206e6f6e6520726f20312c3020353135393939322076657269747920312050415254555549443d2428414e44524f49445f53595354454d5f5041525455554944292050415254555549443d2428414e44524f49445f53595354454d5f504152545555494429203430393620343039362036343439393920363434393939207368613120303963326230616435383532666330663461326430336566396432626535333732653262643133392032386636643630623535346439353332626434353837346162306364636232323139633466343337633933353066343834666131383961383831383738616236203130202428414e44524f49445f5645524954595f4d4f4445292069676e6f72655f7a65726f5f626c6f636b73207573655f6665635f66726f6d5f6465766963652050415254555549443d2428414e44524f49445f53595354454d5f504152545555494429206665635f726f6f74732032206665635f626c6f636b7320363530303830206665635f7374617274203635303038302220726f6f743d2f6465762f646d2d300000000000"
+        val cmdStr2 = "000000000000000300000000000000300000000200000028726f6f743d50415254555549443d2428414e44524f49445f53595354454d5f504152545555494429"
+        val cmd1 = KernelCmdlineDescriptor(ByteArrayInputStream(Hex.decode(cmdStr1)), 0)
+        assertEquals(cmdStr1, String(Hex.encode(cmd1.encode())))
+
+        val cmd2 = KernelCmdlineDescriptor(ByteArrayInputStream(Hex.decode(cmdStr2)), 0)
+        assertEquals(cmdStr2, String(Hex.encode(cmd2.encode())))
+    }
+}
\ No newline at end of file
diff --git a/bbootimg/src/test/kotlin/avb/desc/UnknownDescriptorTest.kt b/bbootimg/src/test/kotlin/avb/desc/UnknownDescriptorTest.kt
new file mode 100644
index 0000000..9ca262d
--- /dev/null
+++ b/bbootimg/src/test/kotlin/avb/desc/UnknownDescriptorTest.kt
@@ -0,0 +1,28 @@
+package avb.desc
+
+import org.bouncycastle.util.encoders.Hex
+import org.junit.Test
+
+import org.slf4j.LoggerFactory
+import java.io.ByteArrayInputStream
+
+class UnknownDescriptorTest {
+    private val log = LoggerFactory.getLogger(UnknownDescriptorTest::class.java)
+
+    @Test
+    fun readDescriptors() {
+        //output by "xxd -p <file>"
+        val descStr = "000000000000000200000000000000b800000000017b9000736861323536" +
+                "000000000000000000000000000000000000000000000000000000000004" +
+                "000000200000002000000000000000000000000000000000000000000000" +
+                "000000000000000000000000000000000000000000000000000000000000" +
+                "000000000000000000000000626f6f7428f6d60b554d9532bd45874ab0cd" +
+                "cb2219c4f437c9350f484fa189a881878ab6156408cd763ff119635ec9db" +
+                "2a9656e220fa1dc27e26e59bd3d85025b412ffc3"
+        val descBA = Hex.decode(descStr + descStr)
+        val descList = UnknownDescriptor.parseDescriptors(ByteArrayInputStream(descBA), descBA.size.toLong())
+        descList.forEach{
+            log.info(it.toString())
+        }
+    }
+}
diff --git a/bbootimg/src/test/kotlin/cfig/io/StructTest.kt b/bbootimg/src/test/kotlin/cfig/io/StructTest.kt
new file mode 100644
index 0000000..3f05f67
--- /dev/null
+++ b/bbootimg/src/test/kotlin/cfig/io/StructTest.kt
@@ -0,0 +1,107 @@
+import cfig.Avb
+import cfig.Helper
+import cfig.io.Struct
+import org.junit.Assert
+import org.junit.Test
+
+import org.junit.Assert.*
+import java.io.ByteArrayInputStream
+
+class StructTest {
+    @Test
+    fun constructTest() {
+        assertEquals(16, Struct("<2i4b4b").calcsize())
+        assertEquals(16, Struct("<Q8b").calcsize())
+        assertEquals(2, Struct(">h").calcsize())
+        assertEquals(3, Struct(">3s").calcsize())
+        assertEquals(4, Struct("!Hh").calcsize())
+
+        try {
+            Struct("abcd")
+            throw Exception("should not reach here")
+        } catch (e: IllegalArgumentException) {
+        }
+    }
+
+    @Test
+    fun integerLE() {
+        //int (4B)
+        assertTrue(Struct("<2i").pack(1, 7321).contentEquals(Helper.fromHexString("01000000991c0000")))
+        val ret = Struct("<2i").unpack(ByteArrayInputStream(Helper.fromHexString("01000000991c0000")))
+        assertEquals(2, ret.size)
+        assertTrue(ret[0] is Int)
+        assertTrue(ret[1] is Int)
+        assertEquals(1, ret[0] as Int)
+        assertEquals(7321, ret[1] as Int)
+
+        //unsigned int (4B)
+        assertTrue(Struct("<I").pack(2L).contentEquals(Helper.fromHexString("02000000")))
+        assertTrue(Struct("<I").pack(2).contentEquals(Helper.fromHexString("02000000")))
+        //greater than Int.MAX_VALUE
+        assertTrue(Struct("<I").pack(2147483748L).contentEquals(Helper.fromHexString("64000080")))
+        assertTrue(Struct("<I").pack(2147483748).contentEquals(Helper.fromHexString("64000080")))
+        try {
+            Struct("<I").pack(-12)
+            throw Exception("should not reach here")
+        } catch (e: IllegalArgumentException) {
+        }
+
+        //negative int
+        assertTrue(Struct("<i").pack(-333).contentEquals(Helper.fromHexString("b3feffff")))
+    }
+
+    @Test
+    fun integerBE() {
+        run {
+            assertTrue(Struct(">2i").pack(1, 7321).contentEquals(Helper.fromHexString("0000000100001c99")))
+            val ret = Struct(">2i").unpack(ByteArrayInputStream(Helper.fromHexString("0000000100001c99")))
+            assertEquals(1, ret[0] as Int)
+            assertEquals(7321, ret[1] as Int)
+        }
+
+        run {
+            assertTrue(Struct("!i").pack(-333).contentEquals(Helper.fromHexString("fffffeb3")))
+            val ret2 = Struct("!i").unpack(ByteArrayInputStream(Helper.fromHexString("fffffeb3")))
+            assertEquals(-333, ret2[0] as Int)
+        }
+    }
+
+    @Test
+    fun byteArrayTest() {
+        //byte array
+        assertTrue(Struct("<4b").pack(byteArrayOf(-128, 2, 55, 127)).contentEquals(Helper.fromHexString("8002377f")))
+        assertTrue(Struct("<4b").pack(intArrayOf(0, 55, 202, 0xff)).contentEquals(Helper.fromHexString("0037caff")))
+        try {
+            Struct("b").pack(intArrayOf(256))
+            throw Exception("should not reach here")
+        } catch (e: IllegalArgumentException) {
+        }
+        try {
+            Struct("b").pack(intArrayOf(-1))
+            throw Exception("should not reach here")
+        } catch (e: IllegalArgumentException) {
+        }
+    }
+
+    @Test
+    fun packCombinedTest() {
+        assertTrue(Struct("<2i4b4b").pack(
+                1, 7321, byteArrayOf(1, 2, 3, 4), byteArrayOf(200.toByte(), 201.toByte(), 202.toByte(), 203.toByte()))!!
+                .contentEquals(Helper.fromHexString("01000000991c000001020304c8c9cacb")))
+        assertTrue(Struct("<2i4b4b").pack(
+                1, 7321, byteArrayOf(1, 2, 3, 4), intArrayOf(200, 201, 202, 203))!!
+                .contentEquals(Helper.fromHexString("01000000991c000001020304c8c9cacb")))
+    }
+
+    @Test
+    fun paddingTest() {
+        assertTrue(Struct("b2x").pack(byteArrayOf(0x13), null).contentEquals(Helper.fromHexString("130000")))
+        assertTrue(Struct("b2xi").pack(byteArrayOf(0x13), null, 55).contentEquals(Helper.fromHexString("13000037000000")))
+    }
+
+    @Test
+    fun stringTest() {
+        Struct("5s").pack("Good".toByteArray()).contentEquals(Helper.fromHexString("476f6f6400"))
+        Struct("5s1b").pack("Good".toByteArray(), byteArrayOf(13)).contentEquals(Helper.fromHexString("476f6f64000d"))
+    }
+}
diff --git a/bbootimg/src/test/resources/simplelogger.properties b/bbootimg/src/test/resources/simplelogger.properties
new file mode 100644
index 0000000..2c0452b
--- /dev/null
+++ b/bbootimg/src/test/resources/simplelogger.properties
@@ -0,0 +1 @@
+org.slf4j.simpleLogger.defaultLogLevel = debug
diff --git a/bbootimg/src/test/resources/taimen.avbfooter b/bbootimg/src/test/resources/taimen.avbfooter
new file mode 100644
index 0000000..7d69a8a
Binary files /dev/null and b/bbootimg/src/test/resources/taimen.avbfooter differ
diff --git a/build.gradle b/build.gradle
index b320986..9272ea4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -16,14 +16,17 @@ project.ext.rootWorkDir = new File(workdir).getAbsolutePath()
 String activeImg = "boot.img"
 String activePath = "/boot"
 if (new File("boot.img").exists()) {
-    activeImg = "boot.img";
+    activeImg = "boot.img"
     activePath = "/boot"
 } else if (new File("recovery.img").exists()) {
-    activeImg = "recovery.img";
+    activeImg = "recovery.img"
     activePath = "/recovery"
 } else if (new File("recovery-two-step.img").exists()) {
-    activeImg = "recovery-two-step.img";
+    activeImg = "recovery-two-step.img"
     activePath = "/boot"
+} else if (new File("vbmeta.img").exists()) {
+    activeImg = "vbmeta.img"
+    activePath = "/vbmeta"
 }
 project.ext.outClearIMg = new File(String.format("%s.clear", activeImg)).getAbsolutePath()
 project.ext.mkbootimgBin = new File("src/mkbootimg/mkbootimg").getAbsolutePath()