package net.i2p.util; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import net.i2p.I2PAppContext; import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.data.SessionKey; //import org.bouncycastle.oldcrypto.digests.MD5Digest; /** * Manage both plaintext and salted/hashed password storage in * router.config. * * There's no state here, so instantiate at will. * * @since 0.9.4 */ public class PasswordManager { private final I2PAppContext _context; protected static final int SALT_LENGTH = 16; /** 48 */ protected static final int SHASH_LENGTH = SALT_LENGTH + SessionKey.KEYSIZE_BYTES; /** stored as plain text */ protected static final String PROP_PW = ".password"; /** stored obfuscated as b64 of the UTF-8 bytes */ protected static final String PROP_B64 = ".b64"; /** stored as the hex of the MD5 hash of the UTF-8 bytes. Compatible with Jetty. */ protected static final String PROP_MD5 = ".md5"; /** stored as a Unix crypt string */ protected static final String PROP_CRYPT = ".crypt"; /** stored as the b64 of the 16 byte salt + the 32 byte hash of the UTF-8 bytes */ protected static final String PROP_SHASH = ".shash"; public PasswordManager(I2PAppContext ctx) { _context = ctx; } /** * Checks both plaintext and hash * * @param realm e.g. i2cp, routerconsole, etc. * @param user null or "" for no user, already trimmed * @param pw plain text, already trimmed * @return if pw verified */ public boolean check(String realm, String user, String pw) { return checkPlain(realm, user, pw) || checkB64(realm, user, pw) || checkHash(realm, user, pw); } /** * @param realm e.g. i2cp, routerconsole, etc. * @param user null or "" for no user, already trimmed * @param pw plain text, already trimmed * @return if pw verified */ public boolean checkPlain(String realm, String user, String pw) { String pfx = realm; if (user != null && user.length() > 0) pfx += '.' + user; return pw.equals(_context.getProperty(pfx + PROP_PW)); } /** * @param realm e.g. i2cp, routerconsole, etc. * @param user null or "" for no user, already trimmed * @param pw plain text, already trimmed * @return if pw verified */ public boolean checkB64(String realm, String user, String pw) { String pfx = realm; if (user != null && user.length() > 0) pfx += '.' + user; String b64 = _context.getProperty(pfx + PROP_B64); if (b64 == null) return false; return b64.equals(Base64.encode(DataHelper.getUTF8(pw))); } /** * With random salt * * @param realm e.g. i2cp, routerconsole, etc. * @param user null or "" for no user, already trimmed * @param pw plain text, already trimmed * @return if pw verified */ public boolean checkHash(String realm, String user, String pw) { String pfx = realm; if (user != null && user.length() > 0) pfx += '.' + user; String shash = _context.getProperty(pfx + PROP_SHASH); if (shash == null) return false; return checkHash(shash, pw); } /** * Check pw against b64 salt+hash, as generated by createHash() * * @param shash b64 string * @param pw plain text non-null, already trimmed * @return if pw verified * @since 0.9.24 */ public boolean checkHash(String shash, String pw) { byte[] shashBytes = Base64.decode(shash); if (shashBytes == null || shashBytes.length != SHASH_LENGTH) return false; byte[] salt = new byte[SALT_LENGTH]; byte[] hash = new byte[SessionKey.KEYSIZE_BYTES]; System.arraycopy(shashBytes, 0, salt, 0, SALT_LENGTH); System.arraycopy(shashBytes, SALT_LENGTH, hash, 0, SessionKey.KEYSIZE_BYTES); byte[] pwHash = _context.keyGenerator().generateSessionKey(salt, DataHelper.getUTF8(pw)).getData(); return DataHelper.eq(hash, pwHash); } /** * Create a salt+hash, to be saved and verified later by verifyHash(). * * @param pw plain text non-null, already trimmed * @return salted+hash b64 string * @since 0.9.24 */ public String createHash(String pw) { byte[] salt = new byte[SALT_LENGTH]; _context.random().nextBytes(salt); byte[] pwHash = _context.keyGenerator().generateSessionKey(salt, DataHelper.getUTF8(pw)).getData(); byte[] shashBytes = new byte[SHASH_LENGTH]; System.arraycopy(salt, 0, shashBytes, 0, SALT_LENGTH); System.arraycopy(pwHash, 0, shashBytes, SALT_LENGTH, SessionKey.KEYSIZE_BYTES); return Base64.encode(shashBytes); } /** * Either plain or b64 * * @param realm e.g. i2cp, routerconsole, etc. * @param user null or "" for no user, already trimmed * @return the pw or null */ public String get(String realm, String user) { String rv = getPlain(realm, user); if (rv != null) return rv; return getB64(realm, user); } /** * @param realm e.g. i2cp, routerconsole, etc. * @param user null or "" for no user, already trimmed * @return the pw or null */ public String getPlain(String realm, String user) { String pfx = realm; if (user != null && user.length() > 0) pfx += '.' + user; return _context.getProperty(pfx + PROP_PW); } /** * @param realm e.g. i2cp, routerconsole, etc. * @param user null or "" for no user, already trimmed * @return the decoded pw or null */ public String getB64(String realm, String user) { String pfx = realm; if (user != null && user.length() > 0) pfx += '.' + user; String b64 = _context.getProperty(pfx + PROP_B64); if (b64 == null) return null; return Base64.decodeToString(b64); } /** * Straight MD5, no salt * Will return the MD5 sum of "user:subrealm:pw", compatible with Jetty * and RFC 2617. * * Updated in 0.9.26 to use UTF-8, as implied in RFC 7616/7617 * See also http://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication * http://stackoverflow.com/questions/702629/utf-8-characters-mangled-in-http-basic-auth-username * * @param subrealm to be used in creating the checksum * @param user non-null, non-empty, already trimmed * @param pw non-null, plain text, already trimmed * @return lower-case hex with leading zeros, 32 chars, or null on error */ public static String md5Hex(String subrealm, String user, String pw) { String fullpw = user + ':' + subrealm + ':' + pw; return md5Hex(fullpw); } /** * Straight MD5, no salt * Will return the MD5 sum of the data, compatible with Jetty * and RFC 2617. * * Updated in 0.9.26 to use UTF-8, as implied in RFC 7616/7617 * See also http://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication * * @param fullpw non-null, plain text, already trimmed * @return lower-case hex with leading zeros, 32 chars, or null on error */ public static String md5Hex(String fullpw) { byte[] data = DataHelper.getUTF8(fullpw); byte[] sum = md5Sum(data); if (sum != null) // adds leading zeros if necessary return DataHelper.toString(sum); return null; } /** * Standard MD5 checksum * * @param data non-null * @return 16 bytes, or null on error */ public static byte[] md5Sum(byte[] data) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(data); return md.digest(); } catch (NoSuchAlgorithmException nsae) {} return null; } /** * speed/comparison test before removing BC version; * JVM was slightly faster */ /***** public static void main(String[] args) { RandomSource rand = RandomSource.getInstance(); byte[] d = new byte[1500]; MD5Digest md = new MD5Digest(); byte[] bc = new byte[16]; // warmup and comparison int runs = 25000; for (int i = 0; i < runs; i++) { rand.nextBytes(d); byte[] jvm = md5Sum(d); md.update(d, 0, d.length); md.doFinal(bc, 0); if (!DataHelper.eq(jvm, bc)) throw new IllegalStateException(); md.reset(); } // real thing runs = 500000; long start = System.currentTimeMillis(); for (int i = 0; i < runs; i++) { md5Sum(d); } System.out.println("JVM " + (System.currentTimeMillis() - start)); start = System.currentTimeMillis(); for (int i = 0; i < runs; i++) { md.update(d, 0, d.length); md.doFinal(bc, 0); md.reset(); } System.out.println("BC " + (System.currentTimeMillis() - start)); } *****/ }