001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.mina.proxy.handlers.http.digest;
021
022import java.io.UnsupportedEncodingException;
023import java.security.MessageDigest;
024import java.security.NoSuchAlgorithmException;
025import java.util.HashMap;
026
027import javax.security.sasl.AuthenticationException;
028
029import org.apache.mina.core.session.IoSession;
030import org.apache.mina.proxy.session.ProxyIoSession;
031import org.apache.mina.proxy.utils.ByteUtilities;
032import org.apache.mina.proxy.utils.StringUtilities;
033
034/**
035 * DigestUtilities.java - A class supporting the HTTP DIGEST authentication (see RFC 2617).
036 * 
037 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
038 * @since MINA 2.0.0-M3
039 */
040public class DigestUtilities {
041
042    public final static String SESSION_HA1 = DigestUtilities.class + ".SessionHA1";
043
044    private static MessageDigest md5;
045
046    static {
047        // Initialize secure random generator 
048        try {
049            md5 = MessageDigest.getInstance("MD5");
050        } catch (NoSuchAlgorithmException e) {
051            throw new RuntimeException(e);
052        }
053    }
054
055    /**
056     * The supported qualities of protections.
057     */
058    public final static String[] SUPPORTED_QOPS = new String[] { "auth", "auth-int" };
059
060    /**
061     * Computes the response to the DIGEST challenge.
062     * 
063     * @param session the current session
064     * @param map the map holding the directives sent by the proxy
065     * @param method the HTTP verb
066     * @param pwd the password
067     * @param charsetName the name of the charset used for the challenge
068     * @param body the html body to be hashed for integrity calculations
069     * @return The response
070     * @throws AuthenticationException if we weren't able to find a directive value in the map
071     * @throws UnsupportedEncodingException If we weren't able to encode to ISO 8859_1 the username or realm,
072     * or if we weren't able to encode the charsetName
073     */
074    public static String computeResponseValue(IoSession session, HashMap<String, String> map, String method,
075            String pwd, String charsetName, String body) throws AuthenticationException, UnsupportedEncodingException{
076
077        byte[] hA1;
078        StringBuilder sb;
079        boolean isMD5Sess = "md5-sess".equalsIgnoreCase(StringUtilities.getDirectiveValue(map, "algorithm", false));
080
081        if (!isMD5Sess || (session.getAttribute(SESSION_HA1) == null)) {
082            // Build A1
083            sb = new StringBuilder();
084            sb.append(StringUtilities.stringTo8859_1(StringUtilities.getDirectiveValue(map, "username", true))).append(
085                    ':');
086
087            String realm = StringUtilities.stringTo8859_1(StringUtilities.getDirectiveValue(map, "realm", false));
088            
089            if (realm != null) {
090                sb.append(realm);
091            }
092
093            sb.append(':').append(pwd);
094
095            if (isMD5Sess) {
096                byte[] prehA1;
097                
098                synchronized (md5) {
099                    md5.reset();
100                    prehA1 = md5.digest(sb.toString().getBytes(charsetName));
101                }
102
103                sb = new StringBuilder();
104                sb.append(ByteUtilities.asHex(prehA1));
105                sb.append(':').append(
106                        StringUtilities.stringTo8859_1(StringUtilities.getDirectiveValue(map, "nonce", true)));
107                sb.append(':').append(
108                        StringUtilities.stringTo8859_1(StringUtilities.getDirectiveValue(map, "cnonce", true)));
109
110                synchronized (md5) {
111                    md5.reset();
112                    hA1 = md5.digest(sb.toString().getBytes(charsetName));
113                }
114
115                session.setAttribute(SESSION_HA1, hA1);
116            } else {
117                synchronized (md5) {
118                    md5.reset();
119                    hA1 = md5.digest(sb.toString().getBytes(charsetName));
120                }
121            }
122        } else {
123            hA1 = (byte[]) session.getAttribute(SESSION_HA1);
124        }
125
126        sb = new StringBuilder(method);
127        sb.append(':');
128        sb.append(StringUtilities.getDirectiveValue(map, "uri", false));
129
130        String qop = StringUtilities.getDirectiveValue(map, "qop", false);
131        
132        if ("auth-int".equalsIgnoreCase(qop)) {
133            ProxyIoSession proxyIoSession = (ProxyIoSession) session.getAttribute(ProxyIoSession.PROXY_SESSION);
134            byte[] hEntity;
135
136            synchronized (md5) {
137                md5.reset();
138                hEntity = md5.digest(body.getBytes(proxyIoSession.getCharsetName()));
139            }
140            
141            sb.append(':').append(hEntity);
142        }
143
144        byte[] hA2;
145        synchronized (md5) {
146            md5.reset();
147            hA2 = md5.digest(sb.toString().getBytes(charsetName));
148        }
149
150        sb = new StringBuilder();
151        sb.append(ByteUtilities.asHex(hA1));
152        sb.append(':').append(StringUtilities.getDirectiveValue(map, "nonce", true));
153        sb.append(":00000001:");
154
155        sb.append(StringUtilities.getDirectiveValue(map, "cnonce", true));
156        sb.append(':').append(qop).append(':');
157        sb.append(ByteUtilities.asHex(hA2));
158
159        byte[] hFinal;
160        
161        synchronized (md5) {
162            md5.reset();
163            hFinal = md5.digest(sb.toString().getBytes(charsetName));
164        }
165
166        return ByteUtilities.asHex(hFinal);
167    }
168}