From 6b578dfd8c4defb1439eaff551b2ed382af5915d Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Sun, 8 May 2016 19:49:14 +0000
Subject: [PATCH] Console: Fix UTF-8 passwords Partial fix for UTF-8 usernames
 Better input checking and help messages

---
 .../i2ptunnel/I2PTunnelHTTPClientBase.java    |  1 +
 .../net/i2p/router/web/ConfigUIHandler.java   | 14 +++++++
 .../i2p/router/web/RouterConsoleRunner.java   | 38 ++++++++++++++++++-
 core/java/src/net/i2p/data/DataHelper.java    |  4 +-
 .../src/net/i2p/util/PasswordManager.java     | 21 ++++++----
 5 files changed, 68 insertions(+), 10 deletions(-)

diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
index 3776d767b5..2d6e053b03 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
@@ -463,6 +463,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
             " realm=\"" + getRealm() + '"' +
             (isDigest ? ", nonce=\"" + getNonce() + "\"," +
                         " algorithm=MD5," +
+                        " charset=UTF-8," +     // RFC 7616/7617
                         " qop=\"auth\"" +
                         (isStale ? ", stale=true" : "")
                       : "") +
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUIHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUIHandler.java
index b5b6a54375..afe897b3ba 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUIHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUIHandler.java
@@ -5,6 +5,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import net.i2p.data.DataHelper;
+
 /** set the theme */
 public class ConfigUIHandler extends FormHandler {
     private boolean _shouldSave;
@@ -80,6 +82,16 @@ public class ConfigUIHandler extends FormHandler {
             addFormError(_t("No user name entered"));
             return;
         }
+        // XSS filters # and ; but not =
+        // We store the username as the part of an option key, so we can't handle '='
+        if (name.contains("=")) {
+            addFormError("User name may not contain '='");
+            return;
+        }
+        byte[] b1 = DataHelper.getUTF8(name);
+        byte[] b2 = DataHelper.getASCII(name);
+        if (!DataHelper.eq(b1, b2))
+            addFormError(_t("Warning: User names outside the ISO-8859-1 character set are not recommended. Support is not standardized and varies by browser."));
         String pw = getJettyString("nofilter_pw");
         if (pw == null || pw.length() <= 0) {
             addFormError(_t("No password entered"));
@@ -91,6 +103,8 @@ public class ConfigUIHandler extends FormHandler {
             if (!_context.getBooleanProperty(RouterConsoleRunner.PROP_PW_ENABLE))
                 _context.router().saveConfig(RouterConsoleRunner.PROP_PW_ENABLE, "true");
             addFormNotice(_t("Added user {0}", name));
+            addFormNotice(_t("To recover from a forgotten or non-working password, stop I2P, edit the file {0}, delete the line {1}, and restart I2P.",
+                             _context.router().getConfigFilename(), RouterConsoleRunner.PROP_PW_ENABLE + "=true"));
             addFormError(_t("Restart required to take effect"));
         } else {
             addFormError(_t("Error saving the configuration (applied but not saved) - please see the error logs."));
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
index a88bbb86ab..0c8064d304 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
@@ -5,6 +5,7 @@ import java.io.File;
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
 import java.net.InetAddress;
 import java.net.Inet4Address;
 import java.net.InetSocketAddress;
@@ -838,16 +839,51 @@ public class RouterConsoleRunner implements RouterApp {
                 HashLoginService realm = new HashLoginService(JETTY_REALM);
                 sec.setLoginService(realm);
                 sec.setAuthenticator(authenticator);
+                String[] role = new String[] {JETTY_ROLE};
                 for (Map.Entry<String, String> e : userpw.entrySet()) {
                     String user = e.getKey();
                     String pw = e.getValue();
-                    realm.putUser(user, Credential.getCredential(MD5.__TYPE + pw), new String[] {JETTY_ROLE});
+                    Credential cred = Credential.getCredential(MD5.__TYPE + pw);
+                    realm.putUser(user, cred, role);
                     Constraint constraint = new Constraint(user, JETTY_ROLE);
                     constraint.setAuthenticate(true);
                     ConstraintMapping cm = new ConstraintMapping();
                     cm.setConstraint(constraint);
                     cm.setPathSpec("/");
                     constraints.add(cm);
+                    // Jetty does auth checking only with ISO-8859-1,
+                    // so register a 2nd and 3rd user with different encodings if necessary.
+                    // Might work, might not...
+                    // There's no standard and browser behavior varies.
+                    // Chrome sends UTF-8. Firefox doesn't send anything.
+                    // https://bugzilla.mozilla.org/show_bug.cgi?id=41489
+                    // see also RFC 7616/7617 (late 2015) and PasswordManager.md5Hex()
+                    byte[] b1 = DataHelper.getUTF8(user);
+                    byte[] b2 = DataHelper.getASCII(user);
+                    if (!DataHelper.eq(b1, b2)) {
+                        try {
+                            // each char truncated to 8 bytes
+                            String user2 = new String(b2, "ISO-8859-1");
+                            realm.putUser(user2, cred, role);
+                            constraint = new Constraint(user2, JETTY_ROLE);
+                            constraint.setAuthenticate(true);
+                            cm = new ConstraintMapping();
+                            cm.setConstraint(constraint);
+                            cm.setPathSpec("/");
+                            constraints.add(cm);
+
+                            // each UTF-8 byte as a char
+                            // this is what chrome does
+                            String user3 = new String(b1, "ISO-8859-1");
+                            realm.putUser(user3, cred, role);
+                            constraint = new Constraint(user3, JETTY_ROLE);
+                            constraint.setAuthenticate(true);
+                            cm = new ConstraintMapping();
+                            cm.setConstraint(constraint);
+                            cm.setPathSpec("/");
+                            constraints.add(cm);
+                        } catch (UnsupportedEncodingException uee) {}
+                    }
                 }
             }
         }
diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java
index 240ff42fbb..6ce1e07896 100644
--- a/core/java/src/net/i2p/data/DataHelper.java
+++ b/core/java/src/net/i2p/data/DataHelper.java
@@ -1873,7 +1873,9 @@ public class DataHelper {
      *  Roughly the same as orig.getBytes("ISO-8859-1") but much faster and
      *  will not throw an exception.
      *
-     *  @param orig non-null, must be 7-bit chars
+     *  Warning - misnamed, converts to ISO-8859-1.
+     *
+     *  @param orig non-null, truncates to 8-bit chars
      *  @since 0.9.5
      */
     public static byte[] getASCII(String orig) {
diff --git a/core/java/src/net/i2p/util/PasswordManager.java b/core/java/src/net/i2p/util/PasswordManager.java
index 5ca812b4e7..b324c95703 100644
--- a/core/java/src/net/i2p/util/PasswordManager.java
+++ b/core/java/src/net/i2p/util/PasswordManager.java
@@ -30,7 +30,7 @@ public class PasswordManager {
     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. */
+    /** 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 */
     protected static final String PROP_CRYPT = ".crypt";
@@ -185,6 +185,10 @@ public class PasswordManager {
      *  Will return the MD5 sum of "user:subrealm:pw", compatible with Jetty
      *  and RFC 2617.
      *
+     *  Updated in 0.9.26 to use UTF-8, as implied in RFC 7616/7617
+     *  See also http://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication
+     *  http://stackoverflow.com/questions/702629/utf-8-characters-mangled-in-http-basic-auth-username
+     *
      *  @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
@@ -200,17 +204,18 @@ public class PasswordManager {
      *  Will return the MD5 sum of the data, compatible with Jetty
      *  and RFC 2617.
      *
+     *  Updated in 0.9.26 to use UTF-8, as implied in RFC 7616/7617
+     *  See also http://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication
+     *
      *  @param fullpw non-null, plain text, already trimmed
      *  @return lower-case hex with leading zeros, 32 chars, or null on error
      */
     public static String md5Hex(String fullpw) {
-        try {
-            byte[] data = fullpw.getBytes("ISO-8859-1");
-            byte[] sum = md5Sum(data);
-            if (sum != null)
-                // adds leading zeros if necessary
-                return DataHelper.toString(sum);
-        } catch (UnsupportedEncodingException uee) {}
+        byte[] data = DataHelper.getUTF8(fullpw);
+        byte[] sum = md5Sum(data);
+        if (sum != null)
+            // adds leading zeros if necessary
+            return DataHelper.toString(sum);
         return null;
     }
 
-- 
GitLab