/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php * * 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.ide.eclipse.adt.internal.wizards.export; import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage; import org.eclipse.core.resources.IProject; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.forms.widgets.FormText; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStore.PrivateKeyEntry; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.UnrecoverableEntryException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Calendar; /** * Final page of the wizard that checks the key and ask for the ouput location. */ final class KeyCheckPage extends ExportWizardPage { private static final int REQUIRED_YEARS = 25; private static final String VALIDITY_WARNING = "

Make sure the certificate is valid for the planned lifetime of the product.

" + "

If the certificate expires, you will be forced to sign your application with " + "a different one.

" + "

Applications cannot be upgraded if their certificate changes from " + "one version to another, forcing a full uninstall/install, which will make " + "the user lose his/her data.

" + "

Google Play(Android Market) currently requires certificates to be valid " + "until 2033.

"; private final ExportWizard mWizard; private PrivateKey mPrivateKey; private X509Certificate mCertificate; private Text mDestination; private boolean mFatalSigningError; private FormText mDetailText; private ScrolledComposite mScrolledComposite; private String mKeyDetails; private String mDestinationDetails; protected KeyCheckPage(ExportWizard wizard, String pageName) { super(pageName); mWizard = wizard; setTitle("Destination and key/certificate checks"); setDescription(""); // TODO } @Override public void createControl(Composite parent) { setErrorMessage(null); setMessage(null); // build the ui. Composite composite = new Composite(parent, SWT.NULL); composite.setLayoutData(new GridData(GridData.FILL_BOTH)); GridLayout gl = new GridLayout(3, false); gl.verticalSpacing *= 3; composite.setLayout(gl); GridData gd; new Label(composite, SWT.NONE).setText("Destination APK file:"); mDestination = new Text(composite, SWT.BORDER); mDestination.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); mDestination.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { onDestinationChange(false /*forceDetailUpdate*/); } }); final Button browseButton = new Button(composite, SWT.PUSH); browseButton.setText("Browse..."); browseButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { FileDialog fileDialog = new FileDialog(browseButton.getShell(), SWT.SAVE); fileDialog.setText("Destination file name"); // get a default apk name based on the project String filename = ProjectHelper.getApkFilename(mWizard.getProject(), null /*config*/); fileDialog.setFileName(filename); String saveLocation = fileDialog.open(); if (saveLocation != null) { mDestination.setText(saveLocation); } } }); mScrolledComposite = new ScrolledComposite(composite, SWT.V_SCROLL); mScrolledComposite.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); gd.horizontalSpan = 3; mScrolledComposite.setExpandHorizontal(true); mScrolledComposite.setExpandVertical(true); mDetailText = new FormText(mScrolledComposite, SWT.NONE); mScrolledComposite.setContent(mDetailText); mScrolledComposite.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { updateScrolling(); } }); setControl(composite); } @Override void onShow() { // fill the texts with information loaded from the project. if ((mProjectDataChanged & DATA_PROJECT) != 0) { // reset the destination from the content of the project IProject project = mWizard.getProject(); String destination = ProjectHelper.loadStringProperty(project, ExportWizard.PROPERTY_DESTINATION); if (destination != null) { mDestination.setText(destination); } } // if anything change we basically reload the data. if (mProjectDataChanged != 0) { mFatalSigningError = false; // reset the wizard with no key/cert to make it not finishable, unless a valid // key/cert is found. mWizard.setSigningInfo(null, null); mPrivateKey = null; mCertificate = null; mKeyDetails = null; if (mWizard.getKeystoreCreationMode() || mWizard.getKeyCreationMode()) { int validity = mWizard.getValidity(); StringBuilder sb = new StringBuilder( String.format("

Certificate expires in %d years.

", validity)); if (validity < REQUIRED_YEARS) { sb.append(VALIDITY_WARNING); } mKeyDetails = sb.toString(); } else { try { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); FileInputStream fis = new FileInputStream(mWizard.getKeystore()); keyStore.load(fis, mWizard.getKeystorePassword().toCharArray()); fis.close(); PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry( mWizard.getKeyAlias(), new KeyStore.PasswordProtection( mWizard.getKeyPassword().toCharArray())); if (entry != null) { mPrivateKey = entry.getPrivateKey(); mCertificate = (X509Certificate)entry.getCertificate(); } else { setErrorMessage("Unable to find key."); setPageComplete(false); } } catch (FileNotFoundException e) { // this was checked at the first previous step and will not happen here, unless // the file was removed during the export wizard execution. onException(e); } catch (KeyStoreException e) { onException(e); } catch (NoSuchAlgorithmException e) { onException(e); } catch (UnrecoverableEntryException e) { onException(e); } catch (CertificateException e) { onException(e); } catch (IOException e) { onException(e); } if (mPrivateKey != null && mCertificate != null) { Calendar expirationCalendar = Calendar.getInstance(); expirationCalendar.setTime(mCertificate.getNotAfter()); Calendar today = Calendar.getInstance(); if (expirationCalendar.before(today)) { mKeyDetails = String.format( "

Certificate expired on %s

", mCertificate.getNotAfter().toString()); // fatal error = nothing can make the page complete. mFatalSigningError = true; setErrorMessage("Certificate is expired."); setPageComplete(false); } else { // valid, key/cert: put it in the wizard so that it can be finished mWizard.setSigningInfo(mPrivateKey, mCertificate); StringBuilder sb = new StringBuilder(String.format( "

Certificate expires on %s.

", mCertificate.getNotAfter().toString())); int expirationYear = expirationCalendar.get(Calendar.YEAR); int thisYear = today.get(Calendar.YEAR); if (thisYear + REQUIRED_YEARS < expirationYear) { // do nothing } else { if (expirationYear == thisYear) { sb.append("

The certificate expires this year.

"); } else { int count = expirationYear-thisYear; sb.append(String.format( "

The Certificate expires in %1$s %2$s.

", count, count == 1 ? "year" : "years")); } sb.append(VALIDITY_WARNING); } // show certificate fingerprints String sha1 = mWizard.getCertSha1Fingerprint(); String md5 = mWizard.getCertMd5Fingerprint(); sb.append("

" /*blank line*/); sb.append("

Certificate fingerprints:

"); sb.append(String.format("
  • MD5 : %s
  • ", md5)); sb.append(String.format("
  • SHA1: %s
  • ", sha1)); sb.append("

    " /*blank line*/); mKeyDetails = sb.toString(); } } else { // fatal error = nothing can make the page complete. mFatalSigningError = true; } } } onDestinationChange(true /*forceDetailUpdate*/); } /** * Callback for destination field edition * @param forceDetailUpdate if true, the detail {@link FormText} is updated even if a fatal * error has happened in the signing. */ private void onDestinationChange(boolean forceDetailUpdate) { if (mFatalSigningError == false) { // reset messages for now. setErrorMessage(null); setMessage(null); String path = mDestination.getText().trim(); if (path.length() == 0) { setErrorMessage("Enter destination for the APK file."); // reset canFinish in the wizard. mWizard.resetDestination(); setPageComplete(false); return; } File file = new File(path); if (file.isDirectory()) { setErrorMessage("Destination is a directory."); // reset canFinish in the wizard. mWizard.resetDestination(); setPageComplete(false); return; } File parentFolder = file.getParentFile(); if (parentFolder == null || parentFolder.isDirectory() == false) { setErrorMessage("Not a valid directory."); // reset canFinish in the wizard. mWizard.resetDestination(); setPageComplete(false); return; } if (file.isFile()) { mDestinationDetails = "
  • WARNING: destination file already exists
  • "; setMessage("Destination file already exists.", WARNING); } // no error, set the destination in the wizard. mWizard.setDestination(file); setPageComplete(true); updateDetailText(); } else if (forceDetailUpdate) { updateDetailText(); } } /** * Updates the scrollbar to match the content of the {@link FormText} or the new size * of the {@link ScrolledComposite}. */ private void updateScrolling() { if (mDetailText != null) { Rectangle r = mScrolledComposite.getClientArea(); mScrolledComposite.setMinSize(mDetailText.computeSize(r.width, SWT.DEFAULT)); mScrolledComposite.layout(); } } private void updateDetailText() { StringBuilder sb = new StringBuilder("
    "); if (mKeyDetails != null) { sb.append(mKeyDetails); } if (mDestinationDetails != null && mFatalSigningError == false) { sb.append(mDestinationDetails); } sb.append("
    "); mDetailText.setText(sb.toString(), true /* parseTags */, true /* expandURLs */); mDetailText.getParent().layout(); updateScrolling(); } @Override protected void onException(Throwable t) { super.onException(t); mKeyDetails = String.format("ERROR: %1$s", ExportWizard.getExceptionMessage(t)); } }