/* See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * Esri Inc. licenses this file to You 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 com.esri.gpt.framework.security.codec; import com.esri.gpt.framework.security.credentials.UsernamePasswordCredentials; import com.esri.gpt.framework.util.LogUtil; import com.esri.gpt.framework.util.Val; import java.net.HttpURLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; /** * Digest authenticator. */ public class Digest { // class variables ============================================================= /** digest signature */ private static final String DIGEST_SIGNATURE = "Digest"; /** authenticate attribute */ private static final String AUTHENTICATE_ATTR = "WWW-Authenticate"; /** authorization attribute */ private static final String AUTHORIZATION_ATTR = "Authorization"; /** algorithm key */ private static final String ALGORITHM_KEY = "algorithm"; /** user name key */ private static final String USERNAME_KEY = "username"; /** cnonce key */ private static final String CNONCE_KEY = "cnonce"; /** uri key */ private static final String URI_KEY = "uri"; /** nc key */ private static final String NC_KEY = "nc"; /** nonce key */ private static final String NONCE_KEY = "nonce"; /** qop key */ private static final String QOP_KEY = "qop"; /** realm key */ private static final String REALM_KEY = "realm"; /** opaque key */ private static final String OPAQUE_KEY = "opaque"; /** response key */ private static final String RESPONSE_KEY = "response"; // instance variables ========================================================== /** attributes */ private Map<String, String> _attrs = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER); /** indicates if response is valid */ private boolean _valid; // constructors ================================================================ /** * Creates instance of the digest. */ private Digest() { } // properties ================================================================== /** * Checks if digest has been found and extracted from the connection. * @return <code>true</code> if digest digest has been extracted from the * connection */ public boolean isValid() { return _valid; } /** * Sets <i>valid</i> flag. * @param valid valid flag */ private void setValid(boolean valid) { _valid = valid; } // methods ===================================================================== /** * Extracts digest from the connection. * If digest has been successfuly extracted, returned digest will have * <i>valid</i> flag set to <code>true</code>. * @param connection HTTP connection * @return digest */ public static Digest extractFrom(HttpURLConnection connection) { Digest digResp = new Digest(); if (connection != null) { String sAuthenticate = extractAuthenticateAttribute(connection); if (sAuthenticate.startsWith(DIGEST_SIGNATURE)) { digResp.setValid(true); sAuthenticate = sAuthenticate.substring(DIGEST_SIGNATURE.length()).trim(); String[] sParams = sAuthenticate.split(","); for (String sParam : sParams) { String[] sNameValue = extractAttribute(sParam); if (sNameValue.length == 2) { digResp.put(sNameValue[0], sNameValue[1]); } } } } return digResp; } /** * Injects digest response into connection. * @param connection HTTP connection * @param credentials credentials to embede within digest request */ public void injectTo(HttpURLConnection connection, UsernamePasswordCredentials credentials) { if (connection != null && credentials != null) { String uri = connection.getURL().getFile(); int idx = uri.indexOf('?'); if (idx != -1) { uri = uri.substring(0, idx); } String cnonce = Integer.toHexString(new java.util.Random().nextInt()); // create complementory digest Digest digest = new Digest(); // create obligatory attributes digest.put(USERNAME_KEY, credentials.getUsername()); digest.put(CNONCE_KEY, cnonce); digest.put(URI_KEY, uri); digest.put(NC_KEY, "00000001"); digest.put(QOP_KEY, "auth"); // copy attributes from current digest digest.put(ALGORITHM_KEY, get(ALGORITHM_KEY)); digest.put(NONCE_KEY, get(NONCE_KEY)); digest.put(REALM_KEY, get(REALM_KEY)); digest.put(OPAQUE_KEY, get(OPAQUE_KEY)); // compute and attach response try { digest.calculateResponse( Val.chkStr(connection.getRequestMethod(),"GET"), credentials); } catch (NoSuchAlgorithmException ex) { LogUtil.getLogger().severe( "Invalid digest algorithm: " + ex.getMessage()); digest.put(RESPONSE_KEY, ""); } digest.storeAuthorization(connection); } } /** * Creates string reprezentation of the digest. * @return string reprezentation of the digest */ @Override public String toString() { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : _attrs.entrySet()) { if (sb.length() > 0) { sb.append(", "); } if (entry.getKey().equalsIgnoreCase(NC_KEY)) { sb.append(entry.getKey()); sb.append("="); sb.append(entry.getValue()); } else { sb.append(entry.getKey()); sb.append("=\""); sb.append(entry.getValue()); sb.append("\""); } } return DIGEST_SIGNATURE + " " + sb.toString(); } /** * Stores attribute. * @param attr attribute name * @param value attribute value */ private void put(String attr, String value) { _attrs.put(attr, value); } /** * Gets attribute. * @param attr attribute name * @return attribute value or empty string if not available */ private String get(String attr) { return Val.chkStr(_attrs.get(attr)); } /** * Calculates response. * @param method HTTP method ("GET" or "POST") * @param credentials credentials used to calculate response attribute * @throws java.security.NoSuchAlgorithmException if invalid digest algorithm */ private void calculateResponse(String method, UsernamePasswordCredentials credentials) throws NoSuchAlgorithmException { String algorithm = Val.chkStr(get(ALGORITHM_KEY), "MD5").toUpperCase(); MessageDigest md = MessageDigest.getInstance(algorithm); String HA1 = encrypt(md, credentials.getUsername() + ":" + get(REALM_KEY) + ":" + credentials.getPassword()); String HA2 = encrypt(md, method + ":" + get(URI_KEY)); String content = HA1 + ":" + get(NONCE_KEY) + ":" + get(NC_KEY) + ":" + get(CNONCE_KEY) + ":" + get(QOP_KEY) + ":" + HA2; String response = encrypt(md, content); put(RESPONSE_KEY, response); } /** * Encrypt data using digest. * @param md message digest * @param data data to encrypt * @return encrypted data */ private String encrypt(MessageDigest md, String data) { md.reset(); md.update(data.getBytes()); byte[] bytes = md.digest(); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { String bhex = Integer.toHexString(b >= 0 ? b : 256 + b); bhex = bhex.substring(bhex.length() > 2 ? bhex.length() - 2 : 0); while (bhex.length() < 2) { bhex = "0" + bhex; } sb.append(bhex); } return sb.toString(); } /** * Extracts attribute name-value pair. * @param param param containing attribute definition * @return two dimensional array of strings having name-value pair */ private static String[] extractAttribute(String param) { param = Val.chkStr(param); String[] sNameValue = param.split("="); for (int i = 0; i < sNameValue.length; i++) { sNameValue[i] = sNameValue[i].replaceAll("^\\p{Blank}*\"|\"\\p{Blank}*$", "").trim(); } return sNameValue; } /** * Extracts authentiate attribute from the connection. * @param connection HTTP connection * @return authenticate attribute or empty string if not availabale */ private static String extractAuthenticateAttribute(HttpURLConnection connection) { Map<String, List<String>> headerFields = new TreeMap<String, List<String>>(new Comparator<String>(){ @Override public int compare(String o1, String o2) { if (o1==null) return -1; if (o2==null) return 1; return o1.compareToIgnoreCase(o2); } }); headerFields.putAll(connection.getHeaderFields()); List<String> wwwAuthenticateList = headerFields.get(AUTHENTICATE_ATTR); if (wwwAuthenticateList != null && wwwAuthenticateList.size() > 0) { return Val.chkStr(wwwAuthenticateList.get(0)); } return ""; } /** * Creates string reprezentation of the digest. * @return string reprezentation of the digest */ private String toDigest() { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : _attrs.entrySet()) { if (sb.length() > 0) { sb.append(", "); } if (entry.getKey().equalsIgnoreCase(NC_KEY)) { sb.append(entry.getKey()); sb.append("="); sb.append(entry.getValue()); } else { sb.append(entry.getKey()); sb.append("=\""); sb.append(entry.getValue()); sb.append("\""); } } return DIGEST_SIGNATURE + " " + sb.toString(); } /** * Stores itself as authorization * @param connection */ private void storeAuthorization(HttpURLConnection connection) { connection.setRequestProperty(AUTHORIZATION_ATTR, toDigest()); } }