diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConsolePasswordManager.java b/apps/routerconsole/java/src/net/i2p/router/web/ConsolePasswordManager.java new file mode 100644 index 000000000..b428de7a4 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConsolePasswordManager.java @@ -0,0 +1,234 @@ +package net.i2p.router.web; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.i2p.data.Base64; +import net.i2p.data.DataHelper; +import net.i2p.router.Router; +import net.i2p.router.RouterContext; +import net.i2p.router.util.RouterPasswordManager; + +//import org.mortbay.jetty.security.UnixCrypt; + +/** + * Manage both plaintext and salted/hashed password storage in + * router.config. + * + * @since 0.9.4 + */ +public class ConsolePasswordManager extends RouterPasswordManager { + + private static final String PROP_MIGRATED = "routerconsole.passwordManager.migrated"; + // migrate these to hash + private static final String PROP_CONSOLE_OLD = "consolePassword"; + public static final String PROP_CONSOLE_NEW = "routerconsole.auth"; + private static final String CONSOLE_USER = "admin"; + + public ConsolePasswordManager(RouterContext ctx) { + super(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 super.check(realm, user, pw) || + //checkCrypt(realm, user, pw) || + checkMD5(realm, user, pw); + } + + /** + * The username is the 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 checkCrypt(String realm, String user, String pw) { + String pfx = realm; + if (user != null && user.length() > 0) + pfx += '.' + user; + String cr = _context.getProperty(pfx + PROP_CRYPT); + if (cr == null) + return false; + return cr.equals(UnixCrypt.crypt(pw, cr)); + } +****/ + + /** + * Straight MD5. Compatible with Jetty. + * + * @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 checkMD5(String realm, String user, String pw) { + String pfx = realm; + if (user != null && user.length() > 0) + pfx += '.' + user; + String hex = _context.getProperty(pfx + PROP_MD5); + if (hex == null) + return false; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(pw.getBytes("ISO-8859-1")); + // must use the method that adds leading zeros + return hex.equals(DataHelper.toString(md.digest())); + } catch (UnsupportedEncodingException uee) { + return false; + } catch (NoSuchAlgorithmException nsae) { + return false; + } + } + + /** + * Get all MD5 usernames and passwords. Compatible with Jetty. + * Any "null" user is NOT included.. + * + * @param realm e.g. i2cp, routerconsole, etc. + * @return Map of usernames to passwords (hex with leading zeros, 32 characters) + */ + public Map getMD5(String realm) { + String pfx = realm + '.'; + Map rv = new HashMap(4); + for (Map.Entry e : _context.router().getConfigMap().entrySet()) { + String prop = e.getKey(); + if (prop.startsWith(pfx) && prop.endsWith(PROP_MD5)) { + String user = prop.substring(0, prop.length() - PROP_MD5.length()).substring(pfx.length()); + String hex = e.getValue(); + if (user.length() > 0 && hex.length() == 32) + rv.put(user, hex); + } + } + return rv; + } + + /** + * Migrate from plaintext to salt/hash + * + * @return success or nothing to migrate + */ + public boolean migrateConsole() { + synchronized(ConsolePasswordManager.class) { + if (_context.getBooleanProperty(PROP_MIGRATED)) + return true; + // consolePassword + String pw = _context.getProperty(PROP_CONSOLE_OLD); + if (pw != null) { + if (pw.length() > 0) + saveMD5(PROP_CONSOLE_NEW, CONSOLE_USER, pw); + return _context.router().saveConfig(PROP_CONSOLE_OLD, null); + } + return true; + } + } + + /** + * This will fail if + * user contains '#' or '=' or starts with '!' + * The user is the salt. + * + * @param realm e.g. i2cp, routerconsole, etc. + * @param user null or "" for no user, already trimmed + * @param pw plain text, already trimmed + * @return success + */ +/**** + public boolean saveCrypt(String realm, String user, String pw) { + String pfx = realm; + if (user != null && user.length() > 0) + pfx += '.' + user; + String salt = user != null ? user : ""; + String crypt = UnixCrypt.crypt(pw, salt); + Map toAdd = Collections.singletonMap(pfx + PROP_CRYPT, crypt); + List toDel = new ArrayList(4); + toDel.add(pfx + PROP_PW); + toDel.add(pfx + PROP_B64); + toDel.add(pfx + PROP_MD5); + toDel.add(pfx + PROP_SHASH); + return _context.router().saveConfig(toAdd, toDel); + } +****/ + + /** + * Straight MD5, no 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 saveMD5(String realm, String user, String pw) { + String pfx = realm; + if (user != null && user.length() > 0) + pfx += '.' + user; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(pw.getBytes("ISO-8859-1")); + String hex = DataHelper.toString(md.digest()); + Map toAdd = Collections.singletonMap(pfx + PROP_MD5, hex); + List toDel = new ArrayList(4); + toDel.add(pfx + PROP_PW); + toDel.add(pfx + PROP_B64); + toDel.add(pfx + PROP_CRYPT); + toDel.add(pfx + PROP_SHASH); + return _context.router().saveConfig(toAdd, toDel); + } catch (UnsupportedEncodingException uee) { + return false; + } catch (NoSuchAlgorithmException nsae) { + return false; + } + } + + public static void main(String args[]) { + RouterContext ctx = (new Router()).getContext(); + ConsolePasswordManager pm = new ConsolePasswordManager(ctx); + if (!pm.migrate()) + System.out.println("Fail 1"); + + System.out.println("Test plain"); + if (!pm.savePlain("type1", "user1", "pw1")) + System.out.println("Fail 2"); + if (!pm.checkPlain("type1", "user1", "pw1")) + System.out.println("Fail 3"); + + System.out.println("Test B64"); + if (!pm.saveB64("type2", "user2", "pw2")) + System.out.println("Fail 4"); + if (!pm.checkB64("type2", "user2", "pw2")) + System.out.println("Fail 5"); + + System.out.println("Test MD5"); + if (!pm.saveMD5("type3", "user3", "pw3")) + System.out.println("Fail 6"); + if (!pm.checkMD5("type3", "user3", "pw3")) + System.out.println("Fail 7"); + + //System.out.println("Test crypt"); + //if (!pm.saveCrypt("type4", "user4", "pw4")) + // System.out.println("Fail 8"); + //if (!pm.checkCrypt("type4", "user4", "pw4")) + // System.out.println("Fail 9"); + + System.out.println("Test hash"); + if (!pm.saveHash("type5", "user5", "pw5")) + System.out.println("Fail 10"); + if (!pm.checkHash("type5", "user5", "pw5")) + System.out.println("Fail 11"); + } +} diff --git a/core/java/src/net/i2p/util/PasswordManager.java b/core/java/src/net/i2p/util/PasswordManager.java new file mode 100644 index 000000000..fa2e33948 --- /dev/null +++ b/core/java/src/net/i2p/util/PasswordManager.java @@ -0,0 +1,147 @@ +package net.i2p.util; + +import net.i2p.I2PAppContext; +import net.i2p.data.Base64; +import net.i2p.data.DataHelper; +import net.i2p.data.SessionKey; + +/** + * 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 ISO-8859-1 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; + 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); + } + + /** + * 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); + } +} diff --git a/router/java/src/net/i2p/router/util/RouterPasswordManager.java b/router/java/src/net/i2p/router/util/RouterPasswordManager.java new file mode 100644 index 000000000..966df7282 --- /dev/null +++ b/router/java/src/net/i2p/router/util/RouterPasswordManager.java @@ -0,0 +1,167 @@ +package net.i2p.router.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.i2p.data.Base64; +import net.i2p.data.DataHelper; +import net.i2p.data.SessionKey; +import net.i2p.router.Router; +import net.i2p.router.RouterContext; +import net.i2p.util.PasswordManager; + +/** + * Manage both plaintext and salted/hashed password storage in + * router.config. + * + * @since 0.9.4 + */ +public class RouterPasswordManager extends PasswordManager { + protected final RouterContext _context; + + private static final String PROP_MIGRATED = "router.passwordManager.migrated"; + // migrate these to hash + private static final String PROP_I2CP_OLD = "i2cp.password"; + private static final String PROP_I2CP_NEW = "i2cp.auth"; + // migrate these to b64 + private static final String[] MIGRATE_FROM = { + "router.reseedProxy.password", + "routerconsole.keyPassword", + "routerconsole.keystorePassword", + "i2cp.keyPassword", + "i2cp.keystorePassword" + }; + private static final String[] MIGRATE_TO = { + "router.reseedProxy.auth", + "routerconsole.ssl.key.auth", + "routerconsole.ssl.keystore.auth", + "i2cp.ssl.key.auth", + "i2cp.ssl.keystore.auth" + }; + + public RouterPasswordManager(RouterContext ctx) { + super(ctx); + _context = ctx; + migrate(); + } + + /** + * Migrate from plaintext to salt/hash + * + * @return success or nothing to migrate + */ + public boolean migrate() { + synchronized(RouterPasswordManager.class) { + if (_context.getBooleanProperty(PROP_MIGRATED)) + return true; + // i2cp.password + String pw = _context.getProperty(PROP_I2CP_OLD); + if (pw != null) { + if (pw.length() > 0) + saveHash(PROP_I2CP_NEW, null, pw); + _context.router().saveConfig(PROP_I2CP_OLD, null); + } + // obfuscation of plaintext passwords + Map toAdd = new HashMap(5); + List toDel = new ArrayList(5); + for (int i = 0; i < MIGRATE_FROM.length; i++) { + if ((pw = _context.getProperty(MIGRATE_FROM[i])) != null) { + toAdd.put(MIGRATE_TO[i], Base64.encode(DataHelper.getUTF8(pw))); + toDel.add(MIGRATE_FROM[i]); + } + } + toAdd.put(PROP_MIGRATED, "true"); + return _context.router().saveConfig(toAdd, toDel); + } + } + + /** + * Same as saveHash() + * + * @param realm e.g. i2cp, routerconsole, etc. + * @param user null or "" for no user, already trimmed + * @param pw plain text, already trimmed + * @return success + */ + public boolean save(String realm, String user, String pw) { + return saveHash(realm, user, pw); + } + + /** + * This will fail if pw contains a '#' + * or if user contains '#' or '=' or starts with '!' + * + * @param realm e.g. i2cp, routerconsole, etc. + * @param user null or "" for no user, already trimmed + * @param pw plain text, already trimmed + * @return success + */ + public boolean savePlain(String realm, String user, String pw) { + String pfx = realm; + if (user != null && user.length() > 0) + pfx += '.' + user; + Map toAdd = Collections.singletonMap(pfx + PROP_PW, pw); + List toDel = new ArrayList(4); + toDel.add(pfx + PROP_B64); + toDel.add(pfx + PROP_MD5); + toDel.add(pfx + PROP_CRYPT); + toDel.add(pfx + PROP_SHASH); + return _context.router().saveConfig(toAdd, toDel); + } + + + /** + * This will fail if + * if user contains '#' or '=' or starts with '!' + * + * @param realm e.g. i2cp, routerconsole, etc. + * @param user null or "" for no user, already trimmed + * @param pw plain text, already trimmed + * @return success + */ + public boolean saveB64(String realm, String user, String pw) { + String pfx = realm; + if (user != null && user.length() > 0) + pfx += '.' + user; + String b64 = Base64.encode(DataHelper.getUTF8(pw)); + Map toAdd = Collections.singletonMap(pfx + PROP_B64, b64); + List toDel = new ArrayList(4); + toDel.add(pfx + PROP_PW); + toDel.add(pfx + PROP_MD5); + toDel.add(pfx + PROP_CRYPT); + toDel.add(pfx + PROP_SHASH); + return _context.router().saveConfig(toAdd, toDel); + } + + /** + * This will fail if + * user contains '#' or '=' or starts with '!' + * + * @param realm e.g. i2cp, routerconsole, etc. + * @param user null or "" for no user, already trimmed + * @param pw plain text, already trimmed + * @return success + */ + public boolean saveHash(String realm, String user, String pw) { + String pfx = realm; + if (user != null && user.length() > 0) + pfx += '.' + user; + 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); + String shash = Base64.encode(shashBytes); + Map toAdd = Collections.singletonMap(pfx + PROP_SHASH, shash); + List toDel = new ArrayList(4); + toDel.add(pfx + PROP_PW); + toDel.add(pfx + PROP_B64); + toDel.add(pfx + PROP_MD5); + toDel.add(pfx + PROP_CRYPT); + return _context.router().saveConfig(toAdd, toDel); + } +}