View Javadoc

1   /*
2    * Copyright (c) 2006-2007 Creative Sphere Limited.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v1.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v10.html
7    *
8    * Contributors:
9    *
10   *   Creative Sphere - initial API and implementation
11   *
12   */
13  package org.abstracthorizon.danube.auth.jaas.keystore;
14  
15  import java.io.IOException;
16  import java.io.InputStream;
17  import java.net.MalformedURLException;
18  import java.net.URL;
19  import java.security.GeneralSecurityException;
20  import java.security.Key;
21  import java.security.KeyStore;
22  import java.security.KeyStoreException;
23  import java.security.NoSuchAlgorithmException;
24  import java.security.NoSuchProviderException;
25  import java.security.PrivateKey;
26  import java.security.UnrecoverableKeyException;
27  import java.security.cert.Certificate;
28  import java.security.cert.CertificateException;
29  import java.security.cert.CertificateFactory;
30  import java.security.cert.X509Certificate;
31  import java.util.Iterator;
32  import java.util.LinkedList;
33  import java.util.Map;
34  import javax.security.auth.DestroyFailedException;
35  import javax.security.auth.Destroyable;
36  import javax.security.auth.Subject;
37  import javax.security.auth.callback.Callback;
38  import javax.security.auth.callback.CallbackHandler;
39  import javax.security.auth.callback.ConfirmationCallback;
40  import javax.security.auth.callback.NameCallback;
41  import javax.security.auth.callback.PasswordCallback;
42  import javax.security.auth.callback.TextOutputCallback;
43  import javax.security.auth.callback.UnsupportedCallbackException;
44  import javax.security.auth.login.FailedLoginException;
45  import javax.security.auth.login.LoginException;
46  import javax.security.auth.spi.LoginModule;
47  import javax.security.auth.x500.X500PrivateCredential;
48  
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  /**
53   * This is keystore login module. This login module checks keystore's
54   * certificates
55   *
56   * @author Daniel Sendula
57   */
58  public class KeyStoreLoginModule implements LoginModule {
59  
60      /** Logger */
61      protected Logger logger = LoggerFactory.getLogger(getClass());
62  
63      /** Subject */
64      protected Subject subject;
65  
66      /** Callback handler */
67      protected CallbackHandler callbackHandler;
68  
69      protected Map<String, ?> sharedState;
70  
71      protected Map<String, ?> options;
72  
73      /** Keystore URL */
74      private String keyStoreURL;
75  
76      /** Keystore password */
77      private char[] keyStorePassword;
78  
79      /** User's password */
80      private char[] userPassword;
81  
82      /** Username */
83      protected String username;
84  
85      /** Keystore type */
86      private String keyStoreType;
87  
88      /** Keystore provider */
89      private String keyStoreProvider;
90  
91      /** Uninitialised state */
92      protected static final int UNINITIALIZED = 0;
93  
94      /** Initialised state */
95      protected static final int INITIALIZED = 1;
96  
97      /** User authenticated state */
98      protected static final int AUTHENTICATED = 2;
99  
100     /** Logged in state */
101     protected static final int LOGGED_IN = 3;
102 
103     /** Current state defaulted to uninitialised */
104     protected int status = UNINITIALIZED;
105 
106     /** x500 principal */
107     private javax.security.auth.x500.X500Principal principal;
108 
109     /** Certificates */
110     private Certificate[] fromKeystore;
111 
112     /** Public credentials */
113     private java.security.cert.CertPath publicCredentials = null;
114 
115     /** Private credential */
116     private X500PrivateCredential privateCredential;
117 
118     /**
119      * Default contructor
120      */
121     public KeyStoreLoginModule() {
122     }
123 
124     /**
125      * Sets keystore password
126      * @param password keystore password
127      */
128     public void setKeystorePassword(String password) {
129         this.keyStorePassword = password.toCharArray();
130     }
131 
132     /**
133      * Sets keystore URL
134      * @param url keystore URL
135      */
136     public void setKeystoreURL(String url) {
137         this.keyStoreURL = url;
138     }
139 
140     /**
141      * Returns keystore URL
142      * @return keystore URL
143      */
144     public String getKeystoreURL() {
145         return keyStoreURL;
146     }
147 
148     /**
149      * Init method
150      * @param subject subject
151      * @param callbackHandler handler
152      * @param sharedState shared state
153      * @param options options
154      */
155     public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
156         this.subject = subject;
157         this.callbackHandler = callbackHandler;
158         this.sharedState = sharedState;
159         this.options = options;
160 
161         keyStoreType = (String)options.get("keyStoreType");
162         if (keyStoreType == null) {
163             keyStoreType = KeyStore.getDefaultType();
164         }
165         keyStoreProvider = (String)options.get("keyStoreProvider");
166 
167         String pass = (String)options.get("keyStorePassword");
168         if (pass != null) {
169             keyStorePassword = pass.toCharArray();
170         } else {
171             keyStorePassword = new char[0];
172         }
173 
174         keyStoreURL = (String) options.get("keyStoreURL");
175 
176         status = INITIALIZED;
177     }
178 
179     /**
180      * Login method
181      * @return <code>true</code> if successful
182      * @throws LoginException
183      */
184     public boolean login() throws LoginException {
185         if (status == LOGGED_IN) {
186             return true;
187         }
188         if ((status == INITIALIZED) || (status == AUTHENTICATED)) {
189             obtainAuthenticationDetails();
190             getKeyStoreInfo();
191             status = AUTHENTICATED;
192             return true;
193         }
194 
195         throw new LoginException("The login module is not initialized");
196     }
197 
198     /**
199      * This method obtains username and password from the party that tries to log in
200      * @throws LoginException
201      */
202     private void obtainAuthenticationDetails() throws LoginException {
203         TextOutputCallback bannerCallback = new TextOutputCallback(TextOutputCallback.INFORMATION, "Please login to keystore");
204         NameCallback aliasCallback = new NameCallback("Keystore alias: ");
205         PasswordCallback privateKeyPasswordCallback = new PasswordCallback("Password: ", false);
206         ConfirmationCallback confirmationCallback = new ConfirmationCallback(ConfirmationCallback.INFORMATION, ConfirmationCallback.OK_CANCEL_OPTION,
207                 ConfirmationCallback.OK);
208         try {
209             callbackHandler.handle(
210                     new Callback[]{
211                             bannerCallback,
212                             aliasCallback,
213                             privateKeyPasswordCallback,
214                             confirmationCallback}
215                 );
216         } catch (IOException e) {
217             throw new LoginException("Exception while getting keystore alias and password: " + e);
218         } catch (UnsupportedCallbackException e) {
219             throw new LoginException("Error: " + e.getCallback().toString() + " is not available to retrieve authentication "
220                     + " information from the user");
221         }
222 
223         int confirmationResult = confirmationCallback.getSelectedIndex();
224         if (confirmationResult == ConfirmationCallback.CANCEL) {
225             throw new LoginException("Login cancelled");
226         }
227 
228         username = aliasCallback.getName();
229 
230         char[] tmpPassword = privateKeyPasswordCallback.getPassword();
231         userPassword = new char[tmpPassword.length];
232         System.arraycopy(tmpPassword, 0, userPassword, 0, tmpPassword.length);
233         for (int i = 0; i < tmpPassword.length; i++)
234             tmpPassword[0] = ' ';
235         tmpPassword = null;
236         privateKeyPasswordCallback.clearPassword();
237     }
238 
239     /**
240      * This method loads keystore and obtains certificates
241      * @throws LoginException
242      */
243     private void getKeyStoreInfo() throws LoginException {
244         /* Get KeyStore instance */
245         KeyStore keystore;
246         try {
247             if ((keyStoreProvider == null) || (keyStoreProvider.length() == 0)) {
248                 keystore = KeyStore.getInstance(keyStoreType);
249             } else {
250                 keystore = KeyStore.getInstance(keyStoreType, keyStoreProvider);
251             }
252 
253             /* Load KeyStore contents from file */
254             logger.debug("Loading keystore from " + keyStoreURL.toString());
255             InputStream in = new URL(keyStoreURL).openStream();
256             try {
257                 keystore.load(in, keyStorePassword);
258             } finally {
259                 in.close();
260             }
261         } catch (KeyStoreException e) {
262             throw new LoginException("The keystore type is not available: " + e);
263         } catch (NoSuchProviderException e) {
264             throw new LoginException("The keystore provider is not available: " + e);
265         } catch (MalformedURLException e) {
266             throw new LoginException("Malformed keystoreURL option: " + e);
267         } catch (GeneralSecurityException e) {
268             throw new LoginException(e.getMessage());
269         } catch (IOException e) {
270             throw new LoginException("IOException: " + e);
271         }
272 
273         try {
274             fromKeystore = keystore.getCertificateChain(username);
275             if (fromKeystore == null || fromKeystore.length == 0 || !(fromKeystore[0] instanceof X509Certificate)) {
276                 throw new FailedLoginException("Unable to find X.509 certificate chain for " + username + " in keystore");
277             } else {
278                 LinkedList<Certificate> certList = new LinkedList<Certificate>();
279                 for (int i = 0; i < fromKeystore.length; i++) {
280                     certList.add(fromKeystore[i]);
281                 }
282                 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
283                 publicCredentials = certificateFactory.generateCertPath(certList);
284             }
285         } catch (KeyStoreException e) {
286             throw new LoginException(e.getMessage());
287         } catch (CertificateException e) {
288             throw new LoginException("X.509 Certificate unavailable: " + e);
289         }
290 
291         try {
292             X509Certificate certificate = (X509Certificate)fromKeystore[0];
293             principal = new javax.security.auth.x500.X500Principal(certificate.getSubjectDN().getName());
294             Key privateKey = keystore.getKey(username, userPassword);
295             if (privateKey == null || !(privateKey instanceof PrivateKey)) {
296                 throw new FailedLoginException("Unable to recover key from keystore");
297             }
298             privateCredential = new X500PrivateCredential(certificate, (PrivateKey)privateKey, username);
299         } catch (KeyStoreException e) {
300             throw new LoginException(e.getMessage());
301         } catch (NoSuchAlgorithmException e) {
302             throw new LoginException("No such algorithm: " + e);
303         } catch (UnrecoverableKeyException e) {
304             throw new FailedLoginException("Unable to recover key from keystore: " + e);
305         }
306     }
307 
308     /**
309      * Performs commit
310      * @return <code>true</code> if successful
311      * @throws LoginException
312      */
313     public boolean commit() throws LoginException {
314         if (status == LOGGED_IN) {
315             return true;
316         }
317         if (status == AUTHENTICATED) {
318             if (subject.isReadOnly()) {
319                 logoutImpl();
320                 throw new LoginException("Subject is set readonly");
321             } else {
322                 subject.getPrincipals().add(principal);
323                 subject.getPublicCredentials().add(publicCredentials);
324                 subject.getPrivateCredentials().add(privateCredential);
325                 status = LOGGED_IN;
326                 return true;
327             }
328         }
329         if (status == INITIALIZED) {
330             logoutImpl();
331             throw new LoginException("Authentication failed");
332         }
333 
334         throw new LoginException("The login module is not initialized");
335     }
336 
337     /**
338      * Aborts login
339      * @return <code>true</code> if successful
340      */
341     public boolean abort() throws LoginException {
342         if ((status == AUTHENTICATED) || (status == LOGGED_IN)) {
343             logoutImpl();
344             return true;
345         }
346 
347         return false;
348     }
349 
350     /**
351      * Logs out
352      * @return <code>true</code> if successful
353      */
354     public boolean logout() throws LoginException {
355         if (status == LOGGED_IN) {
356             logoutImpl();
357             return true;
358         }
359 
360         return false;
361     }
362 
363     /**
364      * Internal log out method
365      * @throws LoginException
366      */
367     private void logoutImpl() throws LoginException {
368         for (int i = 0; i < userPassword.length; i++) {
369             userPassword[i] = '\0';
370         }
371         userPassword = null;
372 
373         if (subject.isReadOnly()) {
374             principal = null;
375             publicCredentials = null;
376             status = INITIALIZED;
377 
378             Iterator<Object> it = subject.getPrivateCredentials().iterator();
379             while (it.hasNext()) {
380                 Object obj = it.next();
381                 if (privateCredential.equals(obj)) {
382                     privateCredential = null;
383                     try {
384                         ((Destroyable) obj).destroy();
385                         break;
386                     } catch (DestroyFailedException dfe) {
387                         throw new LoginException("Unable to destroy private credential, " + obj.getClass().getName() + ": " + dfe.getMessage());
388                     }
389                 }
390             }
391 
392             throw new LoginException("Unable to remove Principal (X500Principal) and public credential from read-only Subject");
393         }
394         if (principal != null) {
395             subject.getPrincipals().remove(principal);
396             principal = null;
397         }
398         if (publicCredentials != null) {
399             subject.getPublicCredentials().remove(publicCredentials);
400             publicCredentials = null;
401         }
402         if (privateCredential != null) {
403             subject.getPrivateCredentials().remove(privateCredential);
404             privateCredential = null;
405         }
406         status = INITIALIZED;
407     }
408 }