/* Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. The MySQL Connector/J is licensed under the terms of the GPLv2 , like most MySQL Connectors. There are special exceptions to the terms and conditions of the GPLv2 as it is applied to this software, see the FOSS License Exception . This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package com.mysql.fabric.proto.xmlrpc; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Random; /** * HTTP/1.1 Digest Authentication - RFC 2617 */ public class DigestAuthentication { private static Random random = new Random(); /** * Get the digest challenge header by connecting to the resource * with no credentials. */ public static String getChallengeHeader(String url) throws IOException { HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setDoOutput(true); conn.getOutputStream().close(); try { conn.getInputStream().close(); } catch (IOException ex) { if (401 == conn.getResponseCode()) { // we expect a 401-unauthorized response with the // WWW-Authenticate header to create the request with the // necessary auth data String hdr = conn.getHeaderField("WWW-Authenticate"); if (hdr != null && !"".equals(hdr)) { return hdr; } } else if (400 == conn.getResponseCode()) { // 400 usually means that auth is disabled on the Fabric node throw new IOException("Fabric returns status 400. If authentication is disabled on the Fabric node, " + "omit the `fabricUsername' and `fabricPassword' properties from your connection."); } else { throw ex; } } return null; } /** * Calculate the request digest for algorithm=MD5. */ public static String calculateMD5RequestDigest(String uri, String username, String password, String realm, String nonce, String nc, String cnonce, String qop) { String reqA1 = username + ":" + realm + ":" + password; // valid only for qop="auth" String reqA2 = "POST:" + uri; String hashA1 = checksumMD5(reqA1); String hashA2 = checksumMD5(reqA2); String requestDigest = digestMD5(hashA1, nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + hashA2); return requestDigest; } /** * MD5 version of the "H()" function from rfc2617. */ private static String checksumMD5(String data) { MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException ex) { throw new RuntimeException("Unable to create MD5 instance", ex); } // TODO encoding return hexEncode(md5.digest(data.getBytes())); } /** * MD5 version of the "KD()" function from rfc2617. */ private static String digestMD5(String secret, String data) { return checksumMD5(secret + ":" + data); } /** * hex-encode a byte array */ private static String hexEncode(byte data[]) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < data.length; ++i) { sb.append(String.format("%02x", data[i])); } return sb.toString(); } /** * Serialize a parameter map into a digest response. This is used * as the "Authorization" header in the request. All parameters in * the supplied map will be added to the header. */ public static String serializeDigestResponse(Map paramMap) { StringBuilder sb = new StringBuilder("Digest "); boolean prefixComma = false; for (Map.Entry entry : paramMap.entrySet()) { if (!prefixComma) { prefixComma = true; } else { sb.append(", "); } sb.append(entry.getKey()); sb.append("="); sb.append(entry.getValue()); } return sb.toString(); } /** * Parse a digest challenge from the WWW-Authenticate header * return as the initial response during the authentication * exchange. */ public static Map parseDigestChallenge(String headerValue) { if (!headerValue.startsWith("Digest ")) { throw new IllegalArgumentException("Header is not a digest challenge"); } String params = headerValue.substring(7); Map paramMap = new HashMap(); for (String param : params.split(",\\s*")) { String pieces[] = param.split("="); paramMap.put(pieces[0], pieces[1].replaceAll("^\"(.*)\"$", "$1")); } return paramMap; } /** * Generate the cnonce value. This allows the client provide a * value used in the digest calculation. Same as Python. (no * motivation given for this algorithm) */ @SuppressWarnings("deprecation") public static String generateCnonce(String nonce, String nc) { // Random string, keep it in basic printable ASCII range byte buf[] = new byte[8]; random.nextBytes(buf); for (int i = 0; i < 8; ++i) { buf[i] = (byte) (0x20 + (buf[i] % 95)); } String combo = String.format("%s:%s:%s:%s", nonce, nc, new Date().toGMTString(), new String(buf)); MessageDigest sha1 = null; try { sha1 = MessageDigest.getInstance("SHA-1"); } catch (NoSuchAlgorithmException ex) { throw new RuntimeException("Unable to create SHA-1 instance", ex); } return hexEncode(sha1.digest(combo.getBytes())); } /** * Quote a parameter to be included in the header. Parameters with * embedded quotes will be rejected. */ private static String quoteParam(String param) { if (param.contains("\"") || param.contains("'")) { throw new IllegalArgumentException("Invalid character in parameter"); } return "\"" + param + "\""; } /** * Generate the Authorization header to make the authenticated * request. */ public static String generateAuthorizationHeader(Map digestChallenge, String username, String password) { String nonce = digestChallenge.get("nonce"); String nc = "00000001"; String cnonce = generateCnonce(nonce, nc); String qop = "auth"; String uri = "/RPC2"; String realm = digestChallenge.get("realm"); String opaque = digestChallenge.get("opaque"); String requestDigest = calculateMD5RequestDigest(uri, username, password, realm, nonce, nc, cnonce, qop); Map digestResponseMap = new HashMap(); digestResponseMap.put("algorithm", "MD5"); digestResponseMap.put("username", quoteParam(username)); digestResponseMap.put("realm", quoteParam(realm)); digestResponseMap.put("nonce", quoteParam(nonce)); digestResponseMap.put("uri", quoteParam(uri)); digestResponseMap.put("qop", qop); digestResponseMap.put("nc", nc); digestResponseMap.put("cnonce", quoteParam(cnonce)); digestResponseMap.put("response", quoteParam(requestDigest)); digestResponseMap.put("opaque", quoteParam(opaque)); return serializeDigestResponse(digestResponseMap); } }