1 package org.bouncycastle.est;
2 
3 import java.io.ByteArrayInputStream;
4 import java.io.IOException;
5 import java.io.OutputStream;
6 import java.security.SecureRandom;
7 import java.util.ArrayList;
8 import java.util.Collections;
9 import java.util.HashMap;
10 import java.util.HashSet;
11 import java.util.Iterator;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Set;
15 
16 import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
17 import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
18 import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
19 import org.bouncycastle.operator.DigestAlgorithmIdentifierFinder;
20 import org.bouncycastle.operator.DigestCalculator;
21 import org.bouncycastle.operator.DigestCalculatorProvider;
22 import org.bouncycastle.operator.OperatorCreationException;
23 import org.bouncycastle.util.Arrays;
24 import org.bouncycastle.util.Strings;
25 import org.bouncycastle.util.encoders.Base64;
26 import org.bouncycastle.util.encoders.Hex;
27 
28 /**
29  * Provides stock implementations for basic auth and digest auth.
30  */
31 public class HttpAuth
32     implements ESTAuth
33 {
34     private static final DigestAlgorithmIdentifierFinder digestAlgorithmIdentifierFinder = new DefaultDigestAlgorithmIdentifierFinder();
35 
36     private final String realm;
37     private final String username;
38     private final char[] password;
39     private final SecureRandom nonceGenerator;
40     private final DigestCalculatorProvider digestCalculatorProvider;
41 
42     private static final Set<String> validParts;
43 
44     static
45     {
46         HashSet<String> s = new HashSet<String>();
47         s.add("realm");
48         s.add("nonce");
49         s.add("opaque");
50         s.add("algorithm");
51         s.add("qop");
52         validParts = Collections.unmodifiableSet(s);
53     }
54 
55     /**
56      * Base constructor for basic auth.
57      *
58      * @param username user id.
59      * @param password user's password.
60      */
HttpAuth(String username, char[] password)61     public HttpAuth(String username, char[] password)
62     {
63         this(null, username, password, null, null);
64     }
65 
66     /**
67      * Constructor for basic auth with a specified realm.
68      *
69      * @param realm    expected server realm.
70      * @param username user id.
71      * @param password user's password.
72      */
HttpAuth(String realm, String username, char[] password)73     public HttpAuth(String realm, String username, char[] password)
74     {
75         this(realm, username, password, null, null);
76     }
77 
78     /**
79      * Base constructor for digest auth. The realm will be set by
80      *
81      * @param username                 user id.
82      * @param password                 user's password.
83      * @param nonceGenerator           random source for generating nonces.
84      * @param digestCalculatorProvider provider for digest calculators needed for calculating hashes.
85      */
HttpAuth(String username, char[] password, SecureRandom nonceGenerator, DigestCalculatorProvider digestCalculatorProvider)86     public HttpAuth(String username, char[] password, SecureRandom nonceGenerator, DigestCalculatorProvider digestCalculatorProvider)
87     {
88         this(null, username, password, nonceGenerator, digestCalculatorProvider);
89     }
90 
91     /**
92      * Constructor for digest auth with a specified realm.
93      *
94      * @param realm                    expected server realm.
95      * @param username                 user id.
96      * @param password                 user's password.
97      * @param nonceGenerator           random source for generating nonces.
98      * @param digestCalculatorProvider provider for digest calculators needed for calculating hashes.
99      */
HttpAuth(String realm, String username, char[] password, SecureRandom nonceGenerator, DigestCalculatorProvider digestCalculatorProvider)100     public HttpAuth(String realm, String username, char[] password, SecureRandom nonceGenerator, DigestCalculatorProvider digestCalculatorProvider)
101     {
102         this.realm = realm;
103         this.username = username;
104         this.password = password;
105         this.nonceGenerator = nonceGenerator;
106         this.digestCalculatorProvider = digestCalculatorProvider;
107     }
108 
applyAuth(final ESTRequestBuilder reqBldr)109     public void applyAuth(final ESTRequestBuilder reqBldr)
110     {
111         reqBldr.withHijacker(new ESTHijacker()
112         {
113             public ESTResponse hijack(ESTRequest req, Source sock)
114                 throws IOException
115             {
116                 ESTResponse res = new ESTResponse(req, sock);
117 
118                 if (res.getStatusCode() == 401)
119                 {
120                     String authHeader = res.getHeader("WWW-Authenticate");
121                     if (authHeader == null)
122                     {
123                         throw new ESTException("Status of 401 but no WWW-Authenticate header");
124                     }
125 
126                     authHeader = Strings.toLowerCase(authHeader);
127 
128                     if (authHeader.startsWith("digest"))
129                     {
130                         res = doDigestFunction(res);
131                     }
132                     else if (authHeader.startsWith("basic"))
133                     {
134                         res.close(); // Close off the last reqBldr.
135 
136                         //
137                         // Check realm field from header.
138                         //
139                         Map<String, String> s = HttpUtil.splitCSL("Basic", res.getHeader("WWW-Authenticate"));
140 
141                         //
142                         // If no realm supplied it will not check the server realm. TODO elaborate in documentation.
143                         //
144                         if (realm != null)
145                         {
146                             if (!realm.equals(s.get("realm")))
147                             {
148                                 // Not equal then fail.
149                                 throw new ESTException("Supplied realm '" + realm + "' does not match server realm '" + s.get("realm") + "'", null, 401, null);
150                             }
151                         }
152 
153                         //
154                         // Prepare basic auth answer.
155                         //
156                         ESTRequestBuilder answer = new ESTRequestBuilder(req).withHijacker(null);
157 
158                         if (realm != null && realm.length() > 0)
159                         {
160                             answer.setHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
161                         }
162                         if (username.contains(":"))
163                         {
164                             throw new IllegalArgumentException("User must not contain a ':'");
165                         }
166                         //userPass = username + ":" + password;
167                         char[]  userPass = new char[username.length() + 1 + password.length];
168                         System.arraycopy(username.toCharArray(), 0, userPass, 0, username.length());
169                         userPass[username.length()] = ':';
170                         System.arraycopy(password, 0, userPass, username.length() + 1, password.length);
171 
172                         answer.setHeader("Authorization", "Basic " + Base64.toBase64String(Strings.toByteArray(userPass)));
173 
174                         res = req.getClient().doRequest(answer.build());
175 
176                         Arrays.fill(userPass, (char)0);
177                     }
178                     else
179                     {
180                         throw new ESTException("Unknown auth mode: " + authHeader);
181                     }
182 
183 
184                     return res;
185                 }
186                 return res;
187             }
188         });
189     }
190 
doDigestFunction(ESTResponse res)191     private ESTResponse doDigestFunction(ESTResponse res)
192         throws IOException
193     {
194         res.close(); // Close off the last request.
195         ESTRequest req = res.getOriginalRequest();
196 
197 
198         Map<String, String> parts = null;
199         try
200         {
201             parts = HttpUtil.splitCSL("Digest", res.getHeader("WWW-Authenticate"));
202         }
203         catch (Throwable t)
204         {
205             throw new ESTException(
206                 "Parsing WWW-Authentication header: " + t.getMessage(),
207                 t,
208                 res.getStatusCode(),
209                 new ByteArrayInputStream(res.getHeader("WWW-Authenticate").getBytes()));
210         }
211 
212 
213         String uri = null;
214         try
215         {
216             uri = req.getURL().toURI().getPath();
217         }
218         catch (Exception e)
219         {
220             throw new IOException("unable to process URL in request: " + e.getMessage());
221         }
222 
223         for (Iterator it = parts.keySet().iterator(); it.hasNext();)
224         {
225             Object k = it.next();
226             if (!validParts.contains(k))
227             {
228                 throw new ESTException("Unrecognised entry in WWW-Authenticate header: '" + k + "'");
229             }
230         }
231 
232         String method = req.getMethod();
233         String realm = parts.get("realm");
234         String nonce = parts.get("nonce");
235         String opaque = parts.get("opaque");
236         String algorithm = parts.get("algorithm");
237         String qop = parts.get("qop");
238 
239 
240         List<String> qopMods = new ArrayList<String>(); // Preserve ordering.
241 
242         if (this.realm != null)
243         {
244             if (!this.realm.equals(realm))
245             {
246                 // Not equal then fail.
247                 throw new ESTException("Supplied realm '" + this.realm + "' does not match server realm '" + realm + "'", null, 401, null);
248             }
249         }
250 
251         // If an algorithm is not specified, default to MD5.
252         if (algorithm == null)
253         {
254             algorithm = "MD5";
255         }
256 
257         if (algorithm.length() == 0)
258         {
259             throw new ESTException("WWW-Authenticate no algorithm defined.");
260         }
261 
262         algorithm = Strings.toUpperCase(algorithm);
263 
264         if (qop != null)
265         {
266             if (qop.length() == 0)
267             {
268                 throw new ESTException("QoP value is empty.");
269             }
270 
271             qop = Strings.toLowerCase(qop);
272             String[] s = qop.split(",");
273             for (int j = 0; j != s.length; j++)
274             {
275                 if (!s[j].equals("auth") && !s[j].equals("auth-int"))
276                 {
277                     throw new ESTException("QoP value unknown: '" + j + "'");
278                 }
279 
280                 String jt = s[j].trim();
281                 if (qopMods.contains(jt))
282                 {
283                     continue;
284                 }
285                 qopMods.add(jt);
286             }
287         }
288         else
289         {
290             throw new ESTException("Qop is not defined in WWW-Authenticate header.");
291         }
292 
293 
294         AlgorithmIdentifier digestAlg = lookupDigest(algorithm);
295         if (digestAlg == null || digestAlg.getAlgorithm() == null)
296         {
297             throw new IOException("auth digest algorithm unknown: " + algorithm);
298         }
299 
300         DigestCalculator dCalc = getDigestCalculator(algorithm, digestAlg);
301         OutputStream dOut = dCalc.getOutputStream();
302 
303         String crnonce = makeNonce(10); // TODO arbitrary?
304 
305         update(dOut, username);
306         update(dOut, ":");
307         update(dOut, realm);
308         update(dOut, ":");
309         update(dOut, password);
310 
311         dOut.close();
312 
313         byte[] ha1 = dCalc.getDigest();
314 
315         if (algorithm.endsWith("-SESS"))
316         {
317             DigestCalculator sessCalc = getDigestCalculator(algorithm, digestAlg);
318             OutputStream sessOut = sessCalc.getOutputStream();
319 
320             String cs = Hex.toHexString(ha1);
321 
322             update(sessOut, cs);
323             update(sessOut, ":");
324             update(sessOut, nonce);
325             update(sessOut, ":");
326             update(sessOut, crnonce);
327 
328             sessOut.close();
329 
330             ha1 = sessCalc.getDigest();
331         }
332 
333         String hashHa1 = Hex.toHexString(ha1);
334 
335         DigestCalculator authCalc = getDigestCalculator(algorithm, digestAlg);
336         OutputStream authOut = authCalc.getOutputStream();
337 
338         if (qopMods.get(0).equals("auth-int"))
339         {
340             DigestCalculator reqCalc = getDigestCalculator(algorithm, digestAlg);
341             OutputStream reqOut = reqCalc.getOutputStream();
342 
343             req.writeData(reqOut);
344 
345             reqOut.close();
346 
347             byte[] b = reqCalc.getDigest();
348 
349             update(authOut, method);
350             update(authOut, ":");
351             update(authOut, uri);
352             update(authOut, ":");
353             update(authOut, Hex.toHexString(b));
354         }
355         else if (qopMods.get(0).equals("auth"))
356         {
357             update(authOut, method);
358             update(authOut, ":");
359             update(authOut, uri);
360         }
361 
362         authOut.close();
363 
364         String hashHa2 = Hex.toHexString(authCalc.getDigest());
365 
366         DigestCalculator responseCalc = getDigestCalculator(algorithm, digestAlg);
367         OutputStream responseOut = responseCalc.getOutputStream();
368 
369         if (qopMods.contains("missing"))
370         {
371             update(responseOut, hashHa1);
372             update(responseOut, ":");
373             update(responseOut, nonce);
374             update(responseOut, ":");
375             update(responseOut, hashHa2);
376         }
377         else
378         {
379             update(responseOut, hashHa1);
380             update(responseOut, ":");
381             update(responseOut, nonce);
382             update(responseOut, ":");
383             update(responseOut, "00000001");
384             update(responseOut, ":");
385             update(responseOut, crnonce);
386             update(responseOut, ":");
387 
388             if (qopMods.get(0).equals("auth-int"))
389             {
390                 update(responseOut, "auth-int");
391             }
392             else
393             {
394                 update(responseOut, "auth");
395             }
396 
397             update(responseOut, ":");
398             update(responseOut, hashHa2);
399         }
400 
401         responseOut.close();
402 
403         String digest = Hex.toHexString(responseCalc.getDigest());
404 
405         Map<String, String> hdr = new HashMap<String, String>();
406         hdr.put("username", username);
407         hdr.put("realm", realm);
408         hdr.put("nonce", nonce);
409         hdr.put("uri", uri);
410         hdr.put("response", digest);
411         if (qopMods.get(0).equals("auth-int"))
412         {
413             hdr.put("qop", "auth-int");
414             hdr.put("nc", "00000001");
415             hdr.put("cnonce", crnonce);
416         }
417         else if (qopMods.get(0).equals("auth"))
418         {
419             hdr.put("qop", "auth");
420             hdr.put("nc", "00000001");
421             hdr.put("cnonce", crnonce);
422         }
423         hdr.put("algorithm", algorithm);
424 
425         if (opaque == null || opaque.length() == 0)
426         {
427             hdr.put("opaque", makeNonce(20));
428         }
429 
430         ESTRequestBuilder answer = new ESTRequestBuilder(req).withHijacker(null);
431 
432         answer.setHeader("Authorization", HttpUtil.mergeCSL("Digest", hdr));
433 
434         return req.getClient().doRequest(answer.build());
435     }
436 
getDigestCalculator(String algorithm, AlgorithmIdentifier digestAlg)437     private DigestCalculator getDigestCalculator(String algorithm, AlgorithmIdentifier digestAlg)
438         throws IOException
439     {
440         DigestCalculator dCalc;
441         try
442         {
443             dCalc = digestCalculatorProvider.get(digestAlg);
444         }
445         catch (OperatorCreationException e)
446         {
447             throw new IOException("cannot create digest calculator for " + algorithm + ": " + e.getMessage());
448         }
449         return dCalc;
450     }
451 
lookupDigest(String algorithm)452     private AlgorithmIdentifier lookupDigest(String algorithm)
453     {
454         if (algorithm.endsWith("-SESS"))
455         {
456             algorithm = algorithm.substring(0, algorithm.length() - "-SESS".length());
457         }
458 
459         if (algorithm.equals("SHA-512-256"))
460         {
461             return digestAlgorithmIdentifierFinder.find(NISTObjectIdentifiers.id_sha512_256);
462         }
463 
464         return digestAlgorithmIdentifierFinder.find(algorithm);
465     }
466 
update(OutputStream dOut, char[] value)467     private void update(OutputStream dOut, char[] value)
468         throws IOException
469     {
470         dOut.write(Strings.toUTF8ByteArray(value));
471     }
472 
update(OutputStream dOut, String value)473     private void update(OutputStream dOut, String value)
474         throws IOException
475     {
476         dOut.write(Strings.toUTF8ByteArray(value));
477     }
478 
makeNonce(int len)479     private String makeNonce(int len)
480     {
481         byte[] b = new byte[len];
482         nonceGenerator.nextBytes(b);
483         return Hex.toHexString(b);
484     }
485 }
486