forked from I2P_Developers/i2p.i2p
Compare commits
1 Commits
jetty12
...
jetty-mult
Author | SHA1 | Date | |
---|---|---|---|
![]() |
983c8d841f |
539
apps/jetty/java/src/net/i2p/jetty/MultiAuthenticator.java
Normal file
539
apps/jetty/java/src/net/i2p/jetty/MultiAuthenticator.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
58
apps/jetty/java/src/net/i2p/jetty/MultiCredential.java
Normal file
58
apps/jetty/java/src/net/i2p/jetty/MultiCredential.java
Normal 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();
|
||||
}
|
||||
}
|
100
apps/jetty/java/src/net/i2p/jetty/SHA256Credential.java
Normal file
100
apps/jetty/java/src/net/i2p/jetty/SHA256Credential.java
Normal 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);
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
@@ -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);
|
||||
|
@@ -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));
|
||||
|
Reference in New Issue
Block a user