SignApk.java revision b6c1cf6de79035f58b512f4400db458c8401379a
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.signapk;
18
19import sun.misc.BASE64Encoder;
20import sun.security.pkcs.ContentInfo;
21import sun.security.pkcs.PKCS7;
22import sun.security.pkcs.SignerInfo;
23import sun.security.x509.AlgorithmId;
24import sun.security.x509.X500Name;
25
26import java.io.BufferedReader;
27import java.io.ByteArrayOutputStream;
28import java.io.DataInputStream;
29import java.io.File;
30import java.io.FileInputStream;
31import java.io.FileOutputStream;
32import java.io.FilterOutputStream;
33import java.io.IOException;
34import java.io.InputStream;
35import java.io.InputStreamReader;
36import java.io.OutputStream;
37import java.io.PrintStream;
38import java.security.AlgorithmParameters;
39import java.security.DigestOutputStream;
40import java.security.GeneralSecurityException;
41import java.security.KeyFactory;
42import java.security.MessageDigest;
43import java.security.PrivateKey;
44import java.security.Signature;
45import java.security.SignatureException;
46import java.security.cert.Certificate;
47import java.security.cert.CertificateFactory;
48import java.security.cert.X509Certificate;
49import java.security.Key;
50import java.security.spec.InvalidKeySpecException;
51import java.security.spec.KeySpec;
52import java.security.spec.PKCS8EncodedKeySpec;
53import java.util.Enumeration;
54import java.util.Map;
55import java.util.jar.Attributes;
56import java.util.jar.JarEntry;
57import java.util.jar.JarFile;
58import java.util.jar.JarOutputStream;
59import java.util.jar.Manifest;
60import javax.crypto.Cipher;
61import javax.crypto.EncryptedPrivateKeyInfo;
62import javax.crypto.SecretKeyFactory;
63import javax.crypto.spec.PBEKeySpec;
64
65/**
66 * Command line tool to sign JAR files (including APKs and OTA updates) in
67 * a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
68 */
69class SignApk {
70    private static X509Certificate readPublicKey(File file)
71            throws IOException, GeneralSecurityException {
72        FileInputStream input = new FileInputStream(file);
73        try {
74            CertificateFactory cf = CertificateFactory.getInstance("X.509");
75            return (X509Certificate) cf.generateCertificate(input);
76        } finally {
77            input.close();
78        }
79    }
80
81    /**
82     * Reads the password from stdin and returns it as a string.
83     *
84     * @param keyFile The file containing the private key.  Used to prompt the user.
85     */
86    private static String readPassword(File keyFile) {
87        // TODO: use Console.readPassword() when it's available.
88        System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
89        System.out.flush();
90        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
91        try {
92            return stdin.readLine();
93        } catch (IOException ex) {
94            return null;
95        }
96    }
97
98    /**
99     * Decrypt an encrypted PKCS 8 format private key.
100     *
101     * Based on ghstark's post on Aug 6, 2006 at
102     * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
103     *
104     * @param encryptedPrivateKey The raw data of the private key
105     * @param keyFile The file containing the private key
106     */
107    private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
108            throws GeneralSecurityException {
109        EncryptedPrivateKeyInfo epkInfo;
110        try {
111            epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
112        } catch (IOException ex) {
113            // Probably not an encrypted key.
114            return null;
115        }
116
117        char[] password = readPassword(keyFile).toCharArray();
118
119        SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
120        Key key = skFactory.generateSecret(new PBEKeySpec(password));
121
122        Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
123        cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
124
125        try {
126            return epkInfo.getKeySpec(cipher);
127        } catch (InvalidKeySpecException ex) {
128            System.err.println("signapk: Password for " + keyFile + " may be bad.");
129            throw ex;
130        }
131    }
132
133    /** Read a PKCS 8 format private key. */
134    private static PrivateKey readPrivateKey(File file)
135            throws IOException, GeneralSecurityException {
136        DataInputStream input = new DataInputStream(new FileInputStream(file));
137        try {
138            byte[] bytes = new byte[(int) file.length()];
139            input.read(bytes);
140
141            KeySpec spec = decryptPrivateKey(bytes, file);
142            if (spec == null) {
143                spec = new PKCS8EncodedKeySpec(bytes);
144            }
145
146            try {
147                return KeyFactory.getInstance("RSA").generatePrivate(spec);
148            } catch (InvalidKeySpecException ex) {
149                return KeyFactory.getInstance("DSA").generatePrivate(spec);
150            }
151        } finally {
152            input.close();
153        }
154    }
155
156    /** Add the SHA1 of every file to the manifest, creating it if necessary. */
157    private static Manifest addDigestsToManifest(JarFile jar)
158            throws IOException, GeneralSecurityException {
159        Manifest input = jar.getManifest();
160        Manifest output = new Manifest();
161        Attributes main = output.getMainAttributes();
162        if (input != null) {
163            main.putAll(input.getMainAttributes());
164        } else {
165            main.putValue("Manifest-Version", "1.0");
166            main.putValue("Created-By", "1.0 (Android SignApk)");
167        }
168
169        BASE64Encoder base64 = new BASE64Encoder();
170        MessageDigest md = MessageDigest.getInstance("SHA1");
171        byte[] buffer = new byte[4096];
172        int num;
173
174        for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
175            JarEntry entry = e.nextElement();
176            String name = entry.getName();
177            if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME)) {
178                InputStream data = jar.getInputStream(entry);
179                while ((num = data.read(buffer)) > 0) {
180                    md.update(buffer, 0, num);
181                }
182
183                Attributes attr = null;
184                if (input != null) attr = input.getAttributes(name);
185                attr = attr != null ? new Attributes(attr) : new Attributes();
186                attr.putValue("SHA1-Digest", base64.encode(md.digest()));
187                output.getEntries().put(name, attr);
188            }
189        }
190
191        return output;
192    }
193
194    /** Write to another stream and also feed it to the Signature object. */
195    private static class SignatureOutputStream extends FilterOutputStream {
196        private Signature mSignature;
197
198        public SignatureOutputStream(OutputStream out, Signature sig) {
199            super(out);
200            mSignature = sig;
201        }
202
203        @Override
204        public void write(int b) throws IOException {
205            try {
206                mSignature.update((byte) b);
207            } catch (SignatureException e) {
208                throw new IOException("SignatureException: " + e);
209            }
210            super.write(b);
211        }
212
213        @Override
214        public void write(byte[] b, int off, int len) throws IOException {
215            try {
216                mSignature.update(b, off, len);
217            } catch (SignatureException e) {
218                throw new IOException("SignatureException: " + e);
219            }
220            super.write(b, off, len);
221        }
222    }
223
224    /** Write a .SF file with a digest the specified manifest. */
225    private static void writeSignatureFile(Manifest manifest, OutputStream out)
226            throws IOException, GeneralSecurityException {
227        Manifest sf = new Manifest();
228        Attributes main = sf.getMainAttributes();
229        main.putValue("Signature-Version", "1.0");
230        main.putValue("Created-By", "1.0 (Android SignApk)");
231
232        BASE64Encoder base64 = new BASE64Encoder();
233        MessageDigest md = MessageDigest.getInstance("SHA1");
234        PrintStream print = new PrintStream(
235                new DigestOutputStream(new ByteArrayOutputStream(), md),
236                true, "UTF-8");
237
238        // Digest of the entire manifest
239        manifest.write(print);
240        print.flush();
241        main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest()));
242
243        Map<String, Attributes> entries = manifest.getEntries();
244        for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
245            // Digest of the manifest stanza for this entry.
246            print.print("Name: " + entry.getKey() + "\r\n");
247            for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
248                print.print(att.getKey() + ": " + att.getValue() + "\r\n");
249            }
250            print.print("\r\n");
251            print.flush();
252
253            Attributes sfAttr = new Attributes();
254            sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
255            sf.getEntries().put(entry.getKey(), sfAttr);
256        }
257
258        sf.write(out);
259    }
260
261    /** Write a .RSA file with a digital signature. */
262    private static void writeSignatureBlock(
263            Signature signature, X509Certificate publicKey, OutputStream out)
264            throws IOException, GeneralSecurityException {
265        SignerInfo signerInfo = new SignerInfo(
266                new X500Name(publicKey.getIssuerX500Principal().getName()),
267                publicKey.getSerialNumber(),
268                AlgorithmId.get("SHA1"),
269                AlgorithmId.get("RSA"),
270                signature.sign());
271
272        PKCS7 pkcs7 = new PKCS7(
273                new AlgorithmId[] { AlgorithmId.get("SHA1") },
274                new ContentInfo(ContentInfo.DATA_OID, null),
275                new X509Certificate[] { publicKey },
276                new SignerInfo[] { signerInfo });
277
278        pkcs7.encodeSignedData(out);
279    }
280
281    /** Copy all the files in a manifest from input to output. */
282    private static void copyFiles(Manifest manifest,
283            JarFile in, JarOutputStream out) throws IOException {
284        byte[] buffer = new byte[4096];
285        int num;
286
287        Map<String, Attributes> entries = manifest.getEntries();
288        for (String name : entries.keySet()) {
289            JarEntry inEntry = in.getJarEntry(name);
290            if (inEntry.getMethod() == JarEntry.STORED) {
291                // Preserve the STORED method of the input entry.
292                out.putNextEntry(new JarEntry(inEntry));
293            } else {
294                // Create a new entry so that the compressed len is recomputed.
295                out.putNextEntry(new JarEntry(name));
296            }
297
298            InputStream data = in.getInputStream(inEntry);
299            while ((num = data.read(buffer)) > 0) {
300                out.write(buffer, 0, num);
301            }
302            out.flush();
303        }
304    }
305
306    public static void main(String[] args) {
307        if (args.length != 4) {
308            System.err.println("Usage: signapk " +
309                    "publickey.x509[.pem] privatekey.pk8 " +
310                    "input.jar output.jar");
311            System.exit(2);
312        }
313
314        JarFile inputJar = null;
315        JarOutputStream outputJar = null;
316
317        try {
318            X509Certificate publicKey = readPublicKey(new File(args[0]));
319            PrivateKey privateKey = readPrivateKey(new File(args[1]));
320            inputJar = new JarFile(new File(args[2]), false);  // Don't verify.
321            outputJar = new JarOutputStream(new FileOutputStream(args[3]));
322            outputJar.setLevel(9);
323
324            // MANIFEST.MF
325            Manifest manifest = addDigestsToManifest(inputJar);
326            manifest.getEntries().remove("META-INF/CERT.SF");
327            manifest.getEntries().remove("META-INF/CERT.RSA");
328            outputJar.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME));
329            manifest.write(outputJar);
330
331            // CERT.SF
332            Signature signature = Signature.getInstance("SHA1withRSA");
333            signature.initSign(privateKey);
334            outputJar.putNextEntry(new JarEntry("META-INF/CERT.SF"));
335            writeSignatureFile(manifest,
336                    new SignatureOutputStream(outputJar, signature));
337
338            // CERT.RSA
339            outputJar.putNextEntry(new JarEntry("META-INF/CERT.RSA"));
340            writeSignatureBlock(signature, publicKey, outputJar);
341
342            // Everything else
343            copyFiles(manifest, inputJar, outputJar);
344        } catch (Exception e) {
345            e.printStackTrace();
346            System.exit(1);
347        } finally {
348            try {
349                if (inputJar != null) inputJar.close();
350                if (outputJar != null) outputJar.close();
351            } catch (IOException e) {
352                e.printStackTrace();
353                System.exit(1);
354            }
355        }
356    }
357}
358