i2ptunnel, eepget: Add support for SHA-256 digest proxy auth (RFC 7616)

Requires re-saving user/pw on proxy side
This commit is contained in:
zzz
2022-09-20 08:26:05 -04:00
parent ab49a149ba
commit c1b241ab8f
6 changed files with 148 additions and 34 deletions

View File

@ -92,11 +92,9 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
"Cache-Control: no-cache\r\n" +
"Connection: close\r\n"+
"Proxy-Connection: close\r\n"+
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.5\r\n" + // try to get a UTF-8-encoded response back for the password
"Proxy-Authenticate: ";
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.5\r\n"; // try to get a UTF-8-encoded response back for the password
// put the auth type and realm in between
private static final String ERR_AUTH2 =
"\r\n" +
"\r\n" +
"<html><body><H1>I2P ERROR: PROXY AUTHENTICATION REQUIRED</H1>" +
"This proxy is configured to require authentication.";
@ -346,6 +344,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
/** new style MD5 auth */
public static final String PROP_PROXY_DIGEST_PREFIX = "proxy.auth.";
public static final String PROP_PROXY_DIGEST_SUFFIX = ".md5";
public static final String PROP_PROXY_DIGEST_SHA256_SUFFIX = ".sha256";
public static final String BASIC_AUTH = "basic";
public static final String DIGEST_AUTH = "digest";
@ -526,6 +525,19 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
_log.info("Bad digest request: " + DataHelper.toString(args));
return AuthResult.AUTH_BAD_REQ;
}
// RFC 7616
String algorithm = args.get("algorithm");
boolean isSHA256 = false;
if (algorithm != null) {
algorithm = algorithm.toLowerCase(Locale.US);
if (algorithm.equals("sha-256")) {
isSHA256 = true;
} else if (!algorithm.equals("md5")) {
if (_log.shouldLog(Log.INFO))
_log.info("Bad digest request: " + DataHelper.toString(args));
return AuthResult.AUTH_BAD_REQ;
}
}
// nonce check
AuthResult check = verifyNonce(nonce, nc);
if (check != AuthResult.AUTH_GOOD) {
@ -535,17 +547,17 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
}
// get H(A1) == stored password
String ha1 = getTunnel().getClientOptions().getProperty(PROP_PROXY_DIGEST_PREFIX + user +
PROP_PROXY_DIGEST_SUFFIX);
(isSHA256 ? PROP_PROXY_DIGEST_SHA256_SUFFIX : PROP_PROXY_DIGEST_SUFFIX));
if (ha1 == null) {
_log.logAlways(Log.WARN, "HTTP proxy authentication failed, user: " + user);
return AuthResult.AUTH_BAD;
}
// get H(A2)
String a2 = method + ':' + uri;
String ha2 = PasswordManager.md5Hex(a2);
String ha2 = isSHA256 ? PasswordManager.sha256Hex(a2) : PasswordManager.md5Hex(a2);
// response check
String kd = ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2;
String hkd = PasswordManager.md5Hex(kd);
String hkd = isSHA256 ? PasswordManager.sha256Hex(kd) : PasswordManager.md5Hex(kd);
if (!response.equals(hkd)) {
_log.logAlways(Log.WARN, "HTTP proxy authentication failed, user: " + user);
if (_log.shouldLog(Log.INFO))
@ -631,17 +643,43 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
*/
protected String getAuthError(boolean isStale) {
boolean isDigest = isDigestAuthRequired();
return
ERR_AUTH1 +
(isDigest ? "Digest" : "Basic") +
" realm=\"" + getRealm() + '"' +
(isDigest ? ", nonce=\"" + getNonce() + "\"," +
" algorithm=MD5," +
" charset=UTF-8," + // RFC 7616/7617
" qop=\"auth\"" +
(isStale ? ", stale=true" : "")
: "") +
ERR_AUTH2;
StringBuilder buf = new StringBuilder(512);
buf.append(ERR_AUTH1)
.append("Proxy-Authenticate: ")
.append(isDigest ? "Digest" : "Basic")
.append(" realm=\"" + getRealm() + '"');
if (isDigest) {
String nonce = getNonce();
// RFC 7616 most-preferred first, client accepts first that he supports
// This is also compatible with eepget < 0.9.56 that will use the last one
// Do we have a SHA256 hash for any user?
for (String k : getTunnel().getClientOptions().stringPropertyNames()) {
if (k.startsWith(PROP_PROXY_DIGEST_PREFIX) &&
k.endsWith(PROP_PROXY_DIGEST_SHA256_SUFFIX)) {
// SHA-256, RFC 7616
buf.append(", nonce=\"" + nonce + "\"," +
" algorithm=SHA-256," +
" charset=UTF-8," +
" qop=\"auth\"");
if (isStale)
buf.append(", stale=true");
buf.append("\r\n" +
"Proxy-Authenticate: Digest" +
" realm=\"" + getRealm() + '"');
break;
}
}
buf.append(", nonce=\"" + nonce + "\"," +
" algorithm=MD5," +
" charset=UTF-8," + // RFC 7616/7617
" qop=\"auth\"");
if (isStale)
buf.append(", stale=true");
}
buf.append("\r\n")
.append(ERR_AUTH2);
return buf.toString();
}
/**

View File

@ -774,7 +774,7 @@ public class TunnelConfig {
config.setProperty(TunnelController.PROP_PROXIES, _proxyList);
}
// Proxy auth including migration to MD5
// Proxy auth including migration to MD5 and SHA256
if (TunnelController.TYPE_HTTP_CLIENT.equals(_type) || TunnelController.TYPE_CONNECT.equals(_type)) {
// Migrate even if auth is disabled
// go get the old from custom options that updateConfigGeneric() put in there
@ -796,6 +796,17 @@ public class TunnelConfig {
config.remove(ppw);
}
}
// SHA256 RFC 7616
String psha256 = OPT + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX +
user + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SHA256_SUFFIX;
if (config.getProperty(psha256) == null) {
// not in there, add it
String realm = _type.equals(TunnelController.TYPE_HTTP_CLIENT) ? I2PTunnelHTTPClient.AUTH_REALM
: I2PTunnelConnectClient.AUTH_REALM;
String hex = PasswordManager.sha256Hex(realm, user, pw);
if (hex != null)
config.setProperty(psha256, hex);
}
}
// New user/password
String auth = _otherOptions.get(I2PTunnelHTTPClientBase.PROP_AUTH);
@ -809,6 +820,11 @@ public class TunnelConfig {
String hex = PasswordManager.md5Hex(realm, _newProxyUser, _newProxyPW);
if (hex != null)
config.setProperty(pmd5, hex);
String psha256 = OPT + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX +
_newProxyUser + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SHA256_SUFFIX;
hex = PasswordManager.sha256Hex(realm, _newProxyUser, _newProxyPW);
if (hex != null)
config.setProperty(psha256, hex);
}
}
}

View File

@ -1785,20 +1785,38 @@ public class EepGet {
// use standard alphabet
authMode = AUTH_MODE.BASIC;
authChallenge = auth.substring(6);
args = parseAuthArgs(authChallenge);
}
} else if (authLC.startsWith("digest ")) {
// better than anything
authMode = AUTH_MODE.DIGEST;
authChallenge = auth.substring(7);
// RFC 7616 take the first one that we support
if (authMode != AUTH_MODE.DIGEST) {
String ac = auth.substring(7);
Map<String, String> aargs = parseAuthArgs(ac);
String algo = aargs.get("algorithm");
if (algo != null) {
algo = algo.toLowerCase(Locale.US);
if (!algo.equals("sha-256") && !algo.equals("md5")) {
if (_log.shouldWarn())
_log.warn("Unsupported auth algorithm " + algo);
return;
}
}
authMode = AUTH_MODE.DIGEST;
authChallenge = ac;
args = aargs;
} else {
if (_log.shouldDebug())
_log.debug("Ignoring auth algorithm " + auth);
}
} else {
// better than NONE only
if (authMode == AUTH_MODE.NONE) {
authMode = AUTH_MODE.UNKNOWN;
authChallenge = null;
args = null;
}
}
nonceCount = 0;
args = null;
}
public String getAuthHeader(String method, String uri) throws IOException {
@ -1812,8 +1830,6 @@ public class EepGet {
case DIGEST:
if (authChallenge == null)
throw new IOException("Bad proxy auth response");
if (args == null)
args = parseAuthArgs(authChallenge);
Map<String, String> outArgs = generateAuthArgs(method, uri);
if (outArgs == null)
throw new IOException("Bad proxy auth response");
@ -1842,7 +1858,14 @@ public class EepGet {
String nonce = args.get("nonce");
String qop = args.get("qop");
String opaque = args.get("opaque");
//String algorithm = args.get("algorithm");
// RFC 7616
String algorithm = args.get("algorithm");
boolean isSHA256 = false;
if (algorithm == null)
algorithm = "MD5";
else
isSHA256 = algorithm.toLowerCase(Locale.US).equals("sha-256");
rv.put("algorithm", algorithm);
//String stale = args.get("stale");
if (realm == null || nonce == null) {
if (_log.shouldLog(Log.INFO))
@ -1872,13 +1895,14 @@ public class EepGet {
}
// get H(A1)
String ha1 = PasswordManager.md5Hex(username + ':' + realm + ':' + password);
String a1 = username + ':' + realm + ':' + password;
String ha1 = isSHA256 ? PasswordManager.sha256Hex(a1) : PasswordManager.md5Hex(a1);
// get H(A2)
String a2 = method + ':' + uri;
String ha2 = PasswordManager.md5Hex(a2);
String ha2 = isSHA256 ? PasswordManager.sha256Hex(a2) : PasswordManager.md5Hex(a2);
// response
String kd = ha1 + ':' + nonce + kdMiddle + ':' + ha2;
rv.put("response", '"' + PasswordManager.md5Hex(kd) + '"');
rv.put("response", '"' + (isSHA256 ? PasswordManager.sha256Hex(kd) : PasswordManager.md5Hex(kd)) + '"');
return rv;
}

View File

@ -5,6 +5,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import net.i2p.I2PAppContext;
import net.i2p.crypto.SHA256Generator;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.data.SessionKey;
@ -232,6 +233,38 @@ public class PasswordManager {
return null;
}
/**
* Straight SHA256, no salt
* Will return the SHA256 sum of "user:subrealm:pw", compatible with RFC 7616.
* NOT currently supported by Jetty.
*
* @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
* @since 0.9.56
*/
public static String sha256Hex(String subrealm, String user, String pw) {
String fullpw = user + ':' + subrealm + ':' + pw;
return sha256Hex(fullpw);
}
/**
* Return the SHA256 sum of the data, compatible with RFC 7616.
* NOT currently supported by Jetty.
*
* @param fullpw non-null, plain text, already trimmed
* @return lower-case hex with leading zeros, 64 chars, or null on error
* @since 0.9.56
*/
public static String sha256Hex(String fullpw) {
byte[] data = DataHelper.getUTF8(fullpw);
byte[] sum = new byte[32];
SHA256Generator.getInstance().calculateHash(data, 0, data.length, sum, 0);
// adds leading zeros if necessary
return DataHelper.toString(sum);
}
/**
* speed/comparison test before removing BC version;
* JVM was slightly faster

View File

@ -1,3 +1,6 @@
2022-09-20 zzz
* i2ptunnel, eepget: Add support for SHA-256 digest proxy auth (RFC 7616)
2022-09-09 zzz
* SSU2: Enable handling of ack-immediate flag by default
@ -38,7 +41,7 @@
2022-08-23 zzz
* Router: Add deadlocks to event log
2022-08-22 1.9.0 released
2022-08-22 1.9.0 (API 0.9.55) released
2022-08-10 zzz
* SSU2: Enable for Android, ARM, and 2% of others
@ -229,7 +232,7 @@
* Crypto: Throw checked exception from ElGamal
so console key import reports the correct error
2022-05-23 1.8.0 released
2022-05-23 1.8.0 (API 0.9.54) released
2022-05-19 zzz
* Pull translations from Transifex
@ -377,7 +380,7 @@
* Update: Add notification for new version
* Util: Speed up PRNG nextInt() and nextLong()
2022-02-21 1.7.0 released
2022-02-21 1.7.0 (API 0.9.53) released
2022-02-18 zzz
* Update translations
@ -492,7 +495,7 @@
* Console: NetDB search form improvements
* i2ptunnel: Increase priority for IRC and standard tunnels
2021-11-29 1.6.1 released
2021-11-29 1.6.1 (API 0.9.52) released
2021-11-29 zzz
* Tunnels: Fix NPE in BuildHandler
@ -575,7 +578,7 @@
* Router: Increase rekey probability
* Tunnels: Enable sending short build messages
2021-08-23 1.5.0 released
2021-08-23 1.5.0 (API 0.9.51) released
2021-08-20 zzz
* Update GeoIP and translations

View File

@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */
public final static String ID = "Git";
public final static String VERSION = CoreVersion.VERSION;
public final static long BUILD = 8;
public final static long BUILD = 9;
/** for example "-test" */
public final static String EXTRA = "";