proguard.io.SignedJarWriter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of proguard-core Show documentation
Show all versions of proguard-core Show documentation
ProGuardCORE is a free library to read, analyze, modify, and write Java class files.
/*
* ProGuardCORE -- library to process Java bytecode.
*
* Copyright (c) 2002-2020 Guardsquare NV
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package proguard.io;
import java.io.*;
import java.security.*;
import java.security.cert.X509Certificate;
import proguard.util.*;
/**
* This {@link JarWriter} sends data entries to a given jar file, automatically adding a manifest
* file and signing it with JAR signature scheme v1.
*
* You'll typically wrap a {@link ZipWriter} or one of its extensions:
*
*
* new SignedApkWriter(privateKeyEntry, new ZipWriter(...))
*
*
* @author Eric Lafortune
*/
public class SignedJarWriter extends JarWriter {
/* JDK 9 modules no longer export the internal sun.security API.
private static final boolean SUN_SECURITY_SIGNING = System.getProperty("sun.security.signing") != null;
//*/
private final KeyStore.PrivateKeyEntry privateKeyEntry;
private final int[] apkSignatureSchemeIDs;
private MessageDigest globalManifestDigest;
private MessageDigest manifestEntryDigest;
private Signature digitalSignature;
private OutputStream signatureFileStream;
private PrintWriter signatureWriter1;
private PrintWriter signatureWriter2;
private ByteArrayOutputStream signatureResultStream1;
private ByteArrayOutputStream signatureResultStream2;
private OutputStream digitalSignatureFileStream;
/**
* Creates a new SignedJarWriter.
*
* @param privateKeyEntry the private key to be used for signing the zip entries.
* @param zipEntryWriter the data entry writer that can provide output streams for the jar
* entries.
*/
public SignedJarWriter(KeyStore.PrivateKeyEntry privateKeyEntry, DataEntryWriter zipEntryWriter) {
this(privateKeyEntry, new String[] {DEFAULT_DIGEST_ALGORITHM}, null, zipEntryWriter);
}
/**
* Creates a new SignedJarWriter with the given settings.
*
* @param privateKeyEntry the private key to be used for signing the zip entries.
* @param digestAlgorithms the manifest digest algorithms, e.g. "SHA-256".
* @param creator the creator to mention in the manifest file.
* @param zipEntryWriter the data entry writer that can provide output streams for the jar
* entries.
*/
public SignedJarWriter(
KeyStore.PrivateKeyEntry privateKeyEntry,
String[] digestAlgorithms,
String creator,
DataEntryWriter zipEntryWriter) {
this(privateKeyEntry, new String[] {DEFAULT_DIGEST_ALGORITHM}, creator, null, zipEntryWriter);
}
/**
* Creates a new SignedJarWriter with the given settings.
*
* @param privateKeyEntry the private key to be used for signing the zip entries.
* @param digestAlgorithms the manifest digest algorithms, e.g. "SHA-256".
* @param creator the creator to mention in the manifest file.
* @param apkSignatureSchemeIDs an optional list of external APK signature scheme IDs to be
* specified with the attribute "X-Android-APK-Signed" in *.SF files.
* @param zipEntryWriter the data entry writer that can provide output streams for the jar
* entries.
*/
public SignedJarWriter(
KeyStore.PrivateKeyEntry privateKeyEntry,
String[] digestAlgorithms,
String creator,
int[] apkSignatureSchemeIDs,
DataEntryWriter zipEntryWriter) {
super(digestAlgorithms, creator, zipEntryWriter);
this.privateKeyEntry = privateKeyEntry;
this.apkSignatureSchemeIDs = apkSignatureSchemeIDs;
try {
PrivateKey privateKey = privateKeyEntry.getPrivateKey();
// We have a digest algorithm (SHA1, SHA-256, SHA-384, SHA-512)
// and an encryption algorithm (RSA, DSA, EC).
// Derive the signature algorithm (SHA1withRSA,
// SHA256withDSA, SHA256withECDSA,...)
String encryptionAlgorithm = privateKey.getAlgorithm();
String digestAlgorithm = digestAlgorithms[0];
String signatureAlgorithm =
digestAlgorithm.replace("-", "")
+ "with"
+ (encryptionAlgorithm.equals("EC") ? "ECDSA" : encryptionAlgorithm);
// We need a global digest of the entire manifest,
// and small digests of the individual entries.
globalManifestDigest = MessageDigest.getInstance(digestAlgorithm);
manifestEntryDigest = MessageDigest.getInstance(digestAlgorithm);
digitalSignature = Signature.getInstance(signatureAlgorithm);
digitalSignature.initSign(privateKey);
} catch (Exception e) {
throw new UnsupportedOperationException(e.getMessage(), e);
}
}
// Implementations for DataEntryWriter.
@Override
public OutputStream createOutputStream(DataEntry dataEntry) throws IOException {
// Get the output stream, with prepared manifest and digest entries.
OutputStream outputStream = super.createOutputStream(dataEntry);
// Digest the output stream (if any), except for the signature itself
// (when the signature writer isn't open yet).
return outputStream == null || signatureWriter2 == null
? null
: new MyMultiDigestOutputStream(
dataEntry.getName(),
new MessageDigest[] {manifestEntryDigest},
signatureWriter2,
outputStream);
}
@Override
public void println(PrintWriter pw, String prefix) {
pw.println(prefix + "SignedJarWriter");
zipEntryWriter.println(pw, prefix + " ");
}
// Overriding methods for JarWriter.
@Override
protected void openManifestFiles() throws IOException {
// Open the manifest file.
super.openManifestFiles();
// Open the signature file.
signatureFileStream =
manifestEntryWriter.createOutputStream(
new RenamedDataEntry(currentManifestEntry, "META-INF/CERT.SF"));
if (signatureFileStream != null) {
String encryptionAlgorithm = privateKeyEntry.getPrivateKey().getAlgorithm();
// Open the signature file.
digitalSignatureFileStream =
manifestEntryWriter.createOutputStream(
new RenamedDataEntry(currentManifestEntry, "META-INF/CERT." + encryptionAlgorithm));
// Digest the above main manifest section for the signature file.
signatureResultStream1 = new ByteArrayOutputStream();
signatureWriter1 = printWriter(signatureResultStream1);
signatureWriter1.println("Signature-Version: 1.0");
if (apkSignatureSchemeIDs != null) {
signatureWriter1.print("X-Android-APK-Signed: ");
for (int index = 0; index < apkSignatureSchemeIDs.length; index++) {
signatureWriter1.print(apkSignatureSchemeIDs[index]);
if (index < apkSignatureSchemeIDs.length - 1) {
signatureWriter1.print(',');
}
}
signatureWriter1.println();
}
if (creator != null) {
signatureWriter1.println("Created-By: " + creator);
}
signatureWriter1.println(
manifestEntryDigest.getAlgorithm()
+ "-Digest-Manifest-Main-Attributes: "
+ Base64Util.encode(manifestEntryDigest.digest()));
// We'll have to append the digest of the entire manifest file
// when we have collected it.
// Start digesting the file sections in a separate writer.
signatureResultStream2 = new ByteArrayOutputStream();
signatureWriter2 = printWriter(signatureResultStream2);
}
}
@Override
protected OutputStream createManifestOutputStream(DataEntry manifestEntry) throws IOException {
// Create the standard manifest stream.
OutputStream manifestOutputStream = super.createManifestOutputStream(manifestEntry);
if (manifestOutputStream == null) {
return null;
}
// We need a global digest of the entire manifest,
// and small digests of the individual entries,
// so we wrap the manifest stream with two digest streams.
return new DigestOutputStream(
new DigestOutputStream(manifestOutputStream, globalManifestDigest), manifestEntryDigest);
}
@Override
protected void finish() throws IOException {
// Finish the manifest file.
super.finish();
if (digitalSignatureFileStream != null) {
String digestAlgorithm = globalManifestDigest.getAlgorithm();
// Add the global manifest digest after the main section in the
// signature.
signatureWriter1.println(
digestAlgorithm
+ "-Digest-Manifest: "
+ Base64Util.encode(globalManifestDigest.digest()));
signatureWriter1.println();
signatureWriter1.flush();
// Work around a bug that is reported in the source code of the
// Android SDK class SignedJarBuilder.java. Up to version 1.6, the
// java.util.jar run-time throws a spurious IOException if the
// length of the signature file is a multiple of 1024 bytes.
if ((signatureResultStream1.size() + signatureResultStream2.size()) % 1024 == 0) {
signatureWriter2.println();
signatureWriter2.flush();
}
// Get the contents of the signature file (main section and file
// sections) and write them out to META-INF/CERT.SF
byte[] signatureBytes1 = signatureResultStream1.toByteArray();
byte[] signatureBytes2 = signatureResultStream2.toByteArray();
signatureFileStream.write(signatureBytes1);
signatureFileStream.write(signatureBytes2);
signatureFileStream.close();
// Get the contents of the signature file, sign them digitally and
// write them out to META-INF/CERT.RSA (or similar).
try {
// Get the digitally signed contents.
digitalSignature.update(signatureBytes1);
digitalSignature.update(signatureBytes2);
byte[] digitalSignatureBytes = digitalSignature.sign();
// Get the certificate.
X509Certificate certificate = (X509Certificate) privateKeyEntry.getCertificate();
// Write them out in a PKCS7 container.
String encryptionAlgorithm = privateKeyEntry.getPrivateKey().getAlgorithm();
/* JDK 9 modules no longer export the internal sun.security API.
if (SUN_SECURITY_SIGNING)
{
// Using the internal sun.security API.
SignerInfo signerInfo =
new SignerInfo(new X500Name(certificate.getIssuerX500Principal().getName()),
certificate.getSerialNumber(),
AlgorithmId.get(digestAlgorithm),
AlgorithmId.get(encryptionAlgorithm),
digitalSignatureBytes);
PKCS7 pkcs7 =
new PKCS7(new AlgorithmId[] { AlgorithmId.get(digestAlgorithm) },
new ContentInfo(ContentInfo.DATA_OID, null),
new X509Certificate[] { certificate },
new SignerInfo[] { signerInfo });
pkcs7.encodeSignedData(digitalSignatureFileStream);
digitalSignatureFileStream.close();
}
else
//*/
{
// Using our own implementation.
PKCS7OutputStream pkcs7OutputStream =
new PKCS7OutputStream(new DEROutputStream(digitalSignatureFileStream));
pkcs7OutputStream.writeSignature(
certificate, digestAlgorithm, encryptionAlgorithm, digitalSignatureBytes);
pkcs7OutputStream.close();
}
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new UnsupportedOperationException(e.getMessage(), e);
}
signatureFileStream = null;
signatureWriter1 = null;
signatureWriter2 = null;
signatureResultStream1 = null;
signatureResultStream2 = null;
digitalSignatureFileStream = null;
}
}
/**
* Provides a simple test for this class, creating a signed apk file (only v1) with the given name
* and a few aligned/compressed/uncompressed zip entries. Arguments: keystore keystore_password
* alias password jar_filename Create a keystore with: keytool -genkey -dname 'CN=John Doe,
* OU=Development, O=Guardsquare, STREET=Tervuursevest, L=Leuven, ST=Brabant, C=Belgium' -keystore
* /tmp/test.keystore -storepass android -alias AndroidDebugKey -keypass android -keyalg RSA
* -keysize 512 List the contents of the keystore with keytool -v -list -keystore
* /tmp/test.keystore -alias androiddebugkey -storepass android Verify the signed jar with:
* jarsigner -verify -verbose /tmp/test.jar or with: android-sdk/build-tools/24.0.3/apksigner
* verify --min-sdk-version 19 -v /tmp/test.jar
*/
public static void main(String[] args) {
try {
// Get the arguments.
String keyStoreFileName = args[0];
String keyStorePassword = args[1];
String keyAlias = args[2];
String keyPassword = args[3];
String jarFileName = args[4];
// Get the private key from the key store.
FileInputStream keyStoreInputStream = new FileInputStream(keyStoreFileName);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(keyStoreInputStream, keyStorePassword.toCharArray());
keyStoreInputStream.close();
KeyStore.ProtectionParameter protectionParameter =
new KeyStore.PasswordProtection(keyPassword.toCharArray());
KeyStore.PrivateKeyEntry privateKeyEntry =
(KeyStore.PrivateKeyEntry) keyStore.getEntry(keyAlias, protectionParameter);
// Start writing out the signed jar file.
DataEntryWriter writer =
new SignedJarWriter(
privateKeyEntry,
new String[] {DEFAULT_DIGEST_ALGORITHM},
"ProGuard",
null,
new ZipWriter(
new FixedStringMatcher("file1.txt"),
4,
false,
0,
new FixedFileWriter(new File(jarFileName))));
PrintWriter printWriter =
new PrintWriter(
writer.createOutputStream(new DummyDataEntry(null, "file1.txt", 0L, false)));
printWriter.println("Hello, world!");
printWriter.close();
printWriter =
new PrintWriter(
writer.createOutputStream(new DummyDataEntry(null, "file2.txt", 0L, false)));
printWriter.println("Hello again, world!");
printWriter.close();
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy