blob: 3ddab11b802ad6d49a990b95d1ad03739ed06067 [file] [log] [blame]
The Android Open Source Project88b60792009-03-03 19:28:42 -08001/*
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
Doug Zongker147626e2012-09-04 13:32:13 -070019import org.bouncycastle.asn1.ASN1InputStream;
20import org.bouncycastle.asn1.ASN1ObjectIdentifier;
21import org.bouncycastle.asn1.DEROutputStream;
22import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
Kenny Root62ea4a52013-09-25 09:59:10 -070023import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
Doug Zongker147626e2012-09-04 13:32:13 -070024import org.bouncycastle.cert.jcajce.JcaCertStore;
25import org.bouncycastle.cms.CMSException;
26import org.bouncycastle.cms.CMSProcessableByteArray;
27import org.bouncycastle.cms.CMSSignedData;
28import org.bouncycastle.cms.CMSSignedDataGenerator;
29import org.bouncycastle.cms.CMSTypedData;
30import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
31import org.bouncycastle.jce.provider.BouncyCastleProvider;
32import org.bouncycastle.operator.ContentSigner;
33import org.bouncycastle.operator.OperatorCreationException;
34import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
35import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
36import org.bouncycastle.util.encoders.Base64;
The Android Open Source Project88b60792009-03-03 19:28:42 -080037
zhang jun22717f92014-07-10 16:34:57 +080038import java.io.Console;
The Android Open Source Project88b60792009-03-03 19:28:42 -080039import java.io.BufferedReader;
Kenny Root62ea4a52013-09-25 09:59:10 -070040import java.io.ByteArrayInputStream;
The Android Open Source Project88b60792009-03-03 19:28:42 -080041import java.io.ByteArrayOutputStream;
42import java.io.DataInputStream;
43import java.io.File;
44import java.io.FileInputStream;
45import java.io.FileOutputStream;
46import java.io.FilterOutputStream;
47import java.io.IOException;
48import java.io.InputStream;
49import java.io.InputStreamReader;
50import java.io.OutputStream;
51import java.io.PrintStream;
Kenny Root89c961a2013-09-25 11:14:33 -070052import java.lang.reflect.Constructor;
The Android Open Source Project88b60792009-03-03 19:28:42 -080053import java.security.DigestOutputStream;
54import java.security.GeneralSecurityException;
55import java.security.Key;
56import java.security.KeyFactory;
57import java.security.MessageDigest;
58import java.security.PrivateKey;
Doug Zongker147626e2012-09-04 13:32:13 -070059import java.security.Provider;
60import java.security.Security;
61import java.security.cert.CertificateEncodingException;
The Android Open Source Project88b60792009-03-03 19:28:42 -080062import java.security.cert.CertificateFactory;
63import java.security.cert.X509Certificate;
64import java.security.spec.InvalidKeySpecException;
The Android Open Source Project88b60792009-03-03 19:28:42 -080065import java.security.spec.PKCS8EncodedKeySpec;
66import java.util.ArrayList;
67import java.util.Collections;
The Android Open Source Project88b60792009-03-03 19:28:42 -080068import java.util.Enumeration;
Kenny Root3d2365c2013-09-19 12:49:36 -070069import java.util.Locale;
The Android Open Source Project88b60792009-03-03 19:28:42 -080070import java.util.Map;
71import java.util.TreeMap;
72import java.util.jar.Attributes;
73import java.util.jar.JarEntry;
74import java.util.jar.JarFile;
75import java.util.jar.JarOutputStream;
76import java.util.jar.Manifest;
Doug Zongkeraf482b62009-06-08 10:46:55 -070077import java.util.regex.Pattern;
The Android Open Source Project88b60792009-03-03 19:28:42 -080078import javax.crypto.Cipher;
79import javax.crypto.EncryptedPrivateKeyInfo;
80import javax.crypto.SecretKeyFactory;
81import javax.crypto.spec.PBEKeySpec;
82
83/**
Doug Zongker8562fd42013-04-10 09:19:32 -070084 * HISTORICAL NOTE:
85 *
86 * Prior to the keylimepie release, SignApk ignored the signature
87 * algorithm specified in the certificate and always used SHA1withRSA.
88 *
Kenny Root3d2365c2013-09-19 12:49:36 -070089 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use
90 * the signature algorithm in the certificate to select which to use
91 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported.
Doug Zongker8562fd42013-04-10 09:19:32 -070092 *
93 * Because there are old keys still in use whose certificate actually
94 * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
95 * for compatibility with older releases. This can be changed by
96 * altering the getAlgorithm() function below.
97 */
98
99
100/**
Kenny Root3d2365c2013-09-19 12:49:36 -0700101 * Command line tool to sign JAR files (including APKs and OTA updates) in a way
102 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or
103 * SHA-256 (see historical note).
The Android Open Source Project88b60792009-03-03 19:28:42 -0800104 */
105class SignApk {
106 private static final String CERT_SF_NAME = "META-INF/CERT.SF";
Kenny Root3d2365c2013-09-19 12:49:36 -0700107 private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
Doug Zongkerb14c9762012-10-15 17:10:13 -0700108 private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF";
Kenny Root3d2365c2013-09-19 12:49:36 -0700109 private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s";
The Android Open Source Project88b60792009-03-03 19:28:42 -0800110
Doug Zongker7bb04232012-05-11 09:20:50 -0700111 private static final String OTACERT_NAME = "META-INF/com/android/otacert";
112
Doug Zongker147626e2012-09-04 13:32:13 -0700113 private static Provider sBouncyCastleProvider;
114
Doug Zongker8562fd42013-04-10 09:19:32 -0700115 // bitmasks for which hash algorithms we need the manifest to include.
116 private static final int USE_SHA1 = 1;
117 private static final int USE_SHA256 = 2;
118
119 /**
120 * Return one of USE_SHA1 or USE_SHA256 according to the signature
121 * algorithm specified in the cert.
122 */
Kenny Root3d2365c2013-09-19 12:49:36 -0700123 private static int getDigestAlgorithm(X509Certificate cert) {
124 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
125 if ("SHA1WITHRSA".equals(sigAlg) ||
126 "MD5WITHRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above.
Doug Zongker8562fd42013-04-10 09:19:32 -0700127 return USE_SHA1;
Kenny Root3d2365c2013-09-19 12:49:36 -0700128 } else if (sigAlg.startsWith("SHA256WITH")) {
Doug Zongker8562fd42013-04-10 09:19:32 -0700129 return USE_SHA256;
130 } else {
131 throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
132 "\" in cert [" + cert.getSubjectDN());
133 }
134 }
135
Kenny Root3d2365c2013-09-19 12:49:36 -0700136 /** Returns the expected signature algorithm for this key type. */
137 private static String getSignatureAlgorithm(X509Certificate cert) {
138 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
139 String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
140 if ("RSA".equalsIgnoreCase(keyType)) {
141 if (getDigestAlgorithm(cert) == USE_SHA256) {
142 return "SHA256withRSA";
143 } else {
144 return "SHA1withRSA";
145 }
146 } else if ("EC".equalsIgnoreCase(keyType)) {
147 return "SHA256withECDSA";
148 } else {
149 throw new IllegalArgumentException("unsupported key type: " + keyType);
150 }
151 }
152
Doug Zongkeraf482b62009-06-08 10:46:55 -0700153 // Files matching this pattern are not copied to the output.
154 private static Pattern stripPattern =
Kenny Root3d2365c2013-09-19 12:49:36 -0700155 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
Doug Zongkerb14c9762012-10-15 17:10:13 -0700156 Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
Doug Zongkeraf482b62009-06-08 10:46:55 -0700157
The Android Open Source Project88b60792009-03-03 19:28:42 -0800158 private static X509Certificate readPublicKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800159 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800160 FileInputStream input = new FileInputStream(file);
161 try {
162 CertificateFactory cf = CertificateFactory.getInstance("X.509");
163 return (X509Certificate) cf.generateCertificate(input);
164 } finally {
165 input.close();
166 }
167 }
168
169 /**
adattatr50c7c5a2015-09-03 11:17:57 -0700170 * If a console doesn't exist, reads the password from stdin
171 * If a console exists, reads the password from console and returns it as a string.
The Android Open Source Project88b60792009-03-03 19:28:42 -0800172 *
173 * @param keyFile The file containing the private key. Used to prompt the user.
174 */
175 private static String readPassword(File keyFile) {
zhang jun22717f92014-07-10 16:34:57 +0800176 Console console;
177 char[] pwd;
adattatr50c7c5a2015-09-03 11:17:57 -0700178 if ((console = System.console()) == null) {
179 System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
180 System.out.flush();
181 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
182 try {
183 return stdin.readLine();
184 } catch (IOException ex) {
185 return null;
186 }
zhang jun22717f92014-07-10 16:34:57 +0800187 } else {
adattatr50c7c5a2015-09-03 11:17:57 -0700188 if ((pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null) {
189 return String.valueOf(pwd);
190 } else {
191 return null;
192 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800193 }
194 }
195
196 /**
Kenny Root62ea4a52013-09-25 09:59:10 -0700197 * Decrypt an encrypted PKCS#8 format private key.
The Android Open Source Project88b60792009-03-03 19:28:42 -0800198 *
199 * Based on ghstark's post on Aug 6, 2006 at
200 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
201 *
202 * @param encryptedPrivateKey The raw data of the private key
203 * @param keyFile The file containing the private key
204 */
Kenny Root62ea4a52013-09-25 09:59:10 -0700205 private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
Koushik Dutta29706d12012-12-17 22:25:22 -0800206 throws GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800207 EncryptedPrivateKeyInfo epkInfo;
208 try {
209 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
210 } catch (IOException ex) {
211 // Probably not an encrypted key.
212 return null;
213 }
214
215 char[] password = readPassword(keyFile).toCharArray();
216
217 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
218 Key key = skFactory.generateSecret(new PBEKeySpec(password));
219
220 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
221 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
222
223 try {
224 return epkInfo.getKeySpec(cipher);
225 } catch (InvalidKeySpecException ex) {
226 System.err.println("signapk: Password for " + keyFile + " may be bad.");
227 throw ex;
228 }
229 }
230
Kenny Root62ea4a52013-09-25 09:59:10 -0700231 /** Read a PKCS#8 format private key. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800232 private static PrivateKey readPrivateKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800233 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800234 DataInputStream input = new DataInputStream(new FileInputStream(file));
235 try {
236 byte[] bytes = new byte[(int) file.length()];
237 input.read(bytes);
238
Kenny Root62ea4a52013-09-25 09:59:10 -0700239 /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
240 PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800241 if (spec == null) {
242 spec = new PKCS8EncodedKeySpec(bytes);
243 }
244
Kenny Root62ea4a52013-09-25 09:59:10 -0700245 /*
246 * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
247 * OID and use that to construct a KeyFactory.
248 */
249 ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()));
250 PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
251 String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
Kenny Root3d2365c2013-09-19 12:49:36 -0700252
Kenny Root62ea4a52013-09-25 09:59:10 -0700253 return KeyFactory.getInstance(algOid).generatePrivate(spec);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800254 } finally {
255 input.close();
256 }
257 }
258
Doug Zongker8562fd42013-04-10 09:19:32 -0700259 /**
260 * Add the hash(es) of every file to the manifest, creating it if
261 * necessary.
262 */
263 private static Manifest addDigestsToManifest(JarFile jar, int hashes)
Koushik Dutta29706d12012-12-17 22:25:22 -0800264 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800265 Manifest input = jar.getManifest();
266 Manifest output = new Manifest();
267 Attributes main = output.getMainAttributes();
268 if (input != null) {
269 main.putAll(input.getMainAttributes());
270 } else {
271 main.putValue("Manifest-Version", "1.0");
272 main.putValue("Created-By", "1.0 (Android SignApk)");
273 }
274
Doug Zongker8562fd42013-04-10 09:19:32 -0700275 MessageDigest md_sha1 = null;
276 MessageDigest md_sha256 = null;
277 if ((hashes & USE_SHA1) != 0) {
278 md_sha1 = MessageDigest.getInstance("SHA1");
279 }
280 if ((hashes & USE_SHA256) != 0) {
281 md_sha256 = MessageDigest.getInstance("SHA256");
282 }
283
The Android Open Source Project88b60792009-03-03 19:28:42 -0800284 byte[] buffer = new byte[4096];
285 int num;
286
287 // We sort the input entries by name, and add them to the
288 // output manifest in sorted order. We expect that the output
289 // map will be deterministic.
290
291 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
292
293 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
294 JarEntry entry = e.nextElement();
295 byName.put(entry.getName(), entry);
296 }
297
298 for (JarEntry entry: byName.values()) {
299 String name = entry.getName();
Doug Zongkerb14c9762012-10-15 17:10:13 -0700300 if (!entry.isDirectory() &&
301 (stripPattern == null || !stripPattern.matcher(name).matches())) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800302 InputStream data = jar.getInputStream(entry);
303 while ((num = data.read(buffer)) > 0) {
Doug Zongker8562fd42013-04-10 09:19:32 -0700304 if (md_sha1 != null) md_sha1.update(buffer, 0, num);
305 if (md_sha256 != null) md_sha256.update(buffer, 0, num);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800306 }
307
308 Attributes attr = null;
309 if (input != null) attr = input.getAttributes(name);
310 attr = attr != null ? new Attributes(attr) : new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700311 if (md_sha1 != null) {
312 attr.putValue("SHA1-Digest",
313 new String(Base64.encode(md_sha1.digest()), "ASCII"));
314 }
315 if (md_sha256 != null) {
316 attr.putValue("SHA-256-Digest",
317 new String(Base64.encode(md_sha256.digest()), "ASCII"));
318 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800319 output.getEntries().put(name, attr);
320 }
321 }
322
323 return output;
324 }
325
Doug Zongker7bb04232012-05-11 09:20:50 -0700326 /**
327 * Add a copy of the public key to the archive; this should
328 * exactly match one of the files in
329 * /system/etc/security/otacerts.zip on the device. (The same
330 * cert can be extracted from the CERT.RSA file but this is much
331 * easier to get at.)
332 */
333 private static void addOtacert(JarOutputStream outputJar,
334 File publicKeyFile,
335 long timestamp,
Doug Zongker8562fd42013-04-10 09:19:32 -0700336 Manifest manifest,
337 int hash)
Doug Zongker7bb04232012-05-11 09:20:50 -0700338 throws IOException, GeneralSecurityException {
Doug Zongker8562fd42013-04-10 09:19:32 -0700339 MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
Doug Zongker7bb04232012-05-11 09:20:50 -0700340
341 JarEntry je = new JarEntry(OTACERT_NAME);
342 je.setTime(timestamp);
343 outputJar.putNextEntry(je);
344 FileInputStream input = new FileInputStream(publicKeyFile);
345 byte[] b = new byte[4096];
346 int read;
347 while ((read = input.read(b)) != -1) {
348 outputJar.write(b, 0, read);
349 md.update(b, 0, read);
350 }
351 input.close();
352
353 Attributes attr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700354 attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
Doug Zongker147626e2012-09-04 13:32:13 -0700355 new String(Base64.encode(md.digest()), "ASCII"));
Doug Zongker7bb04232012-05-11 09:20:50 -0700356 manifest.getEntries().put(OTACERT_NAME, attr);
357 }
358
359
Doug Zongker147626e2012-09-04 13:32:13 -0700360 /** Write to another stream and track how many bytes have been
361 * written.
362 */
363 private static class CountOutputStream extends FilterOutputStream {
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700364 private int mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800365
Doug Zongker147626e2012-09-04 13:32:13 -0700366 public CountOutputStream(OutputStream out) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800367 super(out);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700368 mCount = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800369 }
370
371 @Override
372 public void write(int b) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800373 super.write(b);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700374 mCount++;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800375 }
376
377 @Override
378 public void write(byte[] b, int off, int len) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800379 super.write(b, off, len);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700380 mCount += len;
381 }
382
383 public int size() {
384 return mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800385 }
386 }
387
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700388 /** Write a .SF file with a digest of the specified manifest. */
Doug Zongker8562fd42013-04-10 09:19:32 -0700389 private static void writeSignatureFile(Manifest manifest, OutputStream out,
390 int hash)
Doug Zongker147626e2012-09-04 13:32:13 -0700391 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800392 Manifest sf = new Manifest();
393 Attributes main = sf.getMainAttributes();
394 main.putValue("Signature-Version", "1.0");
395 main.putValue("Created-By", "1.0 (Android SignApk)");
396
Doug Zongker8562fd42013-04-10 09:19:32 -0700397 MessageDigest md = MessageDigest.getInstance(
398 hash == USE_SHA256 ? "SHA256" : "SHA1");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800399 PrintStream print = new PrintStream(
Koushik Dutta29706d12012-12-17 22:25:22 -0800400 new DigestOutputStream(new ByteArrayOutputStream(), md),
401 true, "UTF-8");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800402
403 // Digest of the entire manifest
404 manifest.write(print);
405 print.flush();
Doug Zongker8562fd42013-04-10 09:19:32 -0700406 main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700407 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800408
409 Map<String, Attributes> entries = manifest.getEntries();
410 for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
411 // Digest of the manifest stanza for this entry.
412 print.print("Name: " + entry.getKey() + "\r\n");
413 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
414 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
415 }
416 print.print("\r\n");
417 print.flush();
418
419 Attributes sfAttr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700420 sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700421 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800422 sf.getEntries().put(entry.getKey(), sfAttr);
423 }
424
Doug Zongker147626e2012-09-04 13:32:13 -0700425 CountOutputStream cout = new CountOutputStream(out);
426 sf.write(cout);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700427
428 // A bug in the java.util.jar implementation of Android platforms
429 // up to version 1.6 will cause a spurious IOException to be thrown
430 // if the length of the signature file is a multiple of 1024 bytes.
431 // As a workaround, add an extra CRLF in this case.
Doug Zongker147626e2012-09-04 13:32:13 -0700432 if ((cout.size() % 1024) == 0) {
433 cout.write('\r');
434 cout.write('\n');
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700435 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800436 }
437
Doug Zongker147626e2012-09-04 13:32:13 -0700438 /** Sign data and write the digital signature to 'out'. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800439 private static void writeSignatureBlock(
Doug Zongker147626e2012-09-04 13:32:13 -0700440 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
441 OutputStream out)
442 throws IOException,
443 CertificateEncodingException,
444 OperatorCreationException,
445 CMSException {
446 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
447 certList.add(publicKey);
448 JcaCertStore certs = new JcaCertStore(certList);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800449
Doug Zongker147626e2012-09-04 13:32:13 -0700450 CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
Kenny Root3d2365c2013-09-19 12:49:36 -0700451 ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
Doug Zongker147626e2012-09-04 13:32:13 -0700452 .setProvider(sBouncyCastleProvider)
453 .build(privateKey);
454 gen.addSignerInfoGenerator(
455 new JcaSignerInfoGeneratorBuilder(
456 new JcaDigestCalculatorProviderBuilder()
457 .setProvider(sBouncyCastleProvider)
458 .build())
459 .setDirectSignature(true)
Doug Zongker8562fd42013-04-10 09:19:32 -0700460 .build(signer, publicKey));
Doug Zongker147626e2012-09-04 13:32:13 -0700461 gen.addCertificates(certs);
462 CMSSignedData sigData = gen.generate(data, false);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800463
Doug Zongker147626e2012-09-04 13:32:13 -0700464 ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
465 DEROutputStream dos = new DEROutputStream(out);
466 dos.writeObject(asn1.readObject());
The Android Open Source Project88b60792009-03-03 19:28:42 -0800467 }
468
Koushik Dutta29706d12012-12-17 22:25:22 -0800469 /**
470 * Copy all the files in a manifest from input to output. We set
471 * the modification times in the output to a fixed time, so as to
472 * reduce variation in the output file and make incremental OTAs
473 * more efficient.
474 */
Doug Zongker1d67eec2014-05-15 09:54:26 -0700475 private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out,
476 long timestamp, int alignment) throws IOException {
Koushik Dutta29706d12012-12-17 22:25:22 -0800477 byte[] buffer = new byte[4096];
478 int num;
479
480 Map<String, Attributes> entries = manifest.getEntries();
481 ArrayList<String> names = new ArrayList<String>(entries.keySet());
482 Collections.sort(names);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700483
484 boolean firstEntry = true;
485 long offset = 0L;
486
487 // We do the copy in two passes -- first copying all the
488 // entries that are STORED, then copying all the entries that
489 // have any other compression flag (which in practice means
490 // DEFLATED). This groups all the stored entries together at
491 // the start of the file and makes it easier to do alignment
492 // on them (since only stored entries are aligned).
493
Koushik Dutta29706d12012-12-17 22:25:22 -0800494 for (String name : names) {
495 JarEntry inEntry = in.getJarEntry(name);
496 JarEntry outEntry = null;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700497 if (inEntry.getMethod() != JarEntry.STORED) continue;
498 // Preserve the STORED method of the input entry.
499 outEntry = new JarEntry(inEntry);
500 outEntry.setTime(timestamp);
501
502 // 'offset' is the offset into the file at which we expect
503 // the file data to begin. This is the value we need to
504 // make a multiple of 'alignement'.
505 offset += JarFile.LOCHDR + outEntry.getName().length();
506 if (firstEntry) {
507 // The first entry in a jar file has an extra field of
508 // four bytes that you can't get rid of; any extra
509 // data you specify in the JarEntry is appended to
510 // these forced four bytes. This is JAR_MAGIC in
511 // JarOutputStream; the bytes are 0xfeca0000.
512 offset += 4;
513 firstEntry = false;
Koushik Dutta29706d12012-12-17 22:25:22 -0800514 }
Doug Zongker1d67eec2014-05-15 09:54:26 -0700515 if (alignment > 0 && (offset % alignment != 0)) {
516 // Set the "extra data" of the entry to between 1 and
517 // alignment-1 bytes, to make the file data begin at
518 // an aligned offset.
519 int needed = alignment - (int)(offset % alignment);
520 outEntry.setExtra(new byte[needed]);
521 offset += needed;
522 }
523
524 out.putNextEntry(outEntry);
525
526 InputStream data = in.getInputStream(inEntry);
527 while ((num = data.read(buffer)) > 0) {
528 out.write(buffer, 0, num);
529 offset += num;
530 }
531 out.flush();
532 }
533
534 // Copy all the non-STORED entries. We don't attempt to
535 // maintain the 'offset' variable past this point; we don't do
536 // alignment on these entries.
537
538 for (String name : names) {
539 JarEntry inEntry = in.getJarEntry(name);
540 JarEntry outEntry = null;
541 if (inEntry.getMethod() == JarEntry.STORED) continue;
542 // Create a new entry so that the compressed len is recomputed.
543 outEntry = new JarEntry(name);
Koushik Dutta29706d12012-12-17 22:25:22 -0800544 outEntry.setTime(timestamp);
545 out.putNextEntry(outEntry);
546
547 InputStream data = in.getInputStream(inEntry);
548 while ((num = data.read(buffer)) > 0) {
549 out.write(buffer, 0, num);
550 }
551 out.flush();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700552 }
Koushik Dutta29706d12012-12-17 22:25:22 -0800553 }
554
555 private static class WholeFileSignerOutputStream extends FilterOutputStream {
556 private boolean closing = false;
557 private ByteArrayOutputStream footer = new ByteArrayOutputStream();
558 private OutputStream tee;
559
560 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
561 super(out);
562 this.tee = tee;
563 }
564
565 public void notifyClosing() {
566 closing = true;
567 }
568
569 public void finish() throws IOException {
570 closing = false;
571
572 byte[] data = footer.toByteArray();
573 if (data.length < 2)
574 throw new IOException("Less than two bytes written to footer");
575 write(data, 0, data.length - 2);
576 }
577
578 public byte[] getTail() {
579 return footer.toByteArray();
580 }
581
582 @Override
583 public void write(byte[] b) throws IOException {
584 write(b, 0, b.length);
585 }
586
587 @Override
588 public void write(byte[] b, int off, int len) throws IOException {
589 if (closing) {
590 // if the jar is about to close, save the footer that will be written
591 footer.write(b, off, len);
592 }
593 else {
594 // write to both output streams. out is the CMSTypedData signer and tee is the file.
595 out.write(b, off, len);
596 tee.write(b, off, len);
597 }
598 }
599
600 @Override
601 public void write(int b) throws IOException {
602 if (closing) {
603 // if the jar is about to close, save the footer that will be written
604 footer.write(b);
605 }
606 else {
607 // write to both output streams. out is the CMSTypedData signer and tee is the file.
608 out.write(b);
609 tee.write(b);
610 }
611 }
612 }
613
614 private static class CMSSigner implements CMSTypedData {
615 private JarFile inputJar;
616 private File publicKeyFile;
617 private X509Certificate publicKey;
618 private PrivateKey privateKey;
619 private String outputFile;
620 private OutputStream outputStream;
621 private final ASN1ObjectIdentifier type;
622 private WholeFileSignerOutputStream signer;
623
624 public CMSSigner(JarFile inputJar, File publicKeyFile,
625 X509Certificate publicKey, PrivateKey privateKey,
626 OutputStream outputStream) {
627 this.inputJar = inputJar;
628 this.publicKeyFile = publicKeyFile;
629 this.publicKey = publicKey;
630 this.privateKey = privateKey;
631 this.outputStream = outputStream;
632 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
633 }
634
Kenny Rootbda807d2014-08-07 12:02:54 -0700635 /**
636 * This should actually return byte[] or something similar, but nothing
637 * actually checks it currently.
638 */
Koushik Dutta29706d12012-12-17 22:25:22 -0800639 public Object getContent() {
Kenny Rootbda807d2014-08-07 12:02:54 -0700640 return this;
Koushik Dutta29706d12012-12-17 22:25:22 -0800641 }
642
643 public ASN1ObjectIdentifier getContentType() {
644 return type;
645 }
646
647 public void write(OutputStream out) throws IOException {
648 try {
649 signer = new WholeFileSignerOutputStream(out, outputStream);
650 JarOutputStream outputJar = new JarOutputStream(signer);
651
Kenny Root3d2365c2013-09-19 12:49:36 -0700652 int hash = getDigestAlgorithm(publicKey);
Doug Zongker8562fd42013-04-10 09:19:32 -0700653
654 // Assume the certificate is valid for at least an hour.
655 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
656
657 Manifest manifest = addDigestsToManifest(inputJar, hash);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700658 copyFiles(manifest, inputJar, outputJar, timestamp, 0);
Doug Zongker8562fd42013-04-10 09:19:32 -0700659 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
660
Koushik Dutta29706d12012-12-17 22:25:22 -0800661 signFile(manifest, inputJar,
662 new X509Certificate[]{ publicKey },
663 new PrivateKey[]{ privateKey },
664 outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800665
666 signer.notifyClosing();
667 outputJar.close();
668 signer.finish();
669 }
670 catch (Exception e) {
671 throw new IOException(e);
672 }
673 }
674
675 public void writeSignatureBlock(ByteArrayOutputStream temp)
676 throws IOException,
677 CertificateEncodingException,
678 OperatorCreationException,
679 CMSException {
680 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
681 }
682
683 public WholeFileSignerOutputStream getSigner() {
684 return signer;
685 }
686 }
687
688 private static void signWholeFile(JarFile inputJar, File publicKeyFile,
689 X509Certificate publicKey, PrivateKey privateKey,
690 OutputStream outputStream) throws Exception {
691 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
692 publicKey, privateKey, outputStream);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700693
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700694 ByteArrayOutputStream temp = new ByteArrayOutputStream();
695
696 // put a readable message and a null char at the start of the
697 // archive comment, so that tools that display the comment
698 // (hopefully) show something sensible.
699 // TODO: anything more useful we can put in this message?
700 byte[] message = "signed by SignApk".getBytes("UTF-8");
701 temp.write(message);
702 temp.write(0);
Doug Zongker147626e2012-09-04 13:32:13 -0700703
Koushik Dutta29706d12012-12-17 22:25:22 -0800704 cmsOut.writeSignatureBlock(temp);
705
706 byte[] zipData = cmsOut.getSigner().getTail();
707
708 // For a zip with no archive comment, the
709 // end-of-central-directory record will be 22 bytes long, so
710 // we expect to find the EOCD marker 22 bytes from the end.
711 if (zipData[zipData.length-22] != 0x50 ||
712 zipData[zipData.length-21] != 0x4b ||
713 zipData[zipData.length-20] != 0x05 ||
714 zipData[zipData.length-19] != 0x06) {
715 throw new IllegalArgumentException("zip data already has an archive comment");
716 }
717
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700718 int total_size = temp.size() + 6;
719 if (total_size > 0xffff) {
720 throw new IllegalArgumentException("signature is too big for ZIP file comment");
721 }
722 // signature starts this many bytes from the end of the file
723 int signature_start = total_size - message.length - 1;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700724 temp.write(signature_start & 0xff);
725 temp.write((signature_start >> 8) & 0xff);
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700726 // Why the 0xff bytes? In a zip file with no archive comment,
727 // bytes [-6:-2] of the file are the little-endian offset from
728 // the start of the file to the central directory. So for the
729 // two high bytes to be 0xff 0xff, the archive would have to
Doug Zongker147626e2012-09-04 13:32:13 -0700730 // be nearly 4GB in size. So it's unlikely that a real
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700731 // commentless archive would have 0xffs here, and lets us tell
732 // an old signed archive from a new one.
733 temp.write(0xff);
734 temp.write(0xff);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700735 temp.write(total_size & 0xff);
736 temp.write((total_size >> 8) & 0xff);
737 temp.flush();
738
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700739 // Signature verification checks that the EOCD header is the
740 // last such sequence in the file (to avoid minzip finding a
741 // fake EOCD appended after the signature in its scan). The
742 // odds of producing this sequence by chance are very low, but
743 // let's catch it here if it does.
744 byte[] b = temp.toByteArray();
745 for (int i = 0; i < b.length-3; ++i) {
746 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
747 throw new IllegalArgumentException("found spurious EOCD header at " + i);
748 }
749 }
750
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700751 outputStream.write(total_size & 0xff);
752 outputStream.write((total_size >> 8) & 0xff);
753 temp.writeTo(outputStream);
754 }
755
Koushik Dutta29706d12012-12-17 22:25:22 -0800756 private static void signFile(Manifest manifest, JarFile inputJar,
757 X509Certificate[] publicKey, PrivateKey[] privateKey,
758 JarOutputStream outputJar)
759 throws Exception {
760 // Assume the certificate is valid for at least an hour.
761 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800762
Koushik Dutta29706d12012-12-17 22:25:22 -0800763 // MANIFEST.MF
Doug Zongker8562fd42013-04-10 09:19:32 -0700764 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
Koushik Dutta29706d12012-12-17 22:25:22 -0800765 je.setTime(timestamp);
766 outputJar.putNextEntry(je);
767 manifest.write(outputJar);
768
769 int numKeys = publicKey.length;
770 for (int k = 0; k < numKeys; ++k) {
771 // CERT.SF / CERT#.SF
772 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
773 (String.format(CERT_SF_MULTI_NAME, k)));
774 je.setTime(timestamp);
775 outputJar.putNextEntry(je);
776 ByteArrayOutputStream baos = new ByteArrayOutputStream();
Kenny Root3d2365c2013-09-19 12:49:36 -0700777 writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
Koushik Dutta29706d12012-12-17 22:25:22 -0800778 byte[] signedData = baos.toByteArray();
779 outputJar.write(signedData);
780
Kenny Root3d2365c2013-09-19 12:49:36 -0700781 // CERT.{EC,RSA} / CERT#.{EC,RSA}
Kenny Root62ea4a52013-09-25 09:59:10 -0700782 final String keyType = publicKey[k].getPublicKey().getAlgorithm();
Kenny Root3d2365c2013-09-19 12:49:36 -0700783 je = new JarEntry(numKeys == 1 ?
Kenny Root62ea4a52013-09-25 09:59:10 -0700784 (String.format(CERT_SIG_NAME, keyType)) :
785 (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
Koushik Dutta29706d12012-12-17 22:25:22 -0800786 je.setTime(timestamp);
787 outputJar.putNextEntry(je);
788 writeSignatureBlock(new CMSProcessableByteArray(signedData),
789 publicKey[k], privateKey[k], outputJar);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800790 }
791 }
792
Kenny Root89c961a2013-09-25 11:14:33 -0700793 /**
794 * Tries to load a JSE Provider by class name. This is for custom PrivateKey
795 * types that might be stored in PKCS#11-like storage.
796 */
797 private static void loadProviderIfNecessary(String providerClassName) {
798 if (providerClassName == null) {
799 return;
800 }
801
802 final Class<?> klass;
803 try {
804 final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
805 if (sysLoader != null) {
806 klass = sysLoader.loadClass(providerClassName);
807 } else {
808 klass = Class.forName(providerClassName);
809 }
810 } catch (ClassNotFoundException e) {
811 e.printStackTrace();
812 System.exit(1);
813 return;
814 }
815
816 Constructor<?> constructor = null;
817 for (Constructor<?> c : klass.getConstructors()) {
818 if (c.getParameterTypes().length == 0) {
819 constructor = c;
820 break;
821 }
822 }
823 if (constructor == null) {
824 System.err.println("No zero-arg constructor found for " + providerClassName);
825 System.exit(1);
826 return;
827 }
828
829 final Object o;
830 try {
831 o = constructor.newInstance();
832 } catch (Exception e) {
833 e.printStackTrace();
834 System.exit(1);
835 return;
836 }
837 if (!(o instanceof Provider)) {
838 System.err.println("Not a Provider class: " + providerClassName);
839 System.exit(1);
840 }
841
842 Security.insertProviderAt((Provider) o, 1);
843 }
844
Doug Zongkerb14c9762012-10-15 17:10:13 -0700845 private static void usage() {
846 System.err.println("Usage: signapk [-w] " +
Doug Zongker1d67eec2014-05-15 09:54:26 -0700847 "[-a <alignment>] " +
Kenny Root89c961a2013-09-25 11:14:33 -0700848 "[-providerClass <className>] " +
Doug Zongkerb14c9762012-10-15 17:10:13 -0700849 "publickey.x509[.pem] privatekey.pk8 " +
850 "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
851 "input.jar output.jar");
852 System.exit(2);
853 }
854
The Android Open Source Project88b60792009-03-03 19:28:42 -0800855 public static void main(String[] args) {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700856 if (args.length < 4) usage();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800857
Doug Zongker147626e2012-09-04 13:32:13 -0700858 sBouncyCastleProvider = new BouncyCastleProvider();
859 Security.addProvider(sBouncyCastleProvider);
860
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700861 boolean signWholeFile = false;
Kenny Root89c961a2013-09-25 11:14:33 -0700862 String providerClass = null;
863 String providerArg = null;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700864 int alignment = 4;
Kenny Root89c961a2013-09-25 11:14:33 -0700865
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700866 int argstart = 0;
Kenny Root89c961a2013-09-25 11:14:33 -0700867 while (argstart < args.length && args[argstart].startsWith("-")) {
868 if ("-w".equals(args[argstart])) {
869 signWholeFile = true;
870 ++argstart;
871 } else if ("-providerClass".equals(args[argstart])) {
872 if (argstart + 1 >= args.length) {
873 usage();
874 }
875 providerClass = args[++argstart];
876 ++argstart;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700877 } else if ("-a".equals(args[argstart])) {
878 alignment = Integer.parseInt(args[++argstart]);
879 ++argstart;
Kenny Root89c961a2013-09-25 11:14:33 -0700880 } else {
881 usage();
882 }
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700883 }
884
Doug Zongkerb14c9762012-10-15 17:10:13 -0700885 if ((args.length - argstart) % 2 == 1) usage();
886 int numKeys = ((args.length - argstart) / 2) - 1;
887 if (signWholeFile && numKeys > 1) {
888 System.err.println("Only one key may be used with -w.");
889 System.exit(2);
890 }
891
Kenny Root89c961a2013-09-25 11:14:33 -0700892 loadProviderIfNecessary(providerClass);
893
Doug Zongkerb14c9762012-10-15 17:10:13 -0700894 String inputFilename = args[args.length-2];
895 String outputFilename = args[args.length-1];
896
The Android Open Source Project88b60792009-03-03 19:28:42 -0800897 JarFile inputJar = null;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700898 FileOutputStream outputFile = null;
Doug Zongker8562fd42013-04-10 09:19:32 -0700899 int hashes = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800900
901 try {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700902 File firstPublicKeyFile = new File(args[argstart+0]);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800903
Doug Zongkerb14c9762012-10-15 17:10:13 -0700904 X509Certificate[] publicKey = new X509Certificate[numKeys];
Doug Zongker8562fd42013-04-10 09:19:32 -0700905 try {
906 for (int i = 0; i < numKeys; ++i) {
907 int argNum = argstart + i*2;
908 publicKey[i] = readPublicKey(new File(args[argNum]));
Kenny Root3d2365c2013-09-19 12:49:36 -0700909 hashes |= getDigestAlgorithm(publicKey[i]);
Doug Zongker8562fd42013-04-10 09:19:32 -0700910 }
911 } catch (IllegalArgumentException e) {
912 System.err.println(e);
913 System.exit(1);
Doug Zongkerb14c9762012-10-15 17:10:13 -0700914 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800915
Doug Zongkerb14c9762012-10-15 17:10:13 -0700916 // Set the ZIP file timestamp to the starting valid time
917 // of the 0th certificate plus one hour (to match what
918 // we've historically done).
919 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
920
921 PrivateKey[] privateKey = new PrivateKey[numKeys];
922 for (int i = 0; i < numKeys; ++i) {
923 int argNum = argstart + i*2 + 1;
924 privateKey[i] = readPrivateKey(new File(args[argNum]));
925 }
926 inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700927
Koushik Dutta29706d12012-12-17 22:25:22 -0800928 outputFile = new FileOutputStream(outputFilename);
929
930
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700931 if (signWholeFile) {
Koushik Dutta29706d12012-12-17 22:25:22 -0800932 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
933 publicKey[0], privateKey[0], outputFile);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700934 } else {
Koushik Dutta29706d12012-12-17 22:25:22 -0800935 JarOutputStream outputJar = new JarOutputStream(outputFile);
Doug Zongkere6913732012-07-03 15:03:04 -0700936
Koushik Dutta29706d12012-12-17 22:25:22 -0800937 // For signing .apks, use the maximum compression to make
938 // them as small as possible (since they live forever on
939 // the system partition). For OTA packages, use the
940 // default compression level, which is much much faster
941 // and produces output that is only a tiny bit larger
942 // (~0.1% on full OTA packages I tested).
Doug Zongkere6913732012-07-03 15:03:04 -0700943 outputJar.setLevel(9);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800944
Doug Zongker8562fd42013-04-10 09:19:32 -0700945 Manifest manifest = addDigestsToManifest(inputJar, hashes);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700946 copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
Doug Zongker8562fd42013-04-10 09:19:32 -0700947 signFile(manifest, inputJar, publicKey, privateKey, outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800948 outputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700949 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800950 } catch (Exception e) {
951 e.printStackTrace();
952 System.exit(1);
953 } finally {
954 try {
955 if (inputJar != null) inputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700956 if (outputFile != null) outputFile.close();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800957 } catch (IOException e) {
958 e.printStackTrace();
959 System.exit(1);
960 }
961 }
962 }
963}