diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java index 7807b699f2..a4ffc11733 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java @@ -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" + "

I2P ERROR: PROXY AUTHENTICATION REQUIRED

" + "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(); } /** diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java index f7eb3cdcb4..7b4e296bdc 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java @@ -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); } } } diff --git a/core/java/src/net/i2p/util/EepGet.java b/core/java/src/net/i2p/util/EepGet.java index 24b2a392d5..9ea95faa73 100644 --- a/core/java/src/net/i2p/util/EepGet.java +++ b/core/java/src/net/i2p/util/EepGet.java @@ -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 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 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; } diff --git a/core/java/src/net/i2p/util/PasswordManager.java b/core/java/src/net/i2p/util/PasswordManager.java index bec6ec7997..6c60cea440 100644 --- a/core/java/src/net/i2p/util/PasswordManager.java +++ b/core/java/src/net/i2p/util/PasswordManager.java @@ -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 diff --git a/history.txt b/history.txt index fa96080b75..55ad8510d6 100644 --- a/history.txt +++ b/history.txt @@ -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 diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 6a9511fad2..f40f9f7a0c 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -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 = "";