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