Home | History | Annotate | Download | only in export
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.wizards.export;
     18 
     19 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
     20 import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage;
     21 
     22 import org.eclipse.core.resources.IProject;
     23 import org.eclipse.swt.SWT;
     24 import org.eclipse.swt.custom.ScrolledComposite;
     25 import org.eclipse.swt.events.ControlAdapter;
     26 import org.eclipse.swt.events.ControlEvent;
     27 import org.eclipse.swt.events.ModifyEvent;
     28 import org.eclipse.swt.events.ModifyListener;
     29 import org.eclipse.swt.events.SelectionAdapter;
     30 import org.eclipse.swt.events.SelectionEvent;
     31 import org.eclipse.swt.graphics.Rectangle;
     32 import org.eclipse.swt.layout.GridData;
     33 import org.eclipse.swt.layout.GridLayout;
     34 import org.eclipse.swt.widgets.Button;
     35 import org.eclipse.swt.widgets.Composite;
     36 import org.eclipse.swt.widgets.FileDialog;
     37 import org.eclipse.swt.widgets.Label;
     38 import org.eclipse.swt.widgets.Text;
     39 import org.eclipse.ui.forms.widgets.FormText;
     40 
     41 import java.io.File;
     42 import java.io.FileInputStream;
     43 import java.io.FileNotFoundException;
     44 import java.io.IOException;
     45 import java.security.KeyStore;
     46 import java.security.KeyStore.PrivateKeyEntry;
     47 import java.security.KeyStoreException;
     48 import java.security.NoSuchAlgorithmException;
     49 import java.security.PrivateKey;
     50 import java.security.UnrecoverableEntryException;
     51 import java.security.cert.CertificateException;
     52 import java.security.cert.X509Certificate;
     53 import java.util.Calendar;
     54 
     55 /**
     56  * Final page of the wizard that checks the key and ask for the ouput location.
     57  */
     58 final class KeyCheckPage extends ExportWizardPage {
     59 
     60     private static final int REQUIRED_YEARS = 25;
     61 
     62     private static final String VALIDITY_WARNING =
     63             "<p>Make sure the certificate is valid for the planned lifetime of the product.</p>"
     64             + "<p>If the certificate expires, you will be forced to sign your application with "
     65             + "a different one.</p>"
     66             + "<p>Applications cannot be upgraded if their certificate changes from "
     67             + "one version to another, forcing a full uninstall/install, which will make "
     68             + "the user lose his/her data.</p>"
     69             + "<p>Google Play(Android Market) currently requires certificates to be valid "
     70             + "until 2033.</p>";
     71 
     72     private final ExportWizard mWizard;
     73     private PrivateKey mPrivateKey;
     74     private X509Certificate mCertificate;
     75     private Text mDestination;
     76     private boolean mFatalSigningError;
     77     private FormText mDetailText;
     78     private ScrolledComposite mScrolledComposite;
     79 
     80     private String mKeyDetails;
     81     private String mDestinationDetails;
     82 
     83     protected KeyCheckPage(ExportWizard wizard, String pageName) {
     84         super(pageName);
     85         mWizard = wizard;
     86 
     87         setTitle("Destination and key/certificate checks");
     88         setDescription(""); // TODO
     89     }
     90 
     91     @Override
     92     public void createControl(Composite parent) {
     93         setErrorMessage(null);
     94         setMessage(null);
     95 
     96         // build the ui.
     97         Composite composite = new Composite(parent, SWT.NULL);
     98         composite.setLayoutData(new GridData(GridData.FILL_BOTH));
     99         GridLayout gl = new GridLayout(3, false);
    100         gl.verticalSpacing *= 3;
    101         composite.setLayout(gl);
    102 
    103         GridData gd;
    104 
    105         new Label(composite, SWT.NONE).setText("Destination APK file:");
    106         mDestination = new Text(composite, SWT.BORDER);
    107         mDestination.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
    108         mDestination.addModifyListener(new ModifyListener() {
    109             @Override
    110             public void modifyText(ModifyEvent e) {
    111                 onDestinationChange(false /*forceDetailUpdate*/);
    112             }
    113         });
    114         final Button browseButton = new Button(composite, SWT.PUSH);
    115         browseButton.setText("Browse...");
    116         browseButton.addSelectionListener(new SelectionAdapter() {
    117             @Override
    118             public void widgetSelected(SelectionEvent e) {
    119                 FileDialog fileDialog = new FileDialog(browseButton.getShell(), SWT.SAVE);
    120 
    121                 fileDialog.setText("Destination file name");
    122                 // get a default apk name based on the project
    123                 String filename = ProjectHelper.getApkFilename(mWizard.getProject(),
    124                         null /*config*/);
    125                 fileDialog.setFileName(filename);
    126 
    127                 String saveLocation = fileDialog.open();
    128                 if (saveLocation != null) {
    129                     mDestination.setText(saveLocation);
    130                 }
    131             }
    132         });
    133 
    134         mScrolledComposite = new ScrolledComposite(composite, SWT.V_SCROLL);
    135         mScrolledComposite.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
    136         gd.horizontalSpan = 3;
    137         mScrolledComposite.setExpandHorizontal(true);
    138         mScrolledComposite.setExpandVertical(true);
    139 
    140         mDetailText = new FormText(mScrolledComposite, SWT.NONE);
    141         mScrolledComposite.setContent(mDetailText);
    142 
    143         mScrolledComposite.addControlListener(new ControlAdapter() {
    144             @Override
    145             public void controlResized(ControlEvent e) {
    146                 updateScrolling();
    147             }
    148         });
    149 
    150         setControl(composite);
    151     }
    152 
    153     @Override
    154     void onShow() {
    155         // fill the texts with information loaded from the project.
    156         if ((mProjectDataChanged & DATA_PROJECT) != 0) {
    157             // reset the destination from the content of the project
    158             IProject project = mWizard.getProject();
    159 
    160             String destination = ProjectHelper.loadStringProperty(project,
    161                     ExportWizard.PROPERTY_DESTINATION);
    162             if (destination != null) {
    163                 mDestination.setText(destination);
    164             }
    165         }
    166 
    167         // if anything change we basically reload the data.
    168         if (mProjectDataChanged != 0) {
    169             mFatalSigningError = false;
    170 
    171             // reset the wizard with no key/cert to make it not finishable, unless a valid
    172             // key/cert is found.
    173             mWizard.setSigningInfo(null, null);
    174             mPrivateKey = null;
    175             mCertificate = null;
    176             mKeyDetails = null;
    177 
    178             if (mWizard.getKeystoreCreationMode() || mWizard.getKeyCreationMode()) {
    179                 int validity = mWizard.getValidity();
    180                 StringBuilder sb = new StringBuilder(
    181                         String.format("<p>Certificate expires in %d years.</p>",
    182                         validity));
    183 
    184                 if (validity < REQUIRED_YEARS) {
    185                     sb.append(VALIDITY_WARNING);
    186                 }
    187 
    188                 mKeyDetails = sb.toString();
    189             } else {
    190                 try {
    191                     KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    192                     FileInputStream fis = new FileInputStream(mWizard.getKeystore());
    193                     keyStore.load(fis, mWizard.getKeystorePassword().toCharArray());
    194                     fis.close();
    195                     PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
    196                             mWizard.getKeyAlias(),
    197                             new KeyStore.PasswordProtection(
    198                                     mWizard.getKeyPassword().toCharArray()));
    199 
    200                     if (entry != null) {
    201                         mPrivateKey = entry.getPrivateKey();
    202                         mCertificate = (X509Certificate)entry.getCertificate();
    203                     } else {
    204                         setErrorMessage("Unable to find key.");
    205 
    206                         setPageComplete(false);
    207                     }
    208                 } catch (FileNotFoundException e) {
    209                     // this was checked at the first previous step and will not happen here, unless
    210                     // the file was removed during the export wizard execution.
    211                     onException(e);
    212                 } catch (KeyStoreException e) {
    213                     onException(e);
    214                 } catch (NoSuchAlgorithmException e) {
    215                     onException(e);
    216                 } catch (UnrecoverableEntryException e) {
    217                     onException(e);
    218                 } catch (CertificateException e) {
    219                     onException(e);
    220                 } catch (IOException e) {
    221                     onException(e);
    222                 }
    223 
    224                 if (mPrivateKey != null && mCertificate != null) {
    225                     Calendar expirationCalendar = Calendar.getInstance();
    226                     expirationCalendar.setTime(mCertificate.getNotAfter());
    227                     Calendar today = Calendar.getInstance();
    228 
    229                     if (expirationCalendar.before(today)) {
    230                         mKeyDetails = String.format(
    231                                 "<p>Certificate expired on %s</p>",
    232                                 mCertificate.getNotAfter().toString());
    233 
    234                         // fatal error = nothing can make the page complete.
    235                         mFatalSigningError = true;
    236 
    237                         setErrorMessage("Certificate is expired.");
    238                         setPageComplete(false);
    239                     } else {
    240                         // valid, key/cert: put it in the wizard so that it can be finished
    241                         mWizard.setSigningInfo(mPrivateKey, mCertificate);
    242 
    243                         StringBuilder sb = new StringBuilder(String.format(
    244                                 "<p>Certificate expires on %s.</p>",
    245                                 mCertificate.getNotAfter().toString()));
    246 
    247                         int expirationYear = expirationCalendar.get(Calendar.YEAR);
    248                         int thisYear = today.get(Calendar.YEAR);
    249 
    250                         if (thisYear + REQUIRED_YEARS < expirationYear) {
    251                             // do nothing
    252                         } else {
    253                             if (expirationYear == thisYear) {
    254                                 sb.append("<p>The certificate expires this year.</p>");
    255                             } else {
    256                                 int count = expirationYear-thisYear;
    257                                 sb.append(String.format(
    258                                         "<p>The Certificate expires in %1$s %2$s.</p>",
    259                                         count, count == 1 ? "year" : "years"));
    260                             }
    261                             sb.append(VALIDITY_WARNING);
    262                         }
    263 
    264                         // show certificate fingerprints
    265                         String sha1 = mWizard.getCertSha1Fingerprint();
    266                         String md5 = mWizard.getCertMd5Fingerprint();
    267 
    268                         sb.append("<p></p>" /*blank line*/);
    269                         sb.append("<p>Certificate fingerprints:</p>");
    270                         sb.append(String.format("<li>MD5 : %s</li>", md5));
    271                         sb.append(String.format("<li>SHA1: %s</li>", sha1));
    272                         sb.append("<p></p>" /*blank line*/);
    273 
    274                         mKeyDetails = sb.toString();
    275                     }
    276                 } else {
    277                     // fatal error = nothing can make the page complete.
    278                     mFatalSigningError = true;
    279                 }
    280             }
    281         }
    282 
    283         onDestinationChange(true /*forceDetailUpdate*/);
    284     }
    285 
    286     /**
    287      * Callback for destination field edition
    288      * @param forceDetailUpdate if true, the detail {@link FormText} is updated even if a fatal
    289      * error has happened in the signing.
    290      */
    291     private void onDestinationChange(boolean forceDetailUpdate) {
    292         if (mFatalSigningError == false) {
    293             // reset messages for now.
    294             setErrorMessage(null);
    295             setMessage(null);
    296 
    297             String path = mDestination.getText().trim();
    298 
    299             if (path.length() == 0) {
    300                 setErrorMessage("Enter destination for the APK file.");
    301                 // reset canFinish in the wizard.
    302                 mWizard.resetDestination();
    303                 setPageComplete(false);
    304                 return;
    305             }
    306 
    307             File file = new File(path);
    308             if (file.isDirectory()) {
    309                 setErrorMessage("Destination is a directory.");
    310                 // reset canFinish in the wizard.
    311                 mWizard.resetDestination();
    312                 setPageComplete(false);
    313                 return;
    314             }
    315 
    316             File parentFolder = file.getParentFile();
    317             if (parentFolder == null || parentFolder.isDirectory() == false) {
    318                 setErrorMessage("Not a valid directory.");
    319                 // reset canFinish in the wizard.
    320                 mWizard.resetDestination();
    321                 setPageComplete(false);
    322                 return;
    323             }
    324 
    325             if (file.isFile()) {
    326                 mDestinationDetails = "<li>WARNING: destination file already exists</li>";
    327                 setMessage("Destination file already exists.", WARNING);
    328             }
    329 
    330             // no error, set the destination in the wizard.
    331             mWizard.setDestination(file);
    332             setPageComplete(true);
    333 
    334             updateDetailText();
    335         } else if (forceDetailUpdate) {
    336             updateDetailText();
    337         }
    338     }
    339 
    340     /**
    341      * Updates the scrollbar to match the content of the {@link FormText} or the new size
    342      * of the {@link ScrolledComposite}.
    343      */
    344     private void updateScrolling() {
    345         if (mDetailText != null) {
    346             Rectangle r = mScrolledComposite.getClientArea();
    347             mScrolledComposite.setMinSize(mDetailText.computeSize(r.width, SWT.DEFAULT));
    348             mScrolledComposite.layout();
    349         }
    350     }
    351 
    352     private void updateDetailText() {
    353         StringBuilder sb = new StringBuilder("<form>");
    354         if (mKeyDetails != null) {
    355             sb.append(mKeyDetails);
    356         }
    357 
    358         if (mDestinationDetails != null && mFatalSigningError == false) {
    359             sb.append(mDestinationDetails);
    360         }
    361 
    362         sb.append("</form>");
    363 
    364         mDetailText.setText(sb.toString(), true /* parseTags */,
    365                 true /* expandURLs */);
    366 
    367         mDetailText.getParent().layout();
    368 
    369         updateScrolling();
    370     }
    371 
    372     @Override
    373     protected void onException(Throwable t) {
    374         super.onException(t);
    375 
    376         mKeyDetails = String.format("ERROR: %1$s", ExportWizard.getExceptionMessage(t));
    377     }
    378 }
    379