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