diff options
Diffstat (limited to 'gnu/javax/crypto/sasl/srp/PasswordFile.java')
-rw-r--r-- | gnu/javax/crypto/sasl/srp/PasswordFile.java | 699 |
1 files changed, 699 insertions, 0 deletions
diff --git a/gnu/javax/crypto/sasl/srp/PasswordFile.java b/gnu/javax/crypto/sasl/srp/PasswordFile.java new file mode 100644 index 000000000..1628a4167 --- /dev/null +++ b/gnu/javax/crypto/sasl/srp/PasswordFile.java @@ -0,0 +1,699 @@ +/* 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.hash.IMessageDigest; +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.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; +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; + +/** + * <p>The implementation of SRP password files.</p> + * + * <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> + * + * <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> + * + * <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> + * + * <p>Reference:</p> + * <ol> + * <li><a href="http://srp.stanford.edu/design.html">SRP Protocol Design</a><br> + * Thomas J. Wu.</li> + * </ol> + */ +public class PasswordFile +{ + + // Constants and variables + // ------------------------------------------------------------------------- + + // 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 }; + + // Constructor(s) + // ------------------------------------------------------------------------- + + 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(); + } + + // Class methods + // ------------------------------------------------------------------------- + + /** + * <p>Returns a string representing the decimal value of an integer + * identifying the message digest algorithm to use for the SRP computations. + * </p> + * + * @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"; + } + + // SRP password configuration file methods --------------------------------- + + /** + * <p>Checks if the current configuration file contains the <N, g> pair + * for the designated <code>index</code>.</p> + * + * @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); + } + + /** + * <p>Returns a pair of strings representing the pair of <code>N</code> and + * <code>g</code> MPIs for the designated <code>index</code>.</p> + * + * @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; + } + + // SRP base and extended password configuration files methods -------------- + + 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(); + } + + /** + * <p>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.</p> + * + * @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 }; + } + + // Other instance methods -------------------------------------------------- + + 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()); + } + } + + /** + * <p>Compute the new verifiers for the designated username and password.</p> + * + * <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> + * + * @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().append(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().append(digestID).append(":").append( + user).append( + ":").append( + (String) verifiers.get(digestID)); + pw2.println(sb2.toString()); + } + } + } + } +}
\ No newline at end of file |