SASLDigestMD5Mechanism.java

  1. /**
  2.  *
  3.  * Copyright 2014-2019 Florian Schmaus
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smack.sasl.provided;

  18. import java.nio.charset.StandardCharsets;

  19. import javax.security.auth.callback.CallbackHandler;

  20. import org.jivesoftware.smack.SmackException.SmackSaslException;
  21. import org.jivesoftware.smack.sasl.SASLMechanism;
  22. import org.jivesoftware.smack.util.ByteUtils;
  23. import org.jivesoftware.smack.util.MD5;
  24. import org.jivesoftware.smack.util.StringUtils;

  25. public class SASLDigestMD5Mechanism extends SASLMechanism {

  26.     public static final String NAME = DIGESTMD5;

  27.     private static final String INITIAL_NONCE = "00000001";

  28.     /**
  29.      * The only 'qop' value supported by this implementation
  30.      */
  31.     private static final String QOP_VALUE = "auth";

  32.     private enum State {
  33.         INITIAL,
  34.         RESPONSE_SENT,
  35.         VALID_SERVER_RESPONSE,
  36.     }

  37.     private static boolean verifyServerResponse = true;

  38.     public static void setVerifyServerResponse(boolean verifyServerResponse) {
  39.         SASLDigestMD5Mechanism.verifyServerResponse = verifyServerResponse;
  40.     }

  41.     /**
  42.      * The state of the this instance of SASL DIGEST-MD5 authentication.
  43.      */
  44.     private State state = State.INITIAL;

  45.     private String nonce;
  46.     private String cnonce;
  47.     private String digestUri;
  48.     private String hex_hashed_a1;

  49.     @Override
  50.     protected void authenticateInternal(CallbackHandler cbh) {
  51.         throw new UnsupportedOperationException("CallbackHandler not (yet) supported");
  52.     }

  53.     @Override
  54.     protected byte[] getAuthenticationText() {
  55.         // DIGEST-MD5 has no initial response, return null
  56.         return null;
  57.     }

  58.     @Override
  59.     public String getName() {
  60.         return NAME;
  61.     }

  62.     @Override
  63.     public int getPriority() {
  64.         return 210;
  65.     }

  66.     @Override
  67.     public SASLDigestMD5Mechanism newInstance() {
  68.         return new SASLDigestMD5Mechanism();
  69.     }

  70.     @Override
  71.     public boolean authzidSupported() {
  72.       return true;
  73.     }


  74.     @Override
  75.     public void checkIfSuccessfulOrThrow() throws SmackSaslException {
  76.         if (verifyServerResponse && state != State.VALID_SERVER_RESPONSE) {
  77.             throw new SmackSaslException(NAME + " no valid server response");
  78.         }
  79.     }

  80.     @Override
  81.     protected byte[] evaluateChallenge(byte[] challenge) throws SmackSaslException {
  82.         if (challenge.length == 0) {
  83.             throw new SmackSaslException("Initial challenge has zero length");
  84.         }
  85.         String challengeString = new String(challenge, StandardCharsets.UTF_8);
  86.         String[] challengeParts = challengeString.split(",");
  87.         byte[] response = null;
  88.         switch (state) {
  89.         case INITIAL:
  90.             for (String part : challengeParts) {
  91.                 String[] keyValue = part.split("=", 2);
  92.                 String key = keyValue[0];
  93.                 // RFC 2831 § 7.1 about the formatting of the digest-challenge:
  94.                 // "The full form is "<n>#<m>element" indicating at least <n> and
  95.                 // at most <m> elements, each separated by one or more commas
  96.                 // (",") and OPTIONAL linear white space (LWS)."
  97.                 // Which means the key value may be preceded by whitespace,
  98.                 // which is what we remove: *Only the preceding whitespace*.
  99.                 key = key.replaceFirst("^\\s+", "");
  100.                 String value = keyValue[1];
  101.                 if ("nonce".equals(key)) {
  102.                     if (nonce != null) {
  103.                         throw new SmackSaslException("Nonce value present multiple times");
  104.                     }
  105.                     nonce = value.replace("\"", "");
  106.                 }
  107.                 else if ("qop".equals(key)) {
  108.                     value = value.replace("\"", "");
  109.                     if (!value.equals("auth")) {
  110.                         throw new SmackSaslException("Unsupported qop operation: " + value);
  111.                     }
  112.                 }
  113.             }
  114.             if (nonce == null) {
  115.                 // RFC 2831 2.1.1 about nonce "This directive is required and MUST appear exactly
  116.                 // once; if not present, or if multiple instances are present, the client should
  117.                 // abort the authentication exchange."
  118.                 throw new SmackSaslException("nonce value not present in initial challenge");
  119.             }
  120.             // RFC 2831 2.1.2.1 defines A1, A2, KD and response-value
  121.             byte[] a1FirstPart = MD5.bytes(authenticationId + ':' + serviceName + ':'
  122.                             + password);
  123.             cnonce = StringUtils.randomString(32);
  124.             byte[] a1 = ByteUtils.concat(a1FirstPart, toBytes(':' + nonce + ':' + cnonce));
  125.             digestUri = "xmpp/" + serviceName;
  126.             hex_hashed_a1 = StringUtils.encodeHex(MD5.bytes(a1));
  127.             String responseValue = calcResponse(DigestType.ClientResponse);
  128.             // @formatter:off
  129.             // See RFC 2831 2.1.2 digest-response
  130.             String authzid;
  131.             if (authorizationId == null) {
  132.               authzid = "";
  133.             } else {
  134.               authzid = ",authzid=\"" + authorizationId + '"';
  135.             }
  136.             String saslString = "username=\"" + quoteBackslash(authenticationId) + '"'
  137.                                + authzid
  138.                                + ",realm=\"" + serviceName + '"'
  139.                                + ",nonce=\"" + nonce + '"'
  140.                                + ",cnonce=\"" + cnonce + '"'
  141.                                + ",nc=" + INITIAL_NONCE
  142.                                + ",qop=auth"
  143.                                + ",digest-uri=\"" + digestUri + '"'
  144.                                + ",response=" + responseValue
  145.                                + ",charset=utf-8";
  146.             // @formatter:on
  147.             response = toBytes(saslString);
  148.             state = State.RESPONSE_SENT;
  149.             break;
  150.         case RESPONSE_SENT:
  151.             if (verifyServerResponse) {
  152.                 String serverResponse = null;
  153.                 for (String part : challengeParts) {
  154.                     String[] keyValue = part.split("=");
  155.                     assert keyValue.length == 2;
  156.                     String key = keyValue[0];
  157.                     String value = keyValue[1];
  158.                     if ("rspauth".equals(key)) {
  159.                         serverResponse = value;
  160.                         break;
  161.                     }
  162.                 }
  163.                 if (serverResponse == null) {
  164.                     throw new SmackSaslException("No server response received while performing " + NAME
  165.                                     + " authentication");
  166.                 }
  167.                 String expectedServerResponse = calcResponse(DigestType.ServerResponse);
  168.                 if (!serverResponse.equals(expectedServerResponse)) {
  169.                     throw new SmackSaslException("Invalid server response  while performing " + NAME
  170.                                     + " authentication");
  171.                 }
  172.             }
  173.             state = State.VALID_SERVER_RESPONSE;
  174.             break;
  175.         default:
  176.             throw new IllegalStateException();
  177.         }
  178.         return response;
  179.     }

  180.     private enum DigestType {
  181.         ClientResponse,
  182.         ServerResponse
  183.     }

  184.     private String calcResponse(DigestType digestType) {
  185.         StringBuilder a2 = new StringBuilder();
  186.         if (digestType == DigestType.ClientResponse) {
  187.             a2.append("AUTHENTICATE");
  188.         }
  189.         a2.append(':');
  190.         a2.append(digestUri);
  191.         String hex_hashed_a2 = StringUtils.encodeHex(MD5.bytes(a2.toString()));

  192.         StringBuilder kd_argument = new StringBuilder();
  193.         kd_argument.append(hex_hashed_a1);
  194.         kd_argument.append(':');
  195.         kd_argument.append(nonce);
  196.         kd_argument.append(':');
  197.         kd_argument.append(INITIAL_NONCE);
  198.         kd_argument.append(':');
  199.         kd_argument.append(cnonce);
  200.         kd_argument.append(':');
  201.         kd_argument.append(QOP_VALUE);
  202.         kd_argument.append(':');
  203.         kd_argument.append(hex_hashed_a2);
  204.         byte[] kd = MD5.bytes(kd_argument.toString());
  205.         String responseValue = StringUtils.encodeHex(kd);
  206.         return responseValue;
  207.     }

  208.     /**
  209.      * Quote the backslash in the given String. Replaces all occurrences of "\" with "\\".
  210.      * <p>
  211.      * According to RFC 2831 § 7.2 a quoted-string consists either of qdtext or quoted-pair. And since quoted-pair is a
  212.      * backslash followed by a char, every backslash in qdtext must be quoted, since it otherwise would be treated as
  213.      * qdtext.
  214.      * </p>
  215.      *
  216.      * @param string the input string.
  217.      * @return the input string where the every backslash is quoted.
  218.      */
  219.     public static String quoteBackslash(String string) {
  220.         return string.replace("\\", "\\\\");
  221.     }
  222. }