diff --git a/tools/apksigner/Android.mk b/tools/apksigner/Android.mk new file mode 100644 index 0000000000..a7b4414c5f --- /dev/null +++ b/tools/apksigner/Android.mk @@ -0,0 +1,19 @@ +# +# Copyright (C) 2016 The Android Open Source Project +# +# 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. +# + +LOCAL_PATH := $(call my-dir) + +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tools/apksigner/core/Android.mk b/tools/apksigner/core/Android.mk new file mode 100644 index 0000000000..c86208be97 --- /dev/null +++ b/tools/apksigner/core/Android.mk @@ -0,0 +1,26 @@ +# +# Copyright (C) 2016 The Android Open Source Project +# +# 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. +# +LOCAL_PATH := $(call my-dir) + +# apksigner library, for signing APKs and verification signatures of APKs +# ============================================================ +include $(CLEAR_VARS) +LOCAL_MODULE := apksigner-core +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_JAVA_LIBRARIES = \ + bouncycastle-host \ + bouncycastle-bcpkix-host +include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java new file mode 100644 index 0000000000..71e698b99b --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/DigestAlgorithm.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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 com.android.apksigner.core.internal.apk.v1; + +/** + * Digest algorithm used with JAR signing (aka v1 signing scheme). + */ +public enum DigestAlgorithm { + /** SHA-1 */ + SHA1("SHA-1"), + + /** SHA2-256 */ + SHA256("SHA-256"); + + private final String mJcaMessageDigestAlgorithm; + + private DigestAlgorithm(String jcaMessageDigestAlgoritm) { + mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm; + } + + /** + * Returns the {@link java.security.MessageDigest} algorithm represented by this digest + * algorithm. + */ + String getJcaMessageDigestAlgorithm() { + return mJcaMessageDigestAlgorithm; + } +} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java new file mode 100644 index 0000000000..b99cdeccc1 --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java @@ -0,0 +1,526 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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 com.android.apksigner.core.internal.apk.v1; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.DEROutputStream; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; + +import com.android.apksigner.core.internal.jar.ManifestWriter; +import com.android.apksigner.core.internal.jar.SignatureFileWriter; +import com.android.apksigner.core.internal.util.Pair; + +/** + * APK signer which uses JAR signing (aka v1 signing scheme). + * + * @see Signed JAR File + */ +public abstract class V1SchemeSigner { + + public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; + + private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY = + new Attributes.Name("Created-By"); + private static final String ATTRIBUTE_DEFALT_VALUE_CREATED_BY = "1.0 (Android apksigner)"; + private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0"; + private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0"; + + private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME = + new Attributes.Name("X-Android-APK-Signed"); + + /** + * Signer configuration. + */ + public static class SignerConfig { + /** Name. */ + public String name; + + /** Private key. */ + public PrivateKey privateKey; + + /** + * Certificates, with the first certificate containing the public key corresponding to + * {@link #privateKey}. + */ + public List certificates; + + /** + * Digest algorithm used for the signature. + */ + public DigestAlgorithm signatureDigestAlgorithm; + + /** + * Digest algorithm used for digests of JAR entries and MANIFEST.MF. + */ + public DigestAlgorithm contentDigestAlgorithm; + } + + /** Hidden constructor to prevent instantiation. */ + private V1SchemeSigner() {} + + /** + * Gets the JAR signing digest algorithm to be used for signing an APK using the provided key. + * + * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see + * AndroidManifest.xml minSdkVersion attribute) + * + * @throws InvalidKeyException if the provided key is not suitable for signing APKs using + * JAR signing (aka v1 signature scheme) + */ + public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm( + PublicKey signingKey, int minSdkVersion) throws InvalidKeyException { + String keyAlgorithm = signingKey.getAlgorithm(); + if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + // Prior to API Level 18, only SHA-1 can be used with RSA. + if (minSdkVersion < 18) { + return DigestAlgorithm.SHA1; + } + return DigestAlgorithm.SHA256; + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + // Prior to API Level 21, only SHA-1 can be used with DSA + if (minSdkVersion < 21) { + return DigestAlgorithm.SHA1; + } else { + return DigestAlgorithm.SHA256; + } + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + if (minSdkVersion < 18) { + throw new InvalidKeyException( + "ECDSA signatures only supported for minSdkVersion 18 and higher"); + } + // Prior to API Level 21, only SHA-1 can be used with ECDSA + if (minSdkVersion < 21) { + return DigestAlgorithm.SHA1; + } else { + return DigestAlgorithm.SHA256; + } + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } + + /** + * Returns the JAR signing digest algorithm to be used for JAR entry digests. + * + * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see + * AndroidManifest.xml minSdkVersion attribute) + */ + public static DigestAlgorithm getSuggestedContentDigestAlgorithm(int minSdkVersion) { + return (minSdkVersion >= 18) ? DigestAlgorithm.SHA256 : DigestAlgorithm.SHA1; + } + + /** + * Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm. + */ + public static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm) { + String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm(); + try { + return MessageDigest.getInstance(jcaAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to obtain " + jcaAlgorithm + " MessageDigest", e); + } + } + + /** + * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's + * manifest. + */ + public static boolean isJarEntryDigestNeededInManifest(String entryName) { + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File + + // Entries outside of META-INF must be listed in the manifest. + if (!entryName.startsWith("META-INF/")) { + return true; + } + // Entries in subdirectories of META-INF must be listed in the manifest. + if (entryName.indexOf('/', "META-INF/".length()) != -1) { + return true; + } + + // Ignored file names (case-insensitive) in META-INF directory: + // MANIFEST.MF + // *.SF + // *.RSA + // *.DSA + // *.EC + // SIG-* + String fileNameLowerCase = + entryName.substring("META-INF/".length()).toLowerCase(Locale.US); + if (("manifest.mf".equals(fileNameLowerCase)) + || (fileNameLowerCase.endsWith(".sf")) + || (fileNameLowerCase.endsWith(".rsa")) + || (fileNameLowerCase.endsWith(".dsa")) + || (fileNameLowerCase.endsWith(".ec")) + || (fileNameLowerCase.startsWith("sig-"))) { + return false; + } + return true; + } + + /** + * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of + * JAR entries which need to be added to the APK as part of the signature. + * + * @param signerConfigs signer configurations, one for each signer. At least one signer config + * must be provided. + * + * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or + * cannot be used in general + * @throws SignatureException if an error occurs when computing digests of generating + * signatures + */ + public static List> sign( + List signerConfigs, + DigestAlgorithm jarEntryDigestAlgorithm, + Map jarEntryDigests, + List apkSigningSchemeIds, + byte[] sourceManifestBytes) + throws InvalidKeyException, CertificateEncodingException, SignatureException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + OutputManifestFile manifest = + generateManifestFile(jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes); + + return signManifest(signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, manifest); + } + + /** + * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of + * JAR entries which need to be added to the APK as part of the signature. + * + * @param signerConfigs signer configurations, one for each signer. At least one signer config + * must be provided. + * + * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or + * cannot be used in general + * @throws SignatureException if an error occurs when computing digests of generating + * signatures + */ + public static List> signManifest( + List signerConfigs, + DigestAlgorithm digestAlgorithm, + List apkSigningSchemeIds, + OutputManifestFile manifest) + throws InvalidKeyException, CertificateEncodingException, SignatureException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + + // For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF. + List> signatureJarEntries = + new ArrayList<>(2 * signerConfigs.size() + 1); + byte[] sfBytes = + generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, manifest); + for (SignerConfig signerConfig : signerConfigs) { + String signerName = signerConfig.name; + byte[] signatureBlock; + try { + signatureBlock = generateSignatureBlock(signerConfig, sfBytes); + } catch (InvalidKeyException e) { + throw new InvalidKeyException( + "Failed to sign using signer \"" + signerName + "\"", e); + } catch (CertificateEncodingException e) { + throw new CertificateEncodingException( + "Failed to sign using signer \"" + signerName + "\"", e); + } catch (SignatureException e) { + throw new SignatureException( + "Failed to sign using signer \"" + signerName + "\"", e); + } + signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes)); + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + String signatureBlockFileName = + "META-INF/" + signerName + "." + + publicKey.getAlgorithm().toUpperCase(Locale.US); + signatureJarEntries.add( + Pair.of(signatureBlockFileName, signatureBlock)); + } + signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents)); + return signatureJarEntries; + } + + /** + * Returns the names of JAR entries which this signer will produce as part of v1 signature. + */ + public static Set getOutputEntryNames(List signerConfigs) { + Set result = new HashSet<>(2 * signerConfigs.size() + 1); + for (SignerConfig signerConfig : signerConfigs) { + String signerName = signerConfig.name; + result.add("META-INF/" + signerName + ".SF"); + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + String signatureBlockFileName = + "META-INF/" + signerName + "." + + publicKey.getAlgorithm().toUpperCase(Locale.US); + result.add(signatureBlockFileName); + } + result.add(MANIFEST_ENTRY_NAME); + return result; + } + + /** + * Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional) + * input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest. + */ + public static OutputManifestFile generateManifestFile( + DigestAlgorithm jarEntryDigestAlgorithm, + Map jarEntryDigests, + byte[] sourceManifestBytes) { + Manifest sourceManifest = null; + if (sourceManifestBytes != null) { + try { + sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes)); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to parse source MANIFEST.MF", e); + } + } + ByteArrayOutputStream manifestOut = new ByteArrayOutputStream(); + Attributes mainAttrs = new Attributes(); + // Copy the main section from the source manifest (if provided). Otherwise use defaults. + if (sourceManifest != null) { + mainAttrs.putAll(sourceManifest.getMainAttributes()); + } else { + mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION); + mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY); + } + + try { + ManifestWriter.writeMainSection(manifestOut, mainAttrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e); + } + + List sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet()); + Collections.sort(sortedEntryNames); + SortedMap invidualSectionsContents = new TreeMap<>(); + String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm); + for (String entryName : sortedEntryNames) { + byte[] entryDigest = jarEntryDigests.get(entryName); + Attributes entryAttrs = new Attributes(); + entryAttrs.putValue( + entryDigestAttributeName, + Base64.getEncoder().encodeToString(entryDigest)); + ByteArrayOutputStream sectionOut = new ByteArrayOutputStream(); + byte[] sectionBytes; + try { + ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs); + sectionBytes = sectionOut.toByteArray(); + manifestOut.write(sectionBytes); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e); + } + invidualSectionsContents.put(entryName, sectionBytes); + } + + OutputManifestFile result = new OutputManifestFile(); + result.contents = manifestOut.toByteArray(); + result.mainSectionAttributes = mainAttrs; + result.individualSectionsContents = invidualSectionsContents; + return result; + } + + public static class OutputManifestFile { + public byte[] contents; + public SortedMap individualSectionsContents; + public Attributes mainSectionAttributes; + } + + private static byte[] generateSignatureFile( + List apkSignatureSchemeIds, + DigestAlgorithm manifestDigestAlgorithm, + OutputManifestFile manifest) { + Manifest sf = new Manifest(); + Attributes mainAttrs = sf.getMainAttributes(); + mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION); + mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, ATTRIBUTE_DEFALT_VALUE_CREATED_BY); + if (!apkSignatureSchemeIds.isEmpty()) { + // Add APK Signature Scheme v2 (and newer) signature stripping protection. + // This attribute indicates that this APK is supposed to have been signed using one or + // more APK-specific signature schemes in addition to the standard JAR signature scheme + // used by this code. APK signature verifier should reject the APK if it does not + // contain a signature for the signature scheme the verifier prefers out of this set. + StringBuilder attrValue = new StringBuilder(); + for (int id : apkSignatureSchemeIds) { + if (attrValue.length() > 0) { + attrValue.append(", "); + } + attrValue.append(String.valueOf(id)); + } + mainAttrs.put( + SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME, + attrValue.toString()); + } + + // Add main attribute containing the digest of MANIFEST.MF. + MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm); + mainAttrs.putValue( + getManifestDigestAttributeName(manifestDigestAlgorithm), + Base64.getEncoder().encodeToString(md.digest(manifest.contents))); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + SignatureFileWriter.writeMainSection(out, mainAttrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory .SF file", e); + } + String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm); + for (Map.Entry manifestSection + : manifest.individualSectionsContents.entrySet()) { + String sectionName = manifestSection.getKey(); + byte[] sectionContents = manifestSection.getValue(); + byte[] sectionDigest = md.digest(sectionContents); + Attributes attrs = new Attributes(); + attrs.putValue( + entryDigestAttributeName, + Base64.getEncoder().encodeToString(sectionDigest)); + + try { + SignatureFileWriter.writeIndividualSection(out, sectionName, attrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory .SF file", e); + } + } + + // A bug in the java.util.jar implementation of Android platforms up to version 1.6 will + // cause a spurious IOException to be thrown if the length of the signature file is a + // multiple of 1024 bytes. As a workaround, add an extra CRLF in this case. + if ((out.size() > 0) && ((out.size() % 1024) == 0)) { + try { + SignatureFileWriter.writeSectionDelimiter(out); + } catch (IOException e) { + throw new RuntimeException("Failed to write to ByteArrayOutputStream", e); + } + } + + return out.toByteArray(); + } + + private static byte[] generateSignatureBlock( + SignerConfig signerConfig, byte[] signatureFileBytes) + throws InvalidKeyException, CertificateEncodingException, SignatureException { + JcaCertStore certs = new JcaCertStore(signerConfig.certificates); + X509Certificate signerCert = signerConfig.certificates.get(0); + String jcaSignatureAlgorithm = + getJcaSignatureAlgorithm( + signerCert.getPublicKey(), signerConfig.signatureDigestAlgorithm); + try { + ContentSigner signer = + new JcaContentSignerBuilder(jcaSignatureAlgorithm) + .build(signerConfig.privateKey); + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + gen.addSignerInfoGenerator( + new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().build()) + .setDirectSignature(true) + .build(signer, signerCert)); + gen.addCertificates(certs); + + CMSSignedData sigData = + gen.generate(new CMSProcessableByteArray(signatureFileBytes), false); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { + DEROutputStream dos = new DEROutputStream(out); + dos.writeObject(asn1.readObject()); + } + return out.toByteArray(); + } catch (OperatorCreationException | CMSException | IOException e) { + throw new SignatureException("Failed to generate signature", e); + } + } + + private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) { + switch (digestAlgorithm) { + case SHA1: + return "SHA1-Digest"; + case SHA256: + return "SHA-256-Digest"; + default: + throw new IllegalArgumentException( + "Unexpected content digest algorithm: " + digestAlgorithm); + } + } + + private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) { + switch (digestAlgorithm) { + case SHA1: + return "SHA1-Digest-Manifest"; + case SHA256: + return "SHA-256-Digest-Manifest"; + default: + throw new IllegalArgumentException( + "Unexpected content digest algorithm: " + digestAlgorithm); + } + } + + private static String getJcaSignatureAlgorithm( + PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException { + String keyAlgorithm = publicKey.getAlgorithm(); + String digestPrefixForSigAlg; + switch (digestAlgorithm) { + case SHA1: + digestPrefixForSigAlg = "SHA1"; + break; + case SHA256: + digestPrefixForSigAlg = "SHA256"; + break; + default: + throw new IllegalArgumentException( + "Unexpected digest algorithm: " + digestAlgorithm); + } + if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + return digestPrefixForSigAlg + "withRSA"; + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + return digestPrefixForSigAlg + "withDSA"; + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + return digestPrefixForSigAlg + "withECDSA"; + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } +} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java new file mode 100644 index 0000000000..449953a68b --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/ManifestWriter.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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 com.android.apksigner.core.internal.jar; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.jar.Attributes; + +/** + * Producer of {@code META-INF/MANIFEST.MF} file. + */ +public abstract class ManifestWriter { + + private static final byte[] CRLF = new byte[] {'\r', '\n'}; + private static final int MAX_LINE_LENGTH = 70; + + private ManifestWriter() {} + + public static void writeMainSection(OutputStream out, Attributes attributes) + throws IOException { + + // Main section must start with the Manifest-Version attribute. + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. + String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION); + if (manifestVersion == null) { + throw new IllegalArgumentException( + "Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing"); + } + writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion); + + if (attributes.size() > 1) { + SortedMap namedAttributes = getAttributesSortedByName(attributes); + namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString()); + writeAttributes(out, namedAttributes); + } + writeSectionDelimiter(out); + } + + public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) + throws IOException { + writeAttribute(out, "Name", name); + + if (!attributes.isEmpty()) { + writeAttributes(out, getAttributesSortedByName(attributes)); + } + writeSectionDelimiter(out); + } + + static void writeSectionDelimiter(OutputStream out) throws IOException { + out.write(CRLF); + } + + static void writeAttribute(OutputStream out, Attributes.Name name, String value) + throws IOException { + writeAttribute(out, name.toString(), value); + } + + private static void writeAttribute(OutputStream out, String name, String value) + throws IOException { + writeLine(out, name + ": " + value); + } + + private static void writeLine(OutputStream out, String line) throws IOException { + byte[] lineBytes = line.getBytes("UTF-8"); + int offset = 0; + int remaining = lineBytes.length; + boolean firstLine = true; + while (remaining > 0) { + int chunkLength; + if (firstLine) { + // First line + chunkLength = Math.min(remaining, MAX_LINE_LENGTH); + } else { + // Continuation line + out.write(CRLF); + out.write(' '); + chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1); + } + out.write(lineBytes, offset, chunkLength); + offset += chunkLength; + remaining -= chunkLength; + firstLine = false; + } + out.write(CRLF); + } + + static SortedMap getAttributesSortedByName(Attributes attributes) { + Set> attributesEntries = attributes.entrySet(); + SortedMap namedAttributes = new TreeMap(); + for (Map.Entry attribute : attributesEntries) { + String attrName = attribute.getKey().toString(); + String attrValue = attribute.getValue().toString(); + namedAttributes.put(attrName, attrValue); + } + return namedAttributes; + } + + static void writeAttributes( + OutputStream out, SortedMap attributesSortedByName) throws IOException { + for (Map.Entry attribute : attributesSortedByName.entrySet()) { + String attrName = attribute.getKey(); + String attrValue = attribute.getValue(); + writeAttribute(out, attrName, attrValue); + } + } +} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java new file mode 100644 index 0000000000..9cd25f3c35 --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/jar/SignatureFileWriter.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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 com.android.apksigner.core.internal.jar; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.SortedMap; +import java.util.jar.Attributes; + +/** + * Producer of JAR signature file ({@code *.SF}). + */ +public abstract class SignatureFileWriter { + private SignatureFileWriter() {} + + public static void writeMainSection(OutputStream out, Attributes attributes) + throws IOException { + + // Main section must start with the Signature-Version attribute. + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. + String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION); + if (signatureVersion == null) { + throw new IllegalArgumentException( + "Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing"); + } + ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion); + + if (attributes.size() > 1) { + SortedMap namedAttributes = + ManifestWriter.getAttributesSortedByName(attributes); + namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString()); + ManifestWriter.writeAttributes(out, namedAttributes); + } + writeSectionDelimiter(out); + } + + public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) + throws IOException { + ManifestWriter.writeIndividualSection(out, name, attributes); + } + + public static void writeSectionDelimiter(OutputStream out) throws IOException { + ManifestWriter.writeSectionDelimiter(out); + } +} diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java new file mode 100644 index 0000000000..d59af410b3 --- /dev/null +++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/Pair.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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 com.android.apksigner.core.internal.util; + +/** + * Pair of two elements. + */ +public final class Pair { + private final A mFirst; + private final B mSecond; + + private Pair(A first, B second) { + mFirst = first; + mSecond = second; + } + + public static Pair of(A first, B second) { + return new Pair(first, second); + } + + public A getFirst() { + return mFirst; + } + + public B getSecond() { + return mSecond; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); + result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + @SuppressWarnings("rawtypes") + Pair other = (Pair) obj; + if (mFirst == null) { + if (other.mFirst != null) { + return false; + } + } else if (!mFirst.equals(other.mFirst)) { + return false; + } + if (mSecond == null) { + if (other.mSecond != null) { + return false; + } + } else if (!mSecond.equals(other.mSecond)) { + return false; + } + return true; + } +}