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 }