1 /* 2 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.oracle.ipack.main; 27 28 import com.oracle.ipack.packer.Packer; 29 import com.oracle.ipack.signer.Signer; 30 import com.oracle.ipack.util.ResourceDescriptor; 31 import java.io.File; 32 import java.io.IOException; 33 import java.security.KeyStoreException; 34 import java.security.NoSuchAlgorithmException; 35 import java.security.Security; 36 import java.security.UnrecoverableKeyException; 37 import java.security.cert.CertificateException; 38 import java.util.ArrayList; 39 import java.util.List; 40 import javax.naming.InvalidNameException; 41 import org.bouncycastle.cms.CMSException; 42 import org.bouncycastle.jce.provider.BouncyCastleProvider; 43 import org.bouncycastle.operator.OperatorCreationException; 44 45 public final class Main { 46 private static final String USAGE = 47 "Usage:\n" 48 + "\n" 49 + " ipack <archive> <signing opts> <application opts>" 50 + " [ <application opts> ... ]\n" 51 + "\n" 52 + "Signing options:\n" 53 + "\n" 54 + " -keystore <keystore> " 55 + "keystore to use for signing\n" 56 + " -storepass <password> " 57 + "keystore password\n" 58 + " -alias <alias> " 59 + "alias for the signing certificate chain and\n" 60 + " the associated private key\n" 61 + " -keypass <password> " 62 + "password for the private key\n" 63 + "\n" 64 + "Application options:\n" 65 + "\n" 66 + " -basedir <directory> " 67 + "base directory from which to derive relative paths\n" 68 + " -appdir <directory> " 69 + "directory with the application executable and resources\n" 70 + " -appname <file> " 71 + "name of the application executable\n" 72 + " -appid <id> " 73 + "application identifier\n" 74 + "\n" 75 + "Example:\n" 76 + "\n" 77 + " ipack MyApplication.ipa -keystore ipack.ks" 78 + " -storepass keystorepwd\n" 79 + " " 80 + " -alias mycert -keypass keypwd\n" 81 + " " 82 + " -basedir mysources/MyApplication/dist\n" 83 + " " 84 + " -appdir Payload/MyApplication.app\n" 85 + " " 86 + " -appname MyApplication" 87 + " -appid com.myorg.MyApplication"; 88 89 private Main() { 90 } 91 92 public static void main(final String... args) throws IOException { 93 if (args.length == 0) { 94 System.out.println(USAGE); 95 return; 96 } 97 98 Security.addProvider(new BouncyCastleProvider()); 99 100 try { 101 execute(args); 102 } catch (final RuntimeException e) { 103 System.err.println(e.getMessage()); 104 System.exit(1); 105 } 106 } 107 108 private static void execute(final String... args) throws RuntimeException { 109 final SigningArgs signingArgs = new SigningArgs(); 110 111 File destFile = null; 112 113 File baseDir = null; 114 File appBaseDir = null; 115 String appDir = null; 116 String appName = null; 117 String appId = null; 118 119 final List<ApplicationArgs> appArgsList = 120 new ArrayList<ApplicationArgs>(); 121 122 for (int i = 0; i < args.length; ++i) { 123 final String argument = args[i]; 124 if (!argument.startsWith("-")) { 125 if (destFile != null) { 126 throw new RuntimeException( 127 "Destination file already specified"); 128 } 129 130 destFile = new File(argument); 131 continue; 132 } 133 134 if (i == (args.length - 1)) { 135 throw new RuntimeException("Value missing for " + argument); 136 } 137 138 final String value = args[++i]; 139 if (value.startsWith("-")) { 140 throw new RuntimeException("Illegal value for " + argument); 141 } 142 143 if ("-keystore".equalsIgnoreCase(argument)) { 144 final File keyStore = new File(value); 145 if (!keyStore.exists()) { 146 throw new RuntimeException("Keystore \"" + keyStore 147 + "\" doesn't exist"); 148 } 149 signingArgs.setKeyStore(keyStore); 150 } else if ("-storepass".equalsIgnoreCase(argument)) { 151 signingArgs.setStorePass(value); 152 } else if ("-alias".equalsIgnoreCase(argument)) { 153 signingArgs.setAlias(value); 154 } else if ("-keypass".equalsIgnoreCase(argument)) { 155 signingArgs.setKeyPass(value); 156 } else if ("-basedir".equalsIgnoreCase(argument)) { 157 baseDir = new File(value); 158 if (!baseDir.isDirectory()) { 159 throw new RuntimeException("Base directory \"" + value 160 + "\" doesn't exist"); 161 } 162 } else if ("-appdir".equalsIgnoreCase(argument)) { 163 if (appDir != null) { 164 appArgsList.add(createApplicationArgs( 165 appBaseDir, appDir, appName, appId)); 166 appName = null; 167 appId = null; 168 } 169 170 appDir = value; 171 appBaseDir = baseDir; 172 } else if ("-appname".equalsIgnoreCase(argument)) { 173 if (appName != null) { 174 appArgsList.add(createApplicationArgs( 175 appBaseDir, appDir, appName, appId)); 176 appDir = null; 177 appId = null; 178 } 179 180 appName = value; 181 } else if ("-appid".equalsIgnoreCase(argument)) { 182 if (appId != null) { 183 appArgsList.add(createApplicationArgs( 184 appBaseDir, appDir, appName, appId)); 185 appDir = null; 186 appName = null; 187 } 188 189 appId = value; 190 } 191 } 192 193 if ((appName != null) || (appId != null) || (appDir != null)) { 194 appArgsList.add(createApplicationArgs( 195 appBaseDir, appDir, appName, appId)); 196 } 197 198 if (destFile == null) { 199 throw new RuntimeException("Destination file not specified"); 200 } 201 202 if (appArgsList.isEmpty()) { 203 throw new RuntimeException("No application specified"); 204 } 205 206 signingArgs.validate(); 207 execute(destFile, signingArgs, appArgsList); 208 } 209 210 private static void execute( 211 final File destFile, 212 final SigningArgs signingArgs, 213 final List<ApplicationArgs> appArgsList) throws RuntimeException { 214 final Signer signer = createSigner(signingArgs); 215 216 final Packer packer; 217 try { 218 packer = new Packer(destFile, signer); 219 } catch (final IOException e) { 220 throw new RuntimeException( 221 constructExceptionMessage("Failed to create packer", e)); 222 } 223 224 try { 225 for (final ApplicationArgs appArgs: appArgsList) { 226 try { 227 packer.storeApplication( 228 appArgs.getBaseDir(), 229 appArgs.getAppDir(), 230 appArgs.getAppName(), 231 appArgs.getAppId()); 232 } catch (final IOException e) { 233 throw new RuntimeException( 234 constructExceptionMessage( 235 "Failed to pack " + appArgs.getAppId(), e)); 236 } 237 } 238 } finally { 239 packer.close(); 240 } 241 } 242 243 private static ApplicationArgs createApplicationArgs( 244 final File baseDir, 245 final String appDir, 246 final String appName, 247 final String appId) throws RuntimeException { 248 final ApplicationArgs applicationArgs = new ApplicationArgs(); 249 if (baseDir != null) { 250 applicationArgs.setBaseDir(baseDir); 251 } 252 if (appDir != null) { 253 applicationArgs.setAppDir(appDir); 254 } 255 if (appName != null) { 256 applicationArgs.setAppName(appName); 257 } 258 if (appId != null) { 259 applicationArgs.setAppId(appId); 260 } 261 262 applicationArgs.validate(); 263 return applicationArgs; 264 } 265 266 private static Signer createSigner(final SigningArgs signingArgs) 267 throws RuntimeException { 268 Exception exception; 269 try { 270 return Signer.create(signingArgs.getKeyStore(), 271 signingArgs.getStorePass(), 272 signingArgs.getAlias(), 273 signingArgs.getKeyPass()); 274 } catch (final KeyStoreException e) { 275 exception = e; 276 } catch (final NoSuchAlgorithmException e) { 277 exception = e; 278 } catch (final CertificateException e) { 279 exception = e; 280 } catch (final UnrecoverableKeyException e) { 281 exception = e; 282 } catch (final OperatorCreationException e) { 283 exception = e; 284 } catch (final CMSException e) { 285 exception = e; 286 } catch (final InvalidNameException e) { 287 exception = e; 288 } catch (final IOException e) { 289 exception = e; 290 } 291 292 throw new RuntimeException( 293 constructExceptionMessage("Failed to create signer", 294 exception)); 295 } 296 297 private static String constructExceptionMessage( 298 final String mainMessage, 299 final Exception exception) { 300 final StringBuilder sb = new StringBuilder(mainMessage); 301 final String nl = System.getProperty("line.separator"); 302 303 Throwable cause = exception; 304 while (cause != null) { 305 final String detail = cause.getMessage(); 306 if (detail != null) { 307 sb.append(nl).append(detail); 308 } 309 310 cause = cause.getCause(); 311 } 312 313 return sb.toString(); 314 } 315 316 private static final class SigningArgs { 317 private File keyStore; 318 private String storePass; 319 private String alias; 320 private String keyPass; 321 322 public SigningArgs() { 323 storePass = ""; 324 keyPass = ""; 325 } 326 327 public File getKeyStore() { 328 return keyStore; 329 } 330 331 public void setKeyStore(final File keyStore) { 332 this.keyStore = keyStore; 333 } 334 335 public String getStorePass() { 336 return storePass; 337 } 338 339 public void setStorePass(final String storePass) { 340 this.storePass = storePass; 341 } 342 343 public String getAlias() { 344 return alias; 345 } 346 347 public void setAlias(final String alias) { 348 this.alias = alias; 349 } 350 351 public String getKeyPass() { 352 return keyPass; 353 } 354 355 public void setKeyPass(final String keyPass) { 356 this.keyPass = keyPass; 357 } 358 359 public void validate() throws RuntimeException { 360 if (keyStore == null) { 361 throw new RuntimeException("Key store not specified"); 362 } 363 364 if (alias == null) { 365 throw new RuntimeException("Signing key not specified"); 366 } 367 } 368 } 369 370 private static final class ApplicationArgs { 371 private File baseDir; 372 private String appDir; 373 private String appName; 374 private String appId; 375 376 public ApplicationArgs() { 377 baseDir = new File(""); 378 appDir = ""; 379 } 380 381 public File getBaseDir() { 382 return baseDir; 383 } 384 385 public void setBaseDir(final File baseDir) { 386 this.baseDir = baseDir; 387 } 388 389 public String getAppDir() { 390 return appDir; 391 } 392 393 public void setAppDir(final String appDir) { 394 this.appDir = appDir; 395 } 396 397 public String getAppName() { 398 return appName; 399 } 400 401 public void setAppName(final String appName) { 402 this.appName = appName; 403 } 404 405 public String getAppId() { 406 return appId; 407 } 408 409 public void setAppId(final String appId) { 410 this.appId = appId; 411 } 412 413 public void validate() throws RuntimeException { 414 final ResourceDescriptor appDirDescriptor = 415 new ResourceDescriptor(baseDir, appDir); 416 417 if (!appDirDescriptor.getFile().exists()) { 418 throw new RuntimeException("Directory \"" + appDir 419 + "\" doesn't exist"); 420 } 421 422 if (appName == null) { 423 throw new RuntimeException( 424 "Application name not specified for " + appId); 425 } 426 427 final ResourceDescriptor appExeDescriptor = 428 new ResourceDescriptor(appDirDescriptor.getFile(), 429 appName); 430 if (!appExeDescriptor.getFile().exists()) { 431 throw new RuntimeException( 432 "Application \"" + appName + "\" doesn't exist"); 433 } 434 435 if (appId == null) { 436 throw new RuntimeException( 437 "Application id not specified for " + appName); 438 } 439 440 // all ok, store normalized paths 441 baseDir = appDirDescriptor.getBaseDir(); 442 appDir = appDirDescriptor.getRelativePath(); 443 appName = appExeDescriptor.getRelativePath(); 444 } 445 } 446 }