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