701 lines
26 KiB
Java
701 lines
26 KiB
Java
/*
|
|
* Copyright (c) 2000, 2019, 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.CallbackHandler;
|
|
import javax.security.auth.callback.PasswordCallback;
|
|
import javax.security.auth.callback.NameCallback;
|
|
import javax.security.auth.callback.Callback;
|
|
import javax.security.auth.callback.UnsupportedCallbackException;
|
|
|
|
/**
|
|
* An implementation of the DIGEST-MD5
|
|
* (<a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>) SASL
|
|
* (<a href="http://www.ietf.org/rfc/rfc2222.txt">RFC 2222</a>) mechanism.
|
|
*
|
|
* The DIGEST-MD5 SASL mechanism specifies two modes of authentication.
|
|
* - Initial Authentication
|
|
* - Subsequent Authentication - optional, (currently unsupported)
|
|
*
|
|
* Required callbacks:
|
|
* - RealmChoiceCallback
|
|
* shows user list of realms server has offered; handler must choose one
|
|
* from list
|
|
* - RealmCallback
|
|
* shows user the only realm server has offered or none; handler must
|
|
* enter realm to use
|
|
* - NameCallback
|
|
* handler must enter username to use for authentication
|
|
* - PasswordCallback
|
|
* handler must enter password for username to use for authentication
|
|
*
|
|
* Environment properties that affect behavior of implementation:
|
|
*
|
|
* javax.security.sasl.qop
|
|
* quality of protection; list of auth, auth-int, auth-conf; default is "auth"
|
|
* javax.security.sasl.strength
|
|
* auth-conf strength; list of high, medium, low; default is highest
|
|
* available on platform ["high,medium,low"].
|
|
* high means des3 or rc4 (128); medium des or rc4-56; low is rc4-40;
|
|
* choice of cipher depends on its availablility on platform
|
|
* javax.security.sasl.maxbuf
|
|
* max receive buffer size; default is 65536
|
|
* javax.security.sasl.sendmaxbuffer
|
|
* max send buffer size; default is 65536; (min with server max recv size)
|
|
*
|
|
* com.sun.security.sasl.digest.cipher
|
|
* name a specific cipher to use; setting must be compatible with the
|
|
* setting of the javax.security.sasl.strength property.
|
|
*
|
|
* @see <a href="http://www.ietf.org/rfc/rfc2222.txt">RFC 2222</a>
|
|
* - Simple Authentication and Security Layer (SASL)
|
|
* @see <a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>
|
|
* - Using Digest Authentication as a SASL Mechanism
|
|
* @see <a href="http://java.sun.com/products/jce">Java(TM)
|
|
* Cryptography Extension 1.2.1 (JCE)</a>
|
|
* @see <a href="http://java.sun.com/products/jaas">Java(TM)
|
|
* Authentication and Authorization Service (JAAS)</a>
|
|
*
|
|
* @author Jonathan Bruce
|
|
* @author Rosanna Lee
|
|
*/
|
|
final class DigestMD5Client extends DigestMD5Base implements SaslClient {
|
|
private static final String MY_CLASS_NAME = DigestMD5Client.class.getName();
|
|
|
|
// Property for specifying cipher explicitly
|
|
private static final String CIPHER_PROPERTY =
|
|
"com.sun.security.sasl.digest.cipher";
|
|
|
|
/* Directives encountered in challenges sent by the server. */
|
|
private static final String[] DIRECTIVE_KEY = {
|
|
"realm", // >= 0 times
|
|
"qop", // atmost once; default is "auth"
|
|
"algorithm", // exactly once
|
|
"nonce", // exactly once
|
|
"maxbuf", // atmost once; default is 65536
|
|
"charset", // atmost once; default is ISO 8859-1
|
|
"cipher", // exactly once if qop is "auth-conf"
|
|
"rspauth", // exactly once in 2nd challenge
|
|
"stale", // atmost once for in subsequent auth (not supported)
|
|
};
|
|
|
|
/* Indices into DIRECTIVE_KEY */
|
|
private static final int REALM = 0;
|
|
private static final int QOP = 1;
|
|
private static final int ALGORITHM = 2;
|
|
private static final int NONCE = 3;
|
|
private static final int MAXBUF = 4;
|
|
private static final int CHARSET = 5;
|
|
private static final int CIPHER = 6;
|
|
private static final int RESPONSE_AUTH = 7;
|
|
private static final int STALE = 8;
|
|
|
|
private int nonceCount; // number of times nonce has been used/seen
|
|
|
|
/* User-supplied/generated information */
|
|
private String specifiedCipher; // cipher explicitly requested by user
|
|
private byte[] cnonce; // client generated nonce
|
|
private String username;
|
|
private char[] passwd;
|
|
private byte[] authzidBytes; // byte repr of authzid
|
|
|
|
/**
|
|
* Constructor for DIGEST-MD5 mechanism.
|
|
*
|
|
* @param authzid A non-null String representing the principal
|
|
* for which authorization is being granted..
|
|
* @param digestURI A non-null String representing detailing the
|
|
* combined protocol and host being used for authentication.
|
|
* @param props The possibly null properties to be used by the SASL
|
|
* mechanism to configure the authentication exchange.
|
|
* @param cbh The non-null CallbackHanlder object for callbacks
|
|
* @throws SaslException if no authentication ID or password is supplied
|
|
*/
|
|
DigestMD5Client(String authzid, String protocol, String serverName,
|
|
Map<String, ?> props, CallbackHandler cbh) throws SaslException {
|
|
|
|
super(props, MY_CLASS_NAME, 2, protocol + "/" + serverName, cbh);
|
|
|
|
// authzID can only be encoded in UTF8 - RFC 2222
|
|
if (authzid != null) {
|
|
this.authzid = authzid;
|
|
try {
|
|
authzidBytes = authzid.getBytes("UTF8");
|
|
|
|
} catch (UnsupportedEncodingException e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: Error encoding authzid value into UTF-8", e);
|
|
}
|
|
}
|
|
|
|
if (props != null) {
|
|
specifiedCipher = (String)props.get(CIPHER_PROPERTY);
|
|
|
|
logger.log(Level.FINE, "DIGEST60:Explicitly specified cipher: {0}",
|
|
specifiedCipher);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DIGEST-MD5 has no initial response
|
|
*
|
|
* @return false
|
|
*/
|
|
public boolean hasInitialResponse() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Process the challenge data.
|
|
*
|
|
* The server sends a digest-challenge which the client must reply to
|
|
* in a digest-response. When the authentication is complete, the
|
|
* completed field is set to true.
|
|
*
|
|
* @param challengeData A non-null byte array containing the challenge
|
|
* data from the server.
|
|
* @return A possibly null byte array containing the response to
|
|
* be sent to the server.
|
|
*
|
|
* @throws SaslException If the platform does not have MD5 digest support
|
|
* or if the server sends an invalid challenge.
|
|
*/
|
|
public byte[] evaluateChallenge(byte[] challengeData) throws SaslException {
|
|
|
|
if (challengeData.length > MAX_CHALLENGE_LENGTH) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: Invalid digest-challenge length. Got: " +
|
|
challengeData.length + " Expected < " + MAX_CHALLENGE_LENGTH);
|
|
}
|
|
|
|
/* Extract and process digest-challenge */
|
|
byte[][] challengeVal;
|
|
|
|
switch (step) {
|
|
case 2:
|
|
/* Process server's first challenge (from Step 1) */
|
|
/* Get realm, qop, maxbuf, charset, algorithm, cipher, nonce
|
|
directives */
|
|
List<byte[]> realmChoices = new ArrayList<byte[]>(3);
|
|
challengeVal = parseDirectives(challengeData, DIRECTIVE_KEY,
|
|
realmChoices, REALM);
|
|
|
|
try {
|
|
processChallenge(challengeVal, realmChoices);
|
|
checkQopSupport(challengeVal[QOP], challengeVal[CIPHER]);
|
|
++step;
|
|
return generateClientResponse(challengeVal[CHARSET]);
|
|
} catch (SaslException e) {
|
|
step = 0;
|
|
clearPassword();
|
|
throw e; // rethrow
|
|
} catch (IOException e) {
|
|
step = 0;
|
|
clearPassword();
|
|
throw new SaslException("DIGEST-MD5: Error generating " +
|
|
"digest response-value", e);
|
|
}
|
|
|
|
case 3:
|
|
try {
|
|
/* Process server's step 3 (server response to digest response) */
|
|
/* Get rspauth directive */
|
|
challengeVal = parseDirectives(challengeData, DIRECTIVE_KEY,
|
|
null, REALM);
|
|
validateResponseValue(challengeVal[RESPONSE_AUTH]);
|
|
|
|
|
|
/* Initialize SecurityCtx implementation */
|
|
if (integrity && privacy) {
|
|
secCtx = new DigestPrivacy(true /* client */);
|
|
} else if (integrity) {
|
|
secCtx = new DigestIntegrity(true /* client */);
|
|
}
|
|
|
|
return null; // Mechanism has completed.
|
|
} finally {
|
|
clearPassword();
|
|
step = 0; // Set to invalid state
|
|
completed = true;
|
|
}
|
|
|
|
default:
|
|
// No other possible state
|
|
throw new SaslException("DIGEST-MD5: Client at illegal state");
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Record information from the challengeVal array into variables/fields.
|
|
* Check directive values that are multi-valued and ensure that mandatory
|
|
* directives not missing from the digest-challenge.
|
|
*
|
|
* @throws SaslException if a sasl is a the mechanism cannot
|
|
* correcly handle a callbacks or if a violation in the
|
|
* digest challenge format is detected.
|
|
*/
|
|
private void processChallenge(byte[][] challengeVal, List<byte[]> realmChoices)
|
|
throws SaslException, UnsupportedEncodingException {
|
|
|
|
/* CHARSET: optional atmost once */
|
|
if (challengeVal[CHARSET] != null) {
|
|
if (!"utf-8".equals(new String(challengeVal[CHARSET], encoding))) {
|
|
throw new SaslException("DIGEST-MD5: digest-challenge format " +
|
|
"violation. Unrecognised charset value: " +
|
|
new String(challengeVal[CHARSET]));
|
|
} else {
|
|
encoding = "UTF8";
|
|
useUTF8 = true;
|
|
}
|
|
}
|
|
|
|
/* ALGORITHM: required exactly once */
|
|
if (challengeVal[ALGORITHM] == null) {
|
|
throw new SaslException("DIGEST-MD5: Digest-challenge format " +
|
|
"violation: algorithm directive missing");
|
|
} else if (!"md5-sess".equals(new String(challengeVal[ALGORITHM], encoding))) {
|
|
throw new SaslException("DIGEST-MD5: Digest-challenge format " +
|
|
"violation. Invalid value for 'algorithm' directive: " +
|
|
challengeVal[ALGORITHM]);
|
|
}
|
|
|
|
/* NONCE: required exactly once */
|
|
if (challengeVal[NONCE] == null) {
|
|
throw new SaslException("DIGEST-MD5: Digest-challenge format " +
|
|
"violation: nonce directive missing");
|
|
} else {
|
|
nonce = challengeVal[NONCE];
|
|
}
|
|
|
|
try {
|
|
/* REALM: optional, if multiple, stored in realmChoices */
|
|
String[] realmTokens = null;
|
|
|
|
if (challengeVal[REALM] != null) {
|
|
if (realmChoices == null || realmChoices.size() <= 1) {
|
|
// Only one realm specified
|
|
negotiatedRealm = new String(challengeVal[REALM], encoding);
|
|
} else {
|
|
realmTokens = new String[realmChoices.size()];
|
|
for (int i = 0; i < realmTokens.length; i++) {
|
|
realmTokens[i] =
|
|
new String(realmChoices.get(i), encoding);
|
|
}
|
|
}
|
|
}
|
|
|
|
NameCallback ncb = authzid == null ?
|
|
new NameCallback("DIGEST-MD5 authentication ID: ") :
|
|
new NameCallback("DIGEST-MD5 authentication ID: ", authzid);
|
|
PasswordCallback pcb =
|
|
new PasswordCallback("DIGEST-MD5 password: ", false);
|
|
|
|
if (realmTokens == null) {
|
|
// Server specified <= 1 realm
|
|
// If 0, RFC 2831: the client SHOULD solicit a realm from the user.
|
|
RealmCallback tcb =
|
|
(negotiatedRealm == null? new RealmCallback("DIGEST-MD5 realm: ") :
|
|
new RealmCallback("DIGEST-MD5 realm: ", negotiatedRealm));
|
|
|
|
cbh.handle(new Callback[] {tcb, ncb, pcb});
|
|
|
|
/* Acquire realm from RealmCallback */
|
|
negotiatedRealm = tcb.getText();
|
|
if (negotiatedRealm == null) {
|
|
negotiatedRealm = "";
|
|
}
|
|
} else {
|
|
RealmChoiceCallback ccb = new RealmChoiceCallback(
|
|
"DIGEST-MD5 realm: ",
|
|
realmTokens,
|
|
0, false);
|
|
cbh.handle(new Callback[] {ccb, ncb, pcb});
|
|
|
|
// Acquire realm from RealmChoiceCallback
|
|
int[] selected = ccb.getSelectedIndexes();
|
|
if (selected == null
|
|
|| selected[0] < 0
|
|
|| selected[0] >= realmTokens.length) {
|
|
throw new SaslException("DIGEST-MD5: Invalid realm chosen");
|
|
}
|
|
negotiatedRealm = realmTokens[selected[0]];
|
|
}
|
|
|
|
passwd = pcb.getPassword();
|
|
pcb.clearPassword();
|
|
username = ncb.getName();
|
|
|
|
} catch (SaslException se) {
|
|
throw se;
|
|
|
|
} catch (UnsupportedCallbackException e) {
|
|
throw new SaslException("DIGEST-MD5: Cannot perform callback to " +
|
|
"acquire realm, authentication ID or password", e);
|
|
|
|
} catch (IOException e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: Error acquiring realm, authentication ID or password", e);
|
|
}
|
|
|
|
if (username == null || passwd == null) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: authentication ID and password must be specified");
|
|
}
|
|
|
|
/* MAXBUF: optional atmost once */
|
|
int srvMaxBufSize =
|
|
(challengeVal[MAXBUF] == null) ? DEFAULT_MAXBUF
|
|
: Integer.parseInt(new String(challengeVal[MAXBUF], encoding));
|
|
sendMaxBufSize =
|
|
(sendMaxBufSize == 0) ? srvMaxBufSize
|
|
: Math.min(sendMaxBufSize, srvMaxBufSize);
|
|
}
|
|
|
|
/**
|
|
* Parses the 'qop' directive. If 'auth-conf' is specified by
|
|
* the client and offered as a QOP option by the server, then a check
|
|
* is client-side supported ciphers is performed.
|
|
*
|
|
* @throws IOException
|
|
*/
|
|
private void checkQopSupport(byte[] qopInChallenge, byte[] ciphersInChallenge)
|
|
throws IOException {
|
|
|
|
/* QOP: optional; if multiple, merged earlier */
|
|
String qopOptions;
|
|
|
|
if (qopInChallenge == null) {
|
|
qopOptions = "auth";
|
|
} else {
|
|
qopOptions = new String(qopInChallenge, encoding);
|
|
}
|
|
|
|
// process
|
|
String[] serverQopTokens = new String[3];
|
|
byte[] serverQop = parseQop(qopOptions, serverQopTokens,
|
|
true /* ignore unrecognized tokens */);
|
|
byte serverAllQop = combineMasks(serverQop);
|
|
|
|
switch (findPreferredMask(serverAllQop, qop)) {
|
|
case 0:
|
|
throw new SaslException("DIGEST-MD5: No common protection " +
|
|
"layer between client and server");
|
|
|
|
case NO_PROTECTION:
|
|
negotiatedQop = "auth";
|
|
// buffer sizes not applicable
|
|
break;
|
|
|
|
case INTEGRITY_ONLY_PROTECTION:
|
|
negotiatedQop = "auth-int";
|
|
integrity = true;
|
|
rawSendSize = sendMaxBufSize - 16;
|
|
break;
|
|
|
|
case PRIVACY_PROTECTION:
|
|
negotiatedQop = "auth-conf";
|
|
privacy = integrity = true;
|
|
rawSendSize = sendMaxBufSize - 26;
|
|
checkStrengthSupport(ciphersInChallenge);
|
|
break;
|
|
}
|
|
|
|
if (logger.isLoggable(Level.FINE)) {
|
|
logger.log(Level.FINE, "DIGEST61:Raw send size: {0}",
|
|
new Integer(rawSendSize));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes the 'cipher' digest-challenge directive. This allows the
|
|
* mechanism to check for client-side support against the list of
|
|
* supported ciphers send by the server. If no match is found,
|
|
* the mechanism aborts.
|
|
*
|
|
* @throws SaslException If an error is encountered in processing
|
|
* the cipher digest-challenge directive or if no client-side
|
|
* support is found.
|
|
*/
|
|
private void checkStrengthSupport(byte[] ciphersInChallenge)
|
|
throws IOException {
|
|
|
|
/* CIPHER: required exactly once if qop=auth-conf */
|
|
if (ciphersInChallenge == null) {
|
|
throw new SaslException("DIGEST-MD5: server did not specify " +
|
|
"cipher to use for 'auth-conf'");
|
|
}
|
|
|
|
// First determine ciphers that server supports
|
|
String cipherOptions = new String(ciphersInChallenge, encoding);
|
|
StringTokenizer parser = new StringTokenizer(cipherOptions, ", \t\n");
|
|
int tokenCount = parser.countTokens();
|
|
String token = null;
|
|
byte[] serverCiphers = { UNSET,
|
|
UNSET,
|
|
UNSET,
|
|
UNSET,
|
|
UNSET };
|
|
String[] serverCipherStrs = new String[serverCiphers.length];
|
|
|
|
// Parse ciphers in challenge; mark each that server supports
|
|
for (int i = 0; i < tokenCount; i++) {
|
|
token = parser.nextToken();
|
|
for (int j = 0; j < CIPHER_TOKENS.length; j++) {
|
|
if (token.equals(CIPHER_TOKENS[j])) {
|
|
serverCiphers[j] |= CIPHER_MASKS[j];
|
|
serverCipherStrs[j] = token; // keep for replay to server
|
|
logger.log(Level.FINE, "DIGEST62:Server supports {0}", token);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine which ciphers are available on client
|
|
byte[] clntCiphers = getPlatformCiphers();
|
|
|
|
// Take intersection of server and client supported ciphers
|
|
byte inter = 0;
|
|
for (int i = 0; i < serverCiphers.length; i++) {
|
|
serverCiphers[i] &= clntCiphers[i];
|
|
inter |= serverCiphers[i];
|
|
}
|
|
|
|
if (inter == UNSET) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: Client supports none of these cipher suites: " +
|
|
cipherOptions);
|
|
}
|
|
|
|
// now have a clear picture of user / client; client / server
|
|
// cipher options. Leverage strength array against what is
|
|
// supported to choose a cipher.
|
|
negotiatedCipher = findCipherAndStrength(serverCiphers, serverCipherStrs);
|
|
|
|
if (negotiatedCipher == null) {
|
|
throw new SaslException("DIGEST-MD5: Unable to negotiate " +
|
|
"a strength level for 'auth-conf'");
|
|
}
|
|
logger.log(Level.FINE, "DIGEST63:Cipher suite: {0}", negotiatedCipher);
|
|
}
|
|
|
|
/**
|
|
* Steps through the ordered 'strength' array, and compares it with
|
|
* the 'supportedCiphers' array. The cipher returned represents
|
|
* the best possible cipher based on the strength preference and the
|
|
* available ciphers on both the server and client environments.
|
|
*
|
|
* @param tokens The array of cipher tokens sent by server
|
|
* @return The agreed cipher.
|
|
*/
|
|
private String findCipherAndStrength(byte[] supportedCiphers,
|
|
String[] tokens) {
|
|
byte s;
|
|
for (int i = 0; i < strength.length; i++) {
|
|
if ((s=strength[i]) != 0) {
|
|
for (int j = 0; j < supportedCiphers.length; j++) {
|
|
|
|
// If user explicitly requested cipher, then it
|
|
// must be the one we choose
|
|
|
|
if (s == supportedCiphers[j] &&
|
|
(specifiedCipher == null ||
|
|
specifiedCipher.equals(tokens[j]))) {
|
|
switch (s) {
|
|
case HIGH_STRENGTH:
|
|
negotiatedStrength = "high";
|
|
break;
|
|
case MEDIUM_STRENGTH:
|
|
negotiatedStrength = "medium";
|
|
break;
|
|
case LOW_STRENGTH:
|
|
negotiatedStrength = "low";
|
|
break;
|
|
}
|
|
|
|
return tokens[j];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null; // none found
|
|
}
|
|
|
|
/**
|
|
* Returns digest-response suitable for an initial authentication.
|
|
*
|
|
* The following are qdstr-val (quoted string values) as per RFC 2831,
|
|
* which means that any embedded quotes must be escaped.
|
|
* realm-value
|
|
* nonce-value
|
|
* username-value
|
|
* cnonce-value
|
|
* authzid-value
|
|
* @returns {@code digest-response} in a byte array
|
|
* @throws SaslException if there is an error generating the
|
|
* response value or the cnonce value.
|
|
*/
|
|
private byte[] generateClientResponse(byte[] charset) throws IOException {
|
|
|
|
ByteArrayOutputStream digestResp = new ByteArrayOutputStream();
|
|
|
|
if (useUTF8) {
|
|
digestResp.write("charset=".getBytes(encoding));
|
|
digestResp.write(charset);
|
|
digestResp.write(',');
|
|
}
|
|
|
|
digestResp.write(("username=\"" +
|
|
quotedStringValue(username) + "\",").getBytes(encoding));
|
|
|
|
if (negotiatedRealm.length() > 0) {
|
|
digestResp.write(("realm=\"" +
|
|
quotedStringValue(negotiatedRealm) + "\",").getBytes(encoding));
|
|
}
|
|
|
|
digestResp.write("nonce=\"".getBytes(encoding));
|
|
writeQuotedStringValue(digestResp, nonce);
|
|
digestResp.write('"');
|
|
digestResp.write(',');
|
|
|
|
nonceCount = getNonceCount(nonce);
|
|
digestResp.write(("nc=" +
|
|
nonceCountToHex(nonceCount) + ",").getBytes(encoding));
|
|
|
|
cnonce = generateNonce();
|
|
digestResp.write("cnonce=\"".getBytes(encoding));
|
|
writeQuotedStringValue(digestResp, cnonce);
|
|
digestResp.write("\",".getBytes(encoding));
|
|
digestResp.write(("digest-uri=\"" + digestUri + "\",").getBytes(encoding));
|
|
|
|
digestResp.write("maxbuf=".getBytes(encoding));
|
|
digestResp.write(String.valueOf(recvMaxBufSize).getBytes(encoding));
|
|
digestResp.write(',');
|
|
|
|
try {
|
|
digestResp.write("response=".getBytes(encoding));
|
|
digestResp.write(generateResponseValue("AUTHENTICATE",
|
|
digestUri, negotiatedQop, username,
|
|
negotiatedRealm, passwd, nonce, cnonce,
|
|
nonceCount, authzidBytes));
|
|
digestResp.write(',');
|
|
} catch (Exception e) {
|
|
throw new SaslException(
|
|
"DIGEST-MD5: Error generating response value", e);
|
|
}
|
|
|
|
digestResp.write(("qop=" + negotiatedQop).getBytes(encoding));
|
|
|
|
if (negotiatedCipher != null) {
|
|
digestResp.write((",cipher=\"" + negotiatedCipher + "\"").getBytes(encoding));
|
|
}
|
|
|
|
if (authzidBytes != null) {
|
|
digestResp.write(",authzid=\"".getBytes(encoding));
|
|
writeQuotedStringValue(digestResp, authzidBytes);
|
|
digestResp.write("\"".getBytes(encoding));
|
|
}
|
|
|
|
if (digestResp.size() > MAX_RESPONSE_LENGTH) {
|
|
throw new SaslException ("DIGEST-MD5: digest-response size too " +
|
|
"large. Length: " + digestResp.size());
|
|
}
|
|
return digestResp.toByteArray();
|
|
}
|
|
|
|
|
|
/**
|
|
* From RFC 2831, Section 2.1.3: Step Three
|
|
* [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" }
|
|
*/
|
|
private void validateResponseValue(byte[] fromServer) throws SaslException {
|
|
if (fromServer == null) {
|
|
throw new SaslException("DIGEST-MD5: Authenication failed. " +
|
|
"Expecting 'rspauth' authentication success message");
|
|
}
|
|
|
|
try {
|
|
byte[] expected = generateResponseValue("",
|
|
digestUri, negotiatedQop, username, negotiatedRealm,
|
|
passwd, nonce, cnonce, nonceCount, authzidBytes);
|
|
if (!Arrays.equals(expected, fromServer)) {
|
|
/* Server's rspauth value does not match */
|
|
throw new SaslException(
|
|
"Server's rspauth value does not match what client expects");
|
|
}
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new SaslException(
|
|
"Problem generating response value for verification", e);
|
|
} catch (IOException e) {
|
|
throw new SaslException(
|
|
"Problem generating response value for verification", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the number of requests (including current request)
|
|
* that the client has sent in response to nonceValue.
|
|
* This is 1 the first time nonceValue is seen.
|
|
*
|
|
* We don't cache nonce values seen, and we don't support subsequent
|
|
* authentication, so the value is always 1.
|
|
*/
|
|
private static int getNonceCount(byte[] nonceValue) {
|
|
return 1;
|
|
}
|
|
|
|
private void clearPassword() {
|
|
if (passwd != null) {
|
|
for (int i = 0; i < passwd.length; i++) {
|
|
passwd[i] = 0;
|
|
}
|
|
passwd = null;
|
|
}
|
|
}
|
|
}
|