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