PasswordFile.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. /* PasswordFile.java --
  2. Copyright (C) 2003, 2006 Free Software Foundation, Inc.
  3. This file is a part of GNU Classpath.
  4. GNU Classpath is free software; you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation; either version 2 of the License, or (at
  7. your option) any later version.
  8. GNU Classpath is distributed in the hope that it will be useful, but
  9. WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with GNU Classpath; if not, write to the Free Software
  14. Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
  15. USA
  16. Linking this library statically or dynamically with other modules is
  17. making a combined work based on this library. Thus, the terms and
  18. conditions of the GNU General Public License cover the whole
  19. combination.
  20. As a special exception, the copyright holders of this library give you
  21. permission to link this library with independent modules to produce an
  22. executable, regardless of the license terms of these independent
  23. modules, and to copy and distribute the resulting executable under
  24. terms of your choice, provided that you also meet, for each linked
  25. independent module, the terms and conditions of the license of that
  26. module. An independent module is a module which is not derived from
  27. or based on this library. If you modify this library, you may extend
  28. this exception to your version of the library, but you are not
  29. obligated to do so. If you do not wish to do so, delete this
  30. exception statement from your version. */
  31. package gnu.javax.crypto.sasl.srp;
  32. import gnu.java.lang.CPStringBuilder;
  33. import gnu.java.security.Registry;
  34. import gnu.java.security.util.Util;
  35. import gnu.javax.crypto.key.srp6.SRPAlgorithm;
  36. import gnu.javax.crypto.sasl.NoSuchUserException;
  37. import gnu.javax.crypto.sasl.UserAlreadyExistsException;
  38. import java.io.BufferedReader;
  39. import java.io.File;
  40. import java.io.FileInputStream;
  41. import java.io.FileNotFoundException;
  42. import java.io.FileOutputStream;
  43. import java.io.IOException;
  44. import java.io.InputStream;
  45. import java.io.InputStreamReader;
  46. import java.io.PrintWriter;
  47. import java.io.UnsupportedEncodingException;
  48. import java.math.BigInteger;
  49. import java.util.HashMap;
  50. import java.util.Iterator;
  51. import java.util.NoSuchElementException;
  52. import java.util.StringTokenizer;
  53. /**
  54. * The implementation of SRP password files.
  55. * <p>
  56. * For SRP, there are three (3) files:
  57. * <ol>
  58. * <li>The password configuration file: tpasswd.conf. It contains the pairs
  59. * &lt;N,g> indexed by a number for each pair used for a user. By default, this
  60. * file's pathname is constructed from the base password file pathname by
  61. * prepending it with the ".conf" suffix.</li>
  62. * <li>The base password file: tpasswd. It contains the related password
  63. * entries for all the users with values computed using SRP's default message
  64. * digest algorithm: SHA-1 (with 160-bit output block size).</li>
  65. * <li>The extended password file: tpasswd2. Its name, by default, is
  66. * constructed by adding the suffix "2" to the fully qualified pathname of the
  67. * base password file. It contains, in addition to the same fields as the base
  68. * password file, albeit with a different verifier value, an extra field
  69. * identifying the message digest algorithm used to compute this (verifier)
  70. * value.</li>
  71. * </ol>
  72. * <p>
  73. * This implementation assumes the following message digest algorithm codes:
  74. * <ul>
  75. * <li>0: the default hash algorithm, which is SHA-1 (or its alias SHA-160).</li>
  76. * <li>1: MD5.</li>
  77. * <li>2: RIPEMD-128.</li>
  78. * <li>3: RIPEMD-160.</li>
  79. * <li>4: SHA-256.</li>
  80. * <li>5: SHA-384.</li>
  81. * <li>6: SHA-512.</li>
  82. * </ul>
  83. * <p>
  84. * <b>IMPORTANT:</b> This method computes the verifiers as described in
  85. * RFC-2945, which differs from the description given on the web page for SRP-6.
  86. * <p>
  87. * Reference:
  88. * <ol>
  89. * <li><a href="http://srp.stanford.edu/design.html">SRP Protocol Design</a><br>
  90. * Thomas J. Wu.</li>
  91. * </ol>
  92. */
  93. public class PasswordFile
  94. {
  95. // names of property keys used in this class
  96. private static final String USER_FIELD = "user";
  97. private static final String VERIFIERS_FIELD = "verifier";
  98. private static final String SALT_FIELD = "salt";
  99. private static final String CONFIG_FIELD = "config";
  100. private static String DEFAULT_FILE;
  101. static
  102. {
  103. DEFAULT_FILE = System.getProperty(SRPRegistry.PASSWORD_FILE,
  104. SRPRegistry.DEFAULT_PASSWORD_FILE);
  105. }
  106. /** The SRP algorithm instances used by this object. */
  107. private static final HashMap srps;
  108. static
  109. {
  110. final HashMap map = new HashMap(SRPRegistry.SRP_ALGORITHMS.length);
  111. // The first entry MUST exist. The others are optional.
  112. map.put("0", SRP.instance(SRPRegistry.SRP_ALGORITHMS[0]));
  113. for (int i = 1; i < SRPRegistry.SRP_ALGORITHMS.length; i++)
  114. {
  115. try
  116. {
  117. map.put(String.valueOf(i),
  118. SRP.instance(SRPRegistry.SRP_ALGORITHMS[i]));
  119. }
  120. catch (Exception x)
  121. {
  122. System.err.println("Ignored: " + x);
  123. x.printStackTrace(System.err);
  124. }
  125. }
  126. srps = map;
  127. }
  128. private String confName, pwName, pw2Name;
  129. private File configFile, passwdFile, passwd2File;
  130. private long lastmodPasswdFile, lastmodPasswd2File;
  131. private HashMap entries = new HashMap();
  132. private HashMap configurations = new HashMap();
  133. // default N values to use when creating a new password.conf file
  134. private static final BigInteger[] Nsrp = new BigInteger[] {
  135. SRPAlgorithm.N_2048,
  136. SRPAlgorithm.N_1536,
  137. SRPAlgorithm.N_1280,
  138. SRPAlgorithm.N_1024,
  139. SRPAlgorithm.N_768,
  140. SRPAlgorithm.N_640,
  141. SRPAlgorithm.N_512 };
  142. public PasswordFile() throws IOException
  143. {
  144. this(DEFAULT_FILE);
  145. }
  146. public PasswordFile(final File pwFile) throws IOException
  147. {
  148. this(pwFile.getAbsolutePath());
  149. }
  150. public PasswordFile(final String pwName) throws IOException
  151. {
  152. this(pwName, pwName + "2", pwName + ".conf");
  153. }
  154. public PasswordFile(final String pwName, final String confName)
  155. throws IOException
  156. {
  157. this(pwName, pwName + "2", confName);
  158. }
  159. public PasswordFile(final String pwName, final String pw2Name,
  160. final String confName) throws IOException
  161. {
  162. super();
  163. this.pwName = pwName;
  164. this.pw2Name = pw2Name;
  165. this.confName = confName;
  166. readOrCreateConf();
  167. update();
  168. }
  169. /**
  170. * Returns a string representing the decimal value of an integer identifying
  171. * the message digest algorithm to use for the SRP computations.
  172. *
  173. * @param mdName the canonical name of a message digest algorithm.
  174. * @return a string representing the decimal value of an ID for that
  175. * algorithm.
  176. */
  177. private static final String nameToID(final String mdName)
  178. {
  179. if (Registry.SHA_HASH.equalsIgnoreCase(mdName)
  180. || Registry.SHA1_HASH.equalsIgnoreCase(mdName)
  181. || Registry.SHA160_HASH.equalsIgnoreCase(mdName))
  182. return "0";
  183. else if (Registry.MD5_HASH.equalsIgnoreCase(mdName))
  184. return "1";
  185. else if (Registry.RIPEMD128_HASH.equalsIgnoreCase(mdName))
  186. return "2";
  187. else if (Registry.RIPEMD160_HASH.equalsIgnoreCase(mdName))
  188. return "3";
  189. else if (Registry.SHA256_HASH.equalsIgnoreCase(mdName))
  190. return "4";
  191. else if (Registry.SHA384_HASH.equalsIgnoreCase(mdName))
  192. return "5";
  193. else if (Registry.SHA512_HASH.equalsIgnoreCase(mdName))
  194. return "6";
  195. return "0";
  196. }
  197. /**
  198. * Checks if the current configuration file contains the &lt;N, g> pair for
  199. * the designated <code>index</code>.
  200. *
  201. * @param index a string representing 1-digit identification of an &lt;N, g>
  202. * pair used.
  203. * @return <code>true</code> if the designated <code>index</code> is that
  204. * of a known &lt;N, g> pair, and <code>false</code> otherwise.
  205. * @throws IOException if an exception occurs during the process.
  206. * @see SRPRegistry#N_2048_BITS
  207. * @see SRPRegistry#N_1536_BITS
  208. * @see SRPRegistry#N_1280_BITS
  209. * @see SRPRegistry#N_1024_BITS
  210. * @see SRPRegistry#N_768_BITS
  211. * @see SRPRegistry#N_640_BITS
  212. * @see SRPRegistry#N_512_BITS
  213. */
  214. public synchronized boolean containsConfig(final String index)
  215. throws IOException
  216. {
  217. checkCurrent();
  218. return configurations.containsKey(index);
  219. }
  220. /**
  221. * Returns a pair of strings representing the pair of <code>N</code> and
  222. * <code>g</code> MPIs for the designated <code>index</code>.
  223. *
  224. * @param index a string representing 1-digit identification of an &lt;N, g>
  225. * pair to look up.
  226. * @return a pair of strings, arranged in an array, where the first (at index
  227. * position #0) is the repesentation of the MPI <code>N</code>, and
  228. * the second (at index position #1) is the representation of the MPI
  229. * <code>g</code>. If the <code>index</code> refers to an unknown
  230. * pair, then an empty string array is returned.
  231. * @throws IOException if an exception occurs during the process.
  232. */
  233. public synchronized String[] lookupConfig(final String index)
  234. throws IOException
  235. {
  236. checkCurrent();
  237. String[] result = null;
  238. if (configurations.containsKey(index))
  239. result = (String[]) configurations.get(index);
  240. return result;
  241. }
  242. public synchronized boolean contains(final String user) throws IOException
  243. {
  244. checkCurrent();
  245. return entries.containsKey(user);
  246. }
  247. public synchronized void add(final String user, final String passwd,
  248. final byte[] salt, final String index)
  249. throws IOException
  250. {
  251. checkCurrent();
  252. if (entries.containsKey(user))
  253. throw new UserAlreadyExistsException(user);
  254. final HashMap fields = new HashMap(4);
  255. fields.put(USER_FIELD, user); // 0
  256. fields.put(VERIFIERS_FIELD, newVerifiers(user, salt, passwd, index)); // 1
  257. fields.put(SALT_FIELD, Util.toBase64(salt)); // 2
  258. fields.put(CONFIG_FIELD, index); // 3
  259. entries.put(user, fields);
  260. savePasswd();
  261. }
  262. public synchronized void changePasswd(final String user, final String passwd)
  263. throws IOException
  264. {
  265. checkCurrent();
  266. if (! entries.containsKey(user))
  267. throw new NoSuchUserException(user);
  268. final HashMap fields = (HashMap) entries.get(user);
  269. final byte[] salt;
  270. try
  271. {
  272. salt = Util.fromBase64((String) fields.get(SALT_FIELD));
  273. }
  274. catch (NumberFormatException x)
  275. {
  276. throw new IOException("Password file corrupt");
  277. }
  278. final String index = (String) fields.get(CONFIG_FIELD);
  279. fields.put(VERIFIERS_FIELD, newVerifiers(user, salt, passwd, index));
  280. entries.put(user, fields);
  281. savePasswd();
  282. }
  283. public synchronized void savePasswd() throws IOException
  284. {
  285. final FileOutputStream f1 = new FileOutputStream(passwdFile);
  286. final FileOutputStream f2 = new FileOutputStream(passwd2File);
  287. PrintWriter pw1 = null;
  288. PrintWriter pw2 = null;
  289. try
  290. {
  291. pw1 = new PrintWriter(f1, true);
  292. pw2 = new PrintWriter(f2, true);
  293. this.writePasswd(pw1, pw2);
  294. }
  295. finally
  296. {
  297. if (pw1 != null)
  298. try
  299. {
  300. pw1.flush();
  301. }
  302. finally
  303. {
  304. pw1.close();
  305. }
  306. if (pw2 != null)
  307. try
  308. {
  309. pw2.flush();
  310. }
  311. finally
  312. {
  313. pw2.close();
  314. }
  315. try
  316. {
  317. f1.close();
  318. }
  319. catch (IOException ignored)
  320. {
  321. }
  322. try
  323. {
  324. f2.close();
  325. }
  326. catch (IOException ignored)
  327. {
  328. }
  329. }
  330. lastmodPasswdFile = passwdFile.lastModified();
  331. lastmodPasswd2File = passwd2File.lastModified();
  332. }
  333. /**
  334. * Returns the triplet: verifier, salt and configuration file index, of a
  335. * designated user, and a designated message digest algorithm name, as an
  336. * array of strings.
  337. *
  338. * @param user the username.
  339. * @param mdName the canonical name of the SRP's message digest algorithm.
  340. * @return a string array containing, in this order, the BASE-64 encodings of
  341. * the verifier, the salt and the index in the password configuration
  342. * file of the MPIs N and g of the designated user.
  343. */
  344. public synchronized String[] lookup(final String user, final String mdName)
  345. throws IOException
  346. {
  347. checkCurrent();
  348. if (! entries.containsKey(user))
  349. throw new NoSuchUserException(user);
  350. final HashMap fields = (HashMap) entries.get(user);
  351. final HashMap verifiers = (HashMap) fields.get(VERIFIERS_FIELD);
  352. final String salt = (String) fields.get(SALT_FIELD);
  353. final String index = (String) fields.get(CONFIG_FIELD);
  354. final String verifier = (String) verifiers.get(nameToID(mdName));
  355. return new String[] { verifier, salt, index };
  356. }
  357. private synchronized void readOrCreateConf() throws IOException
  358. {
  359. configurations.clear();
  360. final FileInputStream fis;
  361. configFile = new File(confName);
  362. try
  363. {
  364. fis = new FileInputStream(configFile);
  365. readConf(fis);
  366. }
  367. catch (FileNotFoundException x)
  368. { // create a default one
  369. final String g = Util.toBase64(Util.trim(new BigInteger("2")));
  370. String index, N;
  371. for (int i = 0; i < Nsrp.length; i++)
  372. {
  373. index = String.valueOf(i + 1);
  374. N = Util.toBase64(Util.trim(Nsrp[i]));
  375. configurations.put(index, new String[] { N, g });
  376. }
  377. FileOutputStream f0 = null;
  378. PrintWriter pw0 = null;
  379. try
  380. {
  381. f0 = new FileOutputStream(configFile);
  382. pw0 = new PrintWriter(f0, true);
  383. this.writeConf(pw0);
  384. }
  385. finally
  386. {
  387. if (pw0 != null)
  388. pw0.close();
  389. else if (f0 != null)
  390. f0.close();
  391. }
  392. }
  393. }
  394. private void readConf(final InputStream in) throws IOException
  395. {
  396. final BufferedReader din = new BufferedReader(new InputStreamReader(in));
  397. String line, index, N, g;
  398. StringTokenizer st;
  399. while ((line = din.readLine()) != null)
  400. {
  401. st = new StringTokenizer(line, ":");
  402. try
  403. {
  404. index = st.nextToken();
  405. N = st.nextToken();
  406. g = st.nextToken();
  407. }
  408. catch (NoSuchElementException x)
  409. {
  410. throw new IOException("SRP password configuration file corrupt");
  411. }
  412. configurations.put(index, new String[] { N, g });
  413. }
  414. }
  415. private void writeConf(final PrintWriter pw)
  416. {
  417. String ndx;
  418. String[] mpi;
  419. CPStringBuilder sb;
  420. for (Iterator it = configurations.keySet().iterator(); it.hasNext();)
  421. {
  422. ndx = (String) it.next();
  423. mpi = (String[]) configurations.get(ndx);
  424. sb = new CPStringBuilder(ndx)
  425. .append(":").append(mpi[0])
  426. .append(":").append(mpi[1]);
  427. pw.println(sb.toString());
  428. }
  429. }
  430. /**
  431. * Compute the new verifiers for the designated username and password.
  432. * <p>
  433. * <b>IMPORTANT:</b> This method computes the verifiers as described in
  434. * RFC-2945, which differs from the description given on the web page for
  435. * SRP-6.
  436. *
  437. * @param user the user's name.
  438. * @param s the user's salt.
  439. * @param password the user's password
  440. * @param index the index of the &lt;N, g> pair to use for this user.
  441. * @return a {@link java.util.Map} of user verifiers.
  442. * @throws UnsupportedEncodingException if the US-ASCII decoder is not
  443. * available on this platform.
  444. */
  445. private HashMap newVerifiers(final String user, final byte[] s,
  446. final String password, final String index)
  447. throws UnsupportedEncodingException
  448. {
  449. // to ensure inter-operability with non-java tools
  450. final String[] mpi = (String[]) configurations.get(index);
  451. final BigInteger N = new BigInteger(1, Util.fromBase64(mpi[0]));
  452. final BigInteger g = new BigInteger(1, Util.fromBase64(mpi[1]));
  453. final HashMap result = new HashMap(srps.size());
  454. BigInteger x, v;
  455. SRP srp;
  456. for (int i = 0; i < srps.size(); i++)
  457. {
  458. final String digestID = String.valueOf(i);
  459. srp = (SRP) srps.get(digestID);
  460. x = new BigInteger(1, srp.computeX(s, user, password));
  461. v = g.modPow(x, N);
  462. final String verifier = Util.toBase64(v.toByteArray());
  463. result.put(digestID, verifier);
  464. }
  465. return result;
  466. }
  467. private synchronized void update() throws IOException
  468. {
  469. entries.clear();
  470. FileInputStream fis;
  471. passwdFile = new File(pwName);
  472. lastmodPasswdFile = passwdFile.lastModified();
  473. try
  474. {
  475. fis = new FileInputStream(passwdFile);
  476. readPasswd(fis);
  477. }
  478. catch (FileNotFoundException ignored)
  479. {
  480. }
  481. passwd2File = new File(pw2Name);
  482. lastmodPasswd2File = passwd2File.lastModified();
  483. try
  484. {
  485. fis = new FileInputStream(passwd2File);
  486. readPasswd2(fis);
  487. }
  488. catch (FileNotFoundException ignored)
  489. {
  490. }
  491. }
  492. private void checkCurrent() throws IOException
  493. {
  494. if (passwdFile.lastModified() > lastmodPasswdFile
  495. || passwd2File.lastModified() > lastmodPasswd2File)
  496. update();
  497. }
  498. private void readPasswd(final InputStream in) throws IOException
  499. {
  500. final BufferedReader din = new BufferedReader(new InputStreamReader(in));
  501. String line, user, verifier, salt, index;
  502. StringTokenizer st;
  503. while ((line = din.readLine()) != null)
  504. {
  505. st = new StringTokenizer(line, ":");
  506. try
  507. {
  508. user = st.nextToken();
  509. verifier = st.nextToken();
  510. salt = st.nextToken();
  511. index = st.nextToken();
  512. }
  513. catch (NoSuchElementException x)
  514. {
  515. throw new IOException("SRP base password file corrupt");
  516. }
  517. final HashMap verifiers = new HashMap(6);
  518. verifiers.put("0", verifier);
  519. final HashMap fields = new HashMap(4);
  520. fields.put(USER_FIELD, user);
  521. fields.put(VERIFIERS_FIELD, verifiers);
  522. fields.put(SALT_FIELD, salt);
  523. fields.put(CONFIG_FIELD, index);
  524. entries.put(user, fields);
  525. }
  526. }
  527. private void readPasswd2(final InputStream in) throws IOException
  528. {
  529. final BufferedReader din = new BufferedReader(new InputStreamReader(in));
  530. String line, digestID, user, verifier;
  531. StringTokenizer st;
  532. HashMap fields, verifiers;
  533. while ((line = din.readLine()) != null)
  534. {
  535. st = new StringTokenizer(line, ":");
  536. try
  537. {
  538. digestID = st.nextToken();
  539. user = st.nextToken();
  540. verifier = st.nextToken();
  541. }
  542. catch (NoSuchElementException x)
  543. {
  544. throw new IOException("SRP extended password file corrupt");
  545. }
  546. fields = (HashMap) entries.get(user);
  547. if (fields != null)
  548. {
  549. verifiers = (HashMap) fields.get(VERIFIERS_FIELD);
  550. verifiers.put(digestID, verifier);
  551. }
  552. }
  553. }
  554. private void writePasswd(final PrintWriter pw1, final PrintWriter pw2)
  555. throws IOException
  556. {
  557. String user, digestID;
  558. HashMap fields, verifiers;
  559. CPStringBuilder sb1, sb2;
  560. Iterator j;
  561. final Iterator i = entries.keySet().iterator();
  562. while (i.hasNext())
  563. {
  564. user = (String) i.next();
  565. fields = (HashMap) entries.get(user);
  566. if (! user.equals(fields.get(USER_FIELD)))
  567. throw new IOException("Inconsistent SRP password data");
  568. verifiers = (HashMap) fields.get(VERIFIERS_FIELD);
  569. sb1 = new CPStringBuilder(user)
  570. .append(":").append((String) verifiers.get("0"))
  571. .append(":").append((String) fields.get(SALT_FIELD))
  572. .append(":").append((String) fields.get(CONFIG_FIELD));
  573. pw1.println(sb1.toString());
  574. // write extended information
  575. j = verifiers.keySet().iterator();
  576. while (j.hasNext())
  577. {
  578. digestID = (String) j.next();
  579. if (! "0".equals(digestID))
  580. {
  581. // #0 is the default digest, already present in tpasswd!
  582. sb2 = new CPStringBuilder(digestID)
  583. .append(":").append(user)
  584. .append(":").append((String) verifiers.get(digestID));
  585. pw2.println(sb2.toString());
  586. }
  587. }
  588. }
  589. }
  590. }