Minimal support for importing DES/DES3 keys.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / utils / ExtKeyTool.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3  * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
4  * provided that the following conditions are met: Redistributions of source code must retain the above copyright
5  * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above
6  * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
7  * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
8  * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu>Internet2 Project.
9  * Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
10  * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor
11  * the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
12  * products derived from this software without specific prior written permission. For written permission, please contact
13  * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
14  * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
15  * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
16  * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
18  * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
19  * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
20  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
23  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 package edu.internet2.middleware.shibboleth.utils;
27
28 import java.io.BufferedInputStream;
29 import java.io.BufferedReader;
30 import java.io.ByteArrayOutputStream;
31 import java.io.File;
32 import java.io.FileInputStream;
33 import java.io.FileNotFoundException;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.InputStreamReader;
38 import java.io.PrintStream;
39 import java.security.Key;
40 import java.security.KeyFactory;
41 import java.security.KeyStore;
42 import java.security.KeyStoreException;
43 import java.security.NoSuchAlgorithmException;
44 import java.security.NoSuchProviderException;
45 import java.security.PrivateKey;
46 import java.security.Provider;
47 import java.security.PublicKey;
48 import java.security.Security;
49 import java.security.UnrecoverableKeyException;
50 import java.security.cert.CertificateException;
51 import java.security.cert.CertificateFactory;
52 import java.security.cert.X509Certificate;
53 import java.security.interfaces.RSAKey;
54 import java.security.spec.KeySpec;
55 import java.security.spec.PKCS8EncodedKeySpec;
56 import java.util.ArrayList;
57 import java.util.Collection;
58 import java.util.Properties;
59
60 import javax.crypto.Cipher;
61 import javax.crypto.SecretKey;
62 import javax.crypto.SecretKeyFactory;
63 import javax.crypto.spec.DESKeySpec;
64 import javax.crypto.spec.DESedeKeySpec;
65
66 import org.apache.log4j.ConsoleAppender;
67 import org.apache.log4j.Level;
68 import org.apache.log4j.LogManager;
69 import org.apache.log4j.Logger;
70 import org.apache.log4j.PatternLayout;
71 import sun.misc.BASE64Encoder;
72
73 /**
74  * Extension utility for use alongside Sun's keytool program. Performs useful functions not found in original.
75  * 
76  * @author Walter Hoehn
77  */
78
79 public class ExtKeyTool {
80
81         protected static Logger log = Logger.getLogger(ExtKeyTool.class.getName());
82
83         /**
84          * Creates and initializes a java <code>KeyStore</code>
85          * 
86          * @param provider
87          *            name of the jce provider to use in loading the keystore
88          * @param keyStoreStream
89          *            stream used to retrieve the keystore
90          * @param storeType
91          *            the type of the keystore
92          * @param keyStorePassword
93          *            password used to verify the integrity of the keystore
94          * @throws ExtKeyToolException
95          *             if a problem is encountered loading the keystore
96          */
97
98         protected KeyStore loadKeyStore(String provider, InputStream keyStoreStream, String storeType,
99                         char[] keyStorePassword) throws ExtKeyToolException {
100
101                 try {
102                         if (storeType == null) {
103                                 storeType = "JKS";
104                         }
105
106                         log.debug("Using keystore type: (" + storeType + ")");
107                         log.debug("Using provider: (" + provider + ")");
108
109                         KeyStore keyStore;
110                         if (storeType.equals("JKS")) {
111                                 keyStore = KeyStore.getInstance(storeType, "SUN");
112                         } else if (storeType.equals("JCEKS")) {
113                                 keyStore = KeyStore.getInstance(storeType, "SunJCE");
114                         } else {
115                                 keyStore = KeyStore.getInstance(storeType, provider);
116                         }
117
118                         if (keyStoreStream == null) {
119                                 log.error("Keystore must be specified.");
120                                 throw new ExtKeyToolException("Keystore must be specified.");
121                         }
122                         if (keyStorePassword == null) {
123                                 log.warn("No password given for keystore, integrity will not be verified.");
124                         }
125                         keyStore.load(keyStoreStream, keyStorePassword);
126
127                         return keyStore;
128
129                 } catch (KeyStoreException e) {
130                         log.error("Problem loading keystore: " + e);
131                         throw new ExtKeyToolException("Problem loading keystore: " + e);
132                 } catch (NoSuchProviderException e) {
133                         log.error("The specified provider is not available.");
134                         throw new ExtKeyToolException("The specified provider is not available.");
135                 } catch (CertificateException ce) {
136                         log.error("Could not open keystore: " + ce);
137                         throw new ExtKeyToolException("Could not open keystore: " + ce);
138                 } catch (IOException ioe) {
139                         log.error("Could not export key: " + ioe);
140                         throw new ExtKeyToolException("Could not export key: " + ioe);
141                 } catch (NoSuchAlgorithmException nse) {
142                         log.error("Could not open keystore with the installed JCE providers: " + nse);
143                         throw new ExtKeyToolException("Could not open keystore with the installed JCE providers: " + nse);
144                 }
145         }
146
147         /**
148          * Retrieves a private key from a java keystore and writes it to an <code>PrintStream</code>
149          * 
150          * @param provider
151          *            name of the jce provider to use in retrieving the key
152          * @param outStream
153          *            stream that should be used to output the retrieved key
154          * @param keyStoreStream
155          *            stream used to retrieve the keystore
156          * @param storeType
157          *            the type of the keystore
158          * @param keyStorePassword
159          *            password used to verify the integrity of the keystore
160          * @param keyAlias
161          *            the alias under which the key is stored
162          * @param keyPassword
163          *            the password for recovering the key
164          * @param rfc
165          *            boolean indicator of whether the key should be Base64 encoded before being written to the stream
166          * @throws ExtKeyToolException
167          *             if there a problem retrieving or writing the key
168          */
169
170         public void exportKey(String provider, PrintStream outStream, InputStream keyStoreStream, String storeType,
171                         char[] keyStorePassword, String keyAlias, char[] keyPassword, boolean rfc) throws ExtKeyToolException {
172
173                 try {
174
175                         KeyStore keyStore = loadKeyStore(provider, keyStoreStream, storeType, keyStorePassword);
176
177                         if (keyAlias == null) {
178                                 log.error("Key alias must be specified.");
179                                 throw new ExtKeyToolException("Key alias must be specified.");
180                         }
181                         log.info("Searching for key.");
182
183                         Key key = keyStore.getKey(keyAlias, keyPassword);
184                         if (key == null) {
185                                 log.error("Key not found in store.");
186                                 throw new ExtKeyToolException("Key not found in store.");
187                         }
188                         log.info("Found key.");
189
190                         if (rfc) {
191                                 log.debug("Dumping with rfc encoding");
192                                 outStream.println("-----BEGIN PRIVATE KEY-----");
193                                 BASE64Encoder encoder = new BASE64Encoder();
194                                 encoder.encodeBuffer(key.getEncoded(), outStream);
195                                 outStream.println("-----END PRIVATE KEY-----");
196                         } else {
197                                 log.debug("Dumping with default encoding.");
198                                 outStream.write(key.getEncoded());
199                         }
200
201                 } catch (KeyStoreException e) {
202                         log.error("Problem accessing keystore: " + e);
203                         throw new ExtKeyToolException("Problem loading keystore: " + e);
204                 } catch (IOException ioe) {
205                         log.error("Could not export key: " + ioe);
206                         throw new ExtKeyToolException("Could not export key: " + ioe);
207                 } catch (NoSuchAlgorithmException nse) {
208                         log.error("Could not recover key with the installed JCE providers: " + nse);
209                         throw new ExtKeyToolException("Could not recover key with the installed JCE providers: " + nse);
210                 } catch (UnrecoverableKeyException uke) {
211                         log.error("The key specified key cannot be recovered with the given password: " + uke);
212                         throw new ExtKeyToolException("The key specified key cannot be recovered with the given password: " + uke);
213                 }
214         }
215
216     /**
217      * Attempts to unmarshall a secret key from a given stream.
218      * 
219      * @param keyStream
220      *            the <code>InputStream</code> suppying the key
221      * @param algorithm
222      *            the key algorithm
223      * @throws ExtKeyToolException
224      *             if there a problem unmarshalling the key
225      */
226
227     protected SecretKey readSecretKey(String provider, InputStream keyStream, String algorithm)
228             throws ExtKeyToolException {
229
230         try {
231             SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm, provider);
232
233             byte[] inputBuffer = new byte[8];
234             int i;
235             ByteContainer inputBytes = new ByteContainer(400);
236             do {
237                 i = keyStream.read(inputBuffer);
238                 for (int j = 0; j < i; j++) {
239                     inputBytes.append(inputBuffer[j]);
240                 }
241             } while (i > -1);
242
243             KeySpec keySpec = null;
244             if (algorithm.equals("DESede"))
245                 keySpec = new DESedeKeySpec(inputBytes.toByteArray());
246             else if (algorithm.equals("DES"))
247                 keySpec = new DESKeySpec(inputBytes.toByteArray());
248             return keyFactory.generateSecret(keySpec);
249
250         } catch (Exception e) {
251             log.error("Problem reading secret key: " + e.getMessage());
252             throw new ExtKeyToolException(
253                     "Problem reading secret key.  Keys should be DER encoded native format.");
254         }
255     }
256     
257         /**
258          * Boolean indication of whether a given private key and public key form a valid keypair.
259          * 
260          * @param pubKey
261          *            the public key
262          * @param privKey
263          *            the private key
264          */
265
266         protected boolean isMatchingKey(String algorithm, PublicKey pubKey, PrivateKey privKey) {
267
268                 try {
269                         String controlString = "asdf";
270                         log.debug("Checking for matching private key/public key pair");
271
272                         /*
273                          * If both keys are RSA, compare the modulus. They can't be a pair if that doesn't match. Doing this early
274                          * check means we don't have to do a trial encryption for every public key (faster) and avoids warning
275                          * messages from the depths of the crypto provider if the key lengths differ.
276                          */
277                         if (privKey instanceof RSAKey && pubKey instanceof RSAKey) {
278                                 RSAKey pubRSA = (RSAKey) pubKey;
279                                 RSAKey privRSA = (RSAKey) privKey;
280                                 if (!privRSA.getModulus().equals(pubRSA.getModulus())) {
281                                         log.debug("RSA modulus mismatch");
282                                         return false;
283                                 }
284                         }
285
286                         Cipher cipher = Cipher.getInstance(algorithm);
287                         cipher.init(Cipher.ENCRYPT_MODE, pubKey);
288                         byte[] encryptedData = cipher.doFinal(controlString.getBytes("UTF-8"));
289
290                         cipher.init(Cipher.DECRYPT_MODE, privKey);
291                         byte[] decryptedData = cipher.doFinal(encryptedData);
292                         if (controlString.equals(new String(decryptedData, "UTF-8"))) {
293                                 log.debug("Found match.");
294                                 return true;
295                         }
296                 } catch (Exception e) {
297                         log.warn(e);
298                 }
299                 log.debug("This pair does not match.");
300                 return false;
301         }
302
303         /**
304          * Attempts to unmarshall a private key from a given stream.
305          * 
306          * @param keyStream
307          *            the <code>InputStream</code> suppying the key
308          * @param algorithm
309          *            the key algorithm
310          * @throws ExtKeyToolException
311          *             if there a problem unmarshalling the key
312          */
313
314         protected PrivateKey readPrivateKey(String provider, InputStream keyStream, String algorithm)
315                         throws ExtKeyToolException {
316
317                 try {
318                         KeyFactory keyFactory = KeyFactory.getInstance(algorithm, provider);
319
320                         byte[] inputBuffer = new byte[8];
321                         int i;
322                         ByteContainer inputBytes = new ByteContainer(400);
323                         do {
324                                 i = keyStream.read(inputBuffer);
325                                 for (int j = 0; j < i; j++) {
326                                         inputBytes.append(inputBuffer[j]);
327                                 }
328                         } while (i > -1);
329
330                         PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(inputBytes.toByteArray());
331                         return keyFactory.generatePrivate(keySpec);
332
333                 } catch (Exception e) {
334                         log.error("Problem reading private key: " + e.getMessage());
335                         throw new ExtKeyToolException(
336                                         "Problem reading private key.  Keys should be DER encoded pkcs8 or DER encoded native format.");
337                 }
338         }
339
340         /**
341          * Converts an array of certificates into an ordered chain. A certificate that matches the specified private key
342          * will be returned first and the root certificate will be returned last.
343          * 
344          * @param untestedCerts
345          *            array of certificates
346          * @param privKey
347          *            the private key used to determine the first cert in the chain
348          * @throws InvalidCertificateChainException
349          *             thrown if a chain cannot be constructed from the specified elements
350          */
351
352         protected X509Certificate[] linkChain(String keyAlgorithm, X509Certificate[] untestedCerts, PrivateKey privKey)
353                         throws InvalidCertificateChainException {
354
355                 log.debug("Located " + untestedCerts.length + " cert(s) in input file");
356
357                 log.info("Finding end cert in chain.");
358                 ArrayList replyCerts = new ArrayList();
359                 for (int i = 0; untestedCerts.length > i; i++) {
360                         if (isMatchingKey(keyAlgorithm, untestedCerts[i].getPublicKey(), privKey)) {
361                                 log.debug("Found matching end cert: " + untestedCerts[i].getSubjectDN());
362                                 replyCerts.add(untestedCerts[i]);
363                         }
364                 }
365                 if (replyCerts.size() < 1) {
366                         log.error("No certificate in chain that matches specified private key");
367                         throw new InvalidCertificateChainException("No certificate in chain that matches specified private key");
368                 }
369                 if (replyCerts.size() > 1) {
370                         log.error("More than one certificate in chain that matches specified private key");
371                         throw new InvalidCertificateChainException(
372                                         "More than one certificate in chain that matches specified private key");
373                 }
374
375                 log.info("Populating chain with remaining certs.");
376                 walkChain(untestedCerts, replyCerts);
377
378                 log.info("Verifying that each link in the cert chain is signed appropriately");
379                 for (int i = 0; i < replyCerts.size() - 1; i++) {
380                         PublicKey pubKey = ((X509Certificate) replyCerts.get(i + 1)).getPublicKey();
381                         try {
382                                 ((X509Certificate) replyCerts.get(i)).verify(pubKey);
383                         } catch (Exception e) {
384                                 log.error("Certificate chain cannot be verified: " + e.getMessage());
385                                 throw new InvalidCertificateChainException("Certificate chain cannot be verified: " + e.getMessage());
386                         }
387                 }
388                 log.info("All signatures verified. Certificate chain successfully created.");
389
390                 return (X509Certificate[]) replyCerts.toArray(new X509Certificate[0]);
391         }
392
393         /**
394          * Given an ArrayList containing a base certificate and an array of unordered certificates, populates the ArrayList
395          * with an ordered certificate chain, based on subject and issuer.
396          * 
397          * @param chainSource
398          *            array of certificates to pull from
399          * @param chainDest
400          *            ArrayList containing base certificate
401          * @throws InvalidCertificateChainException
402          *             thrown if a chain cannot be constructed from the specified elements
403          */
404
405         protected void walkChain(X509Certificate[] chainSource, ArrayList chainDest)
406                         throws InvalidCertificateChainException {
407
408                 X509Certificate currentCert = (X509Certificate) chainDest.get(chainDest.size() - 1);
409                 if (currentCert.getSubjectDN().equals(currentCert.getIssuerDN())) {
410                         log.debug("Found self-signed root cert: " + currentCert.getSubjectDN());
411                         return;
412                 } else {
413                         for (int i = 0; chainSource.length > i; i++) {
414                                 if (currentCert.getIssuerDN().equals(chainSource[i].getSubjectDN())) {
415                                         chainDest.add(chainSource[i]);
416                                         walkChain(chainSource, chainDest);
417                                         return;
418                                 }
419                         }
420                         log.error("Incomplete certificate chain.");
421                         throw new InvalidCertificateChainException("Incomplete cerficate chain.");
422                 }
423         }
424
425         /**
426          * Given a java keystore, private key, and matching certificate chain; creates a new keystore containing the union
427          * of these objects
428          * 
429          * @param provider
430          *            the name of the jce provider to use
431          * @param keyAlgorithm
432          *            the algorithm of the key to be added, defaults to RSA if null
433          * @param keyStream
434          *            strema used to retrieve the private key, can contain a PEM encoded or pkcs8 encoded key
435          * @param chainStream
436          *            stream used to retrieve certificates, can contain a series of PEM encoded certs or a pkcs7 chain
437          * @param keyStoreInStream
438          *            stream used to retrieve the initial keystore
439          * @param storeType
440          *            the type of the keystore
441          * @param keyAlias
442          *            the alias under which the key/chain should be saved
443          * @param keyStorePassword
444          *            password used to verify the integrity of the old keystore and save the new keystore
445          * @param keyPassword
446          *            the password for saving the key
447      * @param secret
448      *            indicates this is a secret key import
449          * @return an OutputStream containing the new keystore
450          * @throws ExtKeyToolException
451          *             if there a problem importing the key
452          */
453
454         public ByteArrayOutputStream importKey(String provider, String keyAlgorithm, InputStream keyStream,
455                         InputStream chainStream, InputStream keyStoreInStream, String storeType, String keyAlias,
456                         char[] keyStorePassword, char[] keyPassword, boolean secret) throws ExtKeyToolException {
457
458                 log.info("Importing " + (secret ? "key pair" : "secret key."));
459                 try {
460
461                         // The Sun provider incorrectly reads only the first cert in the stream.
462                         // No loss, it won't even read RSA keys
463                         if (provider == "SUN") {
464                                 log.error("Sorry, this function not supported with the SUN provider.");
465                                 throw new ExtKeyToolException("Sorry, this function not supported with the SUN provider.");
466                         }
467
468                         KeyStore keyStore = loadKeyStore(provider, keyStoreInStream, storeType, keyStorePassword);
469
470                         if (keyAlias == null) {
471                                 log.error("Key alias must be specified.");
472                                 throw new ExtKeyToolException("Key alias must be specified.");
473                         }
474                         if (keyStore.containsAlias(keyAlias) == true && keyStore.isKeyEntry(keyAlias)) {
475                                 log.error("Could not import key: " + "key alias (" + keyAlias + ") already exists");
476                                 throw new ExtKeyToolException("Could not import key: " + "key alias (" + keyAlias + ") already exists");
477                         }
478                         keyStore.deleteEntry(keyAlias);
479
480             if (secret) {
481                 log.info("Reading secret key.");
482                 if (keyAlgorithm == null) {
483                     keyAlgorithm = "AES";
484                 }
485                 log.debug("Using key algorithm: (" + keyAlgorithm + ")");
486                 SecretKey key = readSecretKey(provider, keyStream, keyAlgorithm);
487                 keyStore.setKeyEntry(keyAlias, key, keyPassword, null);
488             }
489             else {
490                         log.info("Reading private key.");
491                         if (keyAlgorithm == null) {
492                                 keyAlgorithm = "RSA";
493                         }
494                         log.debug("Using key algorithm: (" + keyAlgorithm + ")");
495                         PrivateKey key = readPrivateKey(provider, keyStream, keyAlgorithm);
496     
497                         log.info("Reading certificate chain.");
498     
499                         CertificateFactory certFactory = CertificateFactory.getInstance("X.509", provider);
500                         Collection chain = certFactory.generateCertificates(new BufferedInputStream(chainStream));
501                         if (chain.isEmpty()) {
502                                 log.error("Input did not contain any valid certificates.");
503                                 throw new ExtKeyToolException("Input did not contain any valid certificates.");
504                         }
505     
506                         X509Certificate[] verifiedChain = linkChain(keyAlgorithm, (X509Certificate[]) chain
507                                         .toArray(new X509Certificate[0]), key);
508
509                 keyStore.setKeyEntry(keyAlias, key, keyPassword, verifiedChain);
510             }
511             
512                         ByteArrayOutputStream keyStoreOutStream = new ByteArrayOutputStream();
513                         keyStore.store(keyStoreOutStream, keyStorePassword);
514                         log.info("Key Store saved to stream.");
515                         return keyStoreOutStream;
516
517                 } catch (KeyStoreException e) {
518                         log.error("Encountered a problem accessing the keystore: " + e.getMessage());
519                         throw new ExtKeyToolException("Encountered a problem accessing the keystore: " + e.getMessage());
520                 } catch (CertificateException e) {
521                         log.error("Could not load certificate(s): " + e.getMessage());
522                         throw new ExtKeyToolException("Could not load certificate(s): " + e.getMessage());
523                 } catch (NoSuchProviderException e) {
524                         log.error("The specified provider is not available.");
525                         throw new ExtKeyToolException("The specified provider is not available.");
526                 } catch (IOException ioe) {
527                         log.error("Could not export key: " + ioe);
528                         throw new ExtKeyToolException("Could not export key: " + ioe);
529                 } catch (NoSuchAlgorithmException nse) {
530                         log.error("Could not save with the installed JCE providers: " + nse);
531                         throw new ExtKeyToolException("Could not save with the installed JCE providers: " + nse);
532                 }
533         }
534
535         /**
536          * Tries to decipher command line arguments.
537          * 
538          * @throws IllegalArgumentException
539          *             if arguments are not properly formatted
540          */
541
542         private static Properties parseArguments(String[] args) throws IllegalArgumentException {
543
544                 if (args.length < 1) { throw new IllegalArgumentException("No arguments found."); }
545                 Properties parsedArguments = new Properties();
546
547                 for (int i = 0; (i < args.length) && args[i].startsWith("-"); i++) {
548
549                         String flags = args[i];
550
551                         //parse actions
552                         if (flags.equalsIgnoreCase("-exportkey")) {
553                                 parsedArguments.setProperty("command", "exportKey");
554                         } else if (flags.equalsIgnoreCase("-importkey")) {
555                                 parsedArguments.setProperty("command", "importKey");
556                         }
557
558                         //parse specifiers
559             else if (flags.equalsIgnoreCase("-secret")) {
560                 parsedArguments.setProperty("secret", "true");
561             }
562                         else if (flags.equalsIgnoreCase("-alias")) {
563                                 if (++i == args.length) { throw new IllegalArgumentException("The argument -alias requires a parameter"); }
564                                 parsedArguments.setProperty("alias", args[i]);
565                         } else if (flags.equalsIgnoreCase("-keyfile")) {
566                                 if (++i == args.length) { throw new IllegalArgumentException(
567                                                 "The argument -keyfile requires a parameter"); }
568                                 parsedArguments.setProperty("keyFile", args[i]);
569                         } else if (flags.equalsIgnoreCase("-certfile")) {
570                                 if (++i == args.length) { throw new IllegalArgumentException(
571                                                 "The argument -certfile requires a parameter"); }
572                                 parsedArguments.setProperty("certFile", args[i]);
573                         } else if (flags.equalsIgnoreCase("-keystore")) {
574                                 if (++i == args.length) { throw new IllegalArgumentException(
575                                                 "The argument -keystore requires a parameter"); }
576                                 parsedArguments.setProperty("keyStore", args[i]);
577                         } else if (flags.equalsIgnoreCase("-storepass")) {
578                                 if (++i == args.length) { throw new IllegalArgumentException(
579                                                 "The argument -storepass requires a parameter"); }
580                                 parsedArguments.setProperty("storePass", args[i]);
581                         } else if (flags.equalsIgnoreCase("-storetype")) {
582                                 if (++i == args.length) { throw new IllegalArgumentException(
583                                                 "The argument -storetype requires a parameter"); }
584                                 parsedArguments.setProperty("storeType", args[i]);
585                         } else if (flags.equalsIgnoreCase("-keypass")) {
586                                 if (++i == args.length) { throw new IllegalArgumentException(
587                                                 "The argument -keypass requires a parameter"); }
588                                 parsedArguments.setProperty("keyPass", args[i]);
589                         } else if (flags.equalsIgnoreCase("-provider")) {
590                                 if (++i == args.length) { throw new IllegalArgumentException(
591                                                 "The argument -provider requires a parameter"); }
592                                 parsedArguments.setProperty("provider", args[i]);
593                         } else if (flags.equalsIgnoreCase("-file")) {
594                                 if (++i == args.length) { throw new IllegalArgumentException("The argument -file requires a parameter"); }
595                                 parsedArguments.setProperty("file", args[i]);
596                         } else if (flags.equalsIgnoreCase("-algorithm")) {
597                                 if (++i == args.length) { throw new IllegalArgumentException(
598                                                 "The argument -algorithm requires a parameter"); }
599                                 parsedArguments.setProperty("keyAlgorithm", args[i]);
600                         }
601
602                         //options
603                         else if (flags.equalsIgnoreCase("-v")) {
604                                 parsedArguments.setProperty("verbose", "true");
605                         } else if (flags.equalsIgnoreCase("-rfc")) {
606                                 parsedArguments.setProperty("rfc", "true");
607                         } else {
608                                 throw new IllegalArgumentException("Unrecognized argument: " + flags);
609                         }
610                 }
611                 if (parsedArguments.getProperty("command", null) == null) { throw new IllegalArgumentException(
612                                 "No action specified"); }
613                 return parsedArguments;
614         }
615
616         /**
617          * Ensures that providers specified on the command line are in fact loaded into the current environment.
618          * 
619          * @return the name of the provider add, null if no provider was added
620          */
621
622         protected String initProvider(Properties arguments) {
623
624                 try {
625                         if (arguments.getProperty("provider", null) != null) {
626
627                                 Provider provider = (Provider) Class.forName(arguments.getProperty("provider")).newInstance();
628                                 log.info("Adding Provider to environment: (" + provider.getName() + ")");
629                                 Security.addProvider(provider);
630                                 return provider.getName();
631                         }
632                 } catch (Exception e) {
633                         log.error("Could not load specified jce provider: " + e);
634                 }
635                 return null;
636
637         }
638
639         /**
640          * Initializes Log4J logger mode based on command line arguments.
641          */
642
643         protected void startLogger(Properties arguments) {
644
645                 Logger root = Logger.getRootLogger();
646                 if (arguments.getProperty("verbose", null) == null || arguments.getProperty("verbose", null).equals("false")) {
647                         root.addAppender(new ConsoleAppender(new PatternLayout(PatternLayout.DEFAULT_CONVERSION_PATTERN)));
648                         root.setLevel(Level.WARN);
649                 } else {
650                         root.addAppender(new ConsoleAppender(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN)));
651                         root.setLevel(Level.DEBUG);
652                 }
653         }
654
655         public static void main(String[] args) {
656
657                 try {
658                         ExtKeyTool tool = new ExtKeyTool();
659                         Properties arguments = null;
660                         try {
661                                 arguments = parseArguments(args);
662                         } catch (IllegalArgumentException iae) {
663                                 System.err.println("Illegal argument specified: " + iae.getMessage()
664                                                 + System.getProperty("line.separator"));
665                                 printUsage(System.err);
666                                 System.exit(1);
667                         }
668                         tool.startLogger(arguments);
669                         String providerName = tool.initProvider(arguments);
670                         if (providerName != null) {
671                                 arguments.setProperty("providerName", providerName);
672                         }
673                         tool.run(arguments);
674
675                 } catch (ExtKeyToolException ske) {
676                         log.fatal("Cannot Perform Operation: " + ske.getMessage() + System.getProperty("line.separator"));
677                         LogManager.shutdown();
678                         printUsage(System.err);
679                 }
680         }
681
682         /**
683          * Based on on a set of properties, executes <code>ExtKeyTool</code> actions.
684          * 
685          * @param arguments
686          *            runtime parameters specified on the command line
687          */
688
689         private void run(Properties arguments) throws ExtKeyToolException {
690
691                 //common for all actions
692                 char[] storePassword = null;
693                 if (arguments.getProperty("storePass", null) != null) {
694                         storePassword = arguments.getProperty("storePass").toCharArray();
695                 }
696
697                 String providerName = null;
698                 if (arguments.getProperty("providerName", null) != null) {
699                         providerName = arguments.getProperty("providerName");
700                 } else {
701                         providerName = "SUN";
702                 }
703
704                 //export key action
705                 if (arguments.getProperty("command").equals("exportKey")) {
706
707                         boolean rfc = false;
708                         if ("true".equalsIgnoreCase(arguments.getProperty("rfc", null))) {
709                                 rfc = true;
710                         }
711
712                         PrintStream outStream = null;
713                         if (arguments.getProperty("file", null) != null) {
714                                 try {
715                                         outStream = new PrintStream(new FileOutputStream(arguments.getProperty("file")));
716                                 } catch (FileNotFoundException e) {
717                                         throw new ExtKeyToolException("Could not open output file: " + e);
718                                 }
719                         } else {
720                                 outStream = System.out;
721                         }
722
723                         try {
724                                 exportKey(providerName, outStream, new FileInputStream(resolveKeyStore(arguments.getProperty(
725                                                 "keyStore", null))), arguments.getProperty("storeType", null), storePassword, arguments
726                                                 .getProperty("alias", null), resolveKeyPass(arguments.getProperty("keyPass", null),
727                                                 storePassword), rfc);
728                         } catch (FileNotFoundException e) {
729                                 throw new ExtKeyToolException("KeyStore not found.");
730                         }
731                         outStream.close();
732
733                         //import action
734                 } else if (arguments.getProperty("command").equals("importKey")) {
735
736                         InputStream keyInStream = null;
737                         if (arguments.getProperty("keyFile", null) != null) {
738                                 try {
739                                         keyInStream = new FileInputStream(arguments.getProperty("keyFile"));
740                                 } catch (FileNotFoundException e) {
741                                         throw new ExtKeyToolException("Could not open key file." + e.getMessage());
742                                 }
743                         } else {
744                                 throw new IllegalArgumentException("Key file must be specified.");
745                         }
746
747                         InputStream certInStream = null;
748                         if (arguments.getProperty("certFile", null) != null) {
749                                 try {
750                                         certInStream = new FileInputStream(arguments.getProperty("certFile"));
751                                 } catch (FileNotFoundException e) {
752                                         throw new ExtKeyToolException("Could not open cert file." + e.getMessage());
753                                 }
754                         } else if (!arguments.getProperty("secret").equalsIgnoreCase("true")){
755                                 throw new IllegalArgumentException("Certificate file must be specified.");
756                         }
757
758                         try {
759
760                                 ByteArrayOutputStream keyStoreOutStream = importKey(
761                         providerName,
762                         arguments.getProperty("keyAlgorithm", null),
763                         keyInStream,
764                         certInStream,
765                         new FileInputStream(resolveKeyStore(arguments.getProperty("keyStore", null))),
766                         arguments.getProperty("storeType", null),
767                         arguments.getProperty("alias", null),
768                         storePassword,
769                         resolveKeyPass(arguments.getProperty("keyPass", null), storePassword),
770                         arguments.getProperty("secret","false").equalsIgnoreCase("true")
771                         );
772
773                                 keyInStream.close();
774                                 // A quick sanity check before we overwrite the old keystore
775                                 if (keyStoreOutStream == null || keyStoreOutStream.size() < 1) { throw new ExtKeyToolException(
776                                                 "Failed to create keystore: results are null"); }
777                                 keyStoreOutStream
778                                                 .writeTo(new FileOutputStream(resolveKeyStore(arguments.getProperty("keyStore", null))));
779                                 System.out.println("Key import successful.");
780
781                         } catch (FileNotFoundException e) {
782                                 throw new ExtKeyToolException("Could not open keystore file." + e.getMessage());
783                         } catch (IOException e) {
784                                 throw new ExtKeyToolException("Error writing keystore." + e.getMessage());
785                         }
786
787                 } else {
788                         throw new IllegalArgumentException("This keytool cannot perform the operation: ("
789                                         + arguments.getProperty("command") + ")");
790                 }
791
792         }
793
794         /**
795          * Determines the location of the keystore to use when performing the action
796          * 
797          * @return the <code>File</code> representation of the selected keystore
798          */
799
800         protected File resolveKeyStore(String keyStoreLocation) throws ExtKeyToolException, FileNotFoundException {
801
802                 if (keyStoreLocation == null) {
803                         keyStoreLocation = System.getProperty("user.home") + File.separator + ".keystore";
804                 }
805                 log.debug("Using keystore (" + keyStoreLocation + ")");
806                 File file = new File(keyStoreLocation);
807                 if (file.exists() && file.length() == 0) {
808                         log.error("Keystore file is empty.");
809                         throw new ExtKeyToolException("Keystore file is empty.");
810                 }
811                 return file;
812         }
813
814         /**
815          * Decides what password to use for storing/retrieving keys from the keystore. NOTE: Possible terminal interaction
816          * with the user.
817          * 
818          * @return a char array containing the password
819          */
820
821         protected char[] resolveKeyPass(String keyPass, char[] storePass) {
822
823                 if (keyPass != null) {
824                         return keyPass.toCharArray();
825                 } else {
826                         System.out.println("Enter key password");
827                         System.out.print("\t(RETURN if same as keystore password):  ");
828                         System.out.flush();
829                         try {
830                                 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
831                                 String passwordInput = reader.readLine();
832                                 passwordInput.trim();
833                                 if (passwordInput != null && !passwordInput.equals("")) { return passwordInput.toCharArray(); }
834                         } catch (IOException e) {
835                                 log.warn(e.getMessage());
836                         }
837                         log.warn("No password specified, defaulting to keystore password.");
838                         return storePass;
839                 }
840         }
841
842         private static void printUsage(PrintStream out) {
843
844                 out.println("extkeytool usage:");
845                 out.print("-exportkey      [-v] [-rfc] [-alias <alias>] ");
846                 out.println("[-keystore <keystore>] ");
847                 out.print("\t     [-storepass <storepass>] ");
848                 out.println("[-storetype <storetype>]");
849                 out.print("\t     [-keypass <keypass>] ");
850                 out.println("[-provider <provider_class_name>] ");
851                 out.print("\t     [-file <output_file>] ");
852                 out.println();
853                 out.println();
854
855                 out.print("-importkey      [-v] [-secret] [-alias <alias>] ");
856                 out.println("[-keyfile <key_file>]");
857                 out.print("\t     [-keystore <keystore>] ");
858                 out.println("[-storepass <storepass>]");
859                 out.print("\t     [-storetype <storetype>] ");
860                 out.println("[-keypass <keypass>] ");
861                 out.print("\t     [-provider <provider_class_name>] ");
862                 out.println("[-certfile <cert_file>] ");
863                 out.print("\t     [-algorithm <key_algorithm>] ");
864                 out.println();
865
866         }
867
868         /**
869          * Auto-enlarging container for bytes.
870          */
871
872         // Sure makes you wish bytes were first class objects.
873         private class ByteContainer {
874
875                 private byte[] buffer;
876                 private int cushion;
877                 private int currentSize = 0;
878
879                 private ByteContainer(int cushion) {
880
881                         buffer = new byte[cushion];
882                         this.cushion = cushion;
883                 }
884
885                 private void grow() {
886
887                         log.debug("Growing ByteContainer.");
888                         int newSize = currentSize + cushion;
889                         byte[] b = new byte[newSize];
890                         int toCopy = Math.min(currentSize, newSize);
891                         int i;
892                         for (i = 0; i < toCopy; i++) {
893                                 b[i] = buffer[i];
894                         }
895                         buffer = b;
896                 }
897
898                 /**
899                  * Returns an array of the bytes in the container.
900                  * <p>
901                  */
902
903                 private byte[] toByteArray() {
904
905                         byte[] b = new byte[currentSize];
906                         for (int i = 0; i < currentSize; i++) {
907                                 b[i] = buffer[i];
908                         }
909                         return b;
910                 }
911
912                 /**
913                  * Add one byte to the end of the container.
914                  */
915
916                 private void append(byte b) {
917
918                         if (currentSize == buffer.length) {
919                                 grow();
920                         }
921                         buffer[currentSize] = b;
922                         currentSize++;
923                 }
924
925         }
926
927         /**
928          * Signals that an error was encounted while using <code>ExtKeyTool</code> functions.
929          */
930
931         protected class ExtKeyToolException extends Exception {
932
933                 protected ExtKeyToolException(String message) {
934
935                         super(message);
936                 }
937         }
938
939         /**
940          * Signals that an error occurred while trying to constuct a certificate chain.
941          */
942
943         protected class InvalidCertificateChainException extends ExtKeyToolException {
944
945                 protected InvalidCertificateChainException(String message) {
946
947                         super(message);
948                 }
949         }
950
951 }