blob: 740a34354af7462cfe3dd87c7136036f7c8ffe57 [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) {
Kenny Root3d2365c2013-09-19 12:49:36 -0700138 String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
139 if ("RSA".equalsIgnoreCase(keyType)) {
140 if (getDigestAlgorithm(cert) == USE_SHA256) {
141 return "SHA256withRSA";
142 } else {
143 return "SHA1withRSA";
144 }
145 } else if ("EC".equalsIgnoreCase(keyType)) {
146 return "SHA256withECDSA";
147 } else {
148 throw new IllegalArgumentException("unsupported key type: " + keyType);
149 }
150 }
151
Doug Zongkeraf482b62009-06-08 10:46:55 -0700152 // Files matching this pattern are not copied to the output.
153 private static Pattern stripPattern =
Kenny Root3d2365c2013-09-19 12:49:36 -0700154 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
Doug Zongkerb14c9762012-10-15 17:10:13 -0700155 Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
Doug Zongkeraf482b62009-06-08 10:46:55 -0700156
The Android Open Source Project88b60792009-03-03 19:28:42 -0800157 private static X509Certificate readPublicKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800158 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800159 FileInputStream input = new FileInputStream(file);
160 try {
161 CertificateFactory cf = CertificateFactory.getInstance("X.509");
162 return (X509Certificate) cf.generateCertificate(input);
163 } finally {
164 input.close();
165 }
166 }
167
168 /**
adattatr50c7c5a2015-09-03 11:17:57 -0700169 * If a console doesn't exist, reads the password from stdin
170 * 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 -0800171 *
172 * @param keyFile The file containing the private key. Used to prompt the user.
173 */
174 private static String readPassword(File keyFile) {
zhang jun22717f92014-07-10 16:34:57 +0800175 Console console;
176 char[] pwd;
adattatr50c7c5a2015-09-03 11:17:57 -0700177 if ((console = System.console()) == null) {
178 System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");
179 System.out.flush();
180 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
181 try {
182 return stdin.readLine();
183 } catch (IOException ex) {
184 return null;
185 }
zhang jun22717f92014-07-10 16:34:57 +0800186 } else {
adattatr50c7c5a2015-09-03 11:17:57 -0700187 if ((pwd = console.readPassword("[%s]", "Enter password for " + keyFile)) != null) {
188 return String.valueOf(pwd);
189 } else {
190 return null;
191 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800192 }
193 }
194
195 /**
Kenny Root62ea4a52013-09-25 09:59:10 -0700196 * Decrypt an encrypted PKCS#8 format private key.
The Android Open Source Project88b60792009-03-03 19:28:42 -0800197 *
198 * Based on ghstark's post on Aug 6, 2006 at
199 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
200 *
201 * @param encryptedPrivateKey The raw data of the private key
202 * @param keyFile The file containing the private key
203 */
Kenny Root62ea4a52013-09-25 09:59:10 -0700204 private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
Koushik Dutta29706d12012-12-17 22:25:22 -0800205 throws GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800206 EncryptedPrivateKeyInfo epkInfo;
207 try {
208 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
209 } catch (IOException ex) {
210 // Probably not an encrypted key.
211 return null;
212 }
213
214 char[] password = readPassword(keyFile).toCharArray();
215
216 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
217 Key key = skFactory.generateSecret(new PBEKeySpec(password));
218
219 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
220 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
221
222 try {
223 return epkInfo.getKeySpec(cipher);
224 } catch (InvalidKeySpecException ex) {
225 System.err.println("signapk: Password for " + keyFile + " may be bad.");
226 throw ex;
227 }
228 }
229
Kenny Root62ea4a52013-09-25 09:59:10 -0700230 /** Read a PKCS#8 format private key. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800231 private static PrivateKey readPrivateKey(File file)
Koushik Dutta29706d12012-12-17 22:25:22 -0800232 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800233 DataInputStream input = new DataInputStream(new FileInputStream(file));
234 try {
235 byte[] bytes = new byte[(int) file.length()];
236 input.read(bytes);
237
Kenny Root62ea4a52013-09-25 09:59:10 -0700238 /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
239 PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800240 if (spec == null) {
241 spec = new PKCS8EncodedKeySpec(bytes);
242 }
243
Kenny Root62ea4a52013-09-25 09:59:10 -0700244 /*
245 * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
246 * OID and use that to construct a KeyFactory.
247 */
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800248 PrivateKeyInfo pki;
249 try (ASN1InputStream bIn =
250 new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) {
251 pki = PrivateKeyInfo.getInstance(bIn.readObject());
252 }
Kenny Root62ea4a52013-09-25 09:59:10 -0700253 String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
Kenny Root3d2365c2013-09-19 12:49:36 -0700254
Kenny Root62ea4a52013-09-25 09:59:10 -0700255 return KeyFactory.getInstance(algOid).generatePrivate(spec);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800256 } finally {
257 input.close();
258 }
259 }
260
Doug Zongker8562fd42013-04-10 09:19:32 -0700261 /**
262 * Add the hash(es) of every file to the manifest, creating it if
263 * necessary.
264 */
265 private static Manifest addDigestsToManifest(JarFile jar, int hashes)
Koushik Dutta29706d12012-12-17 22:25:22 -0800266 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800267 Manifest input = jar.getManifest();
268 Manifest output = new Manifest();
269 Attributes main = output.getMainAttributes();
270 if (input != null) {
271 main.putAll(input.getMainAttributes());
272 } else {
273 main.putValue("Manifest-Version", "1.0");
274 main.putValue("Created-By", "1.0 (Android SignApk)");
275 }
276
Doug Zongker8562fd42013-04-10 09:19:32 -0700277 MessageDigest md_sha1 = null;
278 MessageDigest md_sha256 = null;
279 if ((hashes & USE_SHA1) != 0) {
280 md_sha1 = MessageDigest.getInstance("SHA1");
281 }
282 if ((hashes & USE_SHA256) != 0) {
283 md_sha256 = MessageDigest.getInstance("SHA256");
284 }
285
The Android Open Source Project88b60792009-03-03 19:28:42 -0800286 byte[] buffer = new byte[4096];
287 int num;
288
289 // We sort the input entries by name, and add them to the
290 // output manifest in sorted order. We expect that the output
291 // map will be deterministic.
292
293 TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
294
295 for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
296 JarEntry entry = e.nextElement();
297 byName.put(entry.getName(), entry);
298 }
299
300 for (JarEntry entry: byName.values()) {
301 String name = entry.getName();
Doug Zongkerb14c9762012-10-15 17:10:13 -0700302 if (!entry.isDirectory() &&
303 (stripPattern == null || !stripPattern.matcher(name).matches())) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800304 InputStream data = jar.getInputStream(entry);
305 while ((num = data.read(buffer)) > 0) {
Doug Zongker8562fd42013-04-10 09:19:32 -0700306 if (md_sha1 != null) md_sha1.update(buffer, 0, num);
307 if (md_sha256 != null) md_sha256.update(buffer, 0, num);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800308 }
309
310 Attributes attr = null;
311 if (input != null) attr = input.getAttributes(name);
312 attr = attr != null ? new Attributes(attr) : new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700313 if (md_sha1 != null) {
314 attr.putValue("SHA1-Digest",
315 new String(Base64.encode(md_sha1.digest()), "ASCII"));
316 }
317 if (md_sha256 != null) {
318 attr.putValue("SHA-256-Digest",
319 new String(Base64.encode(md_sha256.digest()), "ASCII"));
320 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800321 output.getEntries().put(name, attr);
322 }
323 }
324
325 return output;
326 }
327
Doug Zongker7bb04232012-05-11 09:20:50 -0700328 /**
329 * Add a copy of the public key to the archive; this should
330 * exactly match one of the files in
331 * /system/etc/security/otacerts.zip on the device. (The same
332 * cert can be extracted from the CERT.RSA file but this is much
333 * easier to get at.)
334 */
335 private static void addOtacert(JarOutputStream outputJar,
336 File publicKeyFile,
337 long timestamp,
Doug Zongker8562fd42013-04-10 09:19:32 -0700338 Manifest manifest,
339 int hash)
Doug Zongker7bb04232012-05-11 09:20:50 -0700340 throws IOException, GeneralSecurityException {
Doug Zongker8562fd42013-04-10 09:19:32 -0700341 MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
Doug Zongker7bb04232012-05-11 09:20:50 -0700342
343 JarEntry je = new JarEntry(OTACERT_NAME);
344 je.setTime(timestamp);
345 outputJar.putNextEntry(je);
346 FileInputStream input = new FileInputStream(publicKeyFile);
347 byte[] b = new byte[4096];
348 int read;
349 while ((read = input.read(b)) != -1) {
350 outputJar.write(b, 0, read);
351 md.update(b, 0, read);
352 }
353 input.close();
354
355 Attributes attr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700356 attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
Doug Zongker147626e2012-09-04 13:32:13 -0700357 new String(Base64.encode(md.digest()), "ASCII"));
Doug Zongker7bb04232012-05-11 09:20:50 -0700358 manifest.getEntries().put(OTACERT_NAME, attr);
359 }
360
361
Doug Zongker147626e2012-09-04 13:32:13 -0700362 /** Write to another stream and track how many bytes have been
363 * written.
364 */
365 private static class CountOutputStream extends FilterOutputStream {
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700366 private int mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800367
Doug Zongker147626e2012-09-04 13:32:13 -0700368 public CountOutputStream(OutputStream out) {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800369 super(out);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700370 mCount = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800371 }
372
373 @Override
374 public void write(int b) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800375 super.write(b);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700376 mCount++;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800377 }
378
379 @Override
380 public void write(byte[] b, int off, int len) throws IOException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800381 super.write(b, off, len);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700382 mCount += len;
383 }
384
385 public int size() {
386 return mCount;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800387 }
388 }
389
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700390 /** Write a .SF file with a digest of the specified manifest. */
Doug Zongker8562fd42013-04-10 09:19:32 -0700391 private static void writeSignatureFile(Manifest manifest, OutputStream out,
392 int hash)
Doug Zongker147626e2012-09-04 13:32:13 -0700393 throws IOException, GeneralSecurityException {
The Android Open Source Project88b60792009-03-03 19:28:42 -0800394 Manifest sf = new Manifest();
395 Attributes main = sf.getMainAttributes();
396 main.putValue("Signature-Version", "1.0");
397 main.putValue("Created-By", "1.0 (Android SignApk)");
398
Doug Zongker8562fd42013-04-10 09:19:32 -0700399 MessageDigest md = MessageDigest.getInstance(
400 hash == USE_SHA256 ? "SHA256" : "SHA1");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800401 PrintStream print = new PrintStream(
Koushik Dutta29706d12012-12-17 22:25:22 -0800402 new DigestOutputStream(new ByteArrayOutputStream(), md),
403 true, "UTF-8");
The Android Open Source Project88b60792009-03-03 19:28:42 -0800404
405 // Digest of the entire manifest
406 manifest.write(print);
407 print.flush();
Doug Zongker8562fd42013-04-10 09:19:32 -0700408 main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700409 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800410
411 Map<String, Attributes> entries = manifest.getEntries();
412 for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
413 // Digest of the manifest stanza for this entry.
414 print.print("Name: " + entry.getKey() + "\r\n");
415 for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
416 print.print(att.getKey() + ": " + att.getValue() + "\r\n");
417 }
418 print.print("\r\n");
419 print.flush();
420
421 Attributes sfAttr = new Attributes();
Doug Zongker8562fd42013-04-10 09:19:32 -0700422 sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
Doug Zongker147626e2012-09-04 13:32:13 -0700423 new String(Base64.encode(md.digest()), "ASCII"));
The Android Open Source Project88b60792009-03-03 19:28:42 -0800424 sf.getEntries().put(entry.getKey(), sfAttr);
425 }
426
Doug Zongker147626e2012-09-04 13:32:13 -0700427 CountOutputStream cout = new CountOutputStream(out);
428 sf.write(cout);
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700429
430 // A bug in the java.util.jar implementation of Android platforms
431 // up to version 1.6 will cause a spurious IOException to be thrown
432 // if the length of the signature file is a multiple of 1024 bytes.
433 // As a workaround, add an extra CRLF in this case.
Doug Zongker147626e2012-09-04 13:32:13 -0700434 if ((cout.size() % 1024) == 0) {
435 cout.write('\r');
436 cout.write('\n');
Ficus Kirkpatrick7978d502010-09-23 22:57:05 -0700437 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800438 }
439
Doug Zongker147626e2012-09-04 13:32:13 -0700440 /** Sign data and write the digital signature to 'out'. */
The Android Open Source Project88b60792009-03-03 19:28:42 -0800441 private static void writeSignatureBlock(
Doug Zongker147626e2012-09-04 13:32:13 -0700442 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
443 OutputStream out)
444 throws IOException,
445 CertificateEncodingException,
446 OperatorCreationException,
447 CMSException {
448 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
449 certList.add(publicKey);
450 JcaCertStore certs = new JcaCertStore(certList);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800451
Doug Zongker147626e2012-09-04 13:32:13 -0700452 CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
Kenny Root3d2365c2013-09-19 12:49:36 -0700453 ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
Doug Zongker147626e2012-09-04 13:32:13 -0700454 .setProvider(sBouncyCastleProvider)
455 .build(privateKey);
456 gen.addSignerInfoGenerator(
457 new JcaSignerInfoGeneratorBuilder(
458 new JcaDigestCalculatorProviderBuilder()
459 .setProvider(sBouncyCastleProvider)
460 .build())
461 .setDirectSignature(true)
Doug Zongker8562fd42013-04-10 09:19:32 -0700462 .build(signer, publicKey));
Doug Zongker147626e2012-09-04 13:32:13 -0700463 gen.addCertificates(certs);
464 CMSSignedData sigData = gen.generate(data, false);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800465
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800466 try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
467 DEROutputStream dos = new DEROutputStream(out);
468 dos.writeObject(asn1.readObject());
469 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800470 }
471
Koushik Dutta29706d12012-12-17 22:25:22 -0800472 /**
473 * Copy all the files in a manifest from input to output. We set
474 * the modification times in the output to a fixed time, so as to
475 * reduce variation in the output file and make incremental OTAs
476 * more efficient.
477 */
Doug Zongker1d67eec2014-05-15 09:54:26 -0700478 private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out,
479 long timestamp, int alignment) throws IOException {
Koushik Dutta29706d12012-12-17 22:25:22 -0800480 byte[] buffer = new byte[4096];
481 int num;
482
483 Map<String, Attributes> entries = manifest.getEntries();
484 ArrayList<String> names = new ArrayList<String>(entries.keySet());
485 Collections.sort(names);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700486
487 boolean firstEntry = true;
488 long offset = 0L;
489
490 // We do the copy in two passes -- first copying all the
491 // entries that are STORED, then copying all the entries that
492 // have any other compression flag (which in practice means
493 // DEFLATED). This groups all the stored entries together at
494 // the start of the file and makes it easier to do alignment
495 // on them (since only stored entries are aligned).
496
Koushik Dutta29706d12012-12-17 22:25:22 -0800497 for (String name : names) {
498 JarEntry inEntry = in.getJarEntry(name);
499 JarEntry outEntry = null;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700500 if (inEntry.getMethod() != JarEntry.STORED) continue;
501 // Preserve the STORED method of the input entry.
502 outEntry = new JarEntry(inEntry);
503 outEntry.setTime(timestamp);
504
505 // 'offset' is the offset into the file at which we expect
506 // the file data to begin. This is the value we need to
507 // make a multiple of 'alignement'.
508 offset += JarFile.LOCHDR + outEntry.getName().length();
509 if (firstEntry) {
510 // The first entry in a jar file has an extra field of
511 // four bytes that you can't get rid of; any extra
512 // data you specify in the JarEntry is appended to
513 // these forced four bytes. This is JAR_MAGIC in
514 // JarOutputStream; the bytes are 0xfeca0000.
515 offset += 4;
516 firstEntry = false;
Koushik Dutta29706d12012-12-17 22:25:22 -0800517 }
Doug Zongker1d67eec2014-05-15 09:54:26 -0700518 if (alignment > 0 && (offset % alignment != 0)) {
519 // Set the "extra data" of the entry to between 1 and
520 // alignment-1 bytes, to make the file data begin at
521 // an aligned offset.
522 int needed = alignment - (int)(offset % alignment);
523 outEntry.setExtra(new byte[needed]);
524 offset += needed;
525 }
526
527 out.putNextEntry(outEntry);
528
529 InputStream data = in.getInputStream(inEntry);
530 while ((num = data.read(buffer)) > 0) {
531 out.write(buffer, 0, num);
532 offset += num;
533 }
534 out.flush();
535 }
536
537 // Copy all the non-STORED entries. We don't attempt to
538 // maintain the 'offset' variable past this point; we don't do
539 // alignment on these entries.
540
541 for (String name : names) {
542 JarEntry inEntry = in.getJarEntry(name);
543 JarEntry outEntry = null;
544 if (inEntry.getMethod() == JarEntry.STORED) continue;
545 // Create a new entry so that the compressed len is recomputed.
546 outEntry = new JarEntry(name);
Koushik Dutta29706d12012-12-17 22:25:22 -0800547 outEntry.setTime(timestamp);
548 out.putNextEntry(outEntry);
549
550 InputStream data = in.getInputStream(inEntry);
551 while ((num = data.read(buffer)) > 0) {
552 out.write(buffer, 0, num);
553 }
554 out.flush();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700555 }
Koushik Dutta29706d12012-12-17 22:25:22 -0800556 }
557
558 private static class WholeFileSignerOutputStream extends FilterOutputStream {
559 private boolean closing = false;
560 private ByteArrayOutputStream footer = new ByteArrayOutputStream();
561 private OutputStream tee;
562
563 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
564 super(out);
565 this.tee = tee;
566 }
567
568 public void notifyClosing() {
569 closing = true;
570 }
571
572 public void finish() throws IOException {
573 closing = false;
574
575 byte[] data = footer.toByteArray();
576 if (data.length < 2)
577 throw new IOException("Less than two bytes written to footer");
578 write(data, 0, data.length - 2);
579 }
580
581 public byte[] getTail() {
582 return footer.toByteArray();
583 }
584
585 @Override
586 public void write(byte[] b) throws IOException {
587 write(b, 0, b.length);
588 }
589
590 @Override
591 public void write(byte[] b, int off, int len) throws IOException {
592 if (closing) {
593 // if the jar is about to close, save the footer that will be written
594 footer.write(b, off, len);
595 }
596 else {
597 // write to both output streams. out is the CMSTypedData signer and tee is the file.
598 out.write(b, off, len);
599 tee.write(b, off, len);
600 }
601 }
602
603 @Override
604 public void write(int b) throws IOException {
605 if (closing) {
606 // if the jar is about to close, save the footer that will be written
607 footer.write(b);
608 }
609 else {
610 // write to both output streams. out is the CMSTypedData signer and tee is the file.
611 out.write(b);
612 tee.write(b);
613 }
614 }
615 }
616
617 private static class CMSSigner implements CMSTypedData {
618 private JarFile inputJar;
619 private File publicKeyFile;
620 private X509Certificate publicKey;
621 private PrivateKey privateKey;
Koushik Dutta29706d12012-12-17 22:25:22 -0800622 private OutputStream outputStream;
623 private final ASN1ObjectIdentifier type;
624 private WholeFileSignerOutputStream signer;
625
626 public CMSSigner(JarFile inputJar, File publicKeyFile,
627 X509Certificate publicKey, PrivateKey privateKey,
628 OutputStream outputStream) {
629 this.inputJar = inputJar;
630 this.publicKeyFile = publicKeyFile;
631 this.publicKey = publicKey;
632 this.privateKey = privateKey;
633 this.outputStream = outputStream;
634 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
635 }
636
Kenny Rootbda807d2014-08-07 12:02:54 -0700637 /**
638 * This should actually return byte[] or something similar, but nothing
639 * actually checks it currently.
640 */
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800641 @Override
Koushik Dutta29706d12012-12-17 22:25:22 -0800642 public Object getContent() {
Kenny Rootbda807d2014-08-07 12:02:54 -0700643 return this;
Koushik Dutta29706d12012-12-17 22:25:22 -0800644 }
645
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800646 @Override
Koushik Dutta29706d12012-12-17 22:25:22 -0800647 public ASN1ObjectIdentifier getContentType() {
648 return type;
649 }
650
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800651 @Override
Koushik Dutta29706d12012-12-17 22:25:22 -0800652 public void write(OutputStream out) throws IOException {
653 try {
654 signer = new WholeFileSignerOutputStream(out, outputStream);
655 JarOutputStream outputJar = new JarOutputStream(signer);
656
Kenny Root3d2365c2013-09-19 12:49:36 -0700657 int hash = getDigestAlgorithm(publicKey);
Doug Zongker8562fd42013-04-10 09:19:32 -0700658
659 // Assume the certificate is valid for at least an hour.
660 long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
661
662 Manifest manifest = addDigestsToManifest(inputJar, hash);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700663 copyFiles(manifest, inputJar, outputJar, timestamp, 0);
Doug Zongker8562fd42013-04-10 09:19:32 -0700664 addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
665
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800666 signFile(manifest,
Koushik Dutta29706d12012-12-17 22:25:22 -0800667 new X509Certificate[]{ publicKey },
668 new PrivateKey[]{ privateKey },
669 outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800670
671 signer.notifyClosing();
672 outputJar.close();
673 signer.finish();
674 }
675 catch (Exception e) {
676 throw new IOException(e);
677 }
678 }
679
680 public void writeSignatureBlock(ByteArrayOutputStream temp)
681 throws IOException,
682 CertificateEncodingException,
683 OperatorCreationException,
684 CMSException {
685 SignApk.writeSignatureBlock(this, publicKey, privateKey, temp);
686 }
687
688 public WholeFileSignerOutputStream getSigner() {
689 return signer;
690 }
691 }
692
693 private static void signWholeFile(JarFile inputJar, File publicKeyFile,
694 X509Certificate publicKey, PrivateKey privateKey,
695 OutputStream outputStream) throws Exception {
696 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
697 publicKey, privateKey, outputStream);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700698
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700699 ByteArrayOutputStream temp = new ByteArrayOutputStream();
700
701 // put a readable message and a null char at the start of the
702 // archive comment, so that tools that display the comment
703 // (hopefully) show something sensible.
704 // TODO: anything more useful we can put in this message?
705 byte[] message = "signed by SignApk".getBytes("UTF-8");
706 temp.write(message);
707 temp.write(0);
Doug Zongker147626e2012-09-04 13:32:13 -0700708
Koushik Dutta29706d12012-12-17 22:25:22 -0800709 cmsOut.writeSignatureBlock(temp);
710
711 byte[] zipData = cmsOut.getSigner().getTail();
712
713 // For a zip with no archive comment, the
714 // end-of-central-directory record will be 22 bytes long, so
715 // we expect to find the EOCD marker 22 bytes from the end.
716 if (zipData[zipData.length-22] != 0x50 ||
717 zipData[zipData.length-21] != 0x4b ||
718 zipData[zipData.length-20] != 0x05 ||
719 zipData[zipData.length-19] != 0x06) {
720 throw new IllegalArgumentException("zip data already has an archive comment");
721 }
722
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700723 int total_size = temp.size() + 6;
724 if (total_size > 0xffff) {
725 throw new IllegalArgumentException("signature is too big for ZIP file comment");
726 }
727 // signature starts this many bytes from the end of the file
728 int signature_start = total_size - message.length - 1;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700729 temp.write(signature_start & 0xff);
730 temp.write((signature_start >> 8) & 0xff);
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700731 // Why the 0xff bytes? In a zip file with no archive comment,
732 // bytes [-6:-2] of the file are the little-endian offset from
733 // the start of the file to the central directory. So for the
734 // two high bytes to be 0xff 0xff, the archive would have to
Doug Zongker147626e2012-09-04 13:32:13 -0700735 // be nearly 4GB in size. So it's unlikely that a real
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700736 // commentless archive would have 0xffs here, and lets us tell
737 // an old signed archive from a new one.
738 temp.write(0xff);
739 temp.write(0xff);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700740 temp.write(total_size & 0xff);
741 temp.write((total_size >> 8) & 0xff);
742 temp.flush();
743
Doug Zongkerbadd2ca2009-08-14 16:42:35 -0700744 // Signature verification checks that the EOCD header is the
745 // last such sequence in the file (to avoid minzip finding a
746 // fake EOCD appended after the signature in its scan). The
747 // odds of producing this sequence by chance are very low, but
748 // let's catch it here if it does.
749 byte[] b = temp.toByteArray();
750 for (int i = 0; i < b.length-3; ++i) {
751 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
752 throw new IllegalArgumentException("found spurious EOCD header at " + i);
753 }
754 }
755
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700756 outputStream.write(total_size & 0xff);
757 outputStream.write((total_size >> 8) & 0xff);
758 temp.writeTo(outputStream);
759 }
760
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800761 private static void signFile(Manifest manifest,
Koushik Dutta29706d12012-12-17 22:25:22 -0800762 X509Certificate[] publicKey, PrivateKey[] privateKey,
763 JarOutputStream outputJar)
764 throws Exception {
765 // Assume the certificate is valid for at least an hour.
766 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800767
Koushik Dutta29706d12012-12-17 22:25:22 -0800768 // MANIFEST.MF
Doug Zongker8562fd42013-04-10 09:19:32 -0700769 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
Koushik Dutta29706d12012-12-17 22:25:22 -0800770 je.setTime(timestamp);
771 outputJar.putNextEntry(je);
772 manifest.write(outputJar);
773
774 int numKeys = publicKey.length;
775 for (int k = 0; k < numKeys; ++k) {
776 // CERT.SF / CERT#.SF
777 je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
778 (String.format(CERT_SF_MULTI_NAME, k)));
779 je.setTime(timestamp);
780 outputJar.putNextEntry(je);
781 ByteArrayOutputStream baos = new ByteArrayOutputStream();
Kenny Root3d2365c2013-09-19 12:49:36 -0700782 writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k]));
Koushik Dutta29706d12012-12-17 22:25:22 -0800783 byte[] signedData = baos.toByteArray();
784 outputJar.write(signedData);
785
Kenny Root3d2365c2013-09-19 12:49:36 -0700786 // CERT.{EC,RSA} / CERT#.{EC,RSA}
Kenny Root62ea4a52013-09-25 09:59:10 -0700787 final String keyType = publicKey[k].getPublicKey().getAlgorithm();
Kenny Root3d2365c2013-09-19 12:49:36 -0700788 je = new JarEntry(numKeys == 1 ?
Kenny Root62ea4a52013-09-25 09:59:10 -0700789 (String.format(CERT_SIG_NAME, keyType)) :
790 (String.format(CERT_SIG_MULTI_NAME, k, keyType)));
Koushik Dutta29706d12012-12-17 22:25:22 -0800791 je.setTime(timestamp);
792 outputJar.putNextEntry(je);
793 writeSignatureBlock(new CMSProcessableByteArray(signedData),
794 publicKey[k], privateKey[k], outputJar);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800795 }
796 }
797
Kenny Root89c961a2013-09-25 11:14:33 -0700798 /**
799 * Tries to load a JSE Provider by class name. This is for custom PrivateKey
800 * types that might be stored in PKCS#11-like storage.
801 */
802 private static void loadProviderIfNecessary(String providerClassName) {
803 if (providerClassName == null) {
804 return;
805 }
806
807 final Class<?> klass;
808 try {
809 final ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
810 if (sysLoader != null) {
811 klass = sysLoader.loadClass(providerClassName);
812 } else {
813 klass = Class.forName(providerClassName);
814 }
815 } catch (ClassNotFoundException e) {
816 e.printStackTrace();
817 System.exit(1);
818 return;
819 }
820
821 Constructor<?> constructor = null;
822 for (Constructor<?> c : klass.getConstructors()) {
823 if (c.getParameterTypes().length == 0) {
824 constructor = c;
825 break;
826 }
827 }
828 if (constructor == null) {
829 System.err.println("No zero-arg constructor found for " + providerClassName);
830 System.exit(1);
831 return;
832 }
833
834 final Object o;
835 try {
836 o = constructor.newInstance();
837 } catch (Exception e) {
838 e.printStackTrace();
839 System.exit(1);
840 return;
841 }
842 if (!(o instanceof Provider)) {
843 System.err.println("Not a Provider class: " + providerClassName);
844 System.exit(1);
845 }
846
847 Security.insertProviderAt((Provider) o, 1);
848 }
849
Doug Zongkerb14c9762012-10-15 17:10:13 -0700850 private static void usage() {
851 System.err.println("Usage: signapk [-w] " +
Doug Zongker1d67eec2014-05-15 09:54:26 -0700852 "[-a <alignment>] " +
Kenny Root89c961a2013-09-25 11:14:33 -0700853 "[-providerClass <className>] " +
Doug Zongkerb14c9762012-10-15 17:10:13 -0700854 "publickey.x509[.pem] privatekey.pk8 " +
855 "[publickey2.x509[.pem] privatekey2.pk8 ...] " +
856 "input.jar output.jar");
857 System.exit(2);
858 }
859
The Android Open Source Project88b60792009-03-03 19:28:42 -0800860 public static void main(String[] args) {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700861 if (args.length < 4) usage();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800862
Doug Zongker147626e2012-09-04 13:32:13 -0700863 sBouncyCastleProvider = new BouncyCastleProvider();
864 Security.addProvider(sBouncyCastleProvider);
865
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700866 boolean signWholeFile = false;
Kenny Root89c961a2013-09-25 11:14:33 -0700867 String providerClass = null;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700868 int alignment = 4;
Kenny Root89c961a2013-09-25 11:14:33 -0700869
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700870 int argstart = 0;
Kenny Root89c961a2013-09-25 11:14:33 -0700871 while (argstart < args.length && args[argstart].startsWith("-")) {
872 if ("-w".equals(args[argstart])) {
873 signWholeFile = true;
874 ++argstart;
875 } else if ("-providerClass".equals(args[argstart])) {
876 if (argstart + 1 >= args.length) {
877 usage();
878 }
879 providerClass = args[++argstart];
880 ++argstart;
Doug Zongker1d67eec2014-05-15 09:54:26 -0700881 } else if ("-a".equals(args[argstart])) {
882 alignment = Integer.parseInt(args[++argstart]);
883 ++argstart;
Kenny Root89c961a2013-09-25 11:14:33 -0700884 } else {
885 usage();
886 }
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700887 }
888
Doug Zongkerb14c9762012-10-15 17:10:13 -0700889 if ((args.length - argstart) % 2 == 1) usage();
890 int numKeys = ((args.length - argstart) / 2) - 1;
891 if (signWholeFile && numKeys > 1) {
892 System.err.println("Only one key may be used with -w.");
893 System.exit(2);
894 }
895
Kenny Root89c961a2013-09-25 11:14:33 -0700896 loadProviderIfNecessary(providerClass);
897
Doug Zongkerb14c9762012-10-15 17:10:13 -0700898 String inputFilename = args[args.length-2];
899 String outputFilename = args[args.length-1];
900
The Android Open Source Project88b60792009-03-03 19:28:42 -0800901 JarFile inputJar = null;
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700902 FileOutputStream outputFile = null;
Doug Zongker8562fd42013-04-10 09:19:32 -0700903 int hashes = 0;
The Android Open Source Project88b60792009-03-03 19:28:42 -0800904
905 try {
Doug Zongkerb14c9762012-10-15 17:10:13 -0700906 File firstPublicKeyFile = new File(args[argstart+0]);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800907
Doug Zongkerb14c9762012-10-15 17:10:13 -0700908 X509Certificate[] publicKey = new X509Certificate[numKeys];
Doug Zongker8562fd42013-04-10 09:19:32 -0700909 try {
910 for (int i = 0; i < numKeys; ++i) {
911 int argNum = argstart + i*2;
912 publicKey[i] = readPublicKey(new File(args[argNum]));
Kenny Root3d2365c2013-09-19 12:49:36 -0700913 hashes |= getDigestAlgorithm(publicKey[i]);
Doug Zongker8562fd42013-04-10 09:19:32 -0700914 }
915 } catch (IllegalArgumentException e) {
916 System.err.println(e);
917 System.exit(1);
Doug Zongkerb14c9762012-10-15 17:10:13 -0700918 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800919
Doug Zongkerb14c9762012-10-15 17:10:13 -0700920 // Set the ZIP file timestamp to the starting valid time
921 // of the 0th certificate plus one hour (to match what
922 // we've historically done).
923 long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
924
925 PrivateKey[] privateKey = new PrivateKey[numKeys];
926 for (int i = 0; i < numKeys; ++i) {
927 int argNum = argstart + i*2 + 1;
928 privateKey[i] = readPrivateKey(new File(args[argNum]));
929 }
930 inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700931
Koushik Dutta29706d12012-12-17 22:25:22 -0800932 outputFile = new FileOutputStream(outputFilename);
933
934
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700935 if (signWholeFile) {
Koushik Dutta29706d12012-12-17 22:25:22 -0800936 SignApk.signWholeFile(inputJar, firstPublicKeyFile,
937 publicKey[0], privateKey[0], outputFile);
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700938 } else {
Koushik Dutta29706d12012-12-17 22:25:22 -0800939 JarOutputStream outputJar = new JarOutputStream(outputFile);
Doug Zongkere6913732012-07-03 15:03:04 -0700940
Koushik Dutta29706d12012-12-17 22:25:22 -0800941 // For signing .apks, use the maximum compression to make
942 // them as small as possible (since they live forever on
943 // the system partition). For OTA packages, use the
944 // default compression level, which is much much faster
945 // and produces output that is only a tiny bit larger
946 // (~0.1% on full OTA packages I tested).
Doug Zongkere6913732012-07-03 15:03:04 -0700947 outputJar.setLevel(9);
The Android Open Source Project88b60792009-03-03 19:28:42 -0800948
Doug Zongker8562fd42013-04-10 09:19:32 -0700949 Manifest manifest = addDigestsToManifest(inputJar, hashes);
Doug Zongker1d67eec2014-05-15 09:54:26 -0700950 copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
Alex Klyubinc218d3e2015-11-19 13:09:57 -0800951 signFile(manifest, publicKey, privateKey, outputJar);
Koushik Dutta29706d12012-12-17 22:25:22 -0800952 outputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700953 }
The Android Open Source Project88b60792009-03-03 19:28:42 -0800954 } catch (Exception e) {
955 e.printStackTrace();
956 System.exit(1);
957 } finally {
958 try {
959 if (inputJar != null) inputJar.close();
Doug Zongkerc6cf01a2009-08-12 18:20:24 -0700960 if (outputFile != null) outputFile.close();
The Android Open Source Project88b60792009-03-03 19:28:42 -0800961 } catch (IOException e) {
962 e.printStackTrace();
963 System.exit(1);
964 }
965 }
966 }
967}