| /* PasswordFile.java -- |
| Copyright (C) 2003, 2006 Free Software Foundation, Inc. |
| |
| This file is a part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2 of the License, or (at |
| your option) any later version. |
| |
| GNU Classpath is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; if not, write to the Free Software |
| Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 |
| USA |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package gnu.javax.crypto.sasl.srp; |
| |
| import gnu.java.security.Registry; |
| import gnu.java.security.util.Util; |
| import gnu.javax.crypto.key.srp6.SRPAlgorithm; |
| import gnu.javax.crypto.sasl.NoSuchUserException; |
| import gnu.javax.crypto.sasl.UserAlreadyExistsException; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.PrintWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.math.BigInteger; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.NoSuchElementException; |
| import java.util.StringTokenizer; |
| |
| /** |
| * The implementation of SRP password files. |
| * <p> |
| * For SRP, there are three (3) files: |
| * <ol> |
| * <li>The password configuration file: tpasswd.conf. It contains the pairs |
| * <N,g> indexed by a number for each pair used for a user. By default, this |
| * file's pathname is constructed from the base password file pathname by |
| * prepending it with the ".conf" suffix.</li> |
| * <li>The base password file: tpasswd. It contains the related password |
| * entries for all the users with values computed using SRP's default message |
| * digest algorithm: SHA-1 (with 160-bit output block size).</li> |
| * <li>The extended password file: tpasswd2. Its name, by default, is |
| * constructed by adding the suffix "2" to the fully qualified pathname of the |
| * base password file. It contains, in addition to the same fields as the base |
| * password file, albeit with a different verifier value, an extra field |
| * identifying the message digest algorithm used to compute this (verifier) |
| * value.</li> |
| * </ol> |
| * <p> |
| * This implementation assumes the following message digest algorithm codes: |
| * <ul> |
| * <li>0: the default hash algorithm, which is SHA-1 (or its alias SHA-160).</li> |
| * <li>1: MD5.</li> |
| * <li>2: RIPEMD-128.</li> |
| * <li>3: RIPEMD-160.</li> |
| * <li>4: SHA-256.</li> |
| * <li>5: SHA-384.</li> |
| * <li>6: SHA-512.</li> |
| * </ul> |
| * <p> |
| * <b>IMPORTANT:</b> This method computes the verifiers as described in |
| * RFC-2945, which differs from the description given on the web page for SRP-6. |
| * <p> |
| * Reference: |
| * <ol> |
| * <li><a href="http://srp.stanford.edu/design.html">SRP Protocol Design</a><br> |
| * Thomas J. Wu.</li> |
| * </ol> |
| */ |
| public class PasswordFile |
| { |
| // names of property keys used in this class |
| private static final String USER_FIELD = "user"; |
| private static final String VERIFIERS_FIELD = "verifier"; |
| private static final String SALT_FIELD = "salt"; |
| private static final String CONFIG_FIELD = "config"; |
| private static String DEFAULT_FILE; |
| static |
| { |
| DEFAULT_FILE = System.getProperty(SRPRegistry.PASSWORD_FILE, |
| SRPRegistry.DEFAULT_PASSWORD_FILE); |
| } |
| /** The SRP algorithm instances used by this object. */ |
| private static final HashMap srps; |
| static |
| { |
| final HashMap map = new HashMap(SRPRegistry.SRP_ALGORITHMS.length); |
| // The first entry MUST exist. The others are optional. |
| map.put("0", SRP.instance(SRPRegistry.SRP_ALGORITHMS[0])); |
| for (int i = 1; i < SRPRegistry.SRP_ALGORITHMS.length; i++) |
| { |
| try |
| { |
| map.put(String.valueOf(i), |
| SRP.instance(SRPRegistry.SRP_ALGORITHMS[i])); |
| } |
| catch (Exception x) |
| { |
| System.err.println("Ignored: " + x); |
| x.printStackTrace(System.err); |
| } |
| } |
| srps = map; |
| } |
| |
| private String confName, pwName, pw2Name; |
| private File configFile, passwdFile, passwd2File; |
| private long lastmodPasswdFile, lastmodPasswd2File; |
| private HashMap entries = new HashMap(); |
| private HashMap configurations = new HashMap(); |
| // default N values to use when creating a new password.conf file |
| private static final BigInteger[] Nsrp = new BigInteger[] { |
| SRPAlgorithm.N_2048, |
| SRPAlgorithm.N_1536, |
| SRPAlgorithm.N_1280, |
| SRPAlgorithm.N_1024, |
| SRPAlgorithm.N_768, |
| SRPAlgorithm.N_640, |
| SRPAlgorithm.N_512 }; |
| |
| public PasswordFile() throws IOException |
| { |
| this(DEFAULT_FILE); |
| } |
| |
| public PasswordFile(final File pwFile) throws IOException |
| { |
| this(pwFile.getAbsolutePath()); |
| } |
| |
| public PasswordFile(final String pwName) throws IOException |
| { |
| this(pwName, pwName + "2", pwName + ".conf"); |
| } |
| |
| public PasswordFile(final String pwName, final String confName) |
| throws IOException |
| { |
| this(pwName, pwName + "2", confName); |
| } |
| |
| public PasswordFile(final String pwName, final String pw2Name, |
| final String confName) throws IOException |
| { |
| super(); |
| |
| this.pwName = pwName; |
| this.pw2Name = pw2Name; |
| this.confName = confName; |
| |
| readOrCreateConf(); |
| update(); |
| } |
| |
| /** |
| * Returns a string representing the decimal value of an integer identifying |
| * the message digest algorithm to use for the SRP computations. |
| * |
| * @param mdName the canonical name of a message digest algorithm. |
| * @return a string representing the decimal value of an ID for that |
| * algorithm. |
| */ |
| private static final String nameToID(final String mdName) |
| { |
| if (Registry.SHA_HASH.equalsIgnoreCase(mdName) |
| || Registry.SHA1_HASH.equalsIgnoreCase(mdName) |
| || Registry.SHA160_HASH.equalsIgnoreCase(mdName)) |
| return "0"; |
| else if (Registry.MD5_HASH.equalsIgnoreCase(mdName)) |
| return "1"; |
| else if (Registry.RIPEMD128_HASH.equalsIgnoreCase(mdName)) |
| return "2"; |
| else if (Registry.RIPEMD160_HASH.equalsIgnoreCase(mdName)) |
| return "3"; |
| else if (Registry.SHA256_HASH.equalsIgnoreCase(mdName)) |
| return "4"; |
| else if (Registry.SHA384_HASH.equalsIgnoreCase(mdName)) |
| return "5"; |
| else if (Registry.SHA512_HASH.equalsIgnoreCase(mdName)) |
| return "6"; |
| return "0"; |
| } |
| |
| /** |
| * Checks if the current configuration file contains the <N, g> pair for |
| * the designated <code>index</code>. |
| * |
| * @param index a string representing 1-digit identification of an <N, g> |
| * pair used. |
| * @return <code>true</code> if the designated <code>index</code> is that |
| * of a known <N, g> pair, and <code>false</code> otherwise. |
| * @throws IOException if an exception occurs during the process. |
| * @see SRPRegistry#N_2048_BITS |
| * @see SRPRegistry#N_1536_BITS |
| * @see SRPRegistry#N_1280_BITS |
| * @see SRPRegistry#N_1024_BITS |
| * @see SRPRegistry#N_768_BITS |
| * @see SRPRegistry#N_640_BITS |
| * @see SRPRegistry#N_512_BITS |
| */ |
| public synchronized boolean containsConfig(final String index) |
| throws IOException |
| { |
| checkCurrent(); |
| return configurations.containsKey(index); |
| } |
| |
| /** |
| * Returns a pair of strings representing the pair of <code>N</code> and |
| * <code>g</code> MPIs for the designated <code>index</code>. |
| * |
| * @param index a string representing 1-digit identification of an <N, g> |
| * pair to look up. |
| * @return a pair of strings, arranged in an array, where the first (at index |
| * position #0) is the repesentation of the MPI <code>N</code>, and |
| * the second (at index position #1) is the representation of the MPI |
| * <code>g</code>. If the <code>index</code> refers to an unknown |
| * pair, then an empty string array is returned. |
| * @throws IOException if an exception occurs during the process. |
| */ |
| public synchronized String[] lookupConfig(final String index) |
| throws IOException |
| { |
| checkCurrent(); |
| String[] result = null; |
| if (configurations.containsKey(index)) |
| result = (String[]) configurations.get(index); |
| return result; |
| } |
| |
| public synchronized boolean contains(final String user) throws IOException |
| { |
| checkCurrent(); |
| return entries.containsKey(user); |
| } |
| |
| public synchronized void add(final String user, final String passwd, |
| final byte[] salt, final String index) |
| throws IOException |
| { |
| checkCurrent(); |
| if (entries.containsKey(user)) |
| throw new UserAlreadyExistsException(user); |
| final HashMap fields = new HashMap(4); |
| fields.put(USER_FIELD, user); // 0 |
| fields.put(VERIFIERS_FIELD, newVerifiers(user, salt, passwd, index)); // 1 |
| fields.put(SALT_FIELD, Util.toBase64(salt)); // 2 |
| fields.put(CONFIG_FIELD, index); // 3 |
| entries.put(user, fields); |
| savePasswd(); |
| } |
| |
| public synchronized void changePasswd(final String user, final String passwd) |
| throws IOException |
| { |
| checkCurrent(); |
| if (! entries.containsKey(user)) |
| throw new NoSuchUserException(user); |
| final HashMap fields = (HashMap) entries.get(user); |
| final byte[] salt; |
| try |
| { |
| salt = Util.fromBase64((String) fields.get(SALT_FIELD)); |
| } |
| catch (NumberFormatException x) |
| { |
| throw new IOException("Password file corrupt"); |
| } |
| final String index = (String) fields.get(CONFIG_FIELD); |
| fields.put(VERIFIERS_FIELD, newVerifiers(user, salt, passwd, index)); |
| entries.put(user, fields); |
| savePasswd(); |
| } |
| |
| public synchronized void savePasswd() throws IOException |
| { |
| final FileOutputStream f1 = new FileOutputStream(passwdFile); |
| final FileOutputStream f2 = new FileOutputStream(passwd2File); |
| PrintWriter pw1 = null; |
| PrintWriter pw2 = null; |
| try |
| { |
| pw1 = new PrintWriter(f1, true); |
| pw2 = new PrintWriter(f2, true); |
| this.writePasswd(pw1, pw2); |
| } |
| finally |
| { |
| if (pw1 != null) |
| try |
| { |
| pw1.flush(); |
| } |
| finally |
| { |
| pw1.close(); |
| } |
| if (pw2 != null) |
| try |
| { |
| pw2.flush(); |
| } |
| finally |
| { |
| pw2.close(); |
| } |
| try |
| { |
| f1.close(); |
| } |
| catch (IOException ignored) |
| { |
| } |
| try |
| { |
| f2.close(); |
| } |
| catch (IOException ignored) |
| { |
| } |
| } |
| lastmodPasswdFile = passwdFile.lastModified(); |
| lastmodPasswd2File = passwd2File.lastModified(); |
| } |
| |
| /** |
| * Returns the triplet: verifier, salt and configuration file index, of a |
| * designated user, and a designated message digest algorithm name, as an |
| * array of strings. |
| * |
| * @param user the username. |
| * @param mdName the canonical name of the SRP's message digest algorithm. |
| * @return a string array containing, in this order, the BASE-64 encodings of |
| * the verifier, the salt and the index in the password configuration |
| * file of the MPIs N and g of the designated user. |
| */ |
| public synchronized String[] lookup(final String user, final String mdName) |
| throws IOException |
| { |
| checkCurrent(); |
| if (! entries.containsKey(user)) |
| throw new NoSuchUserException(user); |
| final HashMap fields = (HashMap) entries.get(user); |
| final HashMap verifiers = (HashMap) fields.get(VERIFIERS_FIELD); |
| final String salt = (String) fields.get(SALT_FIELD); |
| final String index = (String) fields.get(CONFIG_FIELD); |
| final String verifier = (String) verifiers.get(nameToID(mdName)); |
| return new String[] { verifier, salt, index }; |
| } |
| |
| private synchronized void readOrCreateConf() throws IOException |
| { |
| configurations.clear(); |
| final FileInputStream fis; |
| configFile = new File(confName); |
| try |
| { |
| fis = new FileInputStream(configFile); |
| readConf(fis); |
| } |
| catch (FileNotFoundException x) |
| { // create a default one |
| final String g = Util.toBase64(Util.trim(new BigInteger("2"))); |
| String index, N; |
| for (int i = 0; i < Nsrp.length; i++) |
| { |
| index = String.valueOf(i + 1); |
| N = Util.toBase64(Util.trim(Nsrp[i])); |
| configurations.put(index, new String[] { N, g }); |
| } |
| FileOutputStream f0 = null; |
| PrintWriter pw0 = null; |
| try |
| { |
| f0 = new FileOutputStream(configFile); |
| pw0 = new PrintWriter(f0, true); |
| this.writeConf(pw0); |
| } |
| finally |
| { |
| if (pw0 != null) |
| pw0.close(); |
| else if (f0 != null) |
| f0.close(); |
| } |
| } |
| } |
| |
| private void readConf(final InputStream in) throws IOException |
| { |
| final BufferedReader din = new BufferedReader(new InputStreamReader(in)); |
| String line, index, N, g; |
| StringTokenizer st; |
| while ((line = din.readLine()) != null) |
| { |
| st = new StringTokenizer(line, ":"); |
| try |
| { |
| index = st.nextToken(); |
| N = st.nextToken(); |
| g = st.nextToken(); |
| } |
| catch (NoSuchElementException x) |
| { |
| throw new IOException("SRP password configuration file corrupt"); |
| } |
| configurations.put(index, new String[] { N, g }); |
| } |
| } |
| |
| private void writeConf(final PrintWriter pw) |
| { |
| String ndx; |
| String[] mpi; |
| StringBuffer sb; |
| for (Iterator it = configurations.keySet().iterator(); it.hasNext();) |
| { |
| ndx = (String) it.next(); |
| mpi = (String[]) configurations.get(ndx); |
| sb = new StringBuffer(ndx) |
| .append(":").append(mpi[0]) |
| .append(":").append(mpi[1]); |
| pw.println(sb.toString()); |
| } |
| } |
| |
| /** |
| * Compute the new verifiers for the designated username and password. |
| * <p> |
| * <b>IMPORTANT:</b> This method computes the verifiers as described in |
| * RFC-2945, which differs from the description given on the web page for |
| * SRP-6. |
| * |
| * @param user the user's name. |
| * @param s the user's salt. |
| * @param password the user's password |
| * @param index the index of the <N, g> pair to use for this user. |
| * @return a {@link java.util.Map} of user verifiers. |
| * @throws UnsupportedEncodingException if the US-ASCII decoder is not |
| * available on this platform. |
| */ |
| private HashMap newVerifiers(final String user, final byte[] s, |
| final String password, final String index) |
| throws UnsupportedEncodingException |
| { |
| // to ensure inter-operability with non-java tools |
| final String[] mpi = (String[]) configurations.get(index); |
| final BigInteger N = new BigInteger(1, Util.fromBase64(mpi[0])); |
| final BigInteger g = new BigInteger(1, Util.fromBase64(mpi[1])); |
| final HashMap result = new HashMap(srps.size()); |
| BigInteger x, v; |
| SRP srp; |
| for (int i = 0; i < srps.size(); i++) |
| { |
| final String digestID = String.valueOf(i); |
| srp = (SRP) srps.get(digestID); |
| x = new BigInteger(1, srp.computeX(s, user, password)); |
| v = g.modPow(x, N); |
| final String verifier = Util.toBase64(v.toByteArray()); |
| result.put(digestID, verifier); |
| } |
| return result; |
| } |
| |
| private synchronized void update() throws IOException |
| { |
| entries.clear(); |
| FileInputStream fis; |
| passwdFile = new File(pwName); |
| lastmodPasswdFile = passwdFile.lastModified(); |
| try |
| { |
| fis = new FileInputStream(passwdFile); |
| readPasswd(fis); |
| } |
| catch (FileNotFoundException ignored) |
| { |
| } |
| passwd2File = new File(pw2Name); |
| lastmodPasswd2File = passwd2File.lastModified(); |
| try |
| { |
| fis = new FileInputStream(passwd2File); |
| readPasswd2(fis); |
| } |
| catch (FileNotFoundException ignored) |
| { |
| } |
| } |
| |
| private void checkCurrent() throws IOException |
| { |
| if (passwdFile.lastModified() > lastmodPasswdFile |
| || passwd2File.lastModified() > lastmodPasswd2File) |
| update(); |
| } |
| |
| private void readPasswd(final InputStream in) throws IOException |
| { |
| final BufferedReader din = new BufferedReader(new InputStreamReader(in)); |
| String line, user, verifier, salt, index; |
| StringTokenizer st; |
| while ((line = din.readLine()) != null) |
| { |
| st = new StringTokenizer(line, ":"); |
| try |
| { |
| user = st.nextToken(); |
| verifier = st.nextToken(); |
| salt = st.nextToken(); |
| index = st.nextToken(); |
| } |
| catch (NoSuchElementException x) |
| { |
| throw new IOException("SRP base password file corrupt"); |
| } |
| final HashMap verifiers = new HashMap(6); |
| verifiers.put("0", verifier); |
| final HashMap fields = new HashMap(4); |
| fields.put(USER_FIELD, user); |
| fields.put(VERIFIERS_FIELD, verifiers); |
| fields.put(SALT_FIELD, salt); |
| fields.put(CONFIG_FIELD, index); |
| entries.put(user, fields); |
| } |
| } |
| |
| private void readPasswd2(final InputStream in) throws IOException |
| { |
| final BufferedReader din = new BufferedReader(new InputStreamReader(in)); |
| String line, digestID, user, verifier; |
| StringTokenizer st; |
| HashMap fields, verifiers; |
| while ((line = din.readLine()) != null) |
| { |
| st = new StringTokenizer(line, ":"); |
| try |
| { |
| digestID = st.nextToken(); |
| user = st.nextToken(); |
| verifier = st.nextToken(); |
| } |
| catch (NoSuchElementException x) |
| { |
| throw new IOException("SRP extended password file corrupt"); |
| } |
| fields = (HashMap) entries.get(user); |
| if (fields != null) |
| { |
| verifiers = (HashMap) fields.get(VERIFIERS_FIELD); |
| verifiers.put(digestID, verifier); |
| } |
| } |
| } |
| |
| private void writePasswd(final PrintWriter pw1, final PrintWriter pw2) |
| throws IOException |
| { |
| String user, digestID; |
| HashMap fields, verifiers; |
| StringBuffer sb1, sb2; |
| Iterator j; |
| final Iterator i = entries.keySet().iterator(); |
| while (i.hasNext()) |
| { |
| user = (String) i.next(); |
| fields = (HashMap) entries.get(user); |
| if (! user.equals(fields.get(USER_FIELD))) |
| throw new IOException("Inconsistent SRP password data"); |
| verifiers = (HashMap) fields.get(VERIFIERS_FIELD); |
| sb1 = new StringBuffer(user) |
| .append(":").append((String) verifiers.get("0")) |
| .append(":").append((String) fields.get(SALT_FIELD)) |
| .append(":").append((String) fields.get(CONFIG_FIELD)); |
| pw1.println(sb1.toString()); |
| // write extended information |
| j = verifiers.keySet().iterator(); |
| while (j.hasNext()) |
| { |
| digestID = (String) j.next(); |
| if (! "0".equals(digestID)) |
| { |
| // #0 is the default digest, already present in tpasswd! |
| sb2 = new StringBuffer(digestID) |
| .append(":").append(user) |
| .append(":").append((String) verifiers.get(digestID)); |
| pw2.println(sb2.toString()); |
| } |
| } |
| } |
| } |
| } |