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