Source for gnu.javax.crypto.sasl.srp.PasswordFile

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