1 package com.android.hotspot2.utils; 2 3 import android.util.Base64; 4 5 import java.io.ByteArrayInputStream; 6 import java.io.IOException; 7 import java.io.InputStream; 8 import java.io.OutputStream; 9 import java.net.URL; 10 import java.nio.ByteBuffer; 11 import java.nio.charset.Charset; 12 import java.nio.charset.StandardCharsets; 13 import java.security.GeneralSecurityException; 14 import java.security.MessageDigest; 15 import java.security.SecureRandom; 16 import java.util.Collections; 17 import java.util.HashMap; 18 import java.util.HashSet; 19 import java.util.LinkedHashMap; 20 import java.util.Map; 21 import java.util.Set; 22 23 public class HTTPRequest implements HTTPMessage { 24 private static final Charset HeaderCharset = StandardCharsets.US_ASCII; 25 private static final int HTTPS_PORT = 443; 26 27 private final String mMethodLine; 28 private final Map<String, String> mHeaderFields; 29 private final byte[] mBody; 30 HTTPRequest(Method method, URL url)31 public HTTPRequest(Method method, URL url) { 32 this(null, null, method, url, null, false); 33 } 34 HTTPRequest(String payload, Charset charset, Method method, URL url, String contentType, boolean base64)35 public HTTPRequest(String payload, Charset charset, Method method, URL url, String contentType, 36 boolean base64) { 37 mBody = payload != null ? payload.getBytes(charset) : null; 38 39 mHeaderFields = new LinkedHashMap<>(); 40 mHeaderFields.put(AgentHeader, AgentName); 41 if (url.getPort() != HTTPS_PORT) { 42 mHeaderFields.put(HostHeader, url.getHost() + ':' + url.getPort()); 43 } else { 44 mHeaderFields.put(HostHeader, url.getHost()); 45 } 46 mHeaderFields.put(AcceptHeader, "*/*"); 47 if (payload != null) { 48 if (base64) { 49 mHeaderFields.put(ContentTypeHeader, contentType); 50 mHeaderFields.put(ContentEncodingHeader, "base64"); 51 } else { 52 mHeaderFields.put(ContentTypeHeader, contentType + "; charset=" + 53 charset.displayName().toLowerCase()); 54 } 55 mHeaderFields.put(ContentLengthHeader, Integer.toString(mBody.length)); 56 } 57 58 mMethodLine = method.name() + ' ' + url.getPath() + ' ' + HTTPVersion + CRLF; 59 } 60 doAuthenticate(HTTPResponse httpResponse, String userName, byte[] password, URL url, int sequence)61 public void doAuthenticate(HTTPResponse httpResponse, String userName, byte[] password, 62 URL url, int sequence) throws IOException, GeneralSecurityException { 63 mHeaderFields.put(HTTPMessage.AuthorizationHeader, 64 generateAuthAnswer(httpResponse, userName, password, url, sequence)); 65 } 66 generateAuthAnswer(HTTPResponse httpResponse, String userName, byte[] password, URL url, int sequence)67 private static String generateAuthAnswer(HTTPResponse httpResponse, String userName, 68 byte[] password, URL url, int sequence) 69 throws IOException, GeneralSecurityException { 70 71 String authRequestLine = httpResponse.getHeader(HTTPMessage.AuthHeader); 72 if (authRequestLine == null) { 73 throw new IOException("Missing auth line"); 74 } 75 String[] tokens = authRequestLine.split("[ ,]+"); 76 //System.out.println("Tokens: " + Arrays.toString(tokens)); 77 if (tokens.length < 3 || !tokens[0].equalsIgnoreCase("digest")) { 78 throw new IOException("Bad " + HTTPMessage.AuthHeader + ": '" + authRequestLine + "'"); 79 } 80 81 Map<String, String> itemMap = new HashMap<>(); 82 for (int n = 1; n < tokens.length; n++) { 83 String s = tokens[n]; 84 int split = s.indexOf('='); 85 if (split < 0) { 86 continue; 87 } 88 itemMap.put(s.substring(0, split).trim().toLowerCase(), 89 unquote(s.substring(split + 1).trim())); 90 } 91 92 Set<String> qops = splitValue(itemMap.remove("qop")); 93 if (!qops.contains("auth")) { 94 throw new IOException("Unsupported quality of protection value(s): '" + qops + "'"); 95 } 96 String algorithm = itemMap.remove("algorithm"); 97 if (algorithm != null && !algorithm.equalsIgnoreCase("md5")) { 98 throw new IOException("Unsupported algorithm: '" + algorithm + "'"); 99 } 100 String realm = itemMap.remove("realm"); 101 String nonceText = itemMap.remove("nonce"); 102 if (realm == null || nonceText == null) { 103 throw new IOException("realm and/or nonce missing: '" + authRequestLine + "'"); 104 } 105 //System.out.println("Remaining tokens: " + itemMap); 106 107 byte[] cnonce = new byte[16]; 108 SecureRandom prng = new SecureRandom(); 109 prng.nextBytes(cnonce); 110 111 /* 112 * H(data) = MD5(data) 113 * KD(secret, data) = H(concat(secret, ":", data)) 114 * 115 * A1 = unq(username-value) ":" unq(realm-value) ":" passwd 116 * A2 = Method ":" digest-uri-value 117 * 118 * response = KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" 119 * unq(qop-value) ":" H(A2) ) 120 */ 121 122 String nc = String.format("%08d", sequence); 123 124 /* 125 * This bears witness to the ingenuity of the emerging "web generation" and the authors of 126 * RFC-2617: Strings are treated as a sequence of octets in blind ignorance of character 127 * encoding, whereas octets strings apparently aren't "good enough" and expanded to 128 * "hex strings"... 129 * As a wild guess I apply UTF-8 below. 130 */ 131 String passwordString = new String(password, StandardCharsets.UTF_8); 132 String cNonceString = bytesToHex(cnonce); 133 134 byte[] a1 = hash(userName, realm, passwordString); 135 byte[] a2 = hash("POST", url.getPath()); 136 byte[] response = hash(a1, nonceText, nc, cNonceString, "auth", a2); 137 138 StringBuilder authLine = new StringBuilder(); 139 authLine.append("Digest ") 140 .append("username=\"").append(userName).append("\", ") 141 .append("realm=\"").append(realm).append("\", ") 142 .append("nonce=\"").append(nonceText).append("\", ") 143 .append("uri=\"").append(url.getPath()).append("\", ") 144 .append("qop=\"auth\", ") 145 .append("nc=").append(nc).append(", ") 146 .append("cnonce=\"").append(cNonceString).append("\", ") 147 .append("response=\"").append(bytesToHex(response)).append('"'); 148 String opaque = itemMap.get("opaque"); 149 if (opaque != null) { 150 authLine.append(", \"").append(opaque).append('"'); 151 } 152 153 return authLine.toString(); 154 } 155 splitValue(String value)156 private static Set<String> splitValue(String value) { 157 Set<String> result = new HashSet<>(); 158 if (value != null) { 159 for (String s : value.split(",")) { 160 result.add(s.trim()); 161 } 162 } 163 return result; 164 } 165 hash(Object... objects)166 private static byte[] hash(Object... objects) throws GeneralSecurityException { 167 MessageDigest hash = MessageDigest.getInstance("MD5"); 168 169 //System.out.println("<Hash>"); 170 boolean first = true; 171 for (Object object : objects) { 172 byte[] octets; 173 if (object.getClass() == String.class) { 174 //System.out.println("+= '" + object + "'"); 175 octets = ((String) object).getBytes(StandardCharsets.UTF_8); 176 } else { 177 octets = bytesToHexBytes((byte[]) object); 178 //System.out.println("+= " + new String(octets, StandardCharsets.ISO_8859_1)); 179 } 180 if (first) { 181 first = false; 182 } else { 183 hash.update((byte) ':'); 184 } 185 hash.update(octets); 186 } 187 //System.out.println("</Hash>"); 188 return hash.digest(); 189 } 190 unquote(String s)191 private static String unquote(String s) { 192 return s.startsWith("\"") ? s.substring(1, s.length() - 1) : s; 193 } 194 bytesToHexBytes(byte[] octets)195 private static byte[] bytesToHexBytes(byte[] octets) { 196 return bytesToHex(octets).getBytes(StandardCharsets.ISO_8859_1); 197 } 198 bytesToHex(byte[] octets)199 private static String bytesToHex(byte[] octets) { 200 StringBuilder sb = new StringBuilder(octets.length * 2); 201 for (byte b : octets) { 202 sb.append(String.format("%02x", b & 0xff)); 203 } 204 return sb.toString(); 205 } 206 buildHeader()207 private byte[] buildHeader() { 208 StringBuilder header = new StringBuilder(); 209 header.append(mMethodLine); 210 for (Map.Entry<String, String> entry : mHeaderFields.entrySet()) { 211 header.append(entry.getKey()).append(": ").append(entry.getValue()).append(CRLF); 212 } 213 header.append(CRLF); 214 215 //System.out.println("HTTP Request:"); 216 StringBuilder sb2 = new StringBuilder(); 217 sb2.append(header); 218 if (mBody != null) { 219 sb2.append(new String(mBody, StandardCharsets.ISO_8859_1)); 220 } 221 //System.out.println(sb2); 222 //System.out.println("End HTTP Request."); 223 224 return header.toString().getBytes(HeaderCharset); 225 } 226 send(OutputStream out)227 public void send(OutputStream out) throws IOException { 228 out.write(buildHeader()); 229 if (mBody != null) { 230 out.write(mBody); 231 } 232 out.flush(); 233 } 234 235 @Override getHeaders()236 public Map<String, String> getHeaders() { 237 return Collections.unmodifiableMap(mHeaderFields); 238 } 239 240 @Override getPayloadStream()241 public InputStream getPayloadStream() { 242 return mBody != null ? new ByteArrayInputStream(mBody) : null; 243 } 244 245 @Override getPayload()246 public ByteBuffer getPayload() { 247 return mBody != null ? ByteBuffer.wrap(mBody) : null; 248 } 249 250 @Override getBinaryPayload()251 public ByteBuffer getBinaryPayload() { 252 byte[] binary = Base64.decode(mBody, Base64.DEFAULT); 253 return ByteBuffer.wrap(binary); 254 } 255 main(String[] args)256 public static void main(String[] args) throws GeneralSecurityException { 257 test("Mufasa", "testrealm@host.com", "Circle Of Life", "GET", "/dir/index.html", 258 "dcd98b7102dd2f0e8b11d0f600bfb0c093", "0a4f113b", "00000001", "auth", 259 "6629fae49393a05397450978507c4ef1"); 260 261 // WWW-Authenticate: Digest realm="wi-fi.org", qop="auth", 262 // nonce="MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw==" 263 // Authorization: Digest 264 // username="1c7e1582-604d-4c00-b411-bb73735cbcb0" 265 // realm="wi-fi.org" 266 // nonce="MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw==" 267 // uri="/.well-known/est/simpleenroll" 268 // cnonce="NzA3NDk0" 269 // nc=00000001 270 // qop="auth" 271 // response="2c485d24076452e712b77f4e70776463" 272 273 String nonce = "MTQzMTg1MTIxMzUyNzo0OGFhNGU5ZTg4Y2M4YmFhYzM2MzAwZDg5MGNiYTJlNw=="; 274 String cnonce = "NzA3NDk0"; 275 test("1c7e1582-604d-4c00-b411-bb73735cbcb0", "wi-fi.org", "ruckus1234", "POST", 276 "/.well-known/est/simpleenroll", 277 /*new String(Base64.getDecoder().decode(nonce), StandardCharsets.ISO_8859_1)*/ 278 nonce, 279 /*new String(Base64.getDecoder().decode(cnonce), StandardCharsets.ISO_8859_1)*/ 280 cnonce, "00000001", "auth", "2c485d24076452e712b77f4e70776463"); 281 } 282 test(String user, String realm, String password, String method, String path, String nonce, String cnonce, String nc, String qop, String expect)283 private static void test(String user, String realm, String password, String method, String path, 284 String nonce, String cnonce, String nc, String qop, String expect) 285 throws GeneralSecurityException { 286 byte[] a1 = hash(user, realm, password); 287 System.out.println("HA1: " + bytesToHex(a1)); 288 byte[] a2 = hash(method, path); 289 System.out.println("HA2: " + bytesToHex(a2)); 290 byte[] response = hash(a1, nonce, nc, cnonce, qop, a2); 291 292 StringBuilder authLine = new StringBuilder(); 293 String responseString = bytesToHex(response); 294 authLine.append("Digest ") 295 .append("username=\"").append(user).append("\", ") 296 .append("realm=\"").append(realm).append("\", ") 297 .append("nonce=\"").append(nonce).append("\", ") 298 .append("uri=\"").append(path).append("\", ") 299 .append("qop=\"").append(qop).append("\", ") 300 .append("nc=").append(nc).append(", ") 301 .append("cnonce=\"").append(cnonce).append("\", ") 302 .append("response=\"").append(responseString).append('"'); 303 304 System.out.println(authLine); 305 System.out.println("Success: " + responseString.equals(expect)); 306 } 307 }