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}