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