/** Write the signature file to the given output stream. */ private void generateSignatureFile(Manifest manifest, OutputStream out) throws IOException, GeneralSecurityException { out.write(("Signature-Version: 1.0\r\n").getBytes()); out.write(("Created-By: 1.0 (Android SignApk)\r\n").getBytes()); // BASE64Encoder base64 = new BASE64Encoder(); MessageDigest md = MessageDigest.getInstance("SHA1"); PrintStream print = new PrintStream(new DigestOutputStream(new ByteArrayOutputStream(), md), true, "UTF-8"); // Digest of the entire manifest manifest.write(print); print.flush(); out.write(("SHA1-Digest-Manifest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes()); Map<String, Attributes> entries = manifest.getEntries(); for (Map.Entry<String, Attributes> entry : entries.entrySet()) { if (canceled) break; progressHelper.progress(ProgressEvent.PRORITY_NORMAL, "Generating signature file"); // Digest of the manifest stanza for this entry. String nameEntry = "Name: " + entry.getKey() + "\r\n"; print.print(nameEntry); for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) { print.print(att.getKey() + ": " + att.getValue() + "\r\n"); } print.print("\r\n"); print.flush(); out.write(nameEntry.getBytes()); out.write(("SHA1-Digest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes()); } }
/** Add the SHA1 of every file to the manifest, creating it if necessary. */ private Manifest addDigestsToManifest(Map<String, ZioEntry> entries) throws IOException, GeneralSecurityException { Manifest input = null; ZioEntry manifestEntry = entries.get(JarFile.MANIFEST_NAME); if (manifestEntry != null) { input = new Manifest(); input.read(manifestEntry.getInputStream()); } Manifest output = new Manifest(); Attributes main = output.getMainAttributes(); if (input != null) { main.putAll(input.getMainAttributes()); } else { main.putValue("Manifest-Version", "1.0"); main.putValue("Created-By", "1.0 (Android SignApk)"); } // BASE64Encoder base64 = new BASE64Encoder(); MessageDigest md = MessageDigest.getInstance("SHA1"); byte[] buffer = new byte[512]; int num; // We sort the input entries by name, and add them to the // output manifest in sorted order. We expect that the output // map will be deterministic. TreeMap<String, ZioEntry> byName = new TreeMap<String, ZioEntry>(); byName.putAll(entries); boolean debug = getLogger().isDebugEnabled(); if (debug) getLogger().debug("Manifest entries:"); for (ZioEntry entry : byName.values()) { if (canceled) break; String name = entry.getName(); if (debug) getLogger().debug(name); if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) && !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) && (stripPattern == null || !stripPattern.matcher(name).matches())) { progressHelper.progress(ProgressEvent.PRORITY_NORMAL, "Generating manifest"); InputStream data = entry.getInputStream(); while ((num = data.read(buffer)) > 0) { md.update(buffer, 0, num); } Attributes attr = null; if (input != null) { java.util.jar.Attributes inAttr = input.getAttributes(name); if (inAttr != null) attr = new Attributes(inAttr); } if (attr == null) attr = new Attributes(); attr.putValue("SHA1-Digest", Base64.encode(md.digest())); output.getEntries().put(name, attr); } } return output; }
// Loads one of the built-in keys (media, platform, shared, testkey) public void loadKeys(String name) throws IOException, GeneralSecurityException { keySet = loadedKeys.get(name); if (keySet != null) return; keySet = new KeySet(); keySet.setName(name); loadedKeys.put(name, keySet); if (KEY_NONE.equals(name)) return; progressHelper.progress(ProgressEvent.PRORITY_IMPORTANT, "Loading certificate and private key"); // load the private key URL privateKeyUrl = getClass().getResource("/keys/" + name + ".pk8"); keySet.setPrivateKey(readPrivateKey(privateKeyUrl, null)); // load the certificate URL publicKeyUrl = getClass().getResource("/keys/" + name + ".x509.pem"); keySet.setPublicKey(readPublicKey(publicKeyUrl)); // load the signature block template URL sigBlockTemplateUrl = getClass().getResource("/keys/" + name + ".sbt"); if (sigBlockTemplateUrl != null) { keySet.setSigBlockTemplate(readContentAsBytes(sigBlockTemplateUrl)); } }
/** Copy all the files from input to output. */ private void copyFiles(Map<String, ZioEntry> input, ZipOutput output) throws IOException { int i = 1; for (ZioEntry inEntry : input.values()) { if (canceled) break; progressHelper.progress( ProgressEvent.PRORITY_NORMAL, String.format("Copying zip entry %d of %d", i, input.size())); i += 1; output.write(inEntry); } }
/** * Sign the file using the given public key cert, private key, and signature block template. The * signature block template parameter may be null, but if so android-sun-jarsign-support.jar must * be in the classpath. */ public void signZip(String inputZipFilename, String outputZipFilename) throws IOException, GeneralSecurityException { File inFile = new File(inputZipFilename).getCanonicalFile(); File outFile = new File(outputZipFilename).getCanonicalFile(); if (inFile.equals(outFile)) { throw new IllegalArgumentException( "Input and output files are the same. Specify a different name for the output."); } progressHelper.initProgress(); progressHelper.progress( ProgressEvent.PRORITY_IMPORTANT, "Parsing the input's central directory"); ZipInput input = ZipInput.read(inputZipFilename); signZip(input.getEntries(), new FileOutputStream(outputZipFilename), outputZipFilename); }
/** * Copy all the files in a manifest from input to output. We set the modification times in the * output to a fixed time, so as to reduce variation in the output file and make incremental OTAs * more efficient. */ private void copyFiles( Manifest manifest, Map<String, ZioEntry> input, ZipOutput output, long timestamp) throws IOException { Map<String, Attributes> entries = manifest.getEntries(); List<String> names = new ArrayList<String>(entries.keySet()); Collections.sort(names); int i = 1; for (String name : names) { if (canceled) break; progressHelper.progress( ProgressEvent.PRORITY_NORMAL, String.format("Copying zip entry %d of %d", i, names.size())); i += 1; ZioEntry inEntry = input.get(name); inEntry.setTime(timestamp); output.write(inEntry); } }
/** * Sign the and signature block template. The signature block template parameter may be null, but * if so android-sun-jarsign-support.jar must be in the classpath. */ public void signZip( Map<String, ZioEntry> zioEntries, OutputStream outputStream, String outputZipFilename) throws IOException, GeneralSecurityException { boolean debug = getLogger().isDebugEnabled(); progressHelper.initProgress(); if (keySet == null) { if (!keymode.startsWith(MODE_AUTO)) throw new IllegalStateException("No keys configured for signing the file!"); // Auto-determine which keys to use String keyName = this.autoDetectKey(keymode, zioEntries); if (keyName == null) throw new AutoKeyException( "Unable to auto-select key for signing " + new File(outputZipFilename).getName()); autoKeyObservable.notifyObservers(keyName); loadKeys(keyName); } ZipOutput zipOutput = null; try { zipOutput = new ZipOutput(outputStream); if (KEY_NONE.equals(keySet.getName())) { progressHelper.setProgressTotalItems(zioEntries.size()); progressHelper.setProgressCurrentItem(0); copyFiles(zioEntries, zipOutput); return; } // Calculate total steps to complete for accurate progress percentages. int progressTotalItems = 0; for (ZioEntry entry : zioEntries.values()) { String name = entry.getName(); if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) && !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) && (stripPattern == null || !stripPattern.matcher(name).matches())) { progressTotalItems += 3; // digest for manifest, digest in sig file, copy data } } progressTotalItems += 1; // CERT.RSA generation progressHelper.setProgressTotalItems(progressTotalItems); progressHelper.setProgressCurrentItem(0); // Assume the certificate is valid for at least an hour. long timestamp = keySet.getPublicKey().getNotBefore().getTime() + 3600L * 1000; // MANIFEST.MF // progress(ProgressEvent.PRORITY_NORMAL, JarFile.MANIFEST_NAME); Manifest manifest = addDigestsToManifest(zioEntries); if (canceled) return; ZioEntry ze = new ZioEntry(JarFile.MANIFEST_NAME); ze.setTime(timestamp); manifest.write(ze.getOutputStream()); zipOutput.write(ze); // CERT.SF // progress( ProgressEvent.PRORITY_NORMAL, CERT_SF_NAME); // Can't use default Signature on Android. Although it generates a signature that can be // verified by jarsigner, // the recovery program appears to require a specific algorithm/mode/padding. So we use the // custom ZipSignature instead. // Signature signature = Signature.getInstance("SHA1withRSA"); ZipSignature signature = new ZipSignature(); signature.initSign(keySet.getPrivateKey()); // if (getLogger().isDebugEnabled()) { // getLogger().debug(String.format("Signature provider=%s, alg=%s, class=%s", // signature.getProvider().getName(), // signature.getAlgorithm(), // signature.getClass().getName())); // } ze = new ZioEntry(CERT_SF_NAME); ze.setTime(timestamp); ByteArrayOutputStream out = new ByteArrayOutputStream(); generateSignatureFile(manifest, out); if (canceled) return; byte[] sfBytes = out.toByteArray(); if (debug) { getLogger() .debug( "Signature File: \n" + new String(sfBytes) + "\n" + HexDumpEncoder.encode(sfBytes)); } ze.getOutputStream().write(sfBytes); zipOutput.write(ze); signature.update(sfBytes); byte[] signatureBytes = signature.sign(); if (debug) { MessageDigest md = MessageDigest.getInstance("SHA1"); md.update(sfBytes); byte[] sfDigest = md.digest(); getLogger().debug("Sig File SHA1: \n" + HexDumpEncoder.encode(sfDigest)); getLogger().debug("Signature: \n" + HexDumpEncoder.encode(signatureBytes)); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, keySet.getPublicKey()); byte[] tmpData = cipher.doFinal(signatureBytes); getLogger().debug("Signature Decrypted: \n" + HexDumpEncoder.encode(tmpData)); // getLogger().debug( "SHA1 ID: \n" + // HexDumpEncoder.encode(AlgorithmId.get("SHA1").encode())); // // Compute signature using low-level APIs. // byte[] beforeAlgorithmIdBytes = { 0x30, 0x21 }; // // byte[] algorithmIdBytes = {0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E, 0x03, // 0x02, 0x1A, 0x05, 0x00 }; // byte[] algorithmIdBytes = AlgorithmId.get("SHA1").encode(); // byte[] afterAlgorithmIdBytes = { 0x04, 0x14 }; // cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); // cipher.init(Cipher.ENCRYPT_MODE, privateKey); // getLogger().debug( "Cipher: " + cipher.getAlgorithm() + ", blockSize = " + // cipher.getBlockSize()); // // cipher.update( beforeAlgorithmIdBytes); // cipher.update( algorithmIdBytes); // cipher.update( afterAlgorithmIdBytes); // cipher.update( sfDigest); // byte[] tmpData2 = cipher.doFinal(); // getLogger().debug( "Signature : \n" + HexDumpEncoder.encode(tmpData2)); } // CERT.RSA progressHelper.progress(ProgressEvent.PRORITY_NORMAL, "Generating signature block file"); ze = new ZioEntry(CERT_RSA_NAME); ze.setTime(timestamp); writeSignatureBlock( keySet.getSigBlockTemplate(), signatureBytes, keySet.getPublicKey(), ze.getOutputStream()); zipOutput.write(ze); if (canceled) return; // Everything else copyFiles(manifest, zioEntries, zipOutput, timestamp); if (canceled) return; } finally { zipOutput.close(); if (canceled) { try { if (outputZipFilename != null) new File(outputZipFilename).delete(); } catch (Throwable t) { getLogger().warning(t.getClass().getName() + ":" + t.getMessage()); } } } }