/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eclipse.webdav.internal.authentication;
import java.net.*;
import java.security.SecureRandom;
import java.util.*;
import org.eclipse.webdav.IResponse;
import org.eclipse.webdav.client.Policy;
import org.eclipse.webdav.http.client.*;
import org.eclipse.webdav.internal.kernel.utils.Assert;
/**
* The <code>DigestAuthority</code> provides the necessary behavior to
* authorizes client <code>Request</codes>s for communication with HTTP
* servers using the Digest authentication scheme.
*
* @see AuthorizationAuthority
*/
public class DigestAuthority extends AuthorizationAuthority {
/**
* Creates a new authenticator that stores its authentication information
* in the given authentication store.
* <p>The <code>DigestAuthenticator</code> authenticates according to the
* "Digest" authentication scheme.
* <p>Instances of this class must not be created directly, instead, use
* an instance of the class <code>Authenticator</code> to authorize
* requests.
*
* @param authenticatorStore a store that holds authentication
* information
*/
public DigestAuthority(IAuthenticator authenticatorStore) {
super(authenticatorStore);
}
/**
* @see Authenticator#confirmResponse(Request, Response, URL)
*/
protected boolean confirmResponse(Request request, IResponse response, URL proxyServerUrl) {
Assert.isNotNull(request);
Assert.isNotNull(response);
String authInfoString = response.getContext().get("Authentication-Info"); //$NON-NLS-1$
if (authInfoString == null) {
return false;
}
AuthenticationInfo authInfo = null;
try {
authInfo = new AuthenticationInfo(authInfoString);
} catch (ParserException e) {
return false;
}
String nextNonce = authInfo.getNextNonce();
String messageQop = authInfo.getMessageQop();
String responseAuth = authInfo.getResponseAuth();
String cnonce = authInfo.getCNonce();
String nonceCount = authInfo.getNonceCount();
URL resourceUrl = request.getResourceUrl();
URL serverUrl = null;
try {
serverUrl = new URL(resourceUrl.getProtocol(), resourceUrl.getHost(), resourceUrl.getPort(), "/"); //$NON-NLS-1$
} catch (MalformedURLException e) {
return false;
}
String realm = authenticatorStore.getProtectionSpace(resourceUrl);
if (realm == null) {
return false;
}
Map info = authenticatorStore.getAuthenticationInfo(serverUrl, realm, "Digest"); //$NON-NLS-1$
if (info == null) {
return false;
}
String username = (String) info.get("username"); //$NON-NLS-1$
String password = (String) info.get("password"); //$NON-NLS-1$
String algorithm = (String) info.get("algorithm"); //$NON-NLS-1$
String nonce = (String) info.get("nonce"); //$NON-NLS-1$
String iNonceCount = (String) info.get("nc"); //$NON-NLS-1$
String iCnonce = (String) info.get("cnonce"); //$NON-NLS-1$
if (username == null || password == null || nonce == null) {
return false;
}
if (cnonce != null && !cnonce.equals(iCnonce)) {
return false;
}
if (nonceCount != null && !nonceCount.equals(iNonceCount)) {
return false;
}
if (responseAuth != null) {
try {
String digestUri = resourceUrl.toString();
if (proxyServerUrl == null) {
digestUri = resourceUrl.getFile();
String ref = resourceUrl.getRef();
if (ref != null) {
digestUri += "#" + ref; //$NON-NLS-1$
}
}
String iResponseAuth = response(request, realm, username, password, algorithm, messageQop, nonce, nonceCount, cnonce, request.getMethod(), digestUri);
if (!responseAuth.equals(iResponseAuth)) {
return false;
}
} catch (Exception e) {
return false;
}
}
info.put("nonce", nextNonce); //$NON-NLS-1$
return true;
}
/**
* Returns the Digest authorization credentials for the given directives.
* The credentials have the following form:
* <code>
* credentials = "Digest" digest-response
* digest-response = 1#( username | realm | nonce | digest-uri
* | response | [ algorithm ] | [cnonce] |
* [opaque] | [message-qop] |
* [nonce-count] | [auth-param] )
* username = "username" "=" username-value
* username-value = quoted-string
* realm = "realm" "=" realm-value
* realm-value = quoted-string
* nonce = "nonce" "=" nonce-value
* nonce-value = quoted-string
* digest-uri = "uri" "=" digest-uri-value
* digest-uri-value = request-uri ; As specified by HTTP/1.1
* response = "response" "=" request-digest
* request-digest = <"> 32LHEX <">
* LHEX = "0" | "1" | "2" | "3" |
* "4" | "5" | "6" | "7" |
* "8" | "9" | "a" | "b" |
* "c" | "d" | "e" | "f"
* algorithm = "algorithm" "=" ("MD5" | "MD5-sess" | token)
* cnonce = "cnonce" "=" cnonce-value
* cnonce-value = nonce-value
* opaque = "opaque" "=" quoted-string
* message-qop = "qop" "=" qop-value
* nonce-count = "nc" "=" nc-value
* nc-value = 8LHEX
* </code>
* <P>If the "qop" value is "auth" or "auth-int":
* <code>
* request-digest = <"> < KD ( H(A1), unq(nonce-value)
* ":" nc-value
* ":" unq(cnonce-value)
* ":" unq(qop-value)
* ":" H(A2)
* ) <">
* KD(secret, data) = H(concat(secret, ":", data))
* H(data) = MD5(data)
* unq(data) = unqouted(data)
* </code>
* <P>If the "qop" directive is not present:
* <code>
* request-digest = <"> < KD ( H(A1),
* unq(nonce-value)
* ":" H(A2)
* ) <">
* </code>
* <P>If the "algorithm" directive's value is "MD5" or is unspecified,
* then A1 is:
* <code>
* A1 = unq(username-value) ":" unq(realm-value) ":" passwd
* passwd = < user's password >
* </code>
* <P>If the "algorithm" directive's value is "MD5-sess", then A1 is:
* <code>
* A1 = H( unq(username-value) ":" unq(realm-value)
* ":" passwd )
* ":" unq(nonce-value) ":" unq(cnonce-value)
* </code>
* <P>If the "qop" directive's value is "auth" or is unspecified, then
* A2 is:
* <code>
* A2 = Method ":" digest-uri-value
* </code>
* <P>If the "qop" value is "auth-int", then A2 is:
* <code>
* A2 = Method ":" digest-uri-value ":" H(entity-body)
* </code>
* @param request
* @param realm
* @param username
* @param password
* @param algorithm
* @param messageQop
* @param nonce
* @param nonceCount
* @param opaque
* @param cnonce
* @param method
* @param digestUri
* @return the Digest authorization credentials for the given
* directives
*/
private String credentials(Request request, String realm, String username, String password, String algorithm, String messageQop, String nonce, String nonceCount, String opaque, String cnonce, String method, String digestUri) throws Exception {
Assert.isNotNull(request);
Assert.isNotNull(realm);
Assert.isNotNull(username);
Assert.isNotNull(password);
Assert.isNotNull(nonce);
Assert.isNotNull(method);
Assert.isNotNull(digestUri);
StringBuffer buf = new StringBuffer();
buf.append("Digest username=\""); //$NON-NLS-1$
buf.append(username);
buf.append("\""); //$NON-NLS-1$
buf.append(", realm="); //$NON-NLS-1$
buf.append(realm);
if (messageQop != null) {
buf.append(", qop=\""); //$NON-NLS-1$
buf.append(messageQop);
buf.append("\""); //$NON-NLS-1$
}
if (algorithm != null) {
buf.append(", algorithm="); //$NON-NLS-1$
buf.append(algorithm);
}
buf.append(", uri=\""); //$NON-NLS-1$
buf.append(digestUri);
buf.append("\""); //$NON-NLS-1$
buf.append(", nonce="); //$NON-NLS-1$
buf.append(nonce);
if (nonceCount != null) {
buf.append(", nc="); //$NON-NLS-1$
buf.append(nonceCount);
}
if (cnonce != null) {
buf.append(", cnonce=\""); //$NON-NLS-1$
buf.append(cnonce);
buf.append("\""); //$NON-NLS-1$
}
if (opaque != null) {
buf.append(", opaque="); //$NON-NLS-1$
buf.append(opaque);
}
String response = response(request, realm, username, password, algorithm, messageQop, nonce, nonceCount, cnonce, method, digestUri);
if (response == null) {
return null;
}
buf.append(", response=\""); //$NON-NLS-1$
buf.append(response);
buf.append("\""); //$NON-NLS-1$
return buf.toString();
}
/**
* @see Authenticator#getAuthenticationInfo(AuthenticateChallenge, Map, URL, URL)
*/
protected Map getAuthenticationInfo(AuthenticateChallenge challenge, Map oldInfo, URL serverUrl, URL protectionSpaceUrl) {
Assert.isNotNull(challenge);
Assert.isNotNull(serverUrl);
Assert.isNotNull(protectionSpaceUrl);
Hashtable info = new Hashtable(5);
String stale = challenge.get("stale"); //$NON-NLS-1$
if (oldInfo == null || stale == null || !Boolean.valueOf(stale).booleanValue()) {
Map userpass = authenticatorStore.requestAuthenticationInfo(protectionSpaceUrl, challenge.getRealm(), challenge.getAuthScheme());
if (userpass == null) {
return null;
}
info.put("username", userpass.get("username")); //$NON-NLS-1$ //$NON-NLS-2$
info.put("password", userpass.get("password")); //$NON-NLS-1$ //$NON-NLS-2$
} else {
info.put("username", oldInfo.get("username")); //$NON-NLS-1$ //$NON-NLS-2$
info.put("password", oldInfo.get("password")); //$NON-NLS-1$ //$NON-NLS-2$
}
String realm = challenge.getRealm();
String domain = challenge.get("domain"); //$NON-NLS-1$
boolean addRoot = true;
if (domain != null && domain.charAt(0) == '"' //$NON-NLS-1$
&& domain.charAt(domain.length() - 1) == '"') { //$NON-NLS-1$
int start = 1;
boolean inSpace = false;
for (int i = 1; i < domain.length(); ++i) {
if (Character.isWhitespace(domain.charAt(i)) || i == domain.length() - 1) {
if (!inSpace) {
inSpace = true;
String urlString = domain.substring(start, i);
URL url = null;
try {
url = new URL(urlString);
} catch (MalformedURLException e1) {
try {
url = new URL(serverUrl, urlString);
} catch (MalformedURLException e2) {
// ignore or log?
}
}
if (url != null) {
authenticatorStore.addProtectionSpace(url, realm);
addRoot = false;
}
}
} else {
if (inSpace) {
inSpace = false;
start = i;
}
}
}
}
if (addRoot) {
authenticatorStore.addProtectionSpace(serverUrl, realm);
}
String nonce = challenge.get("nonce"); //$NON-NLS-1$
if (nonce == null) {
return null;
}
info.put("nonce", nonce); //$NON-NLS-1$
String opaque = challenge.get("opaque"); //$NON-NLS-1$
if (opaque != null) {
info.put("opaque", opaque); //$NON-NLS-1$
}
String algorithm = challenge.get("algorithm"); //$NON-NLS-1$
if (algorithm != null) {
info.put("algorithm", algorithm); //$NON-NLS-1$
}
String qop = challenge.get("qop"); //$NON-NLS-1$
if (qop != null && qop.charAt(0) == '"' //$NON-NLS-1$
&& qop.charAt(qop.length() - 1) == '"') { //$NON-NLS-1$
boolean foundAuth = false;
boolean foundAuthInt = false;
try {
String token = null;
boolean first = true;
Parser parser = new Parser(qop.substring(1, qop.length() - 1));
while (parser.pos < parser.s.length()) {
if (first) {
parser.skipWhiteSpace();
first = false;
} else {
parser.match(',');
parser.skipWhiteSpace();
}
token = parser.nextToken();
if (token.equalsIgnoreCase("auth")) { //$NON-NLS-1$
foundAuth = true;
} else if (token.equalsIgnoreCase("auth-int")) { //$NON-NLS-1$
foundAuthInt = true;
}
parser.skipWhiteSpace();
}
} catch (ParserException e) {
// ignore or log?
}
if (foundAuthInt) {
info.put("qop", "auth-int"); //$NON-NLS-1$ //$NON-NLS-2$
} else if (foundAuth) {
info.put("qop", "auth"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
return info;
}
/**
* @see Authenticator#getAuthorization(Request, Map, URL, URL, URL)
*/
protected String getAuthorization(Request request, Map info, URL serverUrl, URL protectionSpaceUrl, URL proxyServerUrl) {
Assert.isNotNull(request);
Assert.isNotNull(info);
Assert.isNotNull(serverUrl);
Assert.isNotNull(protectionSpaceUrl);
String username = (String) info.get("username"); //$NON-NLS-1$
String password = (String) info.get("password"); //$NON-NLS-1$
String algorithm = (String) info.get("algorithm"); //$NON-NLS-1$
String messageQop = (String) info.get("qop"); //$NON-NLS-1$
String nonce = (String) info.get("nonce"); //$NON-NLS-1$
String nonceCount = (String) info.get("nc"); //$NON-NLS-1$
String opaque = (String) info.get("opaque"); //$NON-NLS-1$
String cnonce = null;
if (username == null || password == null || nonce == null) {
return null;
}
if (messageQop != null) {
if (nonceCount == null) {
nonceCount = "00000001"; //$NON-NLS-1$
} else {
int nc = Integer.parseInt(nonceCount, 16) + 1;
nonceCount = HexConverter.toHex(new int[] {nc});
}
info.put("nc", nonceCount); //$NON-NLS-1$
long milliseconds = new Date().getTime();
SecureRandom random = new SecureRandom();
random.setSeed(milliseconds);
byte[] bytes = new byte[16];
random.nextBytes(bytes);
cnonce = HexConverter.toHex(bytes);
info.put("cnonce", cnonce); //$NON-NLS-1$
}
String realm = authenticatorStore.getProtectionSpace(protectionSpaceUrl);
if (realm == null) {
return null;
}
String method = request.getMethod();
URL resourceUrl = request.getResourceUrl();
String digestUri = resourceUrl.toString();
if (proxyServerUrl != null) {
digestUri = resourceUrl.getFile();
String ref = resourceUrl.getRef();
if (ref != null) {
digestUri += "#" + ref; //$NON-NLS-1$
}
}
try {
return credentials(request, realm, username, password, algorithm, messageQop, nonce, nonceCount, opaque, cnonce, method, digestUri);
} catch (Exception e) {
return null;
}
}
private String ha1(String realm, String username, String password, String algorithm, String nonce, String cnonce) throws Exception {
Assert.isNotNull(realm);
Assert.isNotNull(username);
Assert.isNotNull(password);
Assert.isNotNull(nonce);
String ha1 = md5(unquote(username) + ":" + unquote(realm) + ":" + password); //$NON-NLS-1$ //$NON-NLS-2$
if (algorithm != null && !algorithm.equalsIgnoreCase("MD5")) { //$NON-NLS-1$
if (algorithm.equalsIgnoreCase("MD5-sess")) { //$NON-NLS-1$
if (cnonce == null) {
return null;
}
ha1 = md5(ha1 + ":" + unquote(nonce) + ":" + unquote(cnonce)); //$NON-NLS-1$ //$NON-NLS-2$
} else {
throw new Exception(Policy.bind("exception.unregognizedAlgo", algorithm)); //$NON-NLS-1$
}
}
return ha1;
}
private String ha2(Request request, String qop, String method, String digestUri) throws Exception {
Assert.isNotNull(request);
Assert.isNotNull(method);
Assert.isNotNull(digestUri);
String a2 = null;
if (qop == null || qop.equalsIgnoreCase("auth")) { //$NON-NLS-1$
a2 = md5((method == null ? "" : method) + ":" + digestUri); //$NON-NLS-1$ //$NON-NLS-2$
} else if (qop.equalsIgnoreCase("auth-int")) { //$NON-NLS-1$
a2 = md5((method == null ? "" : method) + ":" + digestUri + md5(request)); //$NON-NLS-1$ //$NON-NLS-2$
} else {
throw new Exception(Policy.bind("exception.unregognizedQop", qop)); //$NON-NLS-1$
}
return a2;
}
private String response(Request request, String realm, String username, String password, String algorithm, String qop, String nonce, String nonceCount, String cnonce, String method, String digestUri) throws Exception {
Assert.isNotNull(request);
Assert.isNotNull(realm);
Assert.isNotNull(username);
Assert.isNotNull(password);
Assert.isNotNull(nonce);
Assert.isNotNull(method);
Assert.isNotNull(digestUri);
String ha1 = ha1(realm, username, password, algorithm, nonce, cnonce);
if (ha1 == null) {
return null;
}
String ha2 = ha2(request, qop, method, digestUri);
if (ha2 == null) {
return null;
}
if (qop == null) {
return md5(ha1 + ":" + unquote(nonce) + ":" + ha2); //$NON-NLS-1$ //$NON-NLS-2$
}
if (nonceCount == null || cnonce == null || qop == null) {
return null;
}
return md5(ha1 + ":" //$NON-NLS-1$
+ unquote(nonce) + ":" //$NON-NLS-1$
+ nonceCount + ":" //$NON-NLS-1$
+ unquote(cnonce) + ":" //$NON-NLS-1$
+ unquote(qop) + ":" //$NON-NLS-1$
+ ha2);
}
}