Android源码-SignApk.java

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

 

posted on 2014-09-07 01:16  洛易  阅读(2989)  评论(0编辑  收藏  举报