1package com.android.hotspot2.utils; 2 3import android.util.Base64; 4 5import java.io.ByteArrayInputStream; 6import java.io.IOException; 7import java.io.InputStream; 8import java.io.OutputStream; 9import java.net.URL; 10import java.nio.ByteBuffer; 11import java.nio.charset.Charset; 12import java.nio.charset.StandardCharsets; 13import java.security.GeneralSecurityException; 14import java.security.MessageDigest; 15import java.security.SecureRandom; 16import java.util.Collections; 17import java.util.HashMap; 18import java.util.HashSet; 19import java.util.LinkedHashMap; 20import java.util.Map; 21import java.util.Set; 22 23public 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 31 public HTTPRequest(Method method, URL url) { 32 this(null, null, method, url, null, false); 33 } 34 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 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 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 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 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 191 private static String unquote(String s) { 192 return s.startsWith("\"") ? s.substring(1, s.length() - 1) : s; 193 } 194 195 private static byte[] bytesToHexBytes(byte[] octets) { 196 return bytesToHex(octets).getBytes(StandardCharsets.ISO_8859_1); 197 } 198 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 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 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 236 public Map<String, String> getHeaders() { 237 return Collections.unmodifiableMap(mHeaderFields); 238 } 239 240 @Override 241 public InputStream getPayloadStream() { 242 return mBody != null ? new ByteArrayInputStream(mBody) : null; 243 } 244 245 @Override 246 public ByteBuffer getPayload() { 247 return mBody != null ? ByteBuffer.wrap(mBody) : null; 248 } 249 250 @Override 251 public ByteBuffer getBinaryPayload() { 252 byte[] binary = Base64.decode(mBody, Base64.DEFAULT); 253 return ByteBuffer.wrap(binary); 254 } 255 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 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}