diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java index a99d6a7b5..7896adc27 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java @@ -471,7 +471,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem configPW = getTunnel().getClientOptions().getProperty(PROP_PW); } if (configPW != null) { - if (pw.equals(configPW)) { + if (DataHelper.eqCT(pw, configPW)) { if (_log.shouldLog(Log.INFO)) _log.info(getPrefix(requestId) + "Good auth - user: " + user + " pw: " + pw); return AuthResult.AUTH_GOOD; @@ -562,7 +562,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem // response check String kd = ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2; String hkd = isSHA256 ? PasswordManager.sha256Hex(kd) : PasswordManager.md5Hex(kd); - if (!response.equals(hkd)) { + if (!DataHelper.eqCT(response, hkd)) { _log.logAlways(Log.WARN, "HTTP proxy authentication failed, user: " + user + " IP: " + s.getInetAddress()); if (_log.shouldLog(Log.INFO)) _log.info("Bad digest auth: " + DataHelper.toString(args)); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java index f2fde6d2b..98cef8a2c 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java @@ -183,7 +183,7 @@ class SOCKS5Server extends SOCKSServer { I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SHA256_SUFFIX; String configPW = props.getProperty(psha256); String hex = PasswordManager.sha256Hex(I2PSOCKSTunnel.AUTH_REALM, u, p); - if (configPW == null || !configPW.equals(hex)) { + if (configPW == null || !DataHelper.eqCT(hex, configPW)) { _log.logAlways(Log.WARN, "SOCKS proxy authentication failed, user: " + u + " IP: " + client); try { Thread.sleep(3000); } catch (InterruptedException ie) {} sendAuthReply(AUTH_FAILURE, out); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConsolePasswordManager.java b/apps/routerconsole/java/src/net/i2p/router/web/ConsolePasswordManager.java index 6dc74e1ae..10e64221c 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConsolePasswordManager.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConsolePasswordManager.java @@ -67,7 +67,7 @@ public class ConsolePasswordManager extends RouterPasswordManager { String hex = _context.getProperty(pfx + PROP_MD5); if (hex == null) return false; - return hex.equals(md5Hex(subrealm, user, pw)); + return DataHelper.eqCT(md5Hex(subrealm, user, pw), hex); } /** diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java index 057ef26b5..f139a405a 100644 --- a/core/java/src/net/i2p/data/DataHelper.java +++ b/core/java/src/net/i2p/data/DataHelper.java @@ -1129,6 +1129,33 @@ public class DataHelper { } return r == 0; } + + /** + * This throws NPE if either lhs or rhs is null. + * Constant time, almost. + * Warning: not constant time if secret is empty. + * + * @param user user-supplied String, will attempt for time to be proportional to this length + * @param secret internal String, will attempt for time to be independent of this length + * @throws NullPointerException if either arg is null + * @since 0.9.70 + */ + public final static boolean eqCT(String user, String secret) { + int ul = user.length(); + int sl = secret.length(); + if (ul == 0) + return sl == 0; + int v = ul ^ sl; + if (sl == 0) { + // so charAt() below works + secret = "\0"; + sl = 1; + } + for (int i = 0; i < ul; i++) { + v |= user.charAt(i) ^ secret.charAt(i % sl); + } + return v == 0; + } /** * Big endian compare, treats bytes as unsigned. @@ -2242,4 +2269,26 @@ public class DataHelper { oidx = idx + to.length(); } } + +/* + public static void main(String[] args) { + test("123", "123"); + test("a", "b"); + test("ba", "b"); + test("ba", "baa"); + test("", "xxx"); + test("xxx", ""); + test("xxx", null); + test(null, "xxx"); + } + + private static void test(String a, String b) { + try { + boolean r = eqCT(a, b); + System.out.println(" test: " + a + ' ' + b + " equals? " + r); + } catch (Exception e) { + System.out.println(" test: " + a + ' ' + b + " fails " + e); + } + } +*/ } diff --git a/core/java/src/net/i2p/util/PasswordManager.java b/core/java/src/net/i2p/util/PasswordManager.java index 6c60cea44..340300a9f 100644 --- a/core/java/src/net/i2p/util/PasswordManager.java +++ b/core/java/src/net/i2p/util/PasswordManager.java @@ -31,8 +31,17 @@ public class PasswordManager { 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 */ + + /** + * Stored as a Unix crypt string + * Originally intended as a Jetty-compatible UnixCrypt string, see man crypt(5), + * but not fully implemented and insecure anyway. + * + * @deprecated unused + */ + @Deprecated 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"; @@ -64,7 +73,10 @@ public class PasswordManager { String pfx = realm; if (user != null && user.length() > 0) pfx += '.' + user; - return pw.equals(_context.getProperty(pfx + PROP_PW)); + String s = _context.getProperty(pfx + PROP_PW); + if (s == null) + return false; + return DataHelper.eqCT(pw, s); } /** @@ -80,7 +92,7 @@ public class PasswordManager { String b64 = _context.getProperty(pfx + PROP_B64); if (b64 == null) return false; - return b64.equals(Base64.encode(DataHelper.getUTF8(pw))); + return DataHelper.eqCT(Base64.encode(DataHelper.getUTF8(pw)), b64); } /**