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