/* * Copyright (C) 2013 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.build.gradle.internal.tasks; import com.android.annotations.NonNull; import com.android.build.gradle.internal.variant.BaseVariantData; import com.android.builder.model.SigningConfig; import com.android.ide.common.signing.CertificateInfo; import com.android.ide.common.signing.KeystoreHelper; import com.android.ide.common.signing.KeytoolException; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.gradle.api.DefaultTask; import org.gradle.api.tasks.TaskAction; import org.gradle.logging.StyledTextOutput; import org.gradle.logging.StyledTextOutputFactory; import java.io.FileNotFoundException; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.text.DateFormat; import java.util.Collection; import java.util.Date; import java.util.Locale; import java.util.Map; import java.util.Set; import static org.gradle.logging.StyledTextOutput.Style.Description; import static org.gradle.logging.StyledTextOutput.Style.Failure; import static org.gradle.logging.StyledTextOutput.Style.Identifier; import static org.gradle.logging.StyledTextOutput.Style.Normal; /** * Report tasks displaying the signing information for all variants. */ public class SigningReportTask extends DefaultTask { private Set<BaseVariantData> variants = Sets.newHashSet(); @TaskAction public void generate() throws IOException { StyledTextOutput textOutput = getServices().get( StyledTextOutputFactory.class).create(getClass()); Map<SigningConfig, SigningInfo> cache = Maps.newHashMap(); for (BaseVariantData variant : variants) { textOutput.withStyle(Identifier).text("Variant: "); textOutput.withStyle(Description).text(variant.getName()); textOutput.println(); // get the data SigningConfig signingConfig = variant.getVariantConfiguration().getSigningConfig(); if (signingConfig == null) { textOutput.withStyle(Identifier).text("Config: "); textOutput.withStyle(Normal).text("none"); textOutput.println(); } else { SigningInfo signingInfo = getSigningInfo(signingConfig, cache); textOutput.withStyle(Identifier).text("Config: "); textOutput.withStyle(Description).text(signingConfig.getName()); textOutput.println(); textOutput.withStyle(Identifier).text("Store: "); textOutput.withStyle(Description).text(signingConfig.getStoreFile()); textOutput.println(); textOutput.withStyle(Identifier).text("Alias: "); textOutput.withStyle(Description).text(signingConfig.getKeyAlias()); textOutput.println(); if (signingInfo.isValid()) { if (signingInfo.error != null) { textOutput.withStyle(Identifier).text("Error: "); textOutput.withStyle(Failure).text(signingInfo.error); textOutput.println(); } else { textOutput.withStyle(Identifier).text("MD5: "); textOutput.withStyle(Description).text(signingInfo.md5); textOutput.println(); textOutput.withStyle(Identifier).text("SHA1: "); textOutput.withStyle(Description).text(signingInfo.sha1); textOutput.println(); textOutput.withStyle(Identifier).text("Valid until: "); DateFormat df = DateFormat.getDateInstance(DateFormat.FULL); textOutput.withStyle(Description).text(df.format(signingInfo.notAfter)); textOutput.println(); } } } textOutput.withStyle(Normal).text("----------"); textOutput.println(); } } /** * Sets the configurations to generate the report for. */ public void setVariants(@NonNull Collection<? extends BaseVariantData> variants) { this.variants.addAll(variants); } private static SigningInfo getSigningInfo( @NonNull SigningConfig signingConfig, @NonNull Map<SigningConfig, SigningInfo> cache) { SigningInfo signingInfo = cache.get(signingConfig); if (signingInfo == null) { signingInfo = new SigningInfo(); if (signingConfig.isSigningReady()) { try { CertificateInfo certificateInfo = KeystoreHelper.getCertificateInfo( signingConfig.getStoreType(), signingConfig.getStoreFile(), signingConfig.getStorePassword(), signingConfig.getKeyPassword(), signingConfig.getKeyAlias()); if (certificateInfo != null) { signingInfo.md5 = getFingerprint(certificateInfo.getCertificate(), "MD5"); signingInfo.sha1 = getFingerprint(certificateInfo.getCertificate(), "SHA1"); signingInfo.notAfter = certificateInfo.getCertificate().getNotAfter(); } } catch (KeytoolException e) { signingInfo.error = e.getMessage(); } catch (FileNotFoundException e) { signingInfo.error = "Missing keystore"; } } cache.put(signingConfig, signingInfo); } return signingInfo; } private static final class SigningInfo { String md5; String sha1; Date notAfter; String error; boolean isValid() { return md5 != null || error != null; } } /** * Returns the {@link Certificate} fingerprint as returned by <code>keytool</code>. */ public static String getFingerprint(Certificate cert, String hashAlgorithm) { if (cert == null) { return null; } try { MessageDigest digest = MessageDigest.getInstance(hashAlgorithm); return toHexadecimalString(digest.digest(cert.getEncoded())); } catch(NoSuchAlgorithmException e) { // ignore } catch(CertificateEncodingException e) { // ignore } return null; } private static String toHexadecimalString(byte[] value) { StringBuilder sb = new StringBuilder(); int len = value.length; for (int i = 0; i < len; i++) { int num = ((int) value[i]) & 0xff; if (num < 0x10) { sb.append('0'); } sb.append(Integer.toHexString(num)); if (i < len - 1) { sb.append(':'); } } return sb.toString().toUpperCase(Locale.US); } }