/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* http://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.glassfish.jersey.jdk.connector;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Generates a value of {@code Authorization} header of HTTP request for Digest Http Authentication scheme (RFC 2617).
*
* @author raphael.jolivet@gmail.com
* @author Stefan Katerkamp (stefan@katerkamp.de)
* @author Miroslav Fuksa (miroslav.fuksa at oracle.com)
* @author Ondrej Kosatka (ondrej.kosatka at oracle.com)
*/
class ProxyDigestAuthenticator {
/**
* Encoding used for authentication calculations.
*/
private static final Charset CHARACTER_SET = Charset.forName("iso-8859-1");
private static final Logger logger = Logger.getLogger(ProxyDigestAuthenticator.class.getName());
private static final char[] HEX_ARRAY =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private static final Pattern KEY_VALUE_PAIR_PATTERN =
Pattern.compile("(\\w+)\\s*=\\s*(\"([^\"]+)\"|(\\w+))\\s*,?\\s*");
private static final int CLIENT_NONCE_BYTE_COUNT = 4;
private SecureRandom randomGenerator;
ProxyDigestAuthenticator() {
try {
randomGenerator = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
logger.config("No such algorithm to generate authorization digest http header." + e);
}
}
String generateAuthorizationHeader(URI uri, String method, String authenticateHeader, String userName, String password)
throws ProxyAuthenticationException {
if (userName == null) {
throw new ProxyAuthenticationException(LocalizationMessages.PROXY_USER_NAME_MISSING());
}
if (password == null) {
throw new ProxyAuthenticationException(LocalizationMessages.PROXY_PASSWORD_MISSING());
}
DigestScheme digestScheme;
try {
digestScheme = parseAuthHeaders(authenticateHeader);
} catch (IOException e) {
throw new ProxyAuthenticationException(e.getMessage());
}
if (digestScheme == null) {
throw new ProxyAuthenticationException(LocalizationMessages.PROXY_FAIL_AUTH_HEADER());
}
return createNextAuthToken(digestScheme, uri.toString(), method, userName, password);
}
/**
* Parse digest header.
*
* @param authHeader value of {@code WWW-Authenticate} header
* @return DigestScheme or {@code null} if no digest header exists.
*/
private DigestScheme parseAuthHeaders(final String authHeader) throws IOException {
if (authHeader == null) {
return null;
}
String[] parts = authHeader.trim().split("\\s+", 2);
if (parts.length != 2) {
return null;
}
if (!parts[0].toLowerCase().equals("digest")) {
return null;
}
String realm = null;
String nonce = null;
String opaque = null;
QOP qop = QOP.UNSPECIFIED;
Algorithm algorithm = Algorithm.UNSPECIFIED;
boolean stale = false;
Matcher match = KEY_VALUE_PAIR_PATTERN.matcher(parts[1]);
while (match.find()) {
// expect 4 groups (key)=("(val)" | (val))
int nbGroups = match.groupCount();
if (nbGroups != 4) {
continue;
}
String key = match.group(1);
String valNoQuotes = match.group(3);
String valQuotes = match.group(4);
String val = (valNoQuotes == null) ? valQuotes : valNoQuotes;
if (key.equals("qop")) {
qop = QOP.parse(val);
} else if (key.equals("realm")) {
realm = val;
} else if (key.equals("nonce")) {
nonce = val;
} else if (key.equals("opaque")) {
opaque = val;
} else if (key.equals("stale")) {
stale = Boolean.parseBoolean(val);
} else if (key.equals("algorithm")) {
algorithm = Algorithm.parse(val);
}
}
return new DigestScheme(realm, nonce, opaque, qop, algorithm, stale);
}
/**
* Creates digest string including counter.
*
* @param ds DigestScheme instance
* @param uri client request uri
* @return digest authentication token string
* @throws ProxyAuthenticationException if MD5 hash fails
*/
private String createNextAuthToken(final DigestScheme ds, String uri, String method, String userName, String password) throws
ProxyAuthenticationException {
StringBuilder sb = new StringBuilder(100);
sb.append("Digest ");
append(sb, "username", userName);
append(sb, "realm", ds.getRealm());
append(sb, "nonce", ds.getNonce());
append(sb, "opaque", ds.getOpaque());
append(sb, "algorithm", ds.getAlgorithm().toString(), false);
append(sb, "qop", ds.getQop().toString(), false);
append(sb, "uri", uri);
String ha1;
if (ds.getAlgorithm().equals(Algorithm.MD5_SESS)) {
ha1 = md5(md5(userName, ds.getRealm(), password));
} else {
ha1 = md5(userName, ds.getRealm(), password);
}
String ha2 = md5(method, uri);
String response;
if (ds.getQop().equals(QOP.UNSPECIFIED)) {
response = md5(ha1, ds.getNonce(), ha2);
} else {
String cnonce = randomBytes(CLIENT_NONCE_BYTE_COUNT); // client nonce
append(sb, "cnonce", cnonce);
String nc = String.format("%08x", ds.incrementCounter()); // counter
append(sb, "nc", nc, false);
response = md5(ha1, ds.getNonce(), nc, cnonce, ds.getQop().toString(), ha2);
}
append(sb, "response", response);
return sb.toString();
}
/**
* Append comma separated key=value token
*
* @param sb string builder instance
* @param key key string
* @param value value string
* @param useQuote true if value needs to be enclosed in quotes
*/
private static void append(StringBuilder sb, String key, String value, boolean useQuote) {
if (value == null) {
return;
}
if (sb.length() > 0) {
if (sb.charAt(sb.length() - 1) != ' ') {
sb.append(", ");
}
}
sb.append(key);
sb.append('=');
if (useQuote) {
sb.append('"');
}
sb.append(value);
if (useQuote) {
sb.append('"');
}
}
/**
* Append comma separated key=value token. The value gets enclosed in quotes.
*
* @param sb string builder instance
* @param key key string
* @param value value string
*/
private static void append(StringBuilder sb, String key, String value) {
append(sb, key, value, true);
}
/**
* Convert bytes array to hex string.
*
* @param bytes array of bytes
* @return hex string
*/
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
int v;
for (int j = 0; j < bytes.length; j++) {
v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
/**
* Colon separated value MD5 hash.
*
* @param tokens one or more strings
* @return M5 hash string
* @throws ProxyAuthenticationException if MD5 algorithm cannot be instantiated
*/
private static String md5(String... tokens) throws ProxyAuthenticationException {
StringBuilder sb = new StringBuilder(100);
for (String token : tokens) {
if (sb.length() > 0) {
sb.append(':');
}
sb.append(token);
}
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException ex) {
throw new ProxyAuthenticationException(ex.getMessage());
}
md.update(sb.toString().getBytes(CHARACTER_SET), 0, sb.length());
byte[] md5hash = md.digest();
return bytesToHex(md5hash);
}
/**
* Generate a random sequence of bytes and return its hex representation
*
* @param nbBytes number of bytes to generate
* @return hex string
*/
private String randomBytes(int nbBytes) {
byte[] bytes = new byte[nbBytes];
randomGenerator.nextBytes(bytes);
return bytesToHex(bytes);
}
private enum QOP {
UNSPECIFIED(null),
AUTH("auth");
private final String qop;
QOP(String qop) {
this.qop = qop;
}
@Override
public String toString() {
return qop;
}
public static QOP parse(String val) {
if (val == null || val.isEmpty()) {
return QOP.UNSPECIFIED;
}
if (val.contains("auth")) {
return QOP.AUTH;
}
throw new UnsupportedOperationException(LocalizationMessages.PROXY_QOP_NO_SUPPORTED(val));
}
}
enum Algorithm {
UNSPECIFIED(null),
MD5("MD5"),
MD5_SESS("MD5-sess");
private final String md;
Algorithm(String md) {
this.md = md;
}
@Override
public String toString() {
return md;
}
public static Algorithm parse(String val) {
if (val == null || val.isEmpty()) {
return Algorithm.UNSPECIFIED;
}
val = val.trim();
if (val.contains(MD5_SESS.md) || val.contains(MD5_SESS.md.toLowerCase())) {
return MD5_SESS;
}
return MD5;
}
}
/**
* Digest scheme POJO
*/
final class DigestScheme {
private final String realm;
private final String nonce;
private final String opaque;
private final Algorithm algorithm;
private final QOP qop;
private final boolean stale;
private volatile int nc;
DigestScheme(String realm,
String nonce,
String opaque,
QOP qop,
Algorithm algorithm,
boolean stale) {
this.realm = realm;
this.nonce = nonce;
this.opaque = opaque;
this.qop = qop;
this.algorithm = algorithm;
this.stale = stale;
this.nc = 0;
}
public int incrementCounter() {
return ++nc;
}
public String getNonce() {
return nonce;
}
public String getRealm() {
return realm;
}
public String getOpaque() {
return opaque;
}
public Algorithm getAlgorithm() {
return algorithm;
}
public QOP getQop() {
return qop;
}
public boolean isStale() {
return stale;
}
public int getNc() {
return nc;
}
}
}