com.jrummyapps.busybox.utils.ZipSigner.java Source code

Java tutorial

Introduction

Here is the source code for com.jrummyapps.busybox.utils.ZipSigner.java

Source

/*
 * Copyright (C) 2017 JRummy Apps Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.jrummyapps.busybox.utils;

import android.annotation.SuppressLint;
import android.content.res.AssetManager;
import android.support.annotation.NonNull;
import android.util.Log;
import com.crashlytics.android.Crashlytics;
import com.jrummyapps.android.app.App;
import com.jrummyapps.android.util.IoUtils;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
import org.apache.commons.compress.archivers.jar.JarArchiveOutputStream;

public class ZipSigner {

    private static final String TAG = "ZipSigner";

    private static final String MANIFEST_VERSION = "1.0";
    private static final String CREATED_BY = "Jared Rummler";

    private static final String CERT_SF_NAME = "META-INF/CERT.SF";
    private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
    private static final String PRIVATE_KEY = "keys/testkey.pk8";
    private static final String PUBLIC_KEY = "keys/testkey.x509.pem";
    private static final String TEST_KEY = "keys/testkey.sbt";

    private static final char LAST_2_BYTE = (char) Integer.parseInt("00000011", 2);
    private static final char LAST_4_BYTE = (char) Integer.parseInt("00001111", 2);
    private static final char LAST_6_BYTE = (char) Integer.parseInt("00111111", 2);
    private static final char LEAD_6_BYTE = (char) Integer.parseInt("11111100", 2);
    private static final char LEAD_4_BYTE = (char) Integer.parseInt("11110000", 2);
    private static final char LEAD_2_BYTE = (char) Integer.parseInt("11000000", 2);

    private static final char[] ENCODE_TABLE = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
            'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
            'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0',
            '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' };

    /*
     * Encodes byte array into String using Base64 encoding scheme.
     */
    private static String base64encode(byte[] from) {
        //noinspection StringBufferMayBeStringBuilder
        StringBuffer to = new StringBuffer((int) (from.length * 1.34) + 3);
        int num = 0;
        char currentByte = 0;
        for (int i = 0; i < from.length; i++) {
            num = num % 8;
            while (num < 8) {
                switch (num) {
                case 0:
                    currentByte = (char) (from[i] & LEAD_6_BYTE);
                    currentByte = (char) (currentByte >>> 2);
                    break;
                case 2:
                    currentByte = (char) (from[i] & LAST_6_BYTE);
                    break;
                case 4:
                    currentByte = (char) (from[i] & LAST_4_BYTE);
                    currentByte = (char) (currentByte << 2);
                    if ((i + 1) < from.length) {
                        currentByte |= (from[i + 1] & LEAD_2_BYTE) >>> 6;
                    }
                    break;
                case 6:
                    currentByte = (char) (from[i] & LAST_2_BYTE);
                    currentByte = (char) (currentByte << 4);
                    if ((i + 1) < from.length) {
                        currentByte |= (from[i + 1] & LEAD_4_BYTE) >>> 4;
                    }
                    break;
                }
                to.append(ENCODE_TABLE[currentByte]);
                num += 6;
            }
        }
        if (to.length() % 4 != 0) {
            for (int i = 4 - to.length() % 4; i > 0; i--) {
                to.append('=');
            }
        }
        return to.toString();
    }

    /**
     * Tool to sign JAR files (including APKs and OTA updates) in a way compatible with the mincrypt verifier, using
     * SHA1 and RSA keys.
     *
     * @param unsignedZip
     *     The path to the APK, ZIP, JAR to sign
     * @param destination
     *     The output file
     * @return true if successfully signed the file
     */
    public static boolean signZip(File unsignedZip, File destination) {
        final AssetManager am = App.getContext().getAssets();
        JarArchiveOutputStream outputJar = null;
        JarFile inputJar = null;

        try {
            X509Certificate publicKey = readPublicKey(am.open(PUBLIC_KEY));
            PrivateKey privateKey = readPrivateKey(am.open(PRIVATE_KEY));

            // Assume the certificate is valid for at least an hour.
            long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;

            inputJar = new JarFile(unsignedZip, false); // Don't verify.
            FileOutputStream stream = new FileOutputStream(destination);
            outputJar = new JarArchiveOutputStream(stream);
            outputJar.setLevel(9);

            // MANIFEST.MF
            Manifest manifest = addDigestsToManifest(inputJar);
            JarArchiveEntry je = new JarArchiveEntry(JarFile.MANIFEST_NAME);
            je.setTime(timestamp);
            outputJar.putArchiveEntry(je);
            manifest.write(outputJar);

            ZipSignature signature1 = new ZipSignature();
            signature1.initSign(privateKey);

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            writeSignatureFile(manifest, out);

            // CERT.SF
            Signature signature = Signature.getInstance("SHA1withRSA");
            signature.initSign(privateKey);
            je = new JarArchiveEntry(CERT_SF_NAME);
            je.setTime(timestamp);
            outputJar.putArchiveEntry(je);
            byte[] sfBytes = writeSignatureFile(manifest, new SignatureOutputStream(outputJar, signature));

            signature1.update(sfBytes);
            byte[] signatureBytes = signature1.sign();

            // CERT.RSA
            je = new JarArchiveEntry(CERT_RSA_NAME);
            je.setTime(timestamp);
            outputJar.putArchiveEntry(je);

            outputJar.write(readContentAsBytes(am.open(TEST_KEY)));
            outputJar.write(signatureBytes);

            copyFiles(manifest, inputJar, outputJar, timestamp);
        } catch (Exception e) {
            Crashlytics.logException(e);
            return false;
        } finally {
            IoUtils.closeQuietly(inputJar);
            IoUtils.closeQuietly(outputJar);
        }
        return true;
    }

    /** Write to another stream and also feed it to the Signature object. */
    private static class SignatureOutputStream extends FilterOutputStream {

        private final Signature signature;

        public SignatureOutputStream(OutputStream out, Signature sig) {
            super(out);
            signature = sig;
        }

        @Override
        public void write(@NonNull byte[] buffer, int offset, int length) throws IOException {
            try {
                signature.update(buffer, offset, length);
            } catch (SignatureException e) {
                e.printStackTrace();
            }
            super.write(buffer, offset, length);
        }

        @Override
        public void write(int oneByte) throws IOException {
            try {
                signature.update((byte) oneByte);
            } catch (SignatureException e) {
                e.printStackTrace();
            }
            super.write(oneByte);
        }
    }

    /** Add the SHA1 of every file to the manifest, creating it if necessary. */
    private static Manifest addDigestsToManifest(final JarFile jar) throws IOException, GeneralSecurityException {
        final Manifest input = jar.getManifest();
        final Manifest output = new Manifest();

        final Attributes main = output.getMainAttributes();
        main.putValue("Manifest-Version", MANIFEST_VERSION);
        main.putValue("Created-By", CREATED_BY);

        // We sort the input entries by name, and add them to the output manifest in sorted order.
        // We expect that the output map will be deterministic.
        final TreeMap<String, JarEntry> byName = new TreeMap<>();
        for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements();) {
            JarEntry entry = e.nextElement();
            byName.put(entry.getName(), entry);
        }

        final MessageDigest md = MessageDigest.getInstance("SHA1");
        final byte[] buffer = new byte[4096];
        int num;

        for (JarEntry entry : byName.values()) {
            final String name = entry.getName();
            if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) && !name.equals(CERT_SF_NAME)
                    && !name.equals(CERT_RSA_NAME)) {
                InputStream data = jar.getInputStream(entry);
                while ((num = data.read(buffer)) > 0) {
                    md.update(buffer, 0, num);
                }

                Attributes attr = null;
                if (input != null) {
                    attr = input.getAttributes(name);
                }
                attr = attr != null ? new Attributes(attr) : new Attributes();
                attr.putValue("SHA1-Digest", base64encode(md.digest()));
                output.getEntries().put(name, attr);
            }
        }

        return output;
    }

    /** Copy all the files in a manifest from input to output. */
    private static void copyFiles(Manifest manifest, JarFile in, JarArchiveOutputStream out, long timestamp)
            throws IOException {
        final byte[] buffer = new byte[4096];
        int num;

        final Map<String, Attributes> entries = manifest.getEntries();
        final List<String> names = new ArrayList<>(entries.keySet());
        Collections.sort(names);
        for (final String name : names) {
            final JarEntry inEntry = in.getJarEntry(name);
            if (inEntry.getMethod() == JarArchiveEntry.STORED) {
                // Preserve the STORED method of the input entry.
                out.putArchiveEntry(new JarArchiveEntry(inEntry));
            } else {
                // Create a new entry so that the compressed len is recomputed.
                final JarArchiveEntry je = new JarArchiveEntry(name);
                je.setTime(timestamp);
                out.putArchiveEntry(je);
            }

            final InputStream data = in.getInputStream(inEntry);
            while ((num = data.read(buffer)) > 0) {
                out.write(buffer, 0, num);
            }
            out.flush();
            out.closeArchiveEntry();
        }
    }

    /**
     * Decrypt an encrypted PKCS 8 format private key.
     *
     * Based on ghstark's post on Aug 6, 2006 at
     * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
     *
     * @param encryptedPrivateKey
     *     The raw data of the private key
     */
    private static KeySpec decryptPrivateKey(final byte[] encryptedPrivateKey) throws GeneralSecurityException {
        EncryptedPrivateKeyInfo epkInfo;
        try {
            epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
        } catch (final IOException ex) {
            // Probably not an encrypted key.
            return null;
        }

        final String pass = "android";
        final char[] password = pass.toCharArray();
        final SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
        final Key key = skFactory.generateSecret(new PBEKeySpec(password));
        final Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
        cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());

        try {
            return epkInfo.getKeySpec(cipher);
        } catch (final InvalidKeySpecException ex) {
            Log.e(TAG, "Password for keyFile may be bad.", ex);
            return null;
        }
    }

    private static byte[] readContentAsBytes(InputStream input) throws IOException {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        final byte[] buffer = new byte[2048];
        int numRead = input.read(buffer);
        while (numRead != -1) {
            baos.write(buffer, 0, numRead);
            numRead = input.read(buffer);
        }
        return baos.toByteArray();
    }

    /** Read a PKCS 8 format private key. */
    private static PrivateKey readPrivateKey(InputStream file) throws IOException, GeneralSecurityException {
        final DataInputStream input = new DataInputStream(file);
        try {
            byte[] bytes = new byte[10000];
            int nBytesTotal = 0, nBytes;
            while ((nBytes = input.read(bytes, nBytesTotal, 10000 - nBytesTotal)) != -1) {
                nBytesTotal += nBytes;
            }

            final byte[] bytes2 = new byte[nBytesTotal];
            System.arraycopy(bytes, 0, bytes2, 0, nBytesTotal);
            bytes = bytes2;

            KeySpec spec = decryptPrivateKey(bytes);
            if (spec == null) {
                spec = new PKCS8EncodedKeySpec(bytes);
            }

            try {
                return KeyFactory.getInstance("RSA").generatePrivate(spec);
            } catch (final InvalidKeySpecException ex) {
                return KeyFactory.getInstance("DSA").generatePrivate(spec);
            }
        } finally {
            input.close();
        }
    }

    private static X509Certificate readPublicKey(InputStream file) throws IOException, GeneralSecurityException {
        try {
            return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(file);
        } finally {
            file.close();
        }
    }

    /**
     * Write a .SF file with a digest the specified manifest.
     */
    private static byte[] writeSignatureFile(Manifest manifest, OutputStream out)
            throws IOException, GeneralSecurityException {
        final Manifest sf = new Manifest();
        final Attributes main = sf.getMainAttributes();
        main.putValue("Manifest-Version", MANIFEST_VERSION);
        main.putValue("Created-By", CREATED_BY);

        final MessageDigest md = MessageDigest.getInstance("SHA1");
        final PrintStream print = new PrintStream(new DigestOutputStream(new ByteArrayOutputStream(), md), true,
                "UTF-8");

        // Digest of the entire manifest
        manifest.write(print);
        print.flush();
        main.putValue("SHA1-Digest-Manifest", base64encode(md.digest()));

        final Map<String, Attributes> entries = manifest.getEntries();
        for (final Map.Entry<String, Attributes> entry : entries.entrySet()) {
            // Digest of the manifest stanza for this entry.
            print.print("Name: " + entry.getKey() + "\r\n");
            for (final Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
                print.print(att.getKey() + ": " + att.getValue() + "\r\n");
            }
            print.print("\r\n");
            print.flush();

            final Attributes sfAttr = new Attributes();
            sfAttr.putValue("SHA1-Digest", base64encode(md.digest()));
            sf.getEntries().put(entry.getKey(), sfAttr);
        }

        final ByteArrayOutputStream sos = new ByteArrayOutputStream();
        sf.write(sos);

        String value = sos.toString();
        String done = value.replace("Manifest-Version", "Signature-Version");

        out.write(done.getBytes());

        print.close();
        sos.close();

        return done.getBytes();
    }

    public static class ZipSignature {

        private static final byte[] ALGORITHM_ID_BYTES = { 0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A,
                0x05, 0x00 };
        private static final byte[] BEFORE_ALGORITHM_ID_BYTES = { 0x30, 0x21 };
        private static final byte[] AFTER_ALGORITHM_ID_BYTES = { 0x04, 0x14 };

        private final Cipher cipher;

        private final MessageDigest digest;

        @SuppressLint("GetInstance")
        public ZipSignature() throws IOException, GeneralSecurityException {
            digest = MessageDigest.getInstance("SHA1");
            cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        }

        public void initSign(final PrivateKey privateKey) throws InvalidKeyException {
            cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        }

        public byte[] sign() throws BadPaddingException, IllegalBlockSizeException {
            cipher.update(BEFORE_ALGORITHM_ID_BYTES);
            cipher.update(ALGORITHM_ID_BYTES);
            cipher.update(AFTER_ALGORITHM_ID_BYTES);
            cipher.update(digest.digest());
            return cipher.doFinal();
        }

        public void update(final byte[] data) {
            digest.update(data);
        }

        public void update(final byte[] data, final int offset, final int count) {
            digest.update(data, offset, count);
        }

    }

}