Util: Use constant-time comparison in various password checkers

reported by: bottomlineit.co.za
This commit is contained in:
zzz
2026-05-08 15:02:22 -04:00
parent bbe18c9e9f
commit a4afe588f3
5 changed files with 68 additions and 7 deletions
@@ -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);
}
}
*/
}
@@ -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);
}
/**