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 org.bouncycastle.asn1.ASN1InputStream; 20 import org.bouncycastle.asn1.ASN1ObjectIdentifier; 21 import org.bouncycastle.asn1.DEROutputStream; 22 import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; 23 import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 24 import org.bouncycastle.cert.jcajce.JcaCertStore; 25 import org.bouncycastle.cms.CMSException; 26 import org.bouncycastle.cms.CMSSignedData; 27 import org.bouncycastle.cms.CMSSignedDataGenerator; 28 import org.bouncycastle.cms.CMSTypedData; 29 import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; 30 import org.bouncycastle.jce.provider.BouncyCastleProvider; 31 import org.bouncycastle.operator.ContentSigner; 32 import org.bouncycastle.operator.OperatorCreationException; 33 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 34 import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; 35 import org.conscrypt.OpenSSLProvider; 36 37 import com.android.apksig.ApkSignerEngine; 38 import com.android.apksig.DefaultApkSignerEngine; 39 import com.android.apksig.SigningCertificateLineage; 40 import com.android.apksig.Hints; 41 import com.android.apksig.apk.ApkUtils; 42 import com.android.apksig.apk.MinSdkVersionException; 43 import com.android.apksig.util.DataSink; 44 import com.android.apksig.util.DataSource; 45 import com.android.apksig.util.DataSources; 46 import com.android.apksig.zip.ZipFormatException; 47 48 import java.io.Console; 49 import java.io.BufferedReader; 50 import java.io.ByteArrayInputStream; 51 import java.io.ByteArrayOutputStream; 52 import java.io.DataInputStream; 53 import java.io.File; 54 import java.io.FileInputStream; 55 import java.io.FileOutputStream; 56 import java.io.FilterOutputStream; 57 import java.io.IOException; 58 import java.io.InputStream; 59 import java.io.InputStreamReader; 60 import java.io.OutputStream; 61 import java.io.RandomAccessFile; 62 import java.lang.reflect.Constructor; 63 import java.nio.ByteBuffer; 64 import java.nio.ByteOrder; 65 import java.nio.charset.StandardCharsets; 66 import java.security.GeneralSecurityException; 67 import java.security.NoSuchAlgorithmException; 68 import java.security.Key; 69 import java.security.KeyFactory; 70 import java.security.KeyStore; 71 import java.security.KeyStoreException; 72 import java.security.KeyStore.PrivateKeyEntry; 73 import java.security.PrivateKey; 74 import java.security.Provider; 75 import java.security.Security; 76 import java.security.UnrecoverableEntryException; 77 import java.security.UnrecoverableKeyException; 78 import java.security.cert.CertificateEncodingException; 79 import java.security.cert.CertificateException; 80 import java.security.cert.CertificateFactory; 81 import java.security.cert.X509Certificate; 82 import java.security.spec.InvalidKeySpecException; 83 import java.security.spec.PKCS8EncodedKeySpec; 84 import java.util.ArrayList; 85 import java.util.Collections; 86 import java.util.Enumeration; 87 import java.util.HashSet; 88 import java.util.List; 89 import java.util.Locale; 90 import java.util.TimeZone; 91 import java.util.jar.JarEntry; 92 import java.util.jar.JarFile; 93 import java.util.jar.JarOutputStream; 94 import java.util.regex.Pattern; 95 import java.util.zip.ZipEntry; 96 97 import javax.crypto.Cipher; 98 import javax.crypto.EncryptedPrivateKeyInfo; 99 import javax.crypto.SecretKeyFactory; 100 import javax.crypto.spec.PBEKeySpec; 101 102 /** 103 * HISTORICAL NOTE: 104 * 105 * Prior to the keylimepie release, SignApk ignored the signature 106 * algorithm specified in the certificate and always used SHA1withRSA. 107 * 108 * Starting with JB-MR2, the platform supports SHA256withRSA, so we use 109 * the signature algorithm in the certificate to select which to use 110 * (SHA256withRSA or SHA1withRSA). Also in JB-MR2, EC keys are supported. 111 * 112 * Because there are old keys still in use whose certificate actually 113 * says "MD5withRSA", we treat these as though they say "SHA1withRSA" 114 * for compatibility with older releases. This can be changed by 115 * altering the getAlgorithm() function below. 116 */ 117 118 119 /** 120 * Command line tool to sign JAR files (including APKs and OTA updates) in a way 121 * compatible with the mincrypt verifier, using EC or RSA keys and SHA1 or 122 * SHA-256 (see historical note). The tool can additionally sign APKs using 123 * APK Signature Scheme v2. 124 */ 125 class SignApk { 126 private static final String OTACERT_NAME = "META-INF/com/android/otacert"; 127 128 /** 129 * Extensible data block/field header ID used for storing information about alignment of 130 * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section 131 * 4.5 Extensible data fields. 132 */ 133 private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935; 134 135 /** 136 * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed 137 * entries. 138 */ 139 private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6; 140 141 // bitmasks for which hash algorithms we need the manifest to include. 142 private static final int USE_SHA1 = 1; 143 private static final int USE_SHA256 = 2; 144 145 /** 146 * Returns the digest algorithm ID (one of {@code USE_SHA1} or {@code USE_SHA256}) to be used 147 * for signing an OTA update package using the private key corresponding to the provided 148 * certificate. 149 */ getDigestAlgorithmForOta(X509Certificate cert)150 private static int getDigestAlgorithmForOta(X509Certificate cert) { 151 String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US); 152 if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) { 153 // see "HISTORICAL NOTE" above. 154 return USE_SHA1; 155 } else if (sigAlg.startsWith("SHA256WITH")) { 156 return USE_SHA256; 157 } else { 158 throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg + 159 "\" in cert [" + cert.getSubjectDN()); 160 } 161 } 162 163 /** 164 * Returns the JCA {@link java.security.Signature} algorithm to be used for signing and OTA 165 * update package using the private key corresponding to the provided certificate and the 166 * provided digest algorithm (see {@code USE_SHA1} and {@code USE_SHA256} constants). 167 */ getJcaSignatureAlgorithmForOta( X509Certificate cert, int hash)168 private static String getJcaSignatureAlgorithmForOta( 169 X509Certificate cert, int hash) { 170 String sigAlgDigestPrefix; 171 switch (hash) { 172 case USE_SHA1: 173 sigAlgDigestPrefix = "SHA1"; 174 break; 175 case USE_SHA256: 176 sigAlgDigestPrefix = "SHA256"; 177 break; 178 default: 179 throw new IllegalArgumentException("Unknown hash ID: " + hash); 180 } 181 182 String keyAlgorithm = cert.getPublicKey().getAlgorithm(); 183 if ("RSA".equalsIgnoreCase(keyAlgorithm)) { 184 return sigAlgDigestPrefix + "withRSA"; 185 } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { 186 return sigAlgDigestPrefix + "withECDSA"; 187 } else { 188 throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); 189 } 190 } 191 readPublicKey(File file)192 private static X509Certificate readPublicKey(File file) 193 throws IOException, GeneralSecurityException { 194 FileInputStream input = new FileInputStream(file); 195 try { 196 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 197 return (X509Certificate) cf.generateCertificate(input); 198 } finally { 199 input.close(); 200 } 201 } 202 203 /** 204 * If a console doesn't exist, reads the password from stdin 205 * If a console exists, reads the password from console and returns it as a string. 206 * 207 * @param keyFileName Name of the file containing the private key. Used to prompt the user. 208 */ readPassword(String keyFileName)209 private static char[] readPassword(String keyFileName) { 210 Console console; 211 if ((console = System.console()) == null) { 212 System.out.print( 213 "Enter password for " + keyFileName + " (password will not be hidden): "); 214 System.out.flush(); 215 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 216 try { 217 String result = stdin.readLine(); 218 return result == null ? null : result.toCharArray(); 219 } catch (IOException ex) { 220 return null; 221 } 222 } else { 223 return console.readPassword("[%s]", "Enter password for " + keyFileName); 224 } 225 } 226 227 /** 228 * Decrypt an encrypted PKCS#8 format private key. 229 * 230 * Based on ghstark's post on Aug 6, 2006 at 231 * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 232 * 233 * @param encryptedPrivateKey The raw data of the private key 234 * @param keyFile The file containing the private key 235 */ decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)236 private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) 237 throws GeneralSecurityException { 238 EncryptedPrivateKeyInfo epkInfo; 239 try { 240 epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); 241 } catch (IOException ex) { 242 // Probably not an encrypted key. 243 return null; 244 } 245 246 SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); 247 Key key = skFactory.generateSecret(new PBEKeySpec(readPassword(keyFile.getPath()))); 248 Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); 249 cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); 250 251 try { 252 return epkInfo.getKeySpec(cipher); 253 } catch (InvalidKeySpecException ex) { 254 System.err.println("signapk: Password for " + keyFile + " may be bad."); 255 throw ex; 256 } 257 } 258 259 /** Read a PKCS#8 format private key. */ readPrivateKey(File file)260 private static PrivateKey readPrivateKey(File file) 261 throws IOException, GeneralSecurityException { 262 DataInputStream input = new DataInputStream(new FileInputStream(file)); 263 try { 264 byte[] bytes = new byte[(int) file.length()]; 265 input.read(bytes); 266 267 /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */ 268 PKCS8EncodedKeySpec spec = decryptPrivateKey(bytes, file); 269 if (spec == null) { 270 spec = new PKCS8EncodedKeySpec(bytes); 271 } 272 273 /* 274 * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm 275 * OID and use that to construct a KeyFactory. 276 */ 277 PrivateKeyInfo pki; 278 try (ASN1InputStream bIn = 279 new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()))) { 280 pki = PrivateKeyInfo.getInstance(bIn.readObject()); 281 } 282 String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); 283 284 return KeyFactory.getInstance(algOid).generatePrivate(spec); 285 } finally { 286 input.close(); 287 } 288 } 289 createKeyStore(String keyStoreName, String keyStorePin)290 private static KeyStore createKeyStore(String keyStoreName, String keyStorePin) throws 291 CertificateException, 292 IOException, 293 KeyStoreException, 294 NoSuchAlgorithmException { 295 KeyStore keyStore = KeyStore.getInstance(keyStoreName); 296 keyStore.load(null, keyStorePin == null ? null : keyStorePin.toCharArray()); 297 return keyStore; 298 } 299 300 /** Get a PKCS#11 private key from keyStore */ loadPrivateKeyFromKeyStore( final KeyStore keyStore, final String keyName)301 private static PrivateKey loadPrivateKeyFromKeyStore( 302 final KeyStore keyStore, final String keyName) 303 throws CertificateException, KeyStoreException, NoSuchAlgorithmException, 304 UnrecoverableKeyException, UnrecoverableEntryException { 305 final Key key = keyStore.getKey(keyName, readPassword(keyName)); 306 final PrivateKeyEntry privateKeyEntry = (PrivateKeyEntry) keyStore.getEntry(keyName, null); 307 if (privateKeyEntry == null) { 308 throw new Error( 309 "Key " 310 + keyName 311 + " not found in the token provided by PKCS11 library!"); 312 } 313 return privateKeyEntry.getPrivateKey(); 314 } 315 316 /** 317 * Add a copy of the public key to the archive; this should 318 * exactly match one of the files in 319 * /system/etc/security/otacerts.zip on the device. (The same 320 * cert can be extracted from the OTA update package's signature 321 * block but this is much easier to get at.) 322 */ addOtacert(JarOutputStream outputJar, File publicKeyFile, long timestamp)323 private static void addOtacert(JarOutputStream outputJar, 324 File publicKeyFile, 325 long timestamp) 326 throws IOException { 327 328 JarEntry je = new JarEntry(OTACERT_NAME); 329 je.setTime(timestamp); 330 outputJar.putNextEntry(je); 331 FileInputStream input = new FileInputStream(publicKeyFile); 332 byte[] b = new byte[4096]; 333 int read; 334 while ((read = input.read(b)) != -1) { 335 outputJar.write(b, 0, read); 336 } 337 input.close(); 338 } 339 340 341 /** Sign data and write the digital signature to 'out'. */ writeSignatureBlock( CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, OutputStream out)342 private static void writeSignatureBlock( 343 CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, int hash, 344 OutputStream out) 345 throws IOException, 346 CertificateEncodingException, 347 OperatorCreationException, 348 CMSException { 349 ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1); 350 certList.add(publicKey); 351 JcaCertStore certs = new JcaCertStore(certList); 352 353 CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); 354 ContentSigner signer = 355 new JcaContentSignerBuilder( 356 getJcaSignatureAlgorithmForOta(publicKey, hash)) 357 .build(privateKey); 358 gen.addSignerInfoGenerator( 359 new JcaSignerInfoGeneratorBuilder( 360 new JcaDigestCalculatorProviderBuilder() 361 .build()) 362 .setDirectSignature(true) 363 .build(signer, publicKey)); 364 gen.addCertificates(certs); 365 CMSSignedData sigData = gen.generate(data, false); 366 367 try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { 368 DEROutputStream dos = new DEROutputStream(out); 369 dos.writeObject(asn1.readObject()); 370 } 371 } 372 373 /** 374 * Adds ZIP entries which represent the v1 signature (JAR signature scheme). 375 */ addV1Signature( ApkSignerEngine apkSigner, ApkSignerEngine.OutputJarSignatureRequest v1Signature, JarOutputStream out, long timestamp)376 private static void addV1Signature( 377 ApkSignerEngine apkSigner, 378 ApkSignerEngine.OutputJarSignatureRequest v1Signature, 379 JarOutputStream out, 380 long timestamp) throws IOException { 381 for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry 382 : v1Signature.getAdditionalJarEntries()) { 383 String entryName = entry.getName(); 384 JarEntry outEntry = new JarEntry(entryName); 385 outEntry.setTime(timestamp); 386 out.putNextEntry(outEntry); 387 byte[] entryData = entry.getData(); 388 out.write(entryData); 389 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 390 apkSigner.outputJarEntry(entryName); 391 if (inspectEntryRequest != null) { 392 inspectEntryRequest.getDataSink().consume(entryData, 0, entryData.length); 393 inspectEntryRequest.done(); 394 } 395 } 396 } 397 398 /** 399 * Copy all JAR entries from input to output. We set the modification times in the output to a 400 * fixed time, so as to reduce variation in the output file and make incremental OTAs more 401 * efficient. 402 */ copyFiles( JarFile in, Pattern ignoredFilenamePattern, ApkSignerEngine apkSigner, JarOutputStream out, CountingOutputStream outCounter, long timestamp, int defaultAlignment)403 private static void copyFiles( 404 JarFile in, 405 Pattern ignoredFilenamePattern, 406 ApkSignerEngine apkSigner, 407 JarOutputStream out, 408 CountingOutputStream outCounter, 409 long timestamp, 410 int defaultAlignment) throws IOException { 411 byte[] buffer = new byte[4096]; 412 int num; 413 414 List<Hints.PatternWithRange> pinPatterns = extractPinPatterns(in); 415 ArrayList<Hints.ByteRange> pinByteRanges = pinPatterns == null ? null : new ArrayList<>(); 416 417 ArrayList<String> names = new ArrayList<String>(); 418 for (Enumeration<JarEntry> e = in.entries(); e.hasMoreElements();) { 419 JarEntry entry = e.nextElement(); 420 if (entry.isDirectory()) { 421 continue; 422 } 423 String entryName = entry.getName(); 424 if ((ignoredFilenamePattern != null) 425 && (ignoredFilenamePattern.matcher(entryName).matches())) { 426 continue; 427 } 428 if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) { 429 continue; // We regenerate it below. 430 } 431 names.add(entryName); 432 } 433 Collections.sort(names); 434 435 boolean firstEntry = true; 436 long offset = 0L; 437 438 // We do the copy in two passes -- first copying all the 439 // entries that are STORED, then copying all the entries that 440 // have any other compression flag (which in practice means 441 // DEFLATED). This groups all the stored entries together at 442 // the start of the file and makes it easier to do alignment 443 // on them (since only stored entries are aligned). 444 445 List<String> remainingNames = new ArrayList<>(names.size()); 446 for (String name : names) { 447 JarEntry inEntry = in.getJarEntry(name); 448 if (inEntry.getMethod() != JarEntry.STORED) { 449 // Defer outputting this entry until we're ready to output compressed entries. 450 remainingNames.add(name); 451 continue; 452 } 453 454 if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { 455 continue; 456 } 457 458 // Preserve the STORED method of the input entry. 459 JarEntry outEntry = new JarEntry(inEntry); 460 outEntry.setTime(timestamp); 461 // Discard comment and extra fields of this entry to 462 // simplify alignment logic below and for consistency with 463 // how compressed entries are handled later. 464 outEntry.setComment(null); 465 outEntry.setExtra(null); 466 467 int alignment = getStoredEntryDataAlignment(name, defaultAlignment); 468 // Alignment of the entry's data is achieved by adding a data block to the entry's Local 469 // File Header extra field. The data block contains information about the alignment 470 // value and the necessary padding bytes (0x00) to achieve the alignment. This works 471 // because the entry's data will be located immediately after the extra field. 472 // See ZIP APPNOTE.txt section "4.5 Extensible data fields" for details about the format 473 // of the extra field. 474 475 // 'offset' is the offset into the file at which we expect the entry's data to begin. 476 // This is the value we need to make a multiple of 'alignment'. 477 offset += JarFile.LOCHDR + outEntry.getName().length(); 478 if (firstEntry) { 479 // The first entry in a jar file has an extra field of four bytes that you can't get 480 // rid of; any extra data you specify in the JarEntry is appended to these forced 481 // four bytes. This is JAR_MAGIC in JarOutputStream; the bytes are 0xfeca0000. 482 // See http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6808540 483 // and http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4138619. 484 offset += 4; 485 firstEntry = false; 486 } 487 int extraPaddingSizeBytes = 0; 488 if (alignment > 0) { 489 long paddingStartOffset = offset + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES; 490 extraPaddingSizeBytes = 491 (alignment - (int) (paddingStartOffset % alignment)) % alignment; 492 } 493 byte[] extra = 494 new byte[ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES + extraPaddingSizeBytes]; 495 ByteBuffer extraBuf = ByteBuffer.wrap(extra); 496 extraBuf.order(ByteOrder.LITTLE_ENDIAN); 497 extraBuf.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); // Header ID 498 extraBuf.putShort((short) (2 + extraPaddingSizeBytes)); // Data Size 499 extraBuf.putShort((short) alignment); 500 outEntry.setExtra(extra); 501 offset += extra.length; 502 503 long entryHeaderStart = outCounter.getWrittenBytes(); 504 out.putNextEntry(outEntry); 505 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 506 (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; 507 DataSink entryDataSink = 508 (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; 509 510 long entryDataStart = outCounter.getWrittenBytes(); 511 try (InputStream data = in.getInputStream(inEntry)) { 512 while ((num = data.read(buffer)) > 0) { 513 out.write(buffer, 0, num); 514 if (entryDataSink != null) { 515 entryDataSink.consume(buffer, 0, num); 516 } 517 offset += num; 518 } 519 } 520 out.closeEntry(); 521 out.flush(); 522 if (inspectEntryRequest != null) { 523 inspectEntryRequest.done(); 524 } 525 526 if (pinPatterns != null) { 527 boolean pinFileHeader = false; 528 for (Hints.PatternWithRange pinPattern : pinPatterns) { 529 if (!pinPattern.matcher(name).matches()) { 530 continue; 531 } 532 Hints.ByteRange dataRange = 533 new Hints.ByteRange( 534 entryDataStart, 535 outCounter.getWrittenBytes()); 536 Hints.ByteRange pinRange = 537 pinPattern.ClampToAbsoluteByteRange(dataRange); 538 if (pinRange != null) { 539 pinFileHeader = true; 540 pinByteRanges.add(pinRange); 541 } 542 } 543 if (pinFileHeader) { 544 pinByteRanges.add(new Hints.ByteRange(entryHeaderStart, 545 entryDataStart)); 546 } 547 } 548 } 549 550 // Copy all the non-STORED entries. We don't attempt to 551 // maintain the 'offset' variable past this point; we don't do 552 // alignment on these entries. 553 554 for (String name : remainingNames) { 555 JarEntry inEntry = in.getJarEntry(name); 556 if (!shouldOutputApkEntry(apkSigner, in, inEntry, buffer)) { 557 continue; 558 } 559 560 // Create a new entry so that the compressed len is recomputed. 561 JarEntry outEntry = new JarEntry(name); 562 outEntry.setTime(timestamp); 563 long entryHeaderStart = outCounter.getWrittenBytes(); 564 out.putNextEntry(outEntry); 565 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 566 (apkSigner != null) ? apkSigner.outputJarEntry(name) : null; 567 DataSink entryDataSink = 568 (inspectEntryRequest != null) ? inspectEntryRequest.getDataSink() : null; 569 570 long entryDataStart = outCounter.getWrittenBytes(); 571 InputStream data = in.getInputStream(inEntry); 572 while ((num = data.read(buffer)) > 0) { 573 out.write(buffer, 0, num); 574 if (entryDataSink != null) { 575 entryDataSink.consume(buffer, 0, num); 576 } 577 } 578 out.closeEntry(); 579 out.flush(); 580 if (inspectEntryRequest != null) { 581 inspectEntryRequest.done(); 582 } 583 584 if (pinPatterns != null) { 585 boolean pinFileHeader = false; 586 for (Hints.PatternWithRange pinPattern : pinPatterns) { 587 if (!pinPattern.matcher(name).matches()) { 588 continue; 589 } 590 Hints.ByteRange dataRange = 591 new Hints.ByteRange( 592 entryDataStart, 593 outCounter.getWrittenBytes()); 594 Hints.ByteRange pinRange = 595 pinPattern.ClampToAbsoluteByteRange(dataRange); 596 if (pinRange != null) { 597 pinFileHeader = true; 598 pinByteRanges.add(pinRange); 599 } 600 } 601 if (pinFileHeader) { 602 pinByteRanges.add(new Hints.ByteRange(entryHeaderStart, 603 entryDataStart)); 604 } 605 } 606 } 607 608 if (pinByteRanges != null) { 609 // Cover central directory 610 pinByteRanges.add( 611 new Hints.ByteRange(outCounter.getWrittenBytes(), 612 Long.MAX_VALUE)); 613 addPinByteRanges(out, pinByteRanges, timestamp); 614 } 615 } 616 extractPinPatterns(JarFile in)617 private static List<Hints.PatternWithRange> extractPinPatterns(JarFile in) throws IOException { 618 ZipEntry pinMetaEntry = in.getEntry(Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME); 619 if (pinMetaEntry == null) { 620 return null; 621 } 622 InputStream pinMetaStream = in.getInputStream(pinMetaEntry); 623 byte[] patternBlob = new byte[(int) pinMetaEntry.getSize()]; 624 pinMetaStream.read(patternBlob); 625 return Hints.parsePinPatterns(patternBlob); 626 } 627 addPinByteRanges(JarOutputStream outputJar, ArrayList<Hints.ByteRange> pinByteRanges, long timestamp)628 private static void addPinByteRanges(JarOutputStream outputJar, 629 ArrayList<Hints.ByteRange> pinByteRanges, 630 long timestamp) throws IOException { 631 JarEntry je = new JarEntry(Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME); 632 je.setTime(timestamp); 633 outputJar.putNextEntry(je); 634 outputJar.write(Hints.encodeByteRangeList(pinByteRanges)); 635 } 636 shouldOutputApkEntry( ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf)637 private static boolean shouldOutputApkEntry( 638 ApkSignerEngine apkSigner, JarFile inFile, JarEntry inEntry, byte[] tmpbuf) 639 throws IOException { 640 if (apkSigner == null) { 641 return true; 642 } 643 644 ApkSignerEngine.InputJarEntryInstructions instructions = 645 apkSigner.inputJarEntry(inEntry.getName()); 646 ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = 647 instructions.getInspectJarEntryRequest(); 648 if (inspectEntryRequest != null) { 649 provideJarEntry(inFile, inEntry, inspectEntryRequest, tmpbuf); 650 } 651 switch (instructions.getOutputPolicy()) { 652 case OUTPUT: 653 return true; 654 case SKIP: 655 case OUTPUT_BY_ENGINE: 656 return false; 657 default: 658 throw new RuntimeException( 659 "Unsupported output policy: " + instructions.getOutputPolicy()); 660 } 661 } 662 provideJarEntry( JarFile jarFile, JarEntry jarEntry, ApkSignerEngine.InspectJarEntryRequest request, byte[] tmpbuf)663 private static void provideJarEntry( 664 JarFile jarFile, 665 JarEntry jarEntry, 666 ApkSignerEngine.InspectJarEntryRequest request, 667 byte[] tmpbuf) throws IOException { 668 DataSink dataSink = request.getDataSink(); 669 try (InputStream in = jarFile.getInputStream(jarEntry)) { 670 int chunkSize; 671 while ((chunkSize = in.read(tmpbuf)) > 0) { 672 dataSink.consume(tmpbuf, 0, chunkSize); 673 } 674 request.done(); 675 } 676 } 677 678 /** 679 * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start 680 * relative to start of file or {@code 0} if alignment of this entry's data is not important. 681 */ getStoredEntryDataAlignment(String entryName, int defaultAlignment)682 private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) { 683 if (defaultAlignment <= 0) { 684 return 0; 685 } 686 687 if (entryName.endsWith(".so")) { 688 // Align .so contents to memory page boundary to enable memory-mapped 689 // execution. 690 return 4096; 691 } else { 692 return defaultAlignment; 693 } 694 } 695 696 private static class WholeFileSignerOutputStream extends FilterOutputStream { 697 private boolean closing = false; 698 private ByteArrayOutputStream footer = new ByteArrayOutputStream(); 699 private OutputStream tee; 700 WholeFileSignerOutputStream(OutputStream out, OutputStream tee)701 public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) { 702 super(out); 703 this.tee = tee; 704 } 705 notifyClosing()706 public void notifyClosing() { 707 closing = true; 708 } 709 finish()710 public void finish() throws IOException { 711 closing = false; 712 713 byte[] data = footer.toByteArray(); 714 if (data.length < 2) 715 throw new IOException("Less than two bytes written to footer"); 716 write(data, 0, data.length - 2); 717 } 718 getTail()719 public byte[] getTail() { 720 return footer.toByteArray(); 721 } 722 723 @Override write(byte[] b)724 public void write(byte[] b) throws IOException { 725 write(b, 0, b.length); 726 } 727 728 @Override write(byte[] b, int off, int len)729 public void write(byte[] b, int off, int len) throws IOException { 730 if (closing) { 731 // if the jar is about to close, save the footer that will be written 732 footer.write(b, off, len); 733 } 734 else { 735 // write to both output streams. out is the CMSTypedData signer and tee is the file. 736 out.write(b, off, len); 737 tee.write(b, off, len); 738 } 739 } 740 741 @Override write(int b)742 public void write(int b) throws IOException { 743 if (closing) { 744 // if the jar is about to close, save the footer that will be written 745 footer.write(b); 746 } 747 else { 748 // write to both output streams. out is the CMSTypedData signer and tee is the file. 749 out.write(b); 750 tee.write(b); 751 } 752 } 753 } 754 755 private static class CMSSigner implements CMSTypedData { 756 private final JarFile inputJar; 757 private final File publicKeyFile; 758 private final X509Certificate publicKey; 759 private final PrivateKey privateKey; 760 private final int hash; 761 private final long timestamp; 762 private final OutputStream outputStream; 763 private final ASN1ObjectIdentifier type; 764 private WholeFileSignerOutputStream signer; 765 766 // Files matching this pattern are not copied to the output. 767 private static final Pattern STRIP_PATTERN = 768 Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" 769 + Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); 770 CMSSigner(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream)771 public CMSSigner(JarFile inputJar, File publicKeyFile, 772 X509Certificate publicKey, PrivateKey privateKey, int hash, 773 long timestamp, OutputStream outputStream) { 774 this.inputJar = inputJar; 775 this.publicKeyFile = publicKeyFile; 776 this.publicKey = publicKey; 777 this.privateKey = privateKey; 778 this.hash = hash; 779 this.timestamp = timestamp; 780 this.outputStream = outputStream; 781 this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()); 782 } 783 784 /** 785 * This should actually return byte[] or something similar, but nothing 786 * actually checks it currently. 787 */ 788 @Override getContent()789 public Object getContent() { 790 return this; 791 } 792 793 @Override getContentType()794 public ASN1ObjectIdentifier getContentType() { 795 return type; 796 } 797 798 @Override write(OutputStream out)799 public void write(OutputStream out) throws IOException { 800 try { 801 signer = new WholeFileSignerOutputStream(out, outputStream); 802 CountingOutputStream outputJarCounter = new CountingOutputStream(signer); 803 JarOutputStream outputJar = new JarOutputStream(outputJarCounter); 804 805 copyFiles(inputJar, STRIP_PATTERN, null, outputJar, 806 outputJarCounter, timestamp, 0); 807 addOtacert(outputJar, publicKeyFile, timestamp); 808 809 signer.notifyClosing(); 810 outputJar.close(); 811 signer.finish(); 812 } 813 catch (Exception e) { 814 throw new IOException(e); 815 } 816 } 817 writeSignatureBlock(ByteArrayOutputStream temp)818 public void writeSignatureBlock(ByteArrayOutputStream temp) 819 throws IOException, 820 CertificateEncodingException, 821 OperatorCreationException, 822 CMSException { 823 SignApk.writeSignatureBlock(this, publicKey, privateKey, hash, temp); 824 } 825 getSigner()826 public WholeFileSignerOutputStream getSigner() { 827 return signer; 828 } 829 } 830 signWholeFile(JarFile inputJar, File publicKeyFile, X509Certificate publicKey, PrivateKey privateKey, int hash, long timestamp, OutputStream outputStream)831 private static void signWholeFile(JarFile inputJar, File publicKeyFile, 832 X509Certificate publicKey, PrivateKey privateKey, 833 int hash, long timestamp, 834 OutputStream outputStream) throws Exception { 835 CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile, 836 publicKey, privateKey, hash, timestamp, outputStream); 837 838 ByteArrayOutputStream temp = new ByteArrayOutputStream(); 839 840 // put a readable message and a null char at the start of the 841 // archive comment, so that tools that display the comment 842 // (hopefully) show something sensible. 843 // TODO: anything more useful we can put in this message? 844 byte[] message = "signed by SignApk".getBytes(StandardCharsets.UTF_8); 845 temp.write(message); 846 temp.write(0); 847 848 cmsOut.writeSignatureBlock(temp); 849 850 byte[] zipData = cmsOut.getSigner().getTail(); 851 852 // For a zip with no archive comment, the 853 // end-of-central-directory record will be 22 bytes long, so 854 // we expect to find the EOCD marker 22 bytes from the end. 855 if (zipData[zipData.length-22] != 0x50 || 856 zipData[zipData.length-21] != 0x4b || 857 zipData[zipData.length-20] != 0x05 || 858 zipData[zipData.length-19] != 0x06) { 859 throw new IllegalArgumentException("zip data already has an archive comment"); 860 } 861 862 int total_size = temp.size() + 6; 863 if (total_size > 0xffff) { 864 throw new IllegalArgumentException("signature is too big for ZIP file comment"); 865 } 866 // signature starts this many bytes from the end of the file 867 int signature_start = total_size - message.length - 1; 868 temp.write(signature_start & 0xff); 869 temp.write((signature_start >> 8) & 0xff); 870 // Why the 0xff bytes? In a zip file with no archive comment, 871 // bytes [-6:-2] of the file are the little-endian offset from 872 // the start of the file to the central directory. So for the 873 // two high bytes to be 0xff 0xff, the archive would have to 874 // be nearly 4GB in size. So it's unlikely that a real 875 // commentless archive would have 0xffs here, and lets us tell 876 // an old signed archive from a new one. 877 temp.write(0xff); 878 temp.write(0xff); 879 temp.write(total_size & 0xff); 880 temp.write((total_size >> 8) & 0xff); 881 temp.flush(); 882 883 // Signature verification checks that the EOCD header is the 884 // last such sequence in the file (to avoid minzip finding a 885 // fake EOCD appended after the signature in its scan). The 886 // odds of producing this sequence by chance are very low, but 887 // let's catch it here if it does. 888 byte[] b = temp.toByteArray(); 889 for (int i = 0; i < b.length-3; ++i) { 890 if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { 891 throw new IllegalArgumentException("found spurious EOCD header at " + i); 892 } 893 } 894 895 outputStream.write(total_size & 0xff); 896 outputStream.write((total_size >> 8) & 0xff); 897 temp.writeTo(outputStream); 898 } 899 900 /** 901 * Tries to load a JSE Provider by class name. This is for custom PrivateKey 902 * types that might be stored in PKCS#11-like storage. 903 */ loadProviderIfNecessary(String providerClassName)904 private static void loadProviderIfNecessary(String providerClassName) { 905 if (providerClassName == null) { 906 return; 907 } 908 909 final Class<?> klass; 910 try { 911 final ClassLoader sysLoader = ClassLoader.getSystemClassLoader(); 912 if (sysLoader != null) { 913 klass = sysLoader.loadClass(providerClassName); 914 } else { 915 klass = Class.forName(providerClassName); 916 } 917 } catch (ClassNotFoundException e) { 918 e.printStackTrace(); 919 System.exit(1); 920 return; 921 } 922 923 Constructor<?> constructor = null; 924 for (Constructor<?> c : klass.getConstructors()) { 925 if (c.getParameterTypes().length == 0) { 926 constructor = c; 927 break; 928 } 929 } 930 if (constructor == null) { 931 System.err.println("No zero-arg constructor found for " + providerClassName); 932 System.exit(1); 933 return; 934 } 935 936 final Object o; 937 try { 938 o = constructor.newInstance(); 939 } catch (Exception e) { 940 e.printStackTrace(); 941 System.exit(1); 942 return; 943 } 944 if (!(o instanceof Provider)) { 945 System.err.println("Not a Provider class: " + providerClassName); 946 System.exit(1); 947 } 948 949 Security.insertProviderAt((Provider) o, 1); 950 } 951 createSignerConfigs( PrivateKey[] privateKeys, X509Certificate[] certificates)952 private static List<DefaultApkSignerEngine.SignerConfig> createSignerConfigs( 953 PrivateKey[] privateKeys, X509Certificate[] certificates) { 954 if (privateKeys.length != certificates.length) { 955 throw new IllegalArgumentException( 956 "The number of private keys must match the number of certificates: " 957 + privateKeys.length + " vs" + certificates.length); 958 } 959 List<DefaultApkSignerEngine.SignerConfig> signerConfigs = new ArrayList<>(); 960 String signerNameFormat = (privateKeys.length == 1) ? "CERT" : "CERT%s"; 961 for (int i = 0; i < privateKeys.length; i++) { 962 String signerName = String.format(Locale.US, signerNameFormat, (i + 1)); 963 DefaultApkSignerEngine.SignerConfig signerConfig = 964 new DefaultApkSignerEngine.SignerConfig.Builder( 965 signerName, 966 privateKeys[i], 967 Collections.singletonList(certificates[i])) 968 .build(); 969 signerConfigs.add(signerConfig); 970 } 971 return signerConfigs; 972 } 973 974 private static class ZipSections { 975 ByteBuffer beforeCentralDir; 976 ByteBuffer centralDir; 977 ByteBuffer eocd; 978 } 979 findMainZipSections(ByteBuffer apk)980 private static ZipSections findMainZipSections(ByteBuffer apk) 981 throws IOException, ZipFormatException { 982 apk.slice(); 983 ApkUtils.ZipSections sections = ApkUtils.findZipSections(DataSources.asDataSource(apk)); 984 long centralDirStartOffset = sections.getZipCentralDirectoryOffset(); 985 long centralDirSizeBytes = sections.getZipCentralDirectorySizeBytes(); 986 long centralDirEndOffset = centralDirStartOffset + centralDirSizeBytes; 987 long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset(); 988 if (centralDirEndOffset != eocdStartOffset) { 989 throw new ZipFormatException( 990 "ZIP Central Directory is not immediately followed by End of Central Directory" 991 + ". CD end: " + centralDirEndOffset 992 + ", EoCD start: " + eocdStartOffset); 993 } 994 apk.position(0); 995 apk.limit((int) centralDirStartOffset); 996 ByteBuffer beforeCentralDir = apk.slice(); 997 998 apk.position((int) centralDirStartOffset); 999 apk.limit((int) centralDirEndOffset); 1000 ByteBuffer centralDir = apk.slice(); 1001 1002 apk.position((int) eocdStartOffset); 1003 apk.limit(apk.capacity()); 1004 ByteBuffer eocd = apk.slice(); 1005 1006 apk.position(0); 1007 apk.limit(apk.capacity()); 1008 1009 ZipSections result = new ZipSections(); 1010 result.beforeCentralDir = beforeCentralDir; 1011 result.centralDir = centralDir; 1012 result.eocd = eocd; 1013 return result; 1014 } 1015 1016 /** 1017 * Returns the API Level corresponding to the APK's minSdkVersion. 1018 * 1019 * @throws MinSdkVersionException if the API Level cannot be determined from the APK. 1020 */ getMinSdkVersion(JarFile apk)1021 private static final int getMinSdkVersion(JarFile apk) throws MinSdkVersionException { 1022 JarEntry manifestEntry = apk.getJarEntry("AndroidManifest.xml"); 1023 if (manifestEntry == null) { 1024 throw new MinSdkVersionException("No AndroidManifest.xml in APK"); 1025 } 1026 byte[] manifestBytes; 1027 try { 1028 try (InputStream manifestIn = apk.getInputStream(manifestEntry)) { 1029 manifestBytes = toByteArray(manifestIn); 1030 } 1031 } catch (IOException e) { 1032 throw new MinSdkVersionException("Failed to read AndroidManifest.xml", e); 1033 } 1034 return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(ByteBuffer.wrap(manifestBytes)); 1035 } 1036 toByteArray(InputStream in)1037 private static byte[] toByteArray(InputStream in) throws IOException { 1038 ByteArrayOutputStream result = new ByteArrayOutputStream(); 1039 byte[] buf = new byte[65536]; 1040 int chunkSize; 1041 while ((chunkSize = in.read(buf)) != -1) { 1042 result.write(buf, 0, chunkSize); 1043 } 1044 return result.toByteArray(); 1045 } 1046 usage()1047 private static void usage() { 1048 System.err.println("Usage: signapk [-w] " + 1049 "[-a <alignment>] " + 1050 "[--align-file-size] " + 1051 "[-providerClass <className>] " + 1052 "[-loadPrivateKeysFromKeyStore <keyStoreName>]" + 1053 "[-keyStorePin <pin>]" + 1054 "[--min-sdk-version <n>] " + 1055 "[--disable-v2] " + 1056 "[--enable-v4] " + 1057 "publickey.x509[.pem] privatekey.pk8 " + 1058 "[publickey2.x509[.pem] privatekey2.pk8 ...] " + 1059 "input.jar output.jar [output-v4-file]"); 1060 System.exit(2); 1061 } 1062 main(String[] args)1063 public static void main(String[] args) { 1064 if (args.length < 4) usage(); 1065 1066 // Install Conscrypt as the highest-priority provider. Its crypto primitives are faster than 1067 // the standard or Bouncy Castle ones. 1068 Security.insertProviderAt(new OpenSSLProvider(), 1); 1069 // Install Bouncy Castle (as the lowest-priority provider) because Conscrypt does not offer 1070 // DSA which may still be needed. 1071 // TODO: Stop installing Bouncy Castle provider once DSA is no longer needed. 1072 Security.addProvider(new BouncyCastleProvider()); 1073 1074 boolean signWholeFile = false; 1075 String providerClass = null; 1076 String keyStoreName = null; 1077 String keyStorePin = null; 1078 int alignment = 4; 1079 boolean alignFileSize = false; 1080 Integer minSdkVersionOverride = null; 1081 boolean signUsingApkSignatureSchemeV2 = true; 1082 boolean signUsingApkSignatureSchemeV4 = false; 1083 SigningCertificateLineage certLineage = null; 1084 Integer rotationMinSdkVersion = null; 1085 1086 int argstart = 0; 1087 while (argstart < args.length && args[argstart].startsWith("-")) { 1088 if ("-w".equals(args[argstart])) { 1089 signWholeFile = true; 1090 ++argstart; 1091 } else if ("-providerClass".equals(args[argstart])) { 1092 if (argstart + 1 >= args.length) { 1093 usage(); 1094 } 1095 providerClass = args[++argstart]; 1096 ++argstart; 1097 } else if ("-loadPrivateKeysFromKeyStore".equals(args[argstart])) { 1098 if (argstart + 1 >= args.length) { 1099 usage(); 1100 } 1101 keyStoreName = args[++argstart]; 1102 ++argstart; 1103 } else if ("-keyStorePin".equals(args[argstart])) { 1104 if (argstart + 1 >= args.length) { 1105 usage(); 1106 } 1107 keyStorePin = args[++argstart]; 1108 ++argstart; 1109 } else if ("-a".equals(args[argstart])) { 1110 alignment = Integer.parseInt(args[++argstart]); 1111 ++argstart; 1112 } else if ("--align-file-size".equals(args[argstart])) { 1113 alignFileSize = true; 1114 ++argstart; 1115 } else if ("--min-sdk-version".equals(args[argstart])) { 1116 String minSdkVersionString = args[++argstart]; 1117 try { 1118 minSdkVersionOverride = Integer.parseInt(minSdkVersionString); 1119 } catch (NumberFormatException e) { 1120 throw new IllegalArgumentException( 1121 "--min-sdk-version must be a decimal number: " + minSdkVersionString); 1122 } 1123 ++argstart; 1124 } else if ("--disable-v2".equals(args[argstart])) { 1125 signUsingApkSignatureSchemeV2 = false; 1126 ++argstart; 1127 } else if ("--enable-v4".equals(args[argstart])) { 1128 signUsingApkSignatureSchemeV4 = true; 1129 ++argstart; 1130 } else if ("--lineage".equals(args[argstart])) { 1131 File lineageFile = new File(args[++argstart]); 1132 try { 1133 certLineage = SigningCertificateLineage.readFromFile(lineageFile); 1134 } catch (Exception e) { 1135 throw new IllegalArgumentException( 1136 "Error reading lineage file: " + e.getMessage()); 1137 } 1138 ++argstart; 1139 } else if ("--rotation-min-sdk-version".equals(args[argstart])) { 1140 String rotationMinSdkVersionString = args[++argstart]; 1141 try { 1142 rotationMinSdkVersion = Integer.parseInt(rotationMinSdkVersionString); 1143 } catch (NumberFormatException e) { 1144 throw new IllegalArgumentException( 1145 "--rotation-min-sdk-version must be a decimal number: " + rotationMinSdkVersionString); 1146 } 1147 ++argstart; 1148 } else { 1149 usage(); 1150 } 1151 } 1152 1153 int numArgsExcludeV4FilePath; 1154 if (signUsingApkSignatureSchemeV4) { 1155 numArgsExcludeV4FilePath = args.length - 1; 1156 } else { 1157 numArgsExcludeV4FilePath = args.length; 1158 } 1159 if ((numArgsExcludeV4FilePath - argstart) % 2 == 1) usage(); 1160 int numKeys = ((numArgsExcludeV4FilePath - argstart) / 2) - 1; 1161 if (signWholeFile && numKeys > 1) { 1162 System.err.println("Only one key may be used with -w."); 1163 System.exit(2); 1164 } 1165 1166 loadProviderIfNecessary(providerClass); 1167 1168 String inputFilename = args[numArgsExcludeV4FilePath - 2]; 1169 String outputFilename = args[numArgsExcludeV4FilePath - 1]; 1170 String outputV4Filename = ""; 1171 if (signUsingApkSignatureSchemeV4) { 1172 outputV4Filename = args[args.length - 1]; 1173 } 1174 1175 JarFile inputJar = null; 1176 FileOutputStream outputFile = null; 1177 1178 try { 1179 File firstPublicKeyFile = new File(args[argstart+0]); 1180 1181 X509Certificate[] publicKey = new X509Certificate[numKeys]; 1182 try { 1183 for (int i = 0; i < numKeys; ++i) { 1184 int argNum = argstart + i*2; 1185 publicKey[i] = readPublicKey(new File(args[argNum])); 1186 } 1187 } catch (IllegalArgumentException e) { 1188 System.err.println(e); 1189 System.exit(1); 1190 } 1191 1192 // Set all ZIP file timestamps to Jan 1 2009 00:00:00. 1193 long timestamp = 1230768000000L; 1194 // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS 1195 // timestamp using the current timezone. We thus adjust the milliseconds since epoch 1196 // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00. 1197 timestamp -= TimeZone.getDefault().getOffset(timestamp); 1198 KeyStore keyStore = null; 1199 if (keyStoreName != null) { 1200 keyStore = createKeyStore(keyStoreName, keyStorePin); 1201 } 1202 PrivateKey[] privateKey = new PrivateKey[numKeys]; 1203 for (int i = 0; i < numKeys; ++i) { 1204 int argNum = argstart + i*2 + 1; 1205 if (keyStore == null) { 1206 privateKey[i] = readPrivateKey(new File(args[argNum])); 1207 } else { 1208 final String keyAlias = args[argNum]; 1209 privateKey[i] = loadPrivateKeyFromKeyStore(keyStore, keyAlias); 1210 } 1211 } 1212 inputJar = new JarFile(new File(inputFilename), false); // Don't verify. 1213 1214 outputFile = new FileOutputStream(outputFilename); 1215 1216 // NOTE: Signing currently recompresses any compressed entries using Deflate (default 1217 // compression level for OTA update files and maximum compession level for APKs). 1218 if (signWholeFile) { 1219 int digestAlgorithm = getDigestAlgorithmForOta(publicKey[0]); 1220 signWholeFile(inputJar, firstPublicKeyFile, 1221 publicKey[0], privateKey[0], digestAlgorithm, 1222 timestamp, 1223 outputFile); 1224 } else { 1225 // Determine the value to use as minSdkVersion of the APK being signed 1226 int minSdkVersion; 1227 if (minSdkVersionOverride != null) { 1228 minSdkVersion = minSdkVersionOverride; 1229 } else { 1230 try { 1231 minSdkVersion = getMinSdkVersion(inputJar); 1232 } catch (MinSdkVersionException e) { 1233 throw new IllegalArgumentException( 1234 "Cannot detect minSdkVersion. Use --min-sdk-version to override", 1235 e); 1236 } 1237 } 1238 1239 DefaultApkSignerEngine.Builder builder = new DefaultApkSignerEngine.Builder( 1240 createSignerConfigs(privateKey, publicKey), minSdkVersion) 1241 .setV1SigningEnabled(true) 1242 .setV2SigningEnabled(signUsingApkSignatureSchemeV2) 1243 .setOtherSignersSignaturesPreserved(false) 1244 .setCreatedBy("1.0 (Android SignApk)"); 1245 1246 if (certLineage != null) { 1247 builder = builder.setSigningCertificateLineage(certLineage); 1248 } 1249 1250 if (rotationMinSdkVersion != null) { 1251 builder = builder.setMinSdkVersionForRotation(rotationMinSdkVersion); 1252 } 1253 1254 try (ApkSignerEngine apkSigner = builder.build()) { 1255 // We don't preserve the input APK's APK Signing Block (which contains v2 1256 // signatures) 1257 apkSigner.inputApkSigningBlock(null); 1258 1259 // Build the output APK in memory, by copying input APK's ZIP entries across 1260 // and then signing the output APK. 1261 ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream(); 1262 CountingOutputStream outputJarCounter = 1263 new CountingOutputStream(v1SignedApkBuf); 1264 JarOutputStream outputJar = new JarOutputStream(outputJarCounter); 1265 // Use maximum compression for compressed entries because the APK lives forever 1266 // on the system partition. 1267 outputJar.setLevel(9); 1268 copyFiles(inputJar, null, apkSigner, outputJar, 1269 outputJarCounter, timestamp, alignment); 1270 ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest = 1271 apkSigner.outputJarEntries(); 1272 if (addV1SignatureRequest != null) { 1273 addV1Signature(apkSigner, addV1SignatureRequest, outputJar, timestamp); 1274 addV1SignatureRequest.done(); 1275 } 1276 outputJar.close(); 1277 ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray()); 1278 v1SignedApkBuf.reset(); 1279 ByteBuffer[] outputChunks = new ByteBuffer[] {v1SignedApk}; 1280 1281 ZipSections zipSections = findMainZipSections(v1SignedApk); 1282 1283 ByteBuffer eocd = ByteBuffer.allocate(zipSections.eocd.remaining()); 1284 eocd.put(zipSections.eocd); 1285 eocd.flip(); 1286 eocd.order(ByteOrder.LITTLE_ENDIAN); 1287 // This loop is supposed to be iterated twice at most. 1288 // The second pass is to align the file size after amending EOCD comments 1289 // with assumption that re-generated signing block would be the same size. 1290 while (true) { 1291 ApkSignerEngine.OutputApkSigningBlockRequest2 addV2SignatureRequest = 1292 apkSigner.outputZipSections2( 1293 DataSources.asDataSource(zipSections.beforeCentralDir), 1294 DataSources.asDataSource(zipSections.centralDir), 1295 DataSources.asDataSource(eocd)); 1296 if (addV2SignatureRequest == null) break; 1297 1298 // Need to insert the returned APK Signing Block before ZIP Central 1299 // Directory. 1300 int padding = addV2SignatureRequest.getPaddingSizeBeforeApkSigningBlock(); 1301 byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock(); 1302 // Because the APK Signing Block is inserted before the Central Directory, 1303 // we need to adjust accordingly the offset of Central Directory inside the 1304 // ZIP End of Central Directory (EoCD) record. 1305 ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining()); 1306 modifiedEocd.put(eocd); 1307 modifiedEocd.flip(); 1308 modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); 1309 ApkUtils.setZipEocdCentralDirectoryOffset( 1310 modifiedEocd, 1311 zipSections.beforeCentralDir.remaining() + padding + 1312 apkSigningBlock.length); 1313 outputChunks = 1314 new ByteBuffer[] { 1315 zipSections.beforeCentralDir, 1316 ByteBuffer.allocate(padding), 1317 ByteBuffer.wrap(apkSigningBlock), 1318 zipSections.centralDir, 1319 modifiedEocd}; 1320 addV2SignatureRequest.done(); 1321 1322 // Exit the loop if we don't need to align the file size 1323 if (!alignFileSize || alignment < 2) { 1324 break; 1325 } 1326 1327 // Calculate the file size 1328 eocd = modifiedEocd; 1329 int fileSize = 0; 1330 for (ByteBuffer buf : outputChunks) { 1331 fileSize += buf.remaining(); 1332 } 1333 // Exit the loop because the file size is aligned. 1334 if (fileSize % alignment == 0) { 1335 break; 1336 } 1337 // Pad EOCD comment to align the file size. 1338 int commentLen = alignment - fileSize % alignment; 1339 modifiedEocd = ByteBuffer.allocate(eocd.remaining() + commentLen); 1340 modifiedEocd.put(eocd); 1341 modifiedEocd.rewind(); 1342 modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); 1343 ApkUtils.updateZipEocdCommentLen(modifiedEocd); 1344 // Since V2 signing block should cover modified EOCD, 1345 // re-iterate the loop with modified EOCD. 1346 eocd = modifiedEocd; 1347 } 1348 1349 // This assumes outputChunks are array-backed. To avoid this assumption, the 1350 // code could be rewritten to use FileChannel. 1351 for (ByteBuffer outputChunk : outputChunks) { 1352 outputFile.write( 1353 outputChunk.array(), 1354 outputChunk.arrayOffset() + outputChunk.position(), 1355 outputChunk.remaining()); 1356 outputChunk.position(outputChunk.limit()); 1357 } 1358 1359 outputFile.close(); 1360 outputFile = null; 1361 apkSigner.outputDone(); 1362 1363 if (signUsingApkSignatureSchemeV4) { 1364 final DataSource outputApkIn = DataSources.asDataSource( 1365 new RandomAccessFile(new File(outputFilename), "r")); 1366 final File outputV4File = new File(outputV4Filename); 1367 apkSigner.signV4(outputApkIn, outputV4File, false /* ignore failures */); 1368 } 1369 } 1370 1371 return; 1372 } 1373 } catch (Exception e) { 1374 e.printStackTrace(); 1375 System.exit(1); 1376 } finally { 1377 try { 1378 if (inputJar != null) inputJar.close(); 1379 if (outputFile != null) outputFile.close(); 1380 } catch (IOException e) { 1381 e.printStackTrace(); 1382 System.exit(1); 1383 } 1384 } 1385 } 1386 } 1387