2014-08-01 10:34:47 +02:00
|
|
|
/**
|
|
|
|
*
|
2019-02-10 19:50:46 +01:00
|
|
|
* Copyright 2014-2019 Florian Schmaus
|
2014-08-01 10:34:47 +02:00
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* 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.jivesoftware.smack.sasl.provided;
|
|
|
|
|
2019-05-08 11:34:40 +02:00
|
|
|
import java.nio.charset.StandardCharsets;
|
2017-02-11 16:16:41 +01:00
|
|
|
|
2014-08-01 10:34:47 +02:00
|
|
|
import javax.security.auth.callback.CallbackHandler;
|
|
|
|
|
2019-02-10 19:50:46 +01:00
|
|
|
import org.jivesoftware.smack.SmackException.SmackSaslException;
|
2014-08-01 10:34:47 +02:00
|
|
|
import org.jivesoftware.smack.sasl.SASLMechanism;
|
|
|
|
import org.jivesoftware.smack.util.ByteUtils;
|
2014-11-14 21:13:30 +01:00
|
|
|
import org.jivesoftware.smack.util.MD5;
|
2014-08-01 10:34:47 +02:00
|
|
|
import org.jivesoftware.smack.util.StringUtils;
|
|
|
|
|
|
|
|
public class SASLDigestMD5Mechanism extends SASLMechanism {
|
|
|
|
|
|
|
|
public static final String NAME = DIGESTMD5;
|
|
|
|
|
|
|
|
private static final String INITAL_NONCE = "00000001";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The only 'qop' value supported by this implementation
|
|
|
|
*/
|
|
|
|
private static final String QOP_VALUE = "auth";
|
|
|
|
|
|
|
|
private enum State {
|
|
|
|
INITIAL,
|
|
|
|
RESPONSE_SENT,
|
|
|
|
VALID_SERVER_RESPONSE,
|
|
|
|
}
|
|
|
|
|
|
|
|
private static boolean verifyServerResponse = true;
|
|
|
|
|
|
|
|
public static void setVerifyServerResponse(boolean verifyServerResponse) {
|
|
|
|
SASLDigestMD5Mechanism.verifyServerResponse = verifyServerResponse;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The state of the this instance of SASL DIGEST-MD5 authentication.
|
|
|
|
*/
|
|
|
|
private State state = State.INITIAL;
|
|
|
|
|
|
|
|
private String nonce;
|
|
|
|
private String cnonce;
|
|
|
|
private String digestUri;
|
|
|
|
private String hex_hashed_a1;
|
|
|
|
|
|
|
|
@Override
|
2019-02-10 19:50:46 +01:00
|
|
|
protected void authenticateInternal(CallbackHandler cbh) {
|
2014-08-01 10:34:47 +02:00
|
|
|
throw new UnsupportedOperationException("CallbackHandler not (yet) supported");
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2019-02-10 19:50:46 +01:00
|
|
|
protected byte[] getAuthenticationText() {
|
2014-08-01 10:34:47 +02:00
|
|
|
// DIGEST-MD5 has no initial response, return null
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String getName() {
|
|
|
|
return NAME;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getPriority() {
|
|
|
|
return 210;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public SASLDigestMD5Mechanism newInstance() {
|
|
|
|
return new SASLDigestMD5Mechanism();
|
|
|
|
}
|
|
|
|
|
2015-06-16 18:50:30 +02:00
|
|
|
@Override
|
|
|
|
public boolean authzidSupported() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2014-10-21 11:59:11 +02:00
|
|
|
|
|
|
|
@Override
|
2019-02-10 19:50:46 +01:00
|
|
|
public void checkIfSuccessfulOrThrow() throws SmackSaslException {
|
2014-10-21 11:59:11 +02:00
|
|
|
if (verifyServerResponse && state != State.VALID_SERVER_RESPONSE) {
|
2019-02-10 19:50:46 +01:00
|
|
|
throw new SmackSaslException(NAME + " no valid server response");
|
2014-10-21 11:59:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-08-01 10:34:47 +02:00
|
|
|
@Override
|
2019-02-10 19:50:46 +01:00
|
|
|
protected byte[] evaluateChallenge(byte[] challenge) throws SmackSaslException {
|
2014-08-01 10:34:47 +02:00
|
|
|
if (challenge.length == 0) {
|
2019-02-10 19:50:46 +01:00
|
|
|
throw new SmackSaslException("Initial challenge has zero length");
|
2014-08-01 10:34:47 +02:00
|
|
|
}
|
2019-05-08 11:34:40 +02:00
|
|
|
String challengeString = new String(challenge, StandardCharsets.UTF_8);
|
2017-02-11 16:16:41 +01:00
|
|
|
String[] challengeParts = challengeString.split(",");
|
2014-08-01 10:34:47 +02:00
|
|
|
byte[] response = null;
|
|
|
|
switch (state) {
|
|
|
|
case INITIAL:
|
|
|
|
for (String part : challengeParts) {
|
2017-04-07 18:56:51 +02:00
|
|
|
String[] keyValue = part.split("=", 2);
|
2014-08-01 10:34:47 +02:00
|
|
|
String key = keyValue[0];
|
2017-12-13 23:10:11 +01:00
|
|
|
// RFC 2831 § 7.1 about the formatting of the digest-challenge:
|
2015-03-31 10:04:53 +02:00
|
|
|
// "The full form is "<n>#<m>element" indicating at least <n> and
|
|
|
|
// at most <m> elements, each separated by one or more commas
|
|
|
|
// (",") and OPTIONAL linear white space (LWS)."
|
|
|
|
// Which means the key value may be preceded by whitespace,
|
|
|
|
// which is what we remove: *Only the preceding whitespace*.
|
|
|
|
key = key.replaceFirst("^\\s+", "");
|
2014-08-01 10:34:47 +02:00
|
|
|
String value = keyValue[1];
|
|
|
|
if ("nonce".equals(key)) {
|
|
|
|
if (nonce != null) {
|
2019-02-10 19:50:46 +01:00
|
|
|
throw new SmackSaslException("Nonce value present multiple times");
|
2014-08-01 10:34:47 +02:00
|
|
|
}
|
|
|
|
nonce = value.replace("\"", "");
|
|
|
|
}
|
|
|
|
else if ("qop".equals(key)) {
|
|
|
|
value = value.replace("\"", "");
|
|
|
|
if (!value.equals("auth")) {
|
2019-02-10 19:50:46 +01:00
|
|
|
throw new SmackSaslException("Unsupported qop operation: " + value);
|
2014-08-01 10:34:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (nonce == null) {
|
|
|
|
// RFC 2831 2.1.1 about nonce "This directive is required and MUST appear exactly
|
|
|
|
// once; if not present, or if multiple instances are present, the client should
|
|
|
|
// abort the authentication exchange."
|
2019-02-10 19:50:46 +01:00
|
|
|
throw new SmackSaslException("nonce value not present in initial challenge");
|
2014-08-01 10:34:47 +02:00
|
|
|
}
|
|
|
|
// RFC 2831 2.1.2.1 defines A1, A2, KD and response-value
|
2014-11-14 21:13:30 +01:00
|
|
|
byte[] a1FirstPart = MD5.bytes(authenticationId + ':' + serviceName + ':'
|
|
|
|
+ password);
|
2014-08-01 10:34:47 +02:00
|
|
|
cnonce = StringUtils.randomString(32);
|
2017-12-13 23:10:11 +01:00
|
|
|
byte[] a1 = ByteUtils.concat(a1FirstPart, toBytes(':' + nonce + ':' + cnonce));
|
2014-08-01 10:34:47 +02:00
|
|
|
digestUri = "xmpp/" + serviceName;
|
2014-11-14 21:13:30 +01:00
|
|
|
hex_hashed_a1 = StringUtils.encodeHex(MD5.bytes(a1));
|
2014-08-01 10:34:47 +02:00
|
|
|
String responseValue = calcResponse(DigestType.ClientResponse);
|
|
|
|
// @formatter:off
|
|
|
|
// See RFC 2831 2.1.2 digest-response
|
2015-06-16 18:50:30 +02:00
|
|
|
String authzid;
|
|
|
|
if (authorizationId == null) {
|
|
|
|
authzid = "";
|
|
|
|
} else {
|
|
|
|
authzid = ",authzid=\"" + authorizationId + '"';
|
|
|
|
}
|
2015-12-19 20:15:23 +01:00
|
|
|
String saslString = "username=\"" + quoteBackslash(authenticationId) + '"'
|
2015-06-16 18:50:30 +02:00
|
|
|
+ authzid
|
2014-08-01 10:34:47 +02:00
|
|
|
+ ",realm=\"" + serviceName + '"'
|
|
|
|
+ ",nonce=\"" + nonce + '"'
|
|
|
|
+ ",cnonce=\"" + cnonce + '"'
|
|
|
|
+ ",nc=" + INITAL_NONCE
|
|
|
|
+ ",qop=auth"
|
|
|
|
+ ",digest-uri=\"" + digestUri + '"'
|
|
|
|
+ ",response=" + responseValue
|
|
|
|
+ ",charset=utf-8";
|
|
|
|
// @formatter:on
|
|
|
|
response = toBytes(saslString);
|
|
|
|
state = State.RESPONSE_SENT;
|
|
|
|
break;
|
|
|
|
case RESPONSE_SENT:
|
|
|
|
if (verifyServerResponse) {
|
|
|
|
String serverResponse = null;
|
|
|
|
for (String part : challengeParts) {
|
|
|
|
String[] keyValue = part.split("=");
|
2019-07-24 09:18:39 +02:00
|
|
|
assert keyValue.length == 2;
|
2014-08-01 10:34:47 +02:00
|
|
|
String key = keyValue[0];
|
|
|
|
String value = keyValue[1];
|
|
|
|
if ("rspauth".equals(key)) {
|
|
|
|
serverResponse = value;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (serverResponse == null) {
|
2019-02-10 19:50:46 +01:00
|
|
|
throw new SmackSaslException("No server response received while performing " + NAME
|
2014-08-01 10:34:47 +02:00
|
|
|
+ " authentication");
|
|
|
|
}
|
|
|
|
String expectedServerResponse = calcResponse(DigestType.ServerResponse);
|
|
|
|
if (!serverResponse.equals(expectedServerResponse)) {
|
2019-02-10 19:50:46 +01:00
|
|
|
throw new SmackSaslException("Invalid server response while performing " + NAME
|
2014-08-01 10:34:47 +02:00
|
|
|
+ " authentication");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
state = State.VALID_SERVER_RESPONSE;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new IllegalStateException();
|
|
|
|
}
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
|
|
|
|
private enum DigestType {
|
|
|
|
ClientResponse,
|
|
|
|
ServerResponse
|
|
|
|
}
|
|
|
|
|
|
|
|
private String calcResponse(DigestType digestType) {
|
|
|
|
StringBuilder a2 = new StringBuilder();
|
|
|
|
if (digestType == DigestType.ClientResponse) {
|
|
|
|
a2.append("AUTHENTICATE");
|
|
|
|
}
|
|
|
|
a2.append(':');
|
|
|
|
a2.append(digestUri);
|
2014-11-14 21:13:30 +01:00
|
|
|
String hex_hashed_a2 = StringUtils.encodeHex(MD5.bytes(a2.toString()));
|
2014-08-01 10:34:47 +02:00
|
|
|
|
|
|
|
StringBuilder kd_argument = new StringBuilder();
|
|
|
|
kd_argument.append(hex_hashed_a1);
|
|
|
|
kd_argument.append(':');
|
|
|
|
kd_argument.append(nonce);
|
|
|
|
kd_argument.append(':');
|
|
|
|
kd_argument.append(INITAL_NONCE);
|
|
|
|
kd_argument.append(':');
|
|
|
|
kd_argument.append(cnonce);
|
|
|
|
kd_argument.append(':');
|
|
|
|
kd_argument.append(QOP_VALUE);
|
|
|
|
kd_argument.append(':');
|
|
|
|
kd_argument.append(hex_hashed_a2);
|
2014-11-14 21:13:30 +01:00
|
|
|
byte[] kd = MD5.bytes(kd_argument.toString());
|
2014-08-01 10:34:47 +02:00
|
|
|
String responseValue = StringUtils.encodeHex(kd);
|
|
|
|
return responseValue;
|
|
|
|
}
|
|
|
|
|
2015-12-19 20:15:23 +01:00
|
|
|
/**
|
|
|
|
* Quote the backslash in the given String. Replaces all occurrences of "\" with "\\".
|
|
|
|
* <p>
|
|
|
|
* According to RFC 2831 § 7.2 a quoted-string consists either of qdtext or quoted-pair. And since quoted-pair is a
|
|
|
|
* backslash followed by a char, every backslash in qdtext must be quoted, since it otherwise would be treated as
|
|
|
|
* qdtext.
|
|
|
|
* </p>
|
|
|
|
*
|
|
|
|
* @param string the input string.
|
|
|
|
* @return the input string where the every backslash is quoted.
|
|
|
|
*/
|
|
|
|
public static String quoteBackslash(String string) {
|
|
|
|
return string.replace("\\", "\\\\");
|
|
|
|
}
|
2014-08-01 10:34:47 +02:00
|
|
|
}
|