1. /*
  2. * @(#)KeyStoreLoginModule.java 1.13 03/01/23
  3. *
  4. * Copyright 2003 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package com.sun.security.auth.module;
  8. import javax.security.auth.x500.X500Principal;
  9. import java.io.File;
  10. import java.io.IOException;
  11. import java.io.InputStream;
  12. import java.io.PushbackInputStream;
  13. import java.net.MalformedURLException;
  14. import java.net.URL;
  15. import java.security.GeneralSecurityException;
  16. import java.security.Key;
  17. import java.security.KeyStore;
  18. import java.security.KeyStoreException;
  19. import java.security.NoSuchAlgorithmException;
  20. import java.security.NoSuchProviderException;
  21. import java.security.Principal;
  22. import java.security.PrivateKey;
  23. import java.security.UnrecoverableKeyException;
  24. import java.security.cert.*;
  25. import java.security.cert.X509Certificate;
  26. import java.util.Arrays;
  27. import java.util.Iterator;
  28. import java.util.LinkedList;
  29. import java.util.Map;
  30. import java.util.ResourceBundle;
  31. import javax.security.auth.Destroyable;
  32. import javax.security.auth.DestroyFailedException;
  33. import javax.security.auth.Subject;
  34. import javax.security.auth.x500.*;
  35. import javax.security.auth.Subject;
  36. import javax.security.auth.x500.*;
  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 sun.security.util.AuthResources;
  48. /**
  49. * Provides a JAAS login module that prompts for a key store alias and
  50. * populates the subject with the alias's principal and credentials. Stores
  51. * an <code>X500Principal</code> for the subject distinguished name of the
  52. * first certificate in the alias's credentials in the subject's principals,
  53. * the alias's certificate path in the subject's public credentials, and a
  54. * <code>X500PrivateCredential</code> whose certificate is the first
  55. * certificate in the alias's certificate path and whose private key is the
  56. * alias's private key in the subject's private credentials. <p>
  57. *
  58. * Recognizes the following options in the JAAS authentication policy file:
  59. * <dl>
  60. *
  61. * <dt> <code>keyStoreURL</code> </dt>
  62. * <dd> A URL that specifies the location of the key store file. Defaults to
  63. * the .keystore file in the directory specified by the
  64. * <code>java.home</code> system property. </dd>
  65. *
  66. * <dt> <code>keyStoreType</code> </dt>
  67. * <dd> The key store type. If not specified, defaults to the result of
  68. * calling <code>KeyStore.getDefaultType()</code>. </dd>
  69. *
  70. * <dt> <code>keyStoreProvider</code> </dt>
  71. * <dd> The key store provider. If not specified, uses the standard search
  72. * order to find the provider. </dd>
  73. *
  74. * <dt> <code>keyStoreAlias</code> </dt>
  75. * <dd> The alias in the key store to login as. Required when no callback
  76. * handler is provided. No default value. </dd>
  77. *
  78. * <dt> <code>keyStorePasswordURL</code> </dt>
  79. * <dd> A URL that specifies the location of the key store password. Required
  80. * when no callback handler is provided. No default value. </dd>
  81. *
  82. * <dt> <code>privateKeyPasswordURL</code> </dt>
  83. * <dd> A URL that specifies the location of the specific private key password
  84. * needed to access the private key for this alias.
  85. * The keystore password
  86. * is used if this value is not specified. </dd>
  87. * </dl>
  88. */
  89. public class KeyStoreLoginModule implements LoginModule {
  90. static final java.util.ResourceBundle rb =
  91. java.util.ResourceBundle.getBundle("sun.security.util.AuthResources");
  92. /* -- Fields -- */
  93. private static final int UNINITIALIZED = 0;
  94. private static final int INITIALIZED = 1;
  95. private static final int AUTHENTICATED = 2;
  96. private static final int LOGGED_IN = 3;
  97. private Subject subject;
  98. private CallbackHandler callbackHandler;
  99. private Map sharedState;
  100. private Map options;
  101. private char[] keyStorePassword;
  102. private char[] privateKeyPassword;
  103. private String keyStoreURL;
  104. private String keyStoreType;
  105. private String keyStoreProvider;
  106. private String keyStoreAlias;
  107. private String keyStorePasswordURL;
  108. private String privateKeyPasswordURL;
  109. private boolean debug;
  110. private javax.security.auth.x500.X500Principal principal;
  111. private Certificate[] fromKeyStore;
  112. private java.security.cert.CertPath certP = null;
  113. private X500PrivateCredential privateCredential;
  114. private int status = UNINITIALIZED;
  115. /* -- Methods -- */
  116. /**
  117. * Initialize this <code>LoginModule</code>.
  118. *
  119. * <p>
  120. *
  121. * @param subject the <code>Subject</code> to be authenticated. <p>
  122. *
  123. * @param callbackHandler a <code>CallbackHandler</code> for communicating
  124. * with the end user (prompting for usernames and
  125. * passwords, for example). <p>
  126. *
  127. * @param sharedState shared <code>LoginModule</code> state. <p>
  128. *
  129. * @param options options specified in the login
  130. * <code>Configuration</code> for this particular
  131. * <code>LoginModule</code>.
  132. */
  133. public void initialize(Subject subject,
  134. CallbackHandler callbackHandler,
  135. Map sharedState,
  136. Map options)
  137. {
  138. this.subject = subject;
  139. this.callbackHandler = callbackHandler;
  140. this.sharedState = sharedState;
  141. this.options = options;
  142. processOptions();
  143. status = INITIALIZED;
  144. }
  145. private void processOptions() {
  146. keyStoreURL = (String) options.get("keyStoreURL");
  147. if (keyStoreURL == null) {
  148. keyStoreURL =
  149. "file:" +
  150. System.getProperty("user.home").replace(
  151. File.separatorChar, '/') +
  152. '/' + ".keystore";
  153. }
  154. keyStoreType = (String) options.get("keyStoreType");
  155. if (keyStoreType == null) {
  156. keyStoreType = KeyStore.getDefaultType();
  157. }
  158. keyStoreProvider = (String) options.get("keyStoreProvider");
  159. keyStoreAlias = (String) options.get("keyStoreAlias");
  160. keyStorePasswordURL = (String) options.get("keyStorePasswordURL");
  161. privateKeyPasswordURL = (String) options.get("privateKeyPasswordURL");
  162. debug = "true".equalsIgnoreCase((String) options.get("debug"));
  163. if (debug)
  164. debugPrint("keyStoreURL=" + keyStoreURL +
  165. " keyStoreAlias=" + keyStoreAlias +
  166. " keyStorePasswordURL=" + keyStorePasswordURL +
  167. " privateKeyPasswordURL=" + privateKeyPasswordURL);
  168. }
  169. /**
  170. * Authenticate the user .
  171. *
  172. * <p> Prompt the user for the Keystore alias and the password. Retrieve
  173. * the alias's principal and credentials from the Keystore.
  174. *
  175. * <p>
  176. *
  177. * @exception FailedLoginException if the authentication fails. <p>
  178. *
  179. * @return true in all cases (this <code>LoginModule</code>
  180. * should not be ignored).
  181. */
  182. public boolean login() throws LoginException {
  183. switch (status) {
  184. case UNINITIALIZED:
  185. default:
  186. throw new LoginException("The login module is not initialized");
  187. case INITIALIZED:
  188. case AUTHENTICATED:
  189. getAliasAndPassword();
  190. getKeyStoreInfo();
  191. status = AUTHENTICATED;
  192. return true;
  193. case LOGGED_IN:
  194. return true;
  195. }
  196. }
  197. /** Get the alias and passwords to use for looking up in the KeyStore. */
  198. private void getAliasAndPassword() throws LoginException {
  199. if (callbackHandler == null) {
  200. /*
  201. * No callback handler. Check for alias and password files
  202. * specified in the options.
  203. */
  204. if (keyStoreAlias == null) {
  205. throw new LoginException(
  206. "Need to specify an alias option to use " +
  207. "KeyStoreLoginModule non-interactively.");
  208. }
  209. if (keyStorePasswordURL == null) {
  210. throw new LoginException(
  211. "Need to specify passwordFile option to use " +
  212. "KeyStoreLoginModule non-interactively.");
  213. }
  214. try {
  215. InputStream in = new URL(keyStorePasswordURL).openStream();
  216. keyStorePassword = readPassword(in);
  217. in.close();
  218. } catch (IOException e) {
  219. throw new LoginException(
  220. "Problem accessing keystore password \"" +
  221. keyStorePasswordURL + "\": " + e);
  222. }
  223. if (privateKeyPasswordURL == null) {
  224. privateKeyPassword = keyStorePassword;
  225. } else {
  226. try {
  227. InputStream in =
  228. new URL(privateKeyPasswordURL).openStream();
  229. privateKeyPassword = readPassword(in);
  230. in.close();
  231. } catch (IOException e) {
  232. throw new LoginException(
  233. "Problem accessing private key password \"" +
  234. privateKeyPasswordURL + "\": " + e);
  235. }
  236. }
  237. } else {
  238. TextOutputCallback bannerCallback =
  239. new TextOutputCallback(
  240. TextOutputCallback.INFORMATION,
  241. rb.getString("Please login to keystore"));
  242. NameCallback aliasCallback;
  243. if (keyStoreAlias == null || keyStoreAlias.length() == 0) {
  244. aliasCallback = new NameCallback(
  245. rb.getString("Keystore alias: "));
  246. } else {
  247. aliasCallback =
  248. new NameCallback(rb.getString("Keystore alias: "),
  249. keyStoreAlias);
  250. }
  251. PasswordCallback keyStorePasswordCallback =
  252. new PasswordCallback(rb.getString("Keystore password: "),
  253. false);
  254. PasswordCallback privateKeyPasswordCallback =
  255. new PasswordCallback(
  256. rb.getString("Private key password (optional): "), false);
  257. ConfirmationCallback confirmationCallback =
  258. new ConfirmationCallback(
  259. ConfirmationCallback.INFORMATION,
  260. ConfirmationCallback.OK_CANCEL_OPTION,
  261. ConfirmationCallback.OK);
  262. try {
  263. callbackHandler.handle(
  264. new Callback[] {
  265. bannerCallback, aliasCallback,
  266. keyStorePasswordCallback, privateKeyPasswordCallback,
  267. confirmationCallback
  268. });
  269. } catch (IOException e) {
  270. throw new LoginException(
  271. "Exception while getting keystore alias and password: " +
  272. e);
  273. } catch (UnsupportedCallbackException e) {
  274. throw new LoginException(
  275. "Error: " + e.getCallback().toString() +
  276. " is not available to retrieve authentication " +
  277. " information from the user");
  278. }
  279. int confirmationResult = confirmationCallback.getSelectedIndex();
  280. if (confirmationResult == ConfirmationCallback.CANCEL) {
  281. throw new LoginException("Login cancelled");
  282. }
  283. keyStoreAlias = aliasCallback.getName();
  284. char[] tmpPassword = keyStorePasswordCallback.getPassword();
  285. if (tmpPassword == null) {
  286. /* Treat a NULL password as an empty password */
  287. tmpPassword = new char[0];
  288. }
  289. keyStorePassword = new char[tmpPassword.length];
  290. System.arraycopy(tmpPassword, 0,
  291. keyStorePassword, 0, tmpPassword.length);
  292. keyStorePasswordCallback.clearPassword();
  293. tmpPassword = privateKeyPasswordCallback.getPassword();
  294. if (tmpPassword == null
  295. || tmpPassword.length == 0)
  296. {
  297. /*
  298. * Use keystore password if no private key password is
  299. * specified.
  300. */
  301. privateKeyPassword = keyStorePassword;
  302. } else {
  303. privateKeyPassword = new char[tmpPassword.length];
  304. System.arraycopy(tmpPassword, 0,
  305. privateKeyPassword, 0, tmpPassword.length);
  306. for (int i=0; i <tmpPassword.length ; i++)
  307. tmpPassword[0] = ' ';
  308. tmpPassword=null;
  309. privateKeyPasswordCallback.clearPassword();
  310. }
  311. if (debug)
  312. debugPrint("alias=" + keyStoreAlias);
  313. }
  314. }
  315. /** Get the credentials from the KeyStore. */
  316. private void getKeyStoreInfo() throws LoginException {
  317. /* Get KeyStore instance */
  318. KeyStore keyStore;
  319. try {
  320. if (keyStoreProvider == null) {
  321. keyStore = KeyStore.getInstance(keyStoreType);
  322. } else {
  323. keyStore =
  324. KeyStore.getInstance(keyStoreType, keyStoreProvider);
  325. }
  326. } catch (KeyStoreException e) {
  327. throw new LoginException(
  328. "The specified keystore type was not available: " + e);
  329. } catch (NoSuchProviderException e) {
  330. throw new LoginException(
  331. "The specified keystore provider was not available: " + e);
  332. }
  333. /* Load KeyStore contents from file */
  334. try {
  335. InputStream in = new URL(keyStoreURL).openStream();
  336. keyStore.load(in, keyStorePassword);
  337. in.close();
  338. } catch (MalformedURLException e) {
  339. throw new LoginException("Incorrect keyStoreURL option: " + e);
  340. } catch (GeneralSecurityException e) {
  341. throw new LoginException("Error initializing keystore: " + e);
  342. } catch (IOException e) {
  343. throw new LoginException("Error initializing keystore: " + e);
  344. }
  345. /* Get certificate chain and create a certificate path */
  346. try {
  347. fromKeyStore =
  348. keyStore.getCertificateChain(keyStoreAlias);
  349. if (fromKeyStore == null
  350. || fromKeyStore.length == 0
  351. || !(fromKeyStore[0] instanceof X509Certificate))
  352. {
  353. throw new FailedLoginException(
  354. "Unable to find X.509 certificate chain in keystore");
  355. } else {
  356. LinkedList certList = new LinkedList();
  357. for (int i=0; i < fromKeyStore.length; i++) {
  358. certList.add(fromKeyStore[i]);
  359. }
  360. CertificateFactory certF=
  361. CertificateFactory.getInstance("X.509");
  362. certP =
  363. certF.generateCertPath(certList);
  364. }
  365. } catch (KeyStoreException e) {
  366. throw new LoginException("Error using keystore: " + e);
  367. } catch (CertificateException ce) {
  368. throw new LoginException("Error: X.509 Certificate type unavailable: " + ce);
  369. }
  370. /* Get principal and keys */
  371. try {
  372. X509Certificate certificate = (X509Certificate)fromKeyStore[0];
  373. principal = new javax.security.auth.x500.X500Principal
  374. (certificate.getSubjectDN().getName());
  375. Key privateKey =
  376. keyStore.getKey(keyStoreAlias, privateKeyPassword);
  377. if (privateKey == null
  378. || !(privateKey instanceof PrivateKey))
  379. {
  380. throw new FailedLoginException(
  381. "Unable to recover key from keystore");
  382. }
  383. privateCredential = new X500PrivateCredential(
  384. certificate, (PrivateKey) privateKey, keyStoreAlias);
  385. } catch (KeyStoreException e) {
  386. throw new LoginException("Error using keystore: " + e);
  387. } catch (NoSuchAlgorithmException e) {
  388. throw new LoginException("Error using keystore: " + e);
  389. } catch (UnrecoverableKeyException e) {
  390. throw new FailedLoginException(
  391. "Unable to recover key from " +
  392. "keystore: " + e);
  393. }
  394. if (debug) {
  395. debugPrint("principal=" + principal +
  396. "\n certificate="
  397. + privateCredential.getCertificate() +
  398. "\n alias =" + privateCredential.getAlias());
  399. }
  400. }
  401. /**
  402. * Abstract method to commit the authentication process (phase 2).
  403. *
  404. * <p> This method is called if the LoginContext's
  405. * overall authentication succeeded
  406. * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
  407. * succeeded).
  408. *
  409. * <p> If this LoginModule's own authentication attempt
  410. * succeeded (checked by retrieving the private state saved by the
  411. * <code>login</code> method), then this method associates a
  412. * <code>X500Principal</code> for the subject distinguished name of the
  413. * first certificate in the alias's credentials in the subject's
  414. * principals,the alias's certificate path in the subject's public
  415. * credentials, and a<code>X500PrivateCredential</code> whose certificate
  416. * is the first certificate in the alias's certificate path and whose
  417. * private key is the alias's private key in the subject's private
  418. * credentials. If this LoginModule's own
  419. * authentication attempted failed, then this method removes
  420. * any state that was originally saved.
  421. *
  422. * <p>
  423. *
  424. * @exception LoginException if the commit fails
  425. *
  426. * @return true if this LoginModule's own login and commit
  427. * attempts succeeded, or false otherwise.
  428. */
  429. public boolean commit() throws LoginException {
  430. switch (status) {
  431. case UNINITIALIZED:
  432. default:
  433. throw new LoginException("The login module is not initialized");
  434. case INITIALIZED:
  435. logoutInternal();
  436. throw new LoginException("Authentication failed");
  437. case AUTHENTICATED:
  438. if (commitInternal()) {
  439. return true;
  440. } else {
  441. logoutInternal();
  442. throw new LoginException("Unable to retrieve certificates");
  443. }
  444. case LOGGED_IN:
  445. return true;
  446. }
  447. }
  448. private boolean commitInternal() throws LoginException {
  449. /* If the subject is not readonly add to the principal and credentials
  450. * set; otherwise just return true
  451. */
  452. if (subject.isReadOnly()) {
  453. throw new LoginException ("Subject is set readonly");
  454. } else {
  455. subject.getPrincipals().add(principal);
  456. subject.getPublicCredentials().add(certP);
  457. subject.getPrivateCredentials().add(privateCredential);
  458. status = LOGGED_IN;
  459. return true;
  460. }
  461. }
  462. /**
  463. * <p> This method is called if the LoginContext's
  464. * overall authentication failed.
  465. * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
  466. * did not succeed).
  467. *
  468. * <p> If this LoginModule's own authentication attempt
  469. * succeeded (checked by retrieving the private state saved by the
  470. * <code>login</code> and <code>commit</code> methods),
  471. * then this method cleans up any state that was originally saved.
  472. *
  473. * <p>
  474. *
  475. * @exception LoginException if the abort fails.
  476. *
  477. * @return false if this LoginModule's own login and/or commit attempts
  478. * failed, and true otherwise.
  479. */
  480. public boolean abort() throws LoginException {
  481. switch (status) {
  482. case UNINITIALIZED:
  483. default:
  484. return false;
  485. case INITIALIZED:
  486. return false;
  487. case AUTHENTICATED:
  488. logoutInternal();
  489. return true;
  490. case LOGGED_IN:
  491. logoutInternal();
  492. return true;
  493. }
  494. }
  495. /**
  496. * Logout a user.
  497. *
  498. * <p> This method removes the Principals, public credentials and the
  499. * private credentials that were added by the <code>commit</code> method.
  500. *
  501. * <p>
  502. *
  503. * @exception LoginException if the logout fails.
  504. *
  505. * @return true in all cases since this <code>LoginModule</code>
  506. * should not be ignored.
  507. */
  508. public boolean logout() throws LoginException {
  509. if (debug)
  510. debugPrint("Entering logout " + status);
  511. switch (status) {
  512. case UNINITIALIZED:
  513. throw new LoginException
  514. ("The login module is not initialized");
  515. case INITIALIZED:
  516. case AUTHENTICATED:
  517. default:
  518. // impossible for LoginModule to be in AUTHENTICATED
  519. // state
  520. // assert status != AUTHENTICATED;
  521. return false;
  522. case LOGGED_IN:
  523. logoutInternal();
  524. return true;
  525. }
  526. }
  527. private void logoutInternal() throws LoginException {
  528. if (debug)
  529. debugPrint("Entering logoutInternal");
  530. Arrays.fill(keyStorePassword, '\0');
  531. keyStorePassword = null;
  532. Arrays.fill(privateKeyPassword, '\0');
  533. privateKeyPassword = null;
  534. if (subject.isReadOnly()) {
  535. // attempt to destroy the private credential
  536. // even if the Subject is read-only
  537. principal = null;
  538. certP = null;
  539. status = INITIALIZED;
  540. // destroy the private credential
  541. Iterator it = subject.getPrivateCredentials().iterator();
  542. while (it.hasNext()) {
  543. Object obj = it.next();
  544. if (privateCredential.equals(obj)) {
  545. privateCredential = null;
  546. try {
  547. ((Destroyable)obj).destroy();
  548. if (debug)
  549. debugPrint("Destroyed private credential, " +
  550. obj.getClass().getName());
  551. break;
  552. } catch (DestroyFailedException dfe) {
  553. throw new LoginException
  554. ("Unable to destroy private credential, "
  555. + obj.getClass().getName()
  556. + ": " + dfe.getMessage());
  557. }
  558. }
  559. }
  560. // throw an exception because we can not remove
  561. // the principal and public credential from this
  562. // read-only Subject
  563. throw new LoginException
  564. ("Unable to remove Principal ("
  565. + "X500Principal "
  566. + ") and public credential (certificatepath) "
  567. + "from read-only Subject");
  568. }
  569. if (principal != null) {
  570. subject.getPrincipals().remove(principal);
  571. principal = null;
  572. }
  573. if (certP != null) {
  574. subject.getPublicCredentials().remove(certP);
  575. certP = null;
  576. }
  577. if (privateCredential != null) {
  578. subject.getPrivateCredentials().remove(privateCredential);
  579. privateCredential = null;
  580. }
  581. status = INITIALIZED;
  582. }
  583. /** Reads user password from given input stream. */
  584. private char[] readPassword(InputStream in) throws IOException {
  585. char[] lineBuffer;
  586. char[] buf;
  587. int i;
  588. buf = lineBuffer = new char[128];
  589. int room = buf.length;
  590. int offset = 0;
  591. int c;
  592. boolean done = false;
  593. while (!done) {
  594. switch (c = in.read()) {
  595. case -1:
  596. case '\n':
  597. done = true;
  598. break;
  599. case '\r':
  600. int c2 = in.read();
  601. if ((c2 != '\n') && (c2 != -1)) {
  602. if (!(in instanceof PushbackInputStream)) {
  603. in = new PushbackInputStream(in);
  604. }
  605. ((PushbackInputStream)in).unread(c2);
  606. } else {
  607. done = true;
  608. break;
  609. }
  610. default:
  611. if (--room < 0) {
  612. buf = new char[offset + 128];
  613. room = buf.length - offset - 1;
  614. System.arraycopy(lineBuffer, 0, buf, 0, offset);
  615. Arrays.fill(lineBuffer, ' ');
  616. lineBuffer = buf;
  617. }
  618. buf[offset++] = (char) c;
  619. break;
  620. }
  621. }
  622. if (offset == 0) {
  623. return null;
  624. }
  625. char[] ret = new char[offset];
  626. System.arraycopy(buf, 0, ret, 0, offset);
  627. Arrays.fill(buf, ' ');
  628. return ret;
  629. }
  630. private void debugPrint(String message) {
  631. // we should switch to logging API
  632. System.err.println("Debug KeyStoreLoginModule: " + message);
  633. }
  634. }