package com.android.hotspot2.utils; import android.util.Base64; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; public class HTTPRequest implements HTTPMessage { private static final Charset HeaderCharset = StandardCharsets.US_ASCII; private static final int HTTPS_PORT = 443; private final String mMethodLine; private final Map<String, String> mHeaderFields; private final byte[] mBody; public HTTPRequest(Method method, URL url) { this(null, null, method, url, null, false); } public HTTPRequest(String payload, Charset charset, Method method, URL url, String contentType, boolean base64) { mBody = payload != null ? payload.getBytes(charset) : null; mHeaderFields = new LinkedHashMap<>(); mHeaderFields.put(AgentHeader, AgentName); if (url.getPort() != HTTPS_PORT) { mHeaderFields.put(HostHeader, url.getHost() + ':' + url.getPort()); } else { mHeaderFields.put(HostHeader, url.getHost()); } mHeaderFields.put(AcceptHeader, "*/*"); if (payload != null) { if (base64) { mHeaderFields.put(ContentTypeHeader, contentType); mHeaderFields.put(ContentEncodingHeader, "base64"); } else { mHeaderFields.put(ContentTypeHeader, contentType + "; charset=" + charset.displayName().toLowerCase()); } mHeaderFields.put(ContentLengthHeader, Integer.toString(mBody.length)); } mMethodLine = method.name() + ' ' + url.getPath() + ' ' + HTTPVersion + CRLF; } public void doAuthenticate(HTTPResponse httpResponse, String userName, byte[] password, URL url, int sequence) throws IOException, GeneralSecurityException { mHeaderFields.put(HTTPMessage.AuthorizationHeader, generateAuthAnswer(httpResponse, userName, password, url, sequence)); } private static String generateAuthAnswer(HTTPResponse httpResponse, String userName, byte[] password, URL url, int sequence) throws IOException, GeneralSecurityException { String authRequestLine = httpResponse.getHeader(HTTPMessage.AuthHeader); if (authRequestLine == null) { throw new IOException("Missing auth line"); } String[] tokens = authRequestLine.split("[ ,]+"); //System.out.println("Tokens: " + Arrays.toString(tokens)); if (tokens.length < 3 || !tokens[0].equalsIgnoreCase("digest")) { throw new IOException("Bad " + HTTPMessage.AuthHeader + ": '" + authRequestLine + "'"); } Map<String, String> itemMap = new HashMap<>(); for (int n = 1; n < tokens.length; n++) { String s = tokens[n]; int split = s.indexOf('='); if (split < 0) { continue; } itemMap.put(s.substring(0, split).trim().toLowerCase(), unquote(s.substring(split + 1).trim())); } Set<String> qops = splitValue(itemMap.remove("qop")); if (!qops.contains("auth")) { throw new IOException("Unsupported quality of protection value(s): '" + qops + "'"); } String algorithm = itemMap.remove("algorithm"); if (algorithm != null && !algorithm.equalsIgnoreCase("md5")) { throw new IOException("Unsupported algorithm: '" + algorithm + "'"); } String realm = itemMap.remove("realm"); String nonceText = itemMap.remove("nonce"); if (realm == null || nonceText == null) { throw new IOException("realm and/or nonce missing: '" + authRequestLine + "'"); } //System.out.println("Remaining tokens: " + itemMap); byte[] cnonce = new byte[16]; SecureRandom prng = new SecureRandom(); prng.nextBytes(cnonce); /* * H(data) = MD5(data) * KD(secret, data) = H(concat(secret, ":", data)) * * A1 = unq(username-value) ":" unq(realm-value) ":" passwd * A2 = Method ":" digest-uri-value * * response = KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" * unq(qop-value) ":" H(A2) ) */ String nc = String.format("%08d", sequence); /* * This bears witness to the ingenuity of the emerging "web generation" and the authors of * RFC-2617: Strings are treated as a sequence of octets in blind ignorance of character * encoding, whereas octets strings apparently aren't "good enough" and expanded to * "hex strings"... * As a wild guess I apply UTF-8 below. */ String passwordString = new String(password, StandardCharsets.UTF_8); String cNonceString = bytesToHex(cnonce); byte[] a1 = hash(userName, realm, passwordString); byte[] a2 = hash("POST", url.getPath()); byte[] response = hash(a1, nonceText, nc, cNonceString, "auth", a2); StringBuilder authLine = new StringBuilder(); authLine.append("Digest ") .append("username=\"").append(userName).append("\", ") .append("realm=\"").append(realm).append("\", ") .append("nonce=\"").append(nonceText).append("\", ") .append("uri=\"").append(url.getPath()).append("\", ") .append("qop=\"auth\", ") .append("nc=").append(nc).append(", ") .append("cnonce=\"").append(cNonceString).append("\", ") .append("response=\"").append(bytesToHex(response)).append('"'); String opaque = itemMap.get("opaque"); if (opaque != null) { authLine.append(", \"").append(opaque).append('"'); } return authLine.toString(); } private static Set<String> splitValue(String value) { Set<String> result = new HashSet<>(); if (value != null) { for (String s : value.split(",")) { result.add(s.trim()); } } return result; } private static byte[] hash(Object... objects) throws GeneralSecurityException { MessageDigest hash = MessageDigest.getInstance("MD5"); //System.out.println("<Hash>"); boolean first = true; for (Object object : objects) { byte[] octets; if (object.getClass() == String.class) { //System.out.println("+= '" + object + "'"); octets = ((String) object).getBytes(StandardCharsets.UTF_8); } else { octets = bytesToHexBytes((byte[]) object); //System.out.println("+= " + new String(octets, StandardCharsets.ISO_8859_1)); } if (first) { first = false; } else { hash.update((byte) ':'); } hash.update(octets); } //System.out.println("</Hash>"); return hash.digest(); } private static String unquote(String s) { return s.startsWith("\"") ? s.substring(1, s.length() - 1) : s; } private static byte[] bytesToHexBytes(byte[] octets) { return bytesToHex(octets).getBytes(StandardCharsets.ISO_8859_1); } private static String bytesToHex(byte[] octets) { StringBuilder sb = new StringBuilder(octets.length * 2); for (byte b : octets) { sb.append(String.format("%02x", b & 0xff)); } return sb.toString(); } private byte[] buildHeader() { StringBuilder header = new StringBuilder(); header.append(mMethodLine); for (Map.Entry<String, String> entry : mHeaderFields.entrySet()) { header.append(entry.getKey()).append(": ").append(entry.getValue()).append(CRLF); } header.append(CRLF); //System.out.println("HTTP Request:"); StringBuilder sb2 = new StringBuilder(); sb2.append(header); if (mBody != null) { sb2.append(new String(mBody, StandardCharsets.ISO_8859_1)); } //System.out.println(sb2); //System.out.println("End HTTP Request."); return header.toString().getBytes(HeaderCharset); } public void send(OutputStream out) throws IOException { out.write(buildHeader()); if (mBody != null) { out.write(mBody); } out.flush(); } @Override public Map<String, String> getHeaders() { return Collections.unmodifiableMap(mHeaderFields); } @Override public InputStream getPayloadStream() { return mBody != null ? new ByteArrayInputStream(mBody) : null; } @Override public ByteBuffer getPayload() { return mBody != null ? ByteBuffer.wrap(mBody) : null; } @Override public ByteBuffer getBinaryPayload() { byte[] binary = Base64.decode(mBody, Base64.DEFAULT); return ByteBuffer.wrap(binary); } public static void main(String[] args) throws GeneralSecurityException { test("Mufasa", "testrealm@host.com", "Circle Of Life", "GET", "/dir/index.html", "dcd98b7102dd2f0e8b11d0f600bfb0c093", "0a4f113b", "00000001", "auth", "6629fae49393a05397450978507c4ef1"); // WWW-Authenticate: Digest realm="wi-fi.org", qop="auth", // nonce="MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw==" // Authorization: Digest // username="1c7e1582-604d-4c00-b411-bb73735cbcb0" // realm="wi-fi.org" // nonce="MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw==" // uri="/.well-known/est/simpleenroll" // cnonce="NzA3NDk0" // nc=00000001 // qop="auth" // response="2c485d24076452e712b77f4e70776463" String nonce = "MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw=="; String cnonce = "NzA3NDk0"; test("1c7e1582-604d-4c00-b411-bb73735cbcb0", "wi-fi.org", "ruckus1234", "POST", "/.well-known/est/simpleenroll", /*new String(Base64.getDecoder().decode(nonce), StandardCharsets.ISO_8859_1)*/ nonce, /*new String(Base64.getDecoder().decode(cnonce), StandardCharsets.ISO_8859_1)*/ cnonce, "00000001", "auth", "2c485d24076452e712b77f4e70776463"); } private static void test(String user, String realm, String password, String method, String path, String nonce, String cnonce, String nc, String qop, String expect) throws GeneralSecurityException { byte[] a1 = hash(user, realm, password); System.out.println("HA1: " + bytesToHex(a1)); byte[] a2 = hash(method, path); System.out.println("HA2: " + bytesToHex(a2)); byte[] response = hash(a1, nonce, nc, cnonce, qop, a2); StringBuilder authLine = new StringBuilder(); String responseString = bytesToHex(response); authLine.append("Digest ") .append("username=\"").append(user).append("\", ") .append("realm=\"").append(realm).append("\", ") .append("nonce=\"").append(nonce).append("\", ") .append("uri=\"").append(path).append("\", ") .append("qop=\"").append(qop).append("\", ") .append("nc=").append(nc).append(", ") .append("cnonce=\"").append(cnonce).append("\", ") .append("response=\"").append(responseString).append('"'); System.out.println(authLine); System.out.println("Success: " + responseString.equals(expect)); } }