• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }