SASLDigestMD5Mechanism.java

  1. /**
  2.  *
  3.  * Copyright 2014 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 javax.security.auth.callback.CallbackHandler;

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

  24. public class SASLDigestMD5Mechanism extends SASLMechanism {

  25.     public static final String NAME = DIGESTMD5;

  26.     private static final String INITAL_NONCE = "00000001";

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

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

  36.     private static boolean verifyServerResponse = true;

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

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

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

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

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

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

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

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


  69.     @Override
  70.     public void checkIfSuccessfulOrThrow() throws SmackException {
  71.         if (verifyServerResponse && state != State.VALID_SERVER_RESPONSE) {
  72.             throw new SmackException(NAME + " no valid server response");
  73.         }
  74.     }

  75.     @Override
  76.     protected byte[] evaluateChallenge(byte[] challenge) throws SmackException {
  77.         if (challenge.length == 0) {
  78.             throw new SmackException("Initial challenge has zero length");
  79.         }
  80.         String[] challengeParts = (new String(challenge)).split(",");
  81.         byte[] response = null;
  82.         switch (state) {
  83.         case INITIAL:
  84.             for (String part : challengeParts) {
  85.                 String[] keyValue = part.split("=");
  86.                 assert (keyValue.length == 2);
  87.                 String key = keyValue[0];
  88.                 String value = keyValue[1];
  89.                 if ("nonce".equals(key)) {
  90.                     if (nonce != null) {
  91.                         throw new SmackException("Nonce value present multiple times");
  92.                     }
  93.                     nonce = value.replace("\"", "");
  94.                 }
  95.                 else if ("qop".equals(key)) {
  96.                     value = value.replace("\"", "");
  97.                     if (!value.equals("auth")) {
  98.                         throw new SmackException("Unsupported qop operation: " + value);
  99.                     }
  100.                 }
  101.             }
  102.             if (nonce == null) {
  103.                 // RFC 2831 2.1.1 about nonce "This directive is required and MUST appear exactly
  104.                 // once; if not present, or if multiple instances are present, the client should
  105.                 // abort the authentication exchange."
  106.                 throw new SmackException("nonce value not present in initial challenge");
  107.             }
  108.             // RFC 2831 2.1.2.1 defines A1, A2, KD and response-value
  109.             byte[] a1FirstPart = MD5.bytes(authenticationId + ':' + serviceName + ':'
  110.                             + password);
  111.             cnonce = StringUtils.randomString(32);
  112.             byte[] a1 = ByteUtils.concact(a1FirstPart, toBytes(':' + nonce + ':' + cnonce));
  113.             digestUri = "xmpp/" + serviceName;
  114.             hex_hashed_a1 = StringUtils.encodeHex(MD5.bytes(a1));
  115.             String responseValue = calcResponse(DigestType.ClientResponse);
  116.             // @formatter:off
  117.             // See RFC 2831 2.1.2 digest-response
  118.             String saslString = "username=\"" + authenticationId + '"'
  119.                                + ",realm=\"" + serviceName + '"'
  120.                                + ",nonce=\"" + nonce + '"'
  121.                                + ",cnonce=\"" + cnonce + '"'
  122.                                + ",nc=" + INITAL_NONCE
  123.                                + ",qop=auth"
  124.                                + ",digest-uri=\"" + digestUri + '"'
  125.                                + ",response=" + responseValue
  126.                                + ",charset=utf-8";
  127.             // @formatter:on
  128.             response = toBytes(saslString);
  129.             state = State.RESPONSE_SENT;
  130.             break;
  131.         case RESPONSE_SENT:
  132.             if (verifyServerResponse) {
  133.                 String serverResponse = null;
  134.                 for (String part : challengeParts) {
  135.                     String[] keyValue = part.split("=");
  136.                     assert (keyValue.length == 2);
  137.                     String key = keyValue[0];
  138.                     String value = keyValue[1];
  139.                     if ("rspauth".equals(key)) {
  140.                         serverResponse = value;
  141.                         break;
  142.                     }
  143.                 }
  144.                 if (serverResponse == null) {
  145.                     throw new SmackException("No server response received while performing " + NAME
  146.                                     + " authentication");
  147.                 }
  148.                 String expectedServerResponse = calcResponse(DigestType.ServerResponse);
  149.                 if (!serverResponse.equals(expectedServerResponse)) {
  150.                     throw new SmackException("Invalid server response  while performing " + NAME
  151.                                     + " authentication");
  152.                 }
  153.             }
  154.             state = State.VALID_SERVER_RESPONSE;
  155.             break;
  156.         default:
  157.             throw new IllegalStateException();
  158.         }
  159.         return response;
  160.     }

  161.     private enum DigestType {
  162.         ClientResponse,
  163.         ServerResponse
  164.     }

  165.     private String calcResponse(DigestType digestType) {
  166.         StringBuilder a2 = new StringBuilder();
  167.         if (digestType == DigestType.ClientResponse) {
  168.             a2.append("AUTHENTICATE");
  169.         }
  170.         a2.append(':');
  171.         a2.append(digestUri);
  172.         String hex_hashed_a2 = StringUtils.encodeHex(MD5.bytes(a2.toString()));

  173.         StringBuilder kd_argument = new StringBuilder();
  174.         kd_argument.append(hex_hashed_a1);
  175.         kd_argument.append(':');
  176.         kd_argument.append(nonce);
  177.         kd_argument.append(':');
  178.         kd_argument.append(INITAL_NONCE);
  179.         kd_argument.append(':');
  180.         kd_argument.append(cnonce);
  181.         kd_argument.append(':');
  182.         kd_argument.append(QOP_VALUE);
  183.         kd_argument.append(':');
  184.         kd_argument.append(hex_hashed_a2);
  185.         byte[] kd = MD5.bytes(kd_argument.toString());
  186.         String responseValue = StringUtils.encodeHex(kd);
  187.         return responseValue;
  188.     }

  189. }