/* * Copyright (C) 2008 The Android Open Source Project * * 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.android.signapk;import sun.misc.BASE64Encoder;import sun.security.pkcs.ContentInfo;import sun.security.pkcs.PKCS7;import sun.security.pkcs.SignerInfo;import sun.security.x509.AlgorithmId;import sun.security.x509.X500Name;import java.io.BufferedReader;import java.io.ByteArrayOutputStream;import java.io.DataInputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.FilterOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.OutputStream;import java.io.PrintStream;import java.security.AlgorithmParameters;import java.security.DigestOutputStream;import java.security.GeneralSecurityException;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.Certificate;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.Date;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.JarOutputStream;import java.util.jar.Manifest;import java.util.regex.Pattern;import javax.crypto.Cipher;import javax.crypto.EncryptedPrivateKeyInfo;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.PBEKeySpec;/** * Command line tool to sign JAR files (including APKs and OTA updates) in * a way compatible with the mincrypt verifier, using SHA1 and RSA keys. */class SignApk {    private static final String CERT_SF_NAME = "META-INF/CERT.SF";    private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";    // Files matching this pattern are not copied to the output.    private static Pattern stripPattern =            Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$");    private static X509Certificate readPublicKey(File file)            throws IOException, GeneralSecurityException {        FileInputStream input = new FileInputStream(file);        try {            CertificateFactory cf = CertificateFactory.getInstance("X.509");            return (X509Certificate) cf.generateCertificate(input);        } finally {            input.close();        }    }    /**     * Reads the password from stdin and returns it as a string.     *     * @param keyFile The file containing the private key.  Used to prompt the user.     */    private static String readPassword(File keyFile) {        // TODO: use Console.readPassword() when it's available.        System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");        System.out.flush();        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));        try {            return stdin.readLine();        } catch (IOException ex) {            return null;        }    }    /**     * 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     * @param keyFile The file containing the private key     */    private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)            throws GeneralSecurityException {        EncryptedPrivateKeyInfo epkInfo;        try {            epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);        } catch (IOException ex) {            // Probably not an encrypted key.            return null;        }        char[] password = readPassword(keyFile).toCharArray();        SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());        Key key = skFactory.generateSecret(new PBEKeySpec(password));        Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());        cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());        try {            return epkInfo.getKeySpec(cipher);        } catch (InvalidKeySpecException ex) {            System.err.println("signapk: Password for " + keyFile + " may be bad.");            throw ex;        }    }    /** Read a PKCS 8 format private key. */    private static PrivateKey readPrivateKey(File file)            throws IOException, GeneralSecurityException {        DataInputStream input = new DataInputStream(new FileInputStream(file));        try {            byte[] bytes = new byte[(int) file.length()];            input.read(bytes);            KeySpec spec = decryptPrivateKey(bytes, file);            if (spec == null) {                spec = new PKCS8EncodedKeySpec(bytes);            }            try {                return KeyFactory.getInstance("RSA").generatePrivate(spec);            } catch (InvalidKeySpecException ex) {                return KeyFactory.getInstance("DSA").generatePrivate(spec);            }        } finally {            input.close();        }    }    /** Add the SHA1 of every file to the manifest, creating it if necessary. */    private static Manifest addDigestsToManifest(JarFile jar)            throws IOException, GeneralSecurityException {        Manifest input = jar.getManifest();        Manifest output = new Manifest();        Attributes main = output.getMainAttributes();        if (input != null) {            main.putAll(input.getMainAttributes());        } else {            main.putValue("Manifest-Version", "1.0");            main.putValue("Created-By", "1.0 (Android SignApk)");        }        BASE64Encoder base64 = new BASE64Encoder();        MessageDigest md = MessageDigest.getInstance("SHA1");        byte[] buffer = new byte[4096];        int num;        // 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.        TreeMap byName = new TreeMap();        for (Enumeration e = jar.entries(); e.hasMoreElements(); ) {            JarEntry entry = e.nextElement();            byName.put(entry.getName(), entry);        }        for (JarEntry entry: byName.values()) {            String name = entry.getName();            if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&                !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&                (stripPattern == null ||                 !stripPattern.matcher(name).matches())) {                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", base64.encode(md.digest()));                output.getEntries().put(name, attr);            }        }        return output;    }    /** Write to another stream and also feed it to the Signature object. */    private static class SignatureOutputStream extends FilterOutputStream {        private Signature mSignature;        private int mCount;        public SignatureOutputStream(OutputStream out, Signature sig) {            super(out);            mSignature = sig;            mCount = 0;        }        @Override        public void write(int b) throws IOException {            try {                mSignature.update((byte) b);            } catch (SignatureException e) {                throw new IOException("SignatureException: " + e);            }            super.write(b);            mCount++;        }        @Override        public void write(byte[] b, int off, int len) throws IOException {            try {                mSignature.update(b, off, len);            } catch (SignatureException e) {                throw new IOException("SignatureException: " + e);            }            super.write(b, off, len);            mCount += len;        }        public int size() {            return mCount;        }    }    /** Write a .SF file with a digest of the specified manifest. */    private static void writeSignatureFile(Manifest manifest, SignatureOutputStream out)            throws IOException, GeneralSecurityException {        Manifest sf = new Manifest();        Attributes main = sf.getMainAttributes();        main.putValue("Signature-Version", "1.0");        main.putValue("Created-By", "1.0 (Android SignApk)");        BASE64Encoder base64 = new BASE64Encoder();        MessageDigest md = MessageDigest.getInstance("SHA1");        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", base64.encode(md.digest()));        Map entries = manifest.getEntries();        for (Map.Entry entry : entries.entrySet()) {            // Digest of the manifest stanza for this entry.            print.print("Name: " + entry.getKey() + "\r\n");            for (Map.Entry att : entry.getValue().entrySet()) {                print.print(att.getKey() + ": " + att.getValue() + "\r\n");            }            print.print("\r\n");            print.flush();            Attributes sfAttr = new Attributes();            sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));            sf.getEntries().put(entry.getKey(), sfAttr);        }        sf.write(out);        // A bug in the java.util.jar implementation of Android platforms        // up to version 1.6 will cause a spurious IOException to be thrown        // if the length of the signature file is a multiple of 1024 bytes.        // As a workaround, add an extra CRLF in this case.        if ((out.size() % 1024) == 0) {            out.write('\r');            out.write('\n');        }    }    /** Write a .RSA file with a digital signature. */    private static void writeSignatureBlock(            Signature signature, X509Certificate publicKey, OutputStream out)            throws IOException, GeneralSecurityException {        SignerInfo signerInfo = new SignerInfo(                new X500Name(publicKey.getIssuerX500Principal().getName()),                publicKey.getSerialNumber(),                AlgorithmId.get("SHA1"),                AlgorithmId.get("RSA"),                signature.sign());        PKCS7 pkcs7 = new PKCS7(                new AlgorithmId[] { AlgorithmId.get("SHA1") },                new ContentInfo(ContentInfo.DATA_OID, null),                new X509Certificate[] { publicKey },                new SignerInfo[] { signerInfo });        pkcs7.encodeSignedData(out);    }    private static void signWholeOutputFile(byte[] zipData,                                            OutputStream outputStream,                                            X509Certificate publicKey,                                            PrivateKey privateKey)        throws IOException, GeneralSecurityException {        // For a zip with no archive comment, the        // end-of-central-directory record will be 22 bytes long, so        // we expect to find the EOCD marker 22 bytes from the end.        if (zipData[zipData.length-22] != 0x50 ||            zipData[zipData.length-21] != 0x4b ||            zipData[zipData.length-20] != 0x05 ||            zipData[zipData.length-19] != 0x06) {            throw new IllegalArgumentException("zip data already has an archive comment");        }        Signature signature = Signature.getInstance("SHA1withRSA");        signature.initSign(privateKey);        signature.update(zipData, 0, zipData.length-2);        ByteArrayOutputStream temp = new ByteArrayOutputStream();        // put a readable message and a null char at the start of the        // archive comment, so that tools that display the comment        // (hopefully) show something sensible.        // TODO: anything more useful we can put in this message?        byte[] message = "signed by SignApk".getBytes("UTF-8");        temp.write(message);        temp.write(0);        writeSignatureBlock(signature, publicKey, temp);        int total_size = temp.size() + 6;        if (total_size > 0xffff) {            throw new IllegalArgumentException("signature is too big for ZIP file comment");        }        // signature starts this many bytes from the end of the file        int signature_start = total_size - message.length - 1;        temp.write(signature_start & 0xff);        temp.write((signature_start >> 8) & 0xff);        // Why the 0xff bytes?  In a zip file with no archive comment,        // bytes [-6:-2] of the file are the little-endian offset from        // the start of the file to the central directory.  So for the        // two high bytes to be 0xff 0xff, the archive would have to        // be nearly 4GB in side.  So it's unlikely that a real        // commentless archive would have 0xffs here, and lets us tell        // an old signed archive from a new one.        temp.write(0xff);        temp.write(0xff);        temp.write(total_size & 0xff);        temp.write((total_size >> 8) & 0xff);        temp.flush();        // Signature verification checks that the EOCD header is the        // last such sequence in the file (to avoid minzip finding a        // fake EOCD appended after the signature in its scan).  The        // odds of producing this sequence by chance are very low, but        // let's catch it here if it does.        byte[] b = temp.toByteArray();        for (int i = 0; i < b.length-3; ++i) {            if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {                throw new IllegalArgumentException("found spurious EOCD header at " + i);            }        }        outputStream.write(zipData, 0, zipData.length-2);        outputStream.write(total_size & 0xff);        outputStream.write((total_size >> 8) & 0xff);        temp.writeTo(outputStream);    }    /**     * Copy all the files in a manifest from input to output.  We set     * the modification times in the output to a fixed time, so as to     * reduce variation in the output file and make incremental OTAs     * more efficient.     */    private static void copyFiles(Manifest manifest,        JarFile in, JarOutputStream out, long timestamp) throws IOException {        byte[] buffer = new byte[4096];        int num;        Map entries = manifest.getEntries();        List names = new ArrayList(entries.keySet());        Collections.sort(names);        for (String name : names) {            JarEntry inEntry = in.getJarEntry(name);            JarEntry outEntry = null;            if (inEntry.getMethod() == JarEntry.STORED) {                // Preserve the STORED method of the input entry.                outEntry = new JarEntry(inEntry);            } else {                // Create a new entry so that the compressed len is recomputed.                outEntry = new JarEntry(name);            }            outEntry.setTime(timestamp);            out.putNextEntry(outEntry);            InputStream data = in.getInputStream(inEntry);            while ((num = data.read(buffer)) > 0) {                out.write(buffer, 0, num);            }            out.flush();        }    }    public static void main(String[] args) {        if (args.length != 4 && args.length != 5) {            System.err.println("Usage: signapk [-w] " +                    "publickey.x509[.pem] privatekey.pk8 " +                    "input.jar output.jar");            System.exit(2);        }        boolean signWholeFile = false;        int argstart = 0;        if (args[0].equals("-w")) {            signWholeFile = true;            argstart = 1;        }        JarFile inputJar = null;        JarOutputStream outputJar = null;        FileOutputStream outputFile = null;        try {            X509Certificate publicKey = readPublicKey(new File(args[argstart+0]));            // Assume the certificate is valid for at least an hour.            long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;            PrivateKey privateKey = readPrivateKey(new File(args[argstart+1]));            inputJar = new JarFile(new File(args[argstart+2]), false);  // Don't verify.            OutputStream outputStream = null;            if (signWholeFile) {                outputStream = new ByteArrayOutputStream();            } else {                outputStream = outputFile = new FileOutputStream(args[argstart+3]);            }            outputJar = new JarOutputStream(outputStream);            outputJar.setLevel(9);            JarEntry je;            // MANIFEST.MF            Manifest manifest = addDigestsToManifest(inputJar);            je = new JarEntry(JarFile.MANIFEST_NAME);            je.setTime(timestamp);            outputJar.putNextEntry(je);            manifest.write(outputJar);            // CERT.SF            Signature signature = Signature.getInstance("SHA1withRSA");            signature.initSign(privateKey);            je = new JarEntry(CERT_SF_NAME);            je.setTime(timestamp);            outputJar.putNextEntry(je);            writeSignatureFile(manifest,                    new SignatureOutputStream(outputJar, signature));            // CERT.RSA            je = new JarEntry(CERT_RSA_NAME);            je.setTime(timestamp);            outputJar.putNextEntry(je);            writeSignatureBlock(signature, publicKey, outputJar);            // Everything else            copyFiles(manifest, inputJar, outputJar, timestamp);            outputJar.close();            outputJar = null;            outputStream.flush();            if (signWholeFile) {                outputFile = new FileOutputStream(args[argstart+3]);                signWholeOutputFile(((ByteArrayOutputStream)outputStream).toByteArray(),                                    outputFile, publicKey, privateKey);            }        } catch (Exception e) {            e.printStackTrace();            System.exit(1);        } finally {            try {                if (inputJar != null) inputJar.close();                if (outputFile != null) outputFile.close();            } catch (IOException e) {                e.printStackTrace();                System.exit(1);            }        }    }}

更多相关文章

  1. 代码中设置drawableleft
  2. android 3.0 隐藏 系统标题栏
  3. Android开发中activity切换动画的实现
  4. Android(安卓)学习 笔记_05. 文件下载
  5. Android中直播视频技术探究之—摄像头Camera视频源数据采集解析
  6. 技术博客汇总
  7. android 2.3 wifi (一)
  8. AndRoid Notification的清空和修改
  9. Android中的Chronometer

随机推荐

  1. 浅谈MySQL 统计行数的 count
  2. 快速学习MySQL基础知识
  3. 简单了解MySQL存储引擎
  4. MySQL中SQL分页查询的几种实现方法及优缺
  5. 解决Navicat导入数据库数据结构sql报错da
  6. 浅谈mysql导出表数据到excel关于datetime
  7. 阿里云centos7中安装MySQL8.0.13的方法步
  8. 快速解决mysql导出scv文件乱码、蹿行的问
  9. 快速解决mysql导数据时,格式不对、导入慢
  10. Mysql日期查询的详细介绍