725 lines
28 KiB
Java
725 lines
28 KiB
Java
/*
|
|
* Copyright (c) 2003, 2012, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation. Oracle designates this
|
|
* particular file as subject to the "Classpath" exception as provided
|
|
* by Oracle in the LICENSE file that accompanied this code.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
package com.sun.security.sasl.digest;
|
|
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.util.StringTokenizer;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Arrays;
|
|
|
|
import java.util.logging.Level;
|
|
|
|
import javax.security.sasl.*;
|
|
import javax.security.auth.callback.*;
|
|
|
|
/**
|
|
* An implementation of the DIGEST-MD5 server SASL mechanism.
|
|
* (<a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>)
|
|
* <p>
|
|
* The DIGEST-MD5 SASL mechanism specifies two modes of authentication.
|
|
* <ul><li>Initial Authentication
|
|
* <li>Subsequent Authentication - optional, (currently not supported)
|
|
* </ul>
|
|
*
|
|
* Required callbacks:
|
|
* - RealmCallback
|
|
* used as key by handler to fetch password
|
|
* - NameCallback
|
|
* used as key by handler to fetch password
|
|
* - PasswordCallback
|
|
* handler must enter password for username/realm supplied
|
|
* - AuthorizeCallback
|
|
* handler must verify that authid/authzids are allowed and set
|
|
* authorized ID to be the canonicalized authzid (if applicable).
|
|
*
|
|
* Environment properties that affect the implementation:
|
|
* javax.security.sasl.qop:
|
|
* specifies list of qops; default is "auth"; typically, caller should set
|
|
* this to "auth, auth-int, auth-conf".
|
|
* javax.security.sasl.strength
|
|
* specifies low/medium/high strength of encryption; default is all available
|
|
* ciphers [high,medium,low]; high means des3 or rc4 (128); medium des or
|
|
* rc4-56; low is rc4-40.
|
|
* javax.security.sasl.maxbuf
|
|
* specifies max receive buf size; default is 65536
|
|
* javax.security.sasl.sendmaxbuffer
|
|
* specifies max send buf size; default is 65536 (min of this and client's max
|
|
* recv size)
|
|
*
|
|
* com.sun.security.sasl.digest.utf8:
|
|
* "true" means to use UTF-8 charset; "false" to use ISO-8859-1 encoding;
|
|
* default is "true".
|
|
* com.sun.security.sasl.digest.realm:
|
|
* space-separated list of realms; default is server name (fqdn parameter)
|
|
*
|
|
* @author Rosanna Lee
|
|
*/
|
|
|
|
final class DigestMD5Server extends DigestMD5Base implements SaslServer {
|
|
private static final String MY_CLASS_NAME = DigestMD5Server.class.getName();
|
|
|
|
private static final String UTF8_DIRECTIVE = "charset=utf-8,";
|
|
private static final String ALGORITHM_DIRECTIVE = "algorithm=md5-sess";
|
|
|
|
/*
|
|
* Always expect nonce count value to be 1 because we support only
|
|
* initial authentication.
|
|
*/
|
|
private static final int NONCE_COUNT_VALUE = 1;
|
|
|
|
/* "true" means use UTF8; "false" ISO 8859-1; default is "true" */
|
|
private static final String UTF8_PROPERTY =
|
|
"com.sun.security.sasl.digest.utf8";
|
|
|
|
/* List of space-separated realms used for authentication */
|
|
private static final String REALM_PROPERTY =
|
|
"com.sun.security.sasl.digest.realm";
|
|
|
|
/* Directives encountered in responses sent by the client. */
|
|
private static final String[] DIRECTIVE_KEY = {
|
|
"username", // exactly once
|
|
"realm", // exactly once if sent by server
|
|
"nonce", // exactly once
|
|
"cnonce", // exactly once
|
|
"nonce-count", // atmost once; default is 00000001
|
|
"qop", // atmost once; default is "auth"
|
|
"digest-uri", // atmost once; (default?)
|
|
"response", // exactly once
|
|
"maxbuf", // atmost once; default is 65536
|
|
"charset", // atmost once; default is ISO-8859-1
|
|
"cipher", // exactly once if qop is "auth-conf"
|
|
"authzid", // atmost once; default is none
|
|
"auth-param", // >= 0 times (ignored)
|
|
};
|
|
|
|
/* Indices into DIRECTIVE_KEY */
|
|
private static final int USERNAME = 0;
|
|
private static final int REALM = 1;
|
|
private static final int NONCE = 2;
|
|
private static final int CNONCE = 3;
|
|
private static final int NONCE_COUNT = 4;
|
|
private static final int QOP = 5;
|
|
private static final int DIGEST_URI = 6;
|
|
private static final int RESPONSE = 7;
|
|
private static final int MAXBUF = 8;
|
|
private static final int CHARSET = 9;
|
|
private static final int CIPHER = 10;
|
|
private static final int AUTHZID = 11;
|
|
private static final int AUTH_PARAM = 12;
|
|
|
|
/* Server-generated/supplied information */
|
|
private String specifiedQops;
|
|
private byte[] myCiphers;
|
|
private List<String> serverRealms;
|
|
|
|
DigestMD5Server(String protocol, String serverName, Map<String, ?> props,
|
|
CallbackHandler cbh) throws SaslException {
|
|
super(props, MY_CLASS_NAME, 1,
|
|
protocol + "/" + (serverName==null?"*":serverName),
|
|
cbh);
|
|
|
|
serverRealms = new ArrayList<String>();
|
|
|
|
useUTF8 = true; // default
|
|
|
|
if (props != null) {
|
|
specifiedQops = (String) props.get(Sasl.QOP);
|
|
if ("false".equals((String) props.get(UTF8_PROPERTY))) {
|
|
useUTF8 = false;
|
|
logger.log(Level.FINE, "DIGEST80:Server supports ISO-Latin-1");
|
|
}
|
|
|
|
String realms = (String) props.get(REALM_PROPERTY);
|
|
if (realms != null) {
|
|
StringTokenizer parser = new StringTokenizer(realms, ", \t\n");
|
|
int tokenCount = parser.countTokens();
|
|
String token = null;
|
|
for (int i = 0; i < tokenCount; i++) {
|
|
token = parser.nextToken();
|
|
logger.log(Level.FINE, "DIGEST81:Server supports realm {0}",
|
|
token);
|
|
serverRealms.add(token);
|
|
}
|
|
}
|
|
}
|
|
|
|
encoding = (useUTF8 ? "UTF8" : "8859_1");
|
|
|
|
// By default, use server name as realm
|
|
if (serverRealms.isEmpty()) {
|
|
if (serverName == null) {
|
|
throw new SaslException(
|
|
"A realm must be provided in props or serverName");
|
|
} else {
|
|
serverRealms.add(serverName);
|
|
}
|
|
}
|
|
}
|
|
|
|
public byte[] evaluateResponse(byte[] response) throws SaslException {
|
|
if (response.length > MAX_RESPONSE_LENGTH) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: Invalid digest response length. Got: " +
|
|
response.length + " Expected < " + MAX_RESPONSE_LENGTH);
|
|
}
|
|
|
|
byte[] challenge;
|
|
switch (step) {
|
|
case 1:
|
|
if (response.length != 0) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5 must not have an initial response");
|
|
}
|
|
|
|
/* Generate first challenge */
|
|
String supportedCiphers = null;
|
|
if ((allQop&PRIVACY_PROTECTION) != 0) {
|
|
myCiphers = getPlatformCiphers();
|
|
StringBuffer buf = new StringBuffer();
|
|
|
|
// myCipher[i] is a byte that indicates whether CIPHER_TOKENS[i]
|
|
// is supported
|
|
for (int i = 0; i < CIPHER_TOKENS.length; i++) {
|
|
if (myCiphers[i] != 0) {
|
|
if (buf.length() > 0) {
|
|
buf.append(',');
|
|
}
|
|
buf.append(CIPHER_TOKENS[i]);
|
|
}
|
|
}
|
|
supportedCiphers = buf.toString();
|
|
}
|
|
|
|
try {
|
|
challenge = generateChallenge(serverRealms, specifiedQops,
|
|
supportedCiphers);
|
|
|
|
step = 3;
|
|
return challenge;
|
|
} catch (UnsupportedEncodingException e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: Error encoding challenge", e);
|
|
} catch (IOException e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: Error generating challenge", e);
|
|
}
|
|
|
|
// Step 2 is performed by client
|
|
|
|
case 3:
|
|
/* Validates client's response and generate challenge:
|
|
* response-auth = "rspauth" "=" response-value
|
|
*/
|
|
try {
|
|
byte[][] responseVal = parseDirectives(response, DIRECTIVE_KEY,
|
|
null, REALM);
|
|
challenge = validateClientResponse(responseVal);
|
|
} catch (SaslException e) {
|
|
throw e;
|
|
} catch (UnsupportedEncodingException e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: Error validating client response", e);
|
|
} finally {
|
|
step = 0; // Set to invalid state
|
|
}
|
|
|
|
completed = true;
|
|
|
|
/* Initialize SecurityCtx implementation */
|
|
if (integrity && privacy) {
|
|
secCtx = new DigestPrivacy(false /* not client */);
|
|
} else if (integrity) {
|
|
secCtx = new DigestIntegrity(false /* not client */);
|
|
}
|
|
|
|
return challenge;
|
|
|
|
default:
|
|
// No other possible state
|
|
throw new SaslException("DIGEST-MD5: Server at illegal state");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates challenge to be sent to client.
|
|
* digest-challenge =
|
|
* 1#( realm | nonce | qop-options | stale | maxbuf | charset
|
|
* algorithm | cipher-opts | auth-param )
|
|
*
|
|
* realm = "realm" "=" <"> realm-value <">
|
|
* realm-value = qdstr-val
|
|
* nonce = "nonce" "=" <"> nonce-value <">
|
|
* nonce-value = qdstr-val
|
|
* qop-options = "qop" "=" <"> qop-list <">
|
|
* qop-list = 1#qop-value
|
|
* qop-value = "auth" | "auth-int" | "auth-conf" |
|
|
* token
|
|
* stale = "stale" "=" "true"
|
|
* maxbuf = "maxbuf" "=" maxbuf-value
|
|
* maxbuf-value = 1*DIGIT
|
|
* charset = "charset" "=" "utf-8"
|
|
* algorithm = "algorithm" "=" "md5-sess"
|
|
* cipher-opts = "cipher" "=" <"> 1#cipher-value <">
|
|
* cipher-value = "3des" | "des" | "rc4-40" | "rc4" |
|
|
* "rc4-56" | token
|
|
* auth-param = token "=" ( token | quoted-string )
|
|
*/
|
|
private byte[] generateChallenge(List<String> realms, String qopStr,
|
|
String cipherStr) throws UnsupportedEncodingException, IOException {
|
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
|
|
// Realms (>= 0)
|
|
for (int i = 0; realms != null && i < realms.size(); i++) {
|
|
out.write("realm=\"".getBytes(encoding));
|
|
writeQuotedStringValue(out, realms.get(i).getBytes(encoding));
|
|
out.write('"');
|
|
out.write(',');
|
|
}
|
|
|
|
// Nonce - required (1)
|
|
out.write(("nonce=\"").getBytes(encoding));
|
|
nonce = generateNonce();
|
|
writeQuotedStringValue(out, nonce);
|
|
out.write('"');
|
|
out.write(',');
|
|
|
|
// QOP - optional (1) [default: auth]
|
|
// qop="auth,auth-conf,auth-int"
|
|
if (qopStr != null) {
|
|
out.write(("qop=\"").getBytes(encoding));
|
|
// Check for quotes in case of non-standard qop options
|
|
writeQuotedStringValue(out, qopStr.getBytes(encoding));
|
|
out.write('"');
|
|
out.write(',');
|
|
}
|
|
|
|
// maxbuf - optional (1) [default: 65536]
|
|
if (recvMaxBufSize != DEFAULT_MAXBUF) {
|
|
out.write(("maxbuf=\"" + recvMaxBufSize + "\",").getBytes(encoding));
|
|
}
|
|
|
|
// charset - optional (1) [default: ISO 8859_1]
|
|
if (useUTF8) {
|
|
out.write(UTF8_DIRECTIVE.getBytes(encoding));
|
|
}
|
|
|
|
if (cipherStr != null) {
|
|
out.write("cipher=\"".getBytes(encoding));
|
|
// Check for quotes in case of custom ciphers
|
|
writeQuotedStringValue(out, cipherStr.getBytes(encoding));
|
|
out.write('"');
|
|
out.write(',');
|
|
}
|
|
|
|
// algorithm - required (1)
|
|
out.write(ALGORITHM_DIRECTIVE.getBytes(encoding));
|
|
|
|
return out.toByteArray();
|
|
}
|
|
|
|
/**
|
|
* Validates client's response.
|
|
* digest-response = 1#( username | realm | nonce | cnonce |
|
|
* nonce-count | qop | digest-uri | response |
|
|
* maxbuf | charset | cipher | authzid |
|
|
* auth-param )
|
|
*
|
|
* username = "username" "=" <"> username-value <">
|
|
* username-value = qdstr-val
|
|
* cnonce = "cnonce" "=" <"> cnonce-value <">
|
|
* cnonce-value = qdstr-val
|
|
* nonce-count = "nc" "=" nc-value
|
|
* nc-value = 8LHEX
|
|
* qop = "qop" "=" qop-value
|
|
* digest-uri = "digest-uri" "=" <"> digest-uri-value <">
|
|
* digest-uri-value = serv-type "/" host [ "/" serv-name ]
|
|
* serv-type = 1*ALPHA
|
|
* host = 1*( ALPHA | DIGIT | "-" | "." )
|
|
* serv-name = host
|
|
* response = "response" "=" response-value
|
|
* response-value = 32LHEX
|
|
* LHEX = "0" | "1" | "2" | "3" |
|
|
* "4" | "5" | "6" | "7" |
|
|
* "8" | "9" | "a" | "b" |
|
|
* "c" | "d" | "e" | "f"
|
|
* cipher = "cipher" "=" cipher-value
|
|
* authzid = "authzid" "=" <"> authzid-value <">
|
|
* authzid-value = qdstr-val
|
|
* sets:
|
|
* negotiatedQop
|
|
* negotiatedCipher
|
|
* negotiatedRealm
|
|
* negotiatedStrength
|
|
* digestUri (checked and set to clients to account for case diffs)
|
|
* sendMaxBufSize
|
|
* authzid (gotten from callback)
|
|
* @return response-value ('rspauth') for client to validate
|
|
*/
|
|
private byte[] validateClientResponse(byte[][] responseVal)
|
|
throws SaslException, UnsupportedEncodingException {
|
|
|
|
/* CHARSET: optional atmost once */
|
|
if (responseVal[CHARSET] != null) {
|
|
// The client should send this directive only if the server has
|
|
// indicated it supports UTF-8.
|
|
if (!useUTF8 ||
|
|
!"utf-8".equals(new String(responseVal[CHARSET], encoding))) {
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
"violation. Incompatible charset value: " +
|
|
new String(responseVal[CHARSET]));
|
|
}
|
|
}
|
|
|
|
// maxbuf: atmost once
|
|
int clntMaxBufSize =
|
|
(responseVal[MAXBUF] == null) ? DEFAULT_MAXBUF
|
|
: Integer.parseInt(new String(responseVal[MAXBUF], encoding));
|
|
|
|
// Max send buf size is min of client's max recv buf size and
|
|
// server's max send buf size
|
|
sendMaxBufSize = ((sendMaxBufSize == 0) ? clntMaxBufSize :
|
|
Math.min(sendMaxBufSize, clntMaxBufSize));
|
|
|
|
/* username: exactly once */
|
|
String username;
|
|
if (responseVal[USERNAME] != null) {
|
|
username = new String(responseVal[USERNAME], encoding);
|
|
logger.log(Level.FINE, "DIGEST82:Username: {0}", username);
|
|
} else {
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
"violation. Missing username.");
|
|
}
|
|
|
|
/* realm: exactly once if sent by server */
|
|
negotiatedRealm = ((responseVal[REALM] != null) ?
|
|
new String(responseVal[REALM], encoding) : "");
|
|
logger.log(Level.FINE, "DIGEST83:Client negotiated realm: {0}",
|
|
negotiatedRealm);
|
|
|
|
if (!serverRealms.contains(negotiatedRealm)) {
|
|
// Server had sent at least one realm
|
|
// Check that response is one of these
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
"violation. Nonexistent realm: " + negotiatedRealm);
|
|
}
|
|
// Else, client specified realm was one of server's or server had none
|
|
|
|
/* nonce: exactly once */
|
|
if (responseVal[NONCE] == null) {
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
"violation. Missing nonce.");
|
|
}
|
|
byte[] nonceFromClient = responseVal[NONCE];
|
|
if (!Arrays.equals(nonceFromClient, nonce)) {
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
"violation. Mismatched nonce.");
|
|
}
|
|
|
|
/* cnonce: exactly once */
|
|
if (responseVal[CNONCE] == null) {
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
"violation. Missing cnonce.");
|
|
}
|
|
byte[] cnonce = responseVal[CNONCE];
|
|
|
|
/* nonce-count: atmost once */
|
|
if (responseVal[NONCE_COUNT] != null &&
|
|
NONCE_COUNT_VALUE != Integer.parseInt(
|
|
new String(responseVal[NONCE_COUNT], encoding), 16)) {
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
"violation. Nonce count does not match: " +
|
|
new String(responseVal[NONCE_COUNT]));
|
|
}
|
|
|
|
/* qop: atmost once; default is "auth" */
|
|
negotiatedQop = ((responseVal[QOP] != null) ?
|
|
new String(responseVal[QOP], encoding) : "auth");
|
|
|
|
logger.log(Level.FINE, "DIGEST84:Client negotiated qop: {0}",
|
|
negotiatedQop);
|
|
|
|
// Check that QOP is one sent by server
|
|
byte cQop;
|
|
switch (negotiatedQop) {
|
|
case "auth":
|
|
cQop = NO_PROTECTION;
|
|
break;
|
|
case "auth-int":
|
|
cQop = INTEGRITY_ONLY_PROTECTION;
|
|
integrity = true;
|
|
rawSendSize = sendMaxBufSize - 16;
|
|
break;
|
|
case "auth-conf":
|
|
cQop = PRIVACY_PROTECTION;
|
|
integrity = privacy = true;
|
|
rawSendSize = sendMaxBufSize - 26;
|
|
break;
|
|
default:
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
"violation. Invalid QOP: " + negotiatedQop);
|
|
}
|
|
if ((cQop&allQop) == 0) {
|
|
throw new SaslException("DIGEST-MD5: server does not support " +
|
|
" qop: " + negotiatedQop);
|
|
}
|
|
|
|
if (privacy) {
|
|
negotiatedCipher = ((responseVal[CIPHER] != null) ?
|
|
new String(responseVal[CIPHER], encoding) : null);
|
|
if (negotiatedCipher == null) {
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
"violation. No cipher specified.");
|
|
}
|
|
|
|
int foundCipher = -1;
|
|
logger.log(Level.FINE, "DIGEST85:Client negotiated cipher: {0}",
|
|
negotiatedCipher);
|
|
|
|
// Check that cipher is one that we offered
|
|
for (int j = 0; j < CIPHER_TOKENS.length; j++) {
|
|
if (negotiatedCipher.equals(CIPHER_TOKENS[j]) &&
|
|
myCiphers[j] != 0) {
|
|
foundCipher = j;
|
|
break;
|
|
}
|
|
}
|
|
if (foundCipher == -1) {
|
|
throw new SaslException("DIGEST-MD5: server does not " +
|
|
"support cipher: " + negotiatedCipher);
|
|
}
|
|
// Set negotiatedStrength
|
|
if ((CIPHER_MASKS[foundCipher]&HIGH_STRENGTH) != 0) {
|
|
negotiatedStrength = "high";
|
|
} else if ((CIPHER_MASKS[foundCipher]&MEDIUM_STRENGTH) != 0) {
|
|
negotiatedStrength = "medium";
|
|
} else {
|
|
// assume default low
|
|
negotiatedStrength = "low";
|
|
}
|
|
|
|
logger.log(Level.FINE, "DIGEST86:Negotiated strength: {0}",
|
|
negotiatedStrength);
|
|
}
|
|
|
|
// atmost once
|
|
String digestUriFromResponse = ((responseVal[DIGEST_URI]) != null ?
|
|
new String(responseVal[DIGEST_URI], encoding) : null);
|
|
|
|
if (digestUriFromResponse != null) {
|
|
logger.log(Level.FINE, "DIGEST87:digest URI: {0}",
|
|
digestUriFromResponse);
|
|
}
|
|
|
|
// serv-type "/" host [ "/" serv-name ]
|
|
// e.g.: smtp/mail3.example.com/example.com
|
|
// e.g.: ftp/ftp.example.com
|
|
// e.g.: ldap/ldapserver.example.com
|
|
|
|
// host should match one of service's configured service names
|
|
// Check against digest URI that mech was created with
|
|
|
|
if (uriMatches(digestUri, digestUriFromResponse)) {
|
|
digestUri = digestUriFromResponse; // account for case-sensitive diffs
|
|
} else {
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
"violation. Mismatched URI: " + digestUriFromResponse +
|
|
"; expecting: " + digestUri);
|
|
}
|
|
|
|
// response: exactly once
|
|
byte[] responseFromClient = responseVal[RESPONSE];
|
|
if (responseFromClient == null) {
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
" violation. Missing response.");
|
|
}
|
|
|
|
// authzid: atmost once
|
|
byte[] authzidBytes;
|
|
String authzidFromClient = ((authzidBytes=responseVal[AUTHZID]) != null?
|
|
new String(authzidBytes, encoding) : username);
|
|
|
|
if (authzidBytes != null) {
|
|
logger.log(Level.FINE, "DIGEST88:Authzid: {0}",
|
|
new String(authzidBytes));
|
|
}
|
|
|
|
// Ignore auth-param
|
|
|
|
// Get password need to generate verifying response
|
|
char[] passwd;
|
|
try {
|
|
// Realm and Name callbacks are used to provide info
|
|
RealmCallback rcb = new RealmCallback("DIGEST-MD5 realm: ",
|
|
negotiatedRealm);
|
|
NameCallback ncb = new NameCallback("DIGEST-MD5 authentication ID: ",
|
|
username);
|
|
|
|
// PasswordCallback is used to collect info
|
|
PasswordCallback pcb =
|
|
new PasswordCallback("DIGEST-MD5 password: ", false);
|
|
|
|
cbh.handle(new Callback[] {rcb, ncb, pcb});
|
|
passwd = pcb.getPassword();
|
|
pcb.clearPassword();
|
|
|
|
} catch (UnsupportedCallbackException e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: Cannot perform callback to acquire password", e);
|
|
|
|
} catch (IOException e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: IO error acquiring password", e);
|
|
}
|
|
|
|
if (passwd == null) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: cannot acquire password for " + username +
|
|
" in realm : " + negotiatedRealm);
|
|
}
|
|
|
|
try {
|
|
// Validate response value sent by client
|
|
byte[] expectedResponse;
|
|
|
|
try {
|
|
expectedResponse = generateResponseValue("AUTHENTICATE",
|
|
digestUri, negotiatedQop, username, negotiatedRealm,
|
|
passwd, nonce /* use own nonce */,
|
|
cnonce, NONCE_COUNT_VALUE, authzidBytes);
|
|
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: problem duplicating client response", e);
|
|
} catch (IOException e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: problem duplicating client response", e);
|
|
}
|
|
|
|
if (!Arrays.equals(responseFromClient, expectedResponse)) {
|
|
throw new SaslException("DIGEST-MD5: digest response format " +
|
|
"violation. Mismatched response.");
|
|
}
|
|
|
|
// Ensure that authzid mapping is OK
|
|
try {
|
|
AuthorizeCallback acb =
|
|
new AuthorizeCallback(username, authzidFromClient);
|
|
cbh.handle(new Callback[]{acb});
|
|
|
|
if (acb.isAuthorized()) {
|
|
authzid = acb.getAuthorizedID();
|
|
} else {
|
|
throw new SaslException("DIGEST-MD5: " + username +
|
|
" is not authorized to act as " + authzidFromClient);
|
|
}
|
|
} catch (SaslException e) {
|
|
throw e;
|
|
} catch (UnsupportedCallbackException e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: Cannot perform callback to check authzid", e);
|
|
} catch (IOException e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: IO error checking authzid", e);
|
|
}
|
|
|
|
return generateResponseAuth(username, passwd, cnonce,
|
|
NONCE_COUNT_VALUE, authzidBytes);
|
|
} finally {
|
|
// Clear password
|
|
for (int i = 0; i < passwd.length; i++) {
|
|
passwd[i] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static boolean uriMatches(String thisUri, String incomingUri) {
|
|
// Full match
|
|
if (thisUri.equalsIgnoreCase(incomingUri)) {
|
|
return true;
|
|
}
|
|
// Unbound match
|
|
if (thisUri.endsWith("/*")) {
|
|
int protoAndSlash = thisUri.length() - 1;
|
|
String thisProtoAndSlash = thisUri.substring(0, protoAndSlash);
|
|
String incomingProtoAndSlash = incomingUri.substring(0, protoAndSlash);
|
|
return thisProtoAndSlash.equalsIgnoreCase(incomingProtoAndSlash);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Server sends a message formatted as follows:
|
|
* response-auth = "rspauth" "=" response-value
|
|
* where response-value is calculated as above, using the values sent in
|
|
* step two, except that if qop is "auth", then A2 is
|
|
*
|
|
* A2 = { ":", digest-uri-value }
|
|
*
|
|
* And if qop is "auth-int" or "auth-conf" then A2 is
|
|
*
|
|
* A2 = { ":", digest-uri-value, ":00000000000000000000000000000000" }
|
|
*
|
|
* Clears password afterwards.
|
|
*/
|
|
private byte[] generateResponseAuth(String username, char[] passwd,
|
|
byte[] cnonce, int nonceCount, byte[] authzidBytes) throws SaslException {
|
|
|
|
// Construct response value
|
|
|
|
try {
|
|
byte[] responseValue = generateResponseValue("",
|
|
digestUri, negotiatedQop, username, negotiatedRealm,
|
|
passwd, nonce, cnonce, nonceCount, authzidBytes);
|
|
|
|
byte[] challenge = new byte[responseValue.length + 8];
|
|
System.arraycopy("rspauth=".getBytes(encoding), 0, challenge, 0, 8);
|
|
System.arraycopy(responseValue, 0, challenge, 8,
|
|
responseValue.length );
|
|
|
|
return challenge;
|
|
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new SaslException("DIGEST-MD5: problem generating response", e);
|
|
} catch (IOException e) {
|
|
throw new SaslException("DIGEST-MD5: problem generating response", e);
|
|
}
|
|
}
|
|
|
|
public String getAuthorizationID() {
|
|
if (completed) {
|
|
return authzid;
|
|
} else {
|
|
throw new IllegalStateException(
|
|
"DIGEST-MD5 server negotiation not complete");
|
|
}
|
|
}
|
|
}
|