Compare commits

...

1 Commits

Author SHA1 Message Date
zzz
983c8d841f Console: Add support for multiple authentication schemes
Some checks failed
Java CI / build (push) Has been cancelled
Java CI / javadoc-latest (push) Has been cancelled
Java CI / build-java7 (push) Has been cancelled
Java with IzPack Snapshot Setup / setup (push) Has been cancelled
Add support for SHA-256 digests
Remove separate realm for prometheus plugin, no longer required
2025-05-04 15:49:49 -04:00
6 changed files with 838 additions and 23 deletions

View File

@@ -0,0 +1,539 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package net.i2p.jetty;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.BitSet;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.security.authentication.DeferredAuthentication;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.Authentication.User;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Credential;
import net.i2p.I2PAppContext;
/**
* We'd like to extend DigestAuthenticator but the
* Digest inner class is private, so this is a wholesale copy
* to add multi-scheme support, and some other minor changes.
*
* This supports both Digest and Basic schemes,
* and both SHA-256 and MD5 Digest algorithms,
* all in one.
* Ref: RFC 7616, RFC 2617.
*
* Jetty does not support multiple auth at once
* https://github.com/jetty/jetty.project/issues/5442
* but it's coming for Jetty 12.1.0
* https://github.com/jetty/jetty.project/pull/12393
* However, it's about different auth on different resources?
* Not multiple auth for the same resource.
*
* Any scheme must be compatible with what browsers support, see
* RFC 7616. We extend Jetty DigestAuthenticator to support
* SHA-256 while still supporting MD5.
* Firefox supports SHA-256 as of FF93 (2021)
* Chrome supports it as of Chrome 117 (2023)
* See chart at bottom of https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/WWW-Authenticate
* But Jetty still claiming there's no support in 2024
* and refuses to implement it: https://github.com/jetty/jetty.project/issues/11489
* SHA256 is still NOT supported by Safari / IOS.
*
* Some of this duplicates code we have in I2PTunnelHTTPClientBase,
* and could perhaps be consolidated.
*
* See also SHA256Credential and MultiCredential.
*
* @since 0.9.67
*/
public class MultiAuthenticator extends LoginAuthenticator
{
private static final Logger LOG = Log.getLogger(MultiAuthenticator.class);
private final I2PAppContext _context;
private long _maxNonceAgeMs = 60 * 1000;
private int _maxNC = 1024;
private final ConcurrentMap<String, Nonce> _nonceMap = new ConcurrentHashMap<>();
private final Queue<Nonce> _nonceQueue = new ConcurrentLinkedQueue<>();
private final boolean _enableSHA256, _enableMD5, _enableBasic;
public MultiAuthenticator() {
this(true, true, true);
}
public MultiAuthenticator(boolean enableSHA256, boolean enableMD5, boolean enableBasic)
{
_context = I2PAppContext.getGlobalContext();
_enableSHA256 = enableSHA256;
_enableMD5 = enableMD5;
_enableBasic = enableBasic;
}
@Override
public void setConfiguration(AuthConfiguration configuration)
{
super.setConfiguration(configuration);
String mna = configuration.getInitParameter("maxNonceAge");
if (mna != null)
setMaxNonceAge(Long.valueOf(mna));
String mnc = configuration.getInitParameter("maxNonceCount");
if (mnc != null)
setMaxNonceCount(Integer.valueOf(mnc));
}
public int getMaxNonceCount()
{
return _maxNC;
}
public void setMaxNonceCount(int maxNC)
{
_maxNC = maxNC;
}
public long getMaxNonceAge()
{
return _maxNonceAgeMs;
}
public void setMaxNonceAge(long maxNonceAgeInMillis)
{
_maxNonceAgeMs = maxNonceAgeInMillis;
}
@Override
public String getAuthMethod()
{
return Constraint.__DIGEST_AUTH;
}
@Override
public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
{
return true;
}
@Override
public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
{
if (!mandatory)
return new DeferredAuthentication(this);
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
String credentials = request.getHeader(HttpHeader.AUTHORIZATION.asString());
try
{
boolean stale = false;
if (credentials != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Credentials: " + credentials);
QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false);
Digest digest = null;
String last = null;
String name = null;
String scheme = null;
String credential = null;
while (tokenizer.hasMoreTokens())
{
String tok = tokenizer.nextToken();
// setup based on scheme
if (scheme == null) {
scheme = tok.toLowerCase();
continue;
}
if (scheme.equals("digest")) {
if (digest == null) {
digest = new Digest(_context, request.getMethod());
}
} else if (scheme.equals("basic")) {
// collect the space, then the credential, then break
if (tok.equals(" ")) {
// skip the space
continue;
}
credential = tok;
// processed below
break;
} else {
// unknown scheme
break;
}
char c = (tok.length() == 1) ? tok.charAt(0) : '\0';
switch (c)
{
case '=':
name = last;
last = tok;
break;
case ',':
name = null;
break;
case ' ':
break;
default:
last = tok;
if (name != null)
{
if ("username".equalsIgnoreCase(name))
digest.username = tok;
else if ("realm".equalsIgnoreCase(name))
digest.realm = tok;
else if ("nonce".equalsIgnoreCase(name))
digest.nonce = tok;
else if ("nc".equalsIgnoreCase(name))
digest.nc = tok;
else if ("cnonce".equalsIgnoreCase(name))
digest.cnonce = tok;
else if ("qop".equalsIgnoreCase(name))
digest.qop = tok;
else if ("uri".equalsIgnoreCase(name))
digest.uri = tok;
else if ("response".equalsIgnoreCase(name))
digest.response = tok;
else if ("algorithm".equalsIgnoreCase(name))
digest.algorithm = tok.toLowerCase();
name = null;
}
}
}
// Now validate based on scheme type
if ("digest".equals(scheme) &&
((_enableSHA256 && digest.algorithm.equals("sha-256") ||
(_enableMD5 && digest.algorithm.equals("md5"))))) {
int n = checkNonce(digest, (Request)request);
if (n > 0)
{
UserIdentity user = login(digest.username, digest, req);
if (user != null)
{
return new UserAuthentication(Constraint.__DIGEST_AUTH, user);
}
}
else if (n == 0)
stale = true;
} else if (_enableBasic && "basic".equals(scheme)) {
if (credential != null) {
credential = B64Code.decode(credential, StandardCharsets.ISO_8859_1);
int i = credential.indexOf(':');
if (i>0)
{
String username = credential.substring(0,i);
String password = credential.substring(i+1);
UserIdentity user = login(username, password, request);
if (user!=null)
{
return new UserAuthentication(Constraint.__BASIC_AUTH, user);
}
}
}
} else {
LOG.warn("unsupported auth scheme " + scheme);
}
}
if (!DeferredAuthentication.isDeferred(response))
{
String domain = request.getContextPath();
if (domain == null)
domain = "/";
String nonce = newNonce((Request) request);
String realm = _loginService.getName();
// RFC 7616 preferred first
if (_enableSHA256) {
response.addHeader(HttpHeader.WWW_AUTHENTICATE.asString(), "Digest realm=\"" + realm
+ "\", domain=\""
+ domain
+ "\", nonce=\""
+ nonce
+ "\", algorithm=SHA-256, qop=\"auth\","
+ " stale=" + stale);
}
if (_enableMD5) {
response.addHeader(HttpHeader.WWW_AUTHENTICATE.asString(), "Digest realm=\"" + realm
+ "\", domain=\""
+ domain
+ "\", nonce=\""
+ nonce
+ "\", algorithm=MD5, qop=\"auth\","
+ " stale=" + stale);
}
if (_enableBasic) {
response.addHeader(HttpHeader.WWW_AUTHENTICATE.asString(), "Basic realm=\"" + realm + '"');
}
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return Authentication.SEND_CONTINUE;
}
return Authentication.UNAUTHENTICATED;
}
catch (IOException e)
{
throw new ServerAuthException(e);
}
}
/**
* For digest
*/
@Override
public UserIdentity login(String username, Object credentials, ServletRequest request)
{
Digest digest = (Digest)credentials;
if (!Objects.equals(digest.realm, _loginService.getName()))
return null;
return super.login(username, credentials, request);
}
/**
* For basic
*/
public UserIdentity login(String username, String credentials, ServletRequest request)
{
return super.login(username, credentials, request);
}
public String newNonce(Request request)
{
Nonce nonce;
do
{
byte[] nounce = new byte[24];
_context.random().nextBytes(nounce);
nonce = new Nonce(new String(B64Code.encode(nounce)), request.getTimeStamp(), getMaxNonceCount());
}
while (_nonceMap.putIfAbsent(nonce._nonce, nonce) != null);
_nonceQueue.add(nonce);
return nonce._nonce;
}
/**
* @param digest the digest data to check
* @param request the request object
* @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce
*/
private int checkNonce(Digest digest, Request request)
{
// firstly let's expire old nonces
long expired = request.getTimeStamp() - getMaxNonceAge();
Nonce nonce = _nonceQueue.peek();
while (nonce != null && nonce._ts < expired)
{
_nonceQueue.remove(nonce);
_nonceMap.remove(nonce._nonce);
nonce = _nonceQueue.peek();
}
// Now check the requested nonce
try
{
nonce = _nonceMap.get(digest.nonce);
if (nonce == null)
return 0;
long count = Long.parseLong(digest.nc, 16);
if (count >= _maxNC)
return 0;
if (nonce.seen((int)count))
return -1;
return 1;
}
catch (Exception e)
{
LOG.ignore(e);
}
return -1;
}
private static class Nonce
{
final String _nonce;
final long _ts;
final BitSet _seen;
public Nonce(String nonce, long ts, int size)
{
_nonce = nonce;
_ts = ts;
_seen = new BitSet(size);
}
public boolean seen(int count)
{
synchronized (this)
{
if (count >= _seen.size())
return true;
boolean s = _seen.get(count);
_seen.set(count);
return s;
}
}
}
private static class Digest extends Credential
{
private static final long serialVersionUID = -1111639019549527724L;
private final I2PAppContext _context;
final String method;
String username = "";
String realm = "";
String nonce = "";
String nc = "";
String cnonce = "";
String qop = "";
String uri = "";
String response = "";
// RFC 7616 default
String algorithm = "md5";
/* ------------------------------------------------------------ */
Digest(I2PAppContext ctx, String m)
{
_context = ctx;
method = m;
}
/* ------------------------------------------------------------ */
@Override
public boolean check(Object credentials)
{
if (credentials instanceof char[])
credentials = new String((char[])credentials);
String password = (credentials instanceof String) ? (String)credentials : credentials.toString();
try
{
MessageDigest md;
byte[] ha1 = null;
if (algorithm.equals("sha-256")) {
md = _context.sha().acquire();
if (credentials instanceof SHA256Credential) {
ha1 = ((SHA256Credential)credentials).getDigest();
}
} else if (algorithm.equals("md5")) {
md = MessageDigest.getInstance("MD5");
if (credentials instanceof Credential.MD5) {
// Credentials are already a MD5 digest - assume it's in
// form user:realm:password (we have no way to know since
// it's a digest, alright?)
ha1 = ((Credential.MD5)credentials).getDigest();
}
} else {
LOG.warn("unsupported algorithm " + algorithm);
return false;
}
if (ha1 == null) {
// calc A1 digest
md.update(username.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte)':');
md.update(realm.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte)':');
md.update(password.getBytes(StandardCharsets.ISO_8859_1));
ha1 = md.digest();
}
// calc A2 digest
md.reset();
md.update(method.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte)':');
md.update(uri.getBytes(StandardCharsets.ISO_8859_1));
byte[] ha2 = md.digest();
// calc digest
// request-digest = <"> < KD ( H(A1), unq(nonce-value) ":"
// nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) )
// <">
// request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2)
// ) > <">
md.update(TypeUtil.toString(ha1, 16).getBytes(StandardCharsets.ISO_8859_1));
md.update((byte)':');
md.update(nonce.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte)':');
md.update(nc.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte)':');
md.update(cnonce.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte)':');
md.update(qop.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte)':');
md.update(TypeUtil.toString(ha2, 16).getBytes(StandardCharsets.ISO_8859_1));
byte[] digest = md.digest();
if (algorithm.equals("sha-256"))
_context.sha().release(md);
// check digest
return stringEquals(TypeUtil.toString(digest, 16).toLowerCase(), response == null ? null : response.toLowerCase());
}
catch (Exception e)
{
LOG.warn(e);
}
return false;
}
@Override
public String toString()
{
return username + "," + response;
}
}
}

View File

@@ -0,0 +1,58 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package net.i2p.jetty;
import java.util.List;
import org.eclipse.jetty.util.security.Credential;
/**
* Multiple Credentials
*
* @since 0.9.67
*/
public class MultiCredential extends Credential
{
private static final long serialVersionUID = 1133333330822684240L;
private final List<Credential> creds;
/**
* @param credentials will be checked in-order
*/
public MultiCredential(List<Credential> credentials)
{
creds = credentials;
}
@Override
public boolean check(Object credentials)
{
for (Credential cred : creds) {
if (cred.check(credentials))
return true;
}
return false;
}
@Override
public String toString() {
return "MultiCredential: " + creds.toString();
}
}

View File

@@ -0,0 +1,100 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package net.i2p.jetty;
import java.nio.charset.StandardCharsets;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.security.Credential;
import org.eclipse.jetty.util.security.Password;
import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
/**
* SHA256 Credentials
*
* @since 0.9.67
*/
public class SHA256Credential extends Credential
{
private static final long serialVersionUID = 1111996540822684240L;
private static final String __TYPE = "SHA256:";
private final byte[] _digest;
private final I2PAppContext _context;
public SHA256Credential(I2PAppContext ctx, String digest)
{
digest = digest.startsWith(__TYPE) ? digest.substring(__TYPE.length()) : digest;
_digest = TypeUtil.parseBytes(digest, 16);
_context = ctx;
}
public byte[] getDigest()
{
return _digest;
}
@Override
public boolean check(Object credentials)
{
if (credentials instanceof char[])
credentials=new String((char[])credentials);
if (credentials instanceof Password || credentials instanceof String)
{
byte[] b = credentials.toString().getBytes(StandardCharsets.ISO_8859_1);
byte[] digest = new byte[32];
_context.sha().calculateHash(b, 0, b.length, digest, 0);
return byteEquals(_digest, digest);
}
else if (credentials instanceof SHA256Credential)
{
SHA256Credential sha256 = (SHA256Credential)credentials;
return byteEquals(_digest, sha256._digest);
}
else if (credentials instanceof Credential)
{
// Allow credential to attempt check - i.e. this'll work
// for DigestAuthModule$Digest credentials
return ((Credential)credentials).check(this);
}
else
{
//LOG.warn("Can't check " + credentials.getClass() + " against SHA256");
return false;
}
}
/*
public String digest(String password)
{
byte[] b = password.getBytes(StandardCharsets.ISO_8859_1);
byte[] digest = new byte[32];
_context.sha().calculateHash(b, 0, b.length, digest, 0);
return __TYPE + TypeUtil.toString(digest, 16);
}
*/
@Override
public String toString() {
return "SHA256Credential: " + DataHelper.toString(_digest);
}
}

View File

@@ -60,6 +60,7 @@ public class ConsolePasswordManager extends RouterPasswordManager {
* @param pw plain text, already trimmed
* @return if pw verified
*/
/****
public boolean checkMD5(String realm, String subrealm, String user, String pw) {
String pfx = realm;
if (user != null && user.length() > 0)
@@ -69,10 +70,11 @@ public class ConsolePasswordManager extends RouterPasswordManager {
return false;
return hex.equals(md5Hex(subrealm, user, pw));
}
****/
/**
* Get all MD5 usernames and passwords. Compatible with Jetty.
* Any "null" user is NOT included..
* Any "null" user is NOT included.
*
* @param realm e.g. i2cp, routerconsole, etc.
* @return Map of usernames to passwords (hex with leading zeros, 32 characters)
@@ -145,11 +147,13 @@ public class ConsolePasswordManager extends RouterPasswordManager {
return _context.router().saveConfig(toAdd, toDel);
}
****/
/**
* Straight MD5, no salt
* Compatible with Jetty and RFC 2617.
*
* Any other passwords for this user, realm, and subrealm will be deleted.
*
* @param realm The full realm, e.g. routerconsole.auth.i2prouter, etc.
* @param subrealm to be used in creating the checksum
* @param user non-null, non-empty, already trimmed
@@ -169,9 +173,97 @@ public class ConsolePasswordManager extends RouterPasswordManager {
toDel.add(pfx + PROP_B64);
toDel.add(pfx + PROP_CRYPT);
toDel.add(pfx + PROP_SHASH);
toDel.add(pfx + PROP_SHA256);
return _context.router().saveConfig(toAdd, toDel);
}
/**
* Get all SHA256 usernames and passwords.
* Compatible with our Jetty SHA256DigestAuthenticator and RFC 7616.
* Any "null" user is NOT included.
*
* @param realm e.g. i2cp, routerconsole, etc.
* @return Map of usernames to passwords (hex with leading zeros, 64 characters)
* @since 0.9.67
*/
public Map<String, String> getSHA256(String realm) {
String pfx = realm + '.';
Map<String, String> rv = new HashMap<String, String>(4);
for (Map.Entry<String, String> e : _context.router().getConfigMap().entrySet()) {
String prop = e.getKey();
if (prop.startsWith(pfx) && prop.endsWith(PROP_SHA256)) {
String user = prop.substring(0, prop.length() - PROP_SHA256.length()).substring(pfx.length());
String hex = e.getValue();
if (user.length() > 0 && hex.length() == 64)
rv.put(user, hex);
}
}
return rv;
}
/**
* Straight SHA256, no salt.
* Compatible with our Jetty SHA256DigestAuthenticator and RFC 7616.
* Any other passwords for this user, realm, and subrealm will be deleted.
* To support both, use saveMD5SHA256()
*
* @param realm The full realm, e.g. routerconsole.auth.i2prouter, etc.
* @param subrealm to be used in creating the checksum
* @param user non-null, non-empty, already trimmed
* @param pw plain text
* @return if pw verified
* @since 0.9.67
*/
public boolean saveSHA256(String realm, String subrealm, String user, String pw) {
String pfx = realm;
if (user != null && user.length() > 0)
pfx += '.' + user;
String hex = sha256Hex(subrealm, user, pw);
if (hex == null)
return false;
Map<String, String> toAdd = Collections.singletonMap(pfx + PROP_SHA256, hex);
List<String> toDel = new ArrayList<String>(4);
toDel.add(pfx + PROP_PW);
toDel.add(pfx + PROP_B64);
toDel.add(pfx + PROP_CRYPT);
toDel.add(pfx + PROP_SHASH);
toDel.add(pfx + PROP_MD5);
return _context.router().saveConfig(toAdd, toDel);
}
/**
* Both MD5 and SHA256, no salt.
* Compatible with our Jetty SHA256DigestAuthenticator and RFC 7616.
* Any other passwords for this user, realm, and subrealm will be deleted.
*
* @param realm The full realm, e.g. routerconsole.auth.i2prouter, etc.
* @param subrealm to be used in creating the checksum
* @param user non-null, non-empty, already trimmed
* @param pw plain text
* @return if pw verified
* @since 0.9.67
*/
public boolean saveMD5SHA256(String realm, String subrealm, String user, String pw) {
String pfx = realm;
if (user != null && user.length() > 0)
pfx += '.' + user;
Map<String, String> toAdd = new HashMap<String, String>(2);
String hex = sha256Hex(subrealm, user, pw);
if (hex == null)
return false;
toAdd.put(pfx + PROP_SHA256, hex);
hex = md5Hex(subrealm, user, pw);
if (hex == null)
return false;
toAdd.put(pfx + PROP_MD5, hex);
List<String> toDel = new ArrayList<String>(4);
toDel.add(pfx + PROP_PW);
toDel.add(pfx + PROP_B64);
toDel.add(pfx + PROP_CRYPT);
toDel.add(pfx + PROP_SHASH);
return _context.router().saveConfig(toAdd, toDel);
}
/****
public static void main(String args[]) {
RouterContext ctx = (new Router()).getContext();

View File

@@ -32,6 +32,9 @@ import static net.i2p.app.ClientAppState.*;
import net.i2p.crypto.KeyStoreUtil;
import net.i2p.data.DataHelper;
import net.i2p.jetty.I2PLogger;
import net.i2p.jetty.MultiAuthenticator;
import net.i2p.jetty.MultiCredential;
import net.i2p.jetty.SHA256Credential;
import net.i2p.router.RouterContext;
import net.i2p.router.app.RouterApp;
import net.i2p.router.news.NewsManager;
@@ -49,8 +52,6 @@ import net.i2p.util.SystemVersion;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.server.AbstractConnector;
import org.eclipse.jetty.server.ConnectionFactory;
@@ -76,6 +77,7 @@ import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Credential;
import org.eclipse.jetty.util.security.Credential.MD5;
import org.eclipse.jetty.util.security.Password;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ExecutorThreadPool;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
@@ -116,14 +118,17 @@ public class RouterConsoleRunner implements RouterApp {
private static final String DEFAULT_WEBAPP_CONFIG_FILENAME = "webapps.config";
// Jetty Auth
private static final DigestAuthenticator authenticator = new DigestAuthenticator();
// only for prometheus plugin
private static final BasicAuthenticator basicAuthenticator = new BasicAuthenticator();
public static final String PROMETHEUS_REALM = "prometheus";
// Jetty Auth (MD5, basic)
// A MultiAuthenticator so we can support digest and basic simultaneously,
// using the MD5 hash as the "password", for prometheus plugin and
// other HTTP clients that don't support digest.
private static final MultiAuthenticator authenticator = new MultiAuthenticator(false, true, true);
// Jetty Auth (SHA-256, MD5, Basic)
private static final MultiAuthenticator multiAuthenticator = new MultiAuthenticator(true, true, true);
static {
// default changed from 0 (forever) in Jetty 6 to 60*1000 ms in Jetty 7
authenticator.setMaxNonceAge(7*24*60*60*1000L);
multiAuthenticator.setMaxNonceAge(7*24*60*60*1000L);
}
private static final String NAME = "console";
public static final String JETTY_REALM = "i2prouter";
@@ -985,30 +990,51 @@ public class RouterConsoleRunner implements RouterApp {
ConsolePasswordManager mgr = new ConsolePasswordManager(ctx);
boolean enable = ctx.getBooleanProperty(PROP_PW_ENABLE);
if (enable) {
// The only schemes Jetty supports out of the box are straight password,
// hashed password, and the original UnixCrypt DES format (descrypt),
// which is so bad as to be no better than hashes. See man crypt(5).
// We should supply our own by extending Credential and use the
// BCrypt implemenation in i2pcontrol.
// Any scheme must be compatible with what browsers support, see
// RFC 7616. We can extend Jetty DigestAuthenticator to switch to
// SHA-256. FF supports SHA-256 as of FF93 (2021)
// Chrome supports it as of Chrome 117 (2023)
// See chart at bottom of https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/WWW-Authenticate
// But Jetty still claiming there's no support in 2024
// and refusing to implement it: https://github.com/jetty/jetty.project/issues/11489
// Still NOT supported by Safari / IOS.
Map<String, String> userpw = mgr.getMD5(PROP_CONSOLE_PW);
// todo sha256 only
Map<String, String> userpw2 = mgr.getSHA256(PROP_CONSOLE_PW);
// todo sha256 only
if (userpw.isEmpty()) {
enable = false;
ctx.router().saveConfig(PROP_PW_ENABLE, "false");
} else {
// Prometheus server only supports basic auth
// https://github.com/prometheus/common/issues/352
// Jetty does not support multiple auth at once
// but it's coming for Jetty 12
// https://github.com/jetty/jetty.project/issues/5442
boolean isBasic = context.getContextPath().equals("/prometheus");
// need separate realms so the browser doesn't get them mixed up
String rlm = isBasic ? PROMETHEUS_REALM : JETTY_REALM;
String rlm = JETTY_REALM;
HashLoginService realm = new CustomHashLoginService(rlm, context.getContextPath(),
ctx.logManager().getLog(RouterConsoleRunner.class));
sec.setLoginService(realm);
LoginAuthenticator auth = isBasic ? basicAuthenticator : authenticator;
// don't advertise sha256 unless we have one
// Only passwords stored as of 0.9.67 will have sha256, as set in ConfigUIHandler
LoginAuthenticator auth = userpw2.isEmpty() ? authenticator : multiAuthenticator;
sec.setAuthenticator(auth);
String[] role = new String[] {JETTY_ROLE};
for (Map.Entry<String, String> e : userpw.entrySet()) {
String user = e.getKey();
String pw = e.getValue();
List<Credential> creds = new ArrayList<Credential>(3);
String pw2 = userpw2.get(user);
if (pw2 != null)
creds.add(new SHA256Credential(ctx, pw2));
creds.add(Credential.getCredential(MD5_CREDENTIAL_TYPE + pw));
// for basic, the password will be the md5 hash itself
Credential cred = Credential.getCredential(isBasic ? pw : MD5_CREDENTIAL_TYPE + pw);
// mainly for prometheus plugin, but also for testing
// or for any other HTTP clients not supporting digest
creds.add(new Password(pw));
Credential cred = new MultiCredential(creds);
realm.putUser(user, cred, role);
Constraint constraint = new Constraint(user, JETTY_ROLE);
constraint.setAuthenticate(true);

View File

@@ -111,8 +111,8 @@ public class ConfigUIHandler extends FormHandler {
return;
}
ConsolePasswordManager mgr = new ConsolePasswordManager(_context);
// rfc 2617
if (mgr.saveMD5(RouterConsoleRunner.PROP_CONSOLE_PW, RouterConsoleRunner.JETTY_REALM, name, pw)) {
// rfc 2617 AND rfc 7617
if (mgr.saveMD5SHA256(RouterConsoleRunner.PROP_CONSOLE_PW, RouterConsoleRunner.JETTY_REALM, name, pw)) {
if (!_context.getBooleanProperty(RouterConsoleRunner.PROP_PW_ENABLE))
_context.router().saveConfig(RouterConsoleRunner.PROP_PW_ENABLE, "true");
addFormNotice(_t("Added user {0}", name));