001/**
002 *
003 * Copyright 2014-2017 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smack.sasl.provided;
018
019import java.io.UnsupportedEncodingException;
020
021import javax.security.auth.callback.CallbackHandler;
022
023import org.jivesoftware.smack.SmackException;
024import org.jivesoftware.smack.sasl.SASLMechanism;
025import org.jivesoftware.smack.util.ByteUtils;
026import org.jivesoftware.smack.util.MD5;
027import org.jivesoftware.smack.util.StringUtils;
028
029public class SASLDigestMD5Mechanism extends SASLMechanism {
030
031    public static final String NAME = DIGESTMD5;
032
033    private static final String INITAL_NONCE = "00000001";
034
035    /**
036     * The only 'qop' value supported by this implementation
037     */
038    private static final String QOP_VALUE = "auth";
039
040    private enum State {
041        INITIAL,
042        RESPONSE_SENT,
043        VALID_SERVER_RESPONSE,
044    }
045
046    private static boolean verifyServerResponse = true;
047
048    public static void setVerifyServerResponse(boolean verifyServerResponse) {
049        SASLDigestMD5Mechanism.verifyServerResponse = verifyServerResponse;
050    }
051
052    /**
053     * The state of the this instance of SASL DIGEST-MD5 authentication.
054     */
055    private State state = State.INITIAL;
056
057    private String nonce;
058    private String cnonce;
059    private String digestUri;
060    private String hex_hashed_a1;
061
062    @Override
063    protected void authenticateInternal(CallbackHandler cbh) throws SmackException {
064        throw new UnsupportedOperationException("CallbackHandler not (yet) supported");
065    }
066
067    @Override
068    protected byte[] getAuthenticationText() throws SmackException {
069        // DIGEST-MD5 has no initial response, return null
070        return null;
071    }
072
073    @Override
074    public String getName() {
075        return NAME;
076    }
077
078    @Override
079    public int getPriority() {
080        return 210;
081    }
082
083    @Override
084    public SASLDigestMD5Mechanism newInstance() {
085        return new SASLDigestMD5Mechanism();
086    }
087
088    @Override
089    public boolean authzidSupported() {
090      return true;
091    }
092
093
094    @Override
095    public void checkIfSuccessfulOrThrow() throws SmackException {
096        if (verifyServerResponse && state != State.VALID_SERVER_RESPONSE) {
097            throw new SmackException(NAME + " no valid server response");
098        }
099    }
100
101    @Override
102    protected byte[] evaluateChallenge(byte[] challenge) throws SmackException {
103        if (challenge.length == 0) {
104            throw new SmackException("Initial challenge has zero length");
105        }
106        String challengeString;
107        try {
108            challengeString = new String(challenge, StringUtils.UTF8);
109        }
110        catch (UnsupportedEncodingException e) {
111            throw new AssertionError(e);
112        }
113        String[] challengeParts = challengeString.split(",");
114        byte[] response = null;
115        switch (state) {
116        case INITIAL:
117            for (String part : challengeParts) {
118                String[] keyValue = part.split("=", 2);
119                String key = keyValue[0];
120                // RFC 2831 § 7.1 about the formatting of the digest-challenge:
121                // "The full form is "<n>#<m>element" indicating at least <n> and
122                // at most <m> elements, each separated by one or more commas
123                // (",") and OPTIONAL linear white space (LWS)."
124                // Which means the key value may be preceded by whitespace,
125                // which is what we remove: *Only the preceding whitespace*.
126                key = key.replaceFirst("^\\s+", "");
127                String value = keyValue[1];
128                if ("nonce".equals(key)) {
129                    if (nonce != null) {
130                        throw new SmackException("Nonce value present multiple times");
131                    }
132                    nonce = value.replace("\"", "");
133                }
134                else if ("qop".equals(key)) {
135                    value = value.replace("\"", "");
136                    if (!value.equals("auth")) {
137                        throw new SmackException("Unsupported qop operation: " + value);
138                    }
139                }
140            }
141            if (nonce == null) {
142                // RFC 2831 2.1.1 about nonce "This directive is required and MUST appear exactly
143                // once; if not present, or if multiple instances are present, the client should
144                // abort the authentication exchange."
145                throw new SmackException("nonce value not present in initial challenge");
146            }
147            // RFC 2831 2.1.2.1 defines A1, A2, KD and response-value
148            byte[] a1FirstPart = MD5.bytes(authenticationId + ':' + serviceName + ':'
149                            + password);
150            cnonce = StringUtils.randomString(32);
151            byte[] a1 = ByteUtils.concat(a1FirstPart, toBytes(':' + nonce + ':' + cnonce));
152            digestUri = "xmpp/" + serviceName;
153            hex_hashed_a1 = StringUtils.encodeHex(MD5.bytes(a1));
154            String responseValue = calcResponse(DigestType.ClientResponse);
155            // @formatter:off
156            // See RFC 2831 2.1.2 digest-response
157            String authzid;
158            if (authorizationId == null) {
159              authzid = "";
160            } else {
161              authzid = ",authzid=\"" + authorizationId + '"';
162            }
163            String saslString = "username=\"" + quoteBackslash(authenticationId) + '"'
164                               + authzid
165                               + ",realm=\"" + serviceName + '"'
166                               + ",nonce=\"" + nonce + '"'
167                               + ",cnonce=\"" + cnonce + '"'
168                               + ",nc=" + INITAL_NONCE
169                               + ",qop=auth"
170                               + ",digest-uri=\"" + digestUri + '"'
171                               + ",response=" + responseValue
172                               + ",charset=utf-8";
173            // @formatter:on
174            response = toBytes(saslString);
175            state = State.RESPONSE_SENT;
176            break;
177        case RESPONSE_SENT:
178            if (verifyServerResponse) {
179                String serverResponse = null;
180                for (String part : challengeParts) {
181                    String[] keyValue = part.split("=");
182                    assert (keyValue.length == 2);
183                    String key = keyValue[0];
184                    String value = keyValue[1];
185                    if ("rspauth".equals(key)) {
186                        serverResponse = value;
187                        break;
188                    }
189                }
190                if (serverResponse == null) {
191                    throw new SmackException("No server response received while performing " + NAME
192                                    + " authentication");
193                }
194                String expectedServerResponse = calcResponse(DigestType.ServerResponse);
195                if (!serverResponse.equals(expectedServerResponse)) {
196                    throw new SmackException("Invalid server response  while performing " + NAME
197                                    + " authentication");
198                }
199            }
200            state = State.VALID_SERVER_RESPONSE;
201            break;
202        default:
203            throw new IllegalStateException();
204        }
205        return response;
206    }
207
208    private enum DigestType {
209        ClientResponse,
210        ServerResponse
211    }
212
213    private String calcResponse(DigestType digestType) {
214        StringBuilder a2 = new StringBuilder();
215        if (digestType == DigestType.ClientResponse) {
216            a2.append("AUTHENTICATE");
217        }
218        a2.append(':');
219        a2.append(digestUri);
220        String hex_hashed_a2 = StringUtils.encodeHex(MD5.bytes(a2.toString()));
221
222        StringBuilder kd_argument = new StringBuilder();
223        kd_argument.append(hex_hashed_a1);
224        kd_argument.append(':');
225        kd_argument.append(nonce);
226        kd_argument.append(':');
227        kd_argument.append(INITAL_NONCE);
228        kd_argument.append(':');
229        kd_argument.append(cnonce);
230        kd_argument.append(':');
231        kd_argument.append(QOP_VALUE);
232        kd_argument.append(':');
233        kd_argument.append(hex_hashed_a2);
234        byte[] kd = MD5.bytes(kd_argument.toString());
235        String responseValue = StringUtils.encodeHex(kd);
236        return responseValue;
237    }
238
239    /**
240     * Quote the backslash in the given String. Replaces all occurrences of "\" with "\\".
241     * <p>
242     * According to RFC 2831 § 7.2 a quoted-string consists either of qdtext or quoted-pair. And since quoted-pair is a
243     * backslash followed by a char, every backslash in qdtext must be quoted, since it otherwise would be treated as
244     * qdtext.
245     * </p>
246     *
247     * @param string the input string.
248     * @return the input string where the every backslash is quoted.
249     */
250    public static String quoteBackslash(String string) {
251        return string.replace("\\", "\\\\");
252    }
253}