Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | /* |
michael@0 | 2 | * ==================================================================== |
michael@0 | 3 | * |
michael@0 | 4 | * Licensed to the Apache Software Foundation (ASF) under one or more |
michael@0 | 5 | * contributor license agreements. See the NOTICE file distributed with |
michael@0 | 6 | * this work for additional information regarding copyright ownership. |
michael@0 | 7 | * The ASF licenses this file to You under the Apache License, Version 2.0 |
michael@0 | 8 | * (the "License"); you may not use this file except in compliance with |
michael@0 | 9 | * the License. You may obtain a copy of the License at |
michael@0 | 10 | * |
michael@0 | 11 | * http://www.apache.org/licenses/LICENSE-2.0 |
michael@0 | 12 | * |
michael@0 | 13 | * Unless required by applicable law or agreed to in writing, software |
michael@0 | 14 | * distributed under the License is distributed on an "AS IS" BASIS, |
michael@0 | 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
michael@0 | 16 | * See the License for the specific language governing permissions and |
michael@0 | 17 | * limitations under the License. |
michael@0 | 18 | * ==================================================================== |
michael@0 | 19 | * |
michael@0 | 20 | * This software consists of voluntary contributions made by many |
michael@0 | 21 | * individuals on behalf of the Apache Software Foundation. For more |
michael@0 | 22 | * information on the Apache Software Foundation, please see |
michael@0 | 23 | * <http://www.apache.org/>. |
michael@0 | 24 | * |
michael@0 | 25 | */ |
michael@0 | 26 | |
michael@0 | 27 | package ch.boye.httpclientandroidlib.impl.auth; |
michael@0 | 28 | |
michael@0 | 29 | import java.security.MessageDigest; |
michael@0 | 30 | import java.security.SecureRandom; |
michael@0 | 31 | import java.util.ArrayList; |
michael@0 | 32 | import java.util.Formatter; |
michael@0 | 33 | import java.util.List; |
michael@0 | 34 | import java.util.Locale; |
michael@0 | 35 | import java.util.StringTokenizer; |
michael@0 | 36 | |
michael@0 | 37 | import ch.boye.httpclientandroidlib.annotation.NotThreadSafe; |
michael@0 | 38 | |
michael@0 | 39 | import ch.boye.httpclientandroidlib.Header; |
michael@0 | 40 | import ch.boye.httpclientandroidlib.HttpRequest; |
michael@0 | 41 | import ch.boye.httpclientandroidlib.auth.AuthenticationException; |
michael@0 | 42 | import ch.boye.httpclientandroidlib.auth.Credentials; |
michael@0 | 43 | import ch.boye.httpclientandroidlib.auth.AUTH; |
michael@0 | 44 | import ch.boye.httpclientandroidlib.auth.MalformedChallengeException; |
michael@0 | 45 | import ch.boye.httpclientandroidlib.auth.params.AuthParams; |
michael@0 | 46 | import ch.boye.httpclientandroidlib.message.BasicNameValuePair; |
michael@0 | 47 | import ch.boye.httpclientandroidlib.message.BasicHeaderValueFormatter; |
michael@0 | 48 | import ch.boye.httpclientandroidlib.message.BufferedHeader; |
michael@0 | 49 | import ch.boye.httpclientandroidlib.util.CharArrayBuffer; |
michael@0 | 50 | import ch.boye.httpclientandroidlib.util.EncodingUtils; |
michael@0 | 51 | |
michael@0 | 52 | /** |
michael@0 | 53 | * Digest authentication scheme as defined in RFC 2617. |
michael@0 | 54 | * Both MD5 (default) and MD5-sess are supported. |
michael@0 | 55 | * Currently only qop=auth or no qop is supported. qop=auth-int |
michael@0 | 56 | * is unsupported. If auth and auth-int are provided, auth is |
michael@0 | 57 | * used. |
michael@0 | 58 | * <p> |
michael@0 | 59 | * Credential charset is configured via the |
michael@0 | 60 | * {@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET} |
michael@0 | 61 | * parameter of the HTTP request. |
michael@0 | 62 | * <p> |
michael@0 | 63 | * Since the digest username is included as clear text in the generated |
michael@0 | 64 | * Authentication header, the charset of the username must be compatible |
michael@0 | 65 | * with the |
michael@0 | 66 | * {@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET |
michael@0 | 67 | * http element charset}. |
michael@0 | 68 | * <p> |
michael@0 | 69 | * The following parameters can be used to customize the behavior of this |
michael@0 | 70 | * class: |
michael@0 | 71 | * <ul> |
michael@0 | 72 | * <li>{@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET}</li> |
michael@0 | 73 | * </ul> |
michael@0 | 74 | * |
michael@0 | 75 | * @since 4.0 |
michael@0 | 76 | */ |
michael@0 | 77 | @NotThreadSafe |
michael@0 | 78 | public class DigestScheme extends RFC2617Scheme { |
michael@0 | 79 | |
michael@0 | 80 | /** |
michael@0 | 81 | * Hexa values used when creating 32 character long digest in HTTP DigestScheme |
michael@0 | 82 | * in case of authentication. |
michael@0 | 83 | * |
michael@0 | 84 | * @see #encode(byte[]) |
michael@0 | 85 | */ |
michael@0 | 86 | private static final char[] HEXADECIMAL = { |
michael@0 | 87 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', |
michael@0 | 88 | 'e', 'f' |
michael@0 | 89 | }; |
michael@0 | 90 | |
michael@0 | 91 | /** Whether the digest authentication process is complete */ |
michael@0 | 92 | private boolean complete; |
michael@0 | 93 | |
michael@0 | 94 | private static final int QOP_UNKNOWN = -1; |
michael@0 | 95 | private static final int QOP_MISSING = 0; |
michael@0 | 96 | private static final int QOP_AUTH_INT = 1; |
michael@0 | 97 | private static final int QOP_AUTH = 2; |
michael@0 | 98 | |
michael@0 | 99 | private String lastNonce; |
michael@0 | 100 | private long nounceCount; |
michael@0 | 101 | private String cnonce; |
michael@0 | 102 | private String a1; |
michael@0 | 103 | private String a2; |
michael@0 | 104 | |
michael@0 | 105 | /** |
michael@0 | 106 | * Default constructor for the digest authetication scheme. |
michael@0 | 107 | */ |
michael@0 | 108 | public DigestScheme() { |
michael@0 | 109 | super(); |
michael@0 | 110 | this.complete = false; |
michael@0 | 111 | } |
michael@0 | 112 | |
michael@0 | 113 | /** |
michael@0 | 114 | * Processes the Digest challenge. |
michael@0 | 115 | * |
michael@0 | 116 | * @param header the challenge header |
michael@0 | 117 | * |
michael@0 | 118 | * @throws MalformedChallengeException is thrown if the authentication challenge |
michael@0 | 119 | * is malformed |
michael@0 | 120 | */ |
michael@0 | 121 | @Override |
michael@0 | 122 | public void processChallenge( |
michael@0 | 123 | final Header header) throws MalformedChallengeException { |
michael@0 | 124 | super.processChallenge(header); |
michael@0 | 125 | |
michael@0 | 126 | if (getParameter("realm") == null) { |
michael@0 | 127 | throw new MalformedChallengeException("missing realm in challenge"); |
michael@0 | 128 | } |
michael@0 | 129 | if (getParameter("nonce") == null) { |
michael@0 | 130 | throw new MalformedChallengeException("missing nonce in challenge"); |
michael@0 | 131 | } |
michael@0 | 132 | this.complete = true; |
michael@0 | 133 | } |
michael@0 | 134 | |
michael@0 | 135 | /** |
michael@0 | 136 | * Tests if the Digest authentication process has been completed. |
michael@0 | 137 | * |
michael@0 | 138 | * @return <tt>true</tt> if Digest authorization has been processed, |
michael@0 | 139 | * <tt>false</tt> otherwise. |
michael@0 | 140 | */ |
michael@0 | 141 | public boolean isComplete() { |
michael@0 | 142 | String s = getParameter("stale"); |
michael@0 | 143 | if ("true".equalsIgnoreCase(s)) { |
michael@0 | 144 | return false; |
michael@0 | 145 | } else { |
michael@0 | 146 | return this.complete; |
michael@0 | 147 | } |
michael@0 | 148 | } |
michael@0 | 149 | |
michael@0 | 150 | /** |
michael@0 | 151 | * Returns textual designation of the digest authentication scheme. |
michael@0 | 152 | * |
michael@0 | 153 | * @return <code>digest</code> |
michael@0 | 154 | */ |
michael@0 | 155 | public String getSchemeName() { |
michael@0 | 156 | return "digest"; |
michael@0 | 157 | } |
michael@0 | 158 | |
michael@0 | 159 | /** |
michael@0 | 160 | * Returns <tt>false</tt>. Digest authentication scheme is request based. |
michael@0 | 161 | * |
michael@0 | 162 | * @return <tt>false</tt>. |
michael@0 | 163 | */ |
michael@0 | 164 | public boolean isConnectionBased() { |
michael@0 | 165 | return false; |
michael@0 | 166 | } |
michael@0 | 167 | |
michael@0 | 168 | public void overrideParamter(final String name, final String value) { |
michael@0 | 169 | getParameters().put(name, value); |
michael@0 | 170 | } |
michael@0 | 171 | |
michael@0 | 172 | /** |
michael@0 | 173 | * Produces a digest authorization string for the given set of |
michael@0 | 174 | * {@link Credentials}, method name and URI. |
michael@0 | 175 | * |
michael@0 | 176 | * @param credentials A set of credentials to be used for athentication |
michael@0 | 177 | * @param request The request being authenticated |
michael@0 | 178 | * |
michael@0 | 179 | * @throws ch.boye.httpclientandroidlib.auth.InvalidCredentialsException if authentication credentials |
michael@0 | 180 | * are not valid or not applicable for this authentication scheme |
michael@0 | 181 | * @throws AuthenticationException if authorization string cannot |
michael@0 | 182 | * be generated due to an authentication failure |
michael@0 | 183 | * |
michael@0 | 184 | * @return a digest authorization string |
michael@0 | 185 | */ |
michael@0 | 186 | public Header authenticate( |
michael@0 | 187 | final Credentials credentials, |
michael@0 | 188 | final HttpRequest request) throws AuthenticationException { |
michael@0 | 189 | |
michael@0 | 190 | if (credentials == null) { |
michael@0 | 191 | throw new IllegalArgumentException("Credentials may not be null"); |
michael@0 | 192 | } |
michael@0 | 193 | if (request == null) { |
michael@0 | 194 | throw new IllegalArgumentException("HTTP request may not be null"); |
michael@0 | 195 | } |
michael@0 | 196 | |
michael@0 | 197 | // Add method name and request-URI to the parameter map |
michael@0 | 198 | getParameters().put("methodname", request.getRequestLine().getMethod()); |
michael@0 | 199 | getParameters().put("uri", request.getRequestLine().getUri()); |
michael@0 | 200 | String charset = getParameter("charset"); |
michael@0 | 201 | if (charset == null) { |
michael@0 | 202 | charset = AuthParams.getCredentialCharset(request.getParams()); |
michael@0 | 203 | getParameters().put("charset", charset); |
michael@0 | 204 | } |
michael@0 | 205 | return createDigestHeader(credentials); |
michael@0 | 206 | } |
michael@0 | 207 | |
michael@0 | 208 | private static MessageDigest createMessageDigest( |
michael@0 | 209 | final String digAlg) throws UnsupportedDigestAlgorithmException { |
michael@0 | 210 | try { |
michael@0 | 211 | return MessageDigest.getInstance(digAlg); |
michael@0 | 212 | } catch (Exception e) { |
michael@0 | 213 | throw new UnsupportedDigestAlgorithmException( |
michael@0 | 214 | "Unsupported algorithm in HTTP Digest authentication: " |
michael@0 | 215 | + digAlg); |
michael@0 | 216 | } |
michael@0 | 217 | } |
michael@0 | 218 | |
michael@0 | 219 | /** |
michael@0 | 220 | * Creates digest-response header as defined in RFC2617. |
michael@0 | 221 | * |
michael@0 | 222 | * @param credentials User credentials |
michael@0 | 223 | * |
michael@0 | 224 | * @return The digest-response as String. |
michael@0 | 225 | */ |
michael@0 | 226 | private Header createDigestHeader( |
michael@0 | 227 | final Credentials credentials) throws AuthenticationException { |
michael@0 | 228 | String uri = getParameter("uri"); |
michael@0 | 229 | String realm = getParameter("realm"); |
michael@0 | 230 | String nonce = getParameter("nonce"); |
michael@0 | 231 | String opaque = getParameter("opaque"); |
michael@0 | 232 | String method = getParameter("methodname"); |
michael@0 | 233 | String algorithm = getParameter("algorithm"); |
michael@0 | 234 | if (uri == null) { |
michael@0 | 235 | throw new IllegalStateException("URI may not be null"); |
michael@0 | 236 | } |
michael@0 | 237 | if (realm == null) { |
michael@0 | 238 | throw new IllegalStateException("Realm may not be null"); |
michael@0 | 239 | } |
michael@0 | 240 | if (nonce == null) { |
michael@0 | 241 | throw new IllegalStateException("Nonce may not be null"); |
michael@0 | 242 | } |
michael@0 | 243 | |
michael@0 | 244 | //TODO: add support for QOP_INT |
michael@0 | 245 | int qop = QOP_UNKNOWN; |
michael@0 | 246 | String qoplist = getParameter("qop"); |
michael@0 | 247 | if (qoplist != null) { |
michael@0 | 248 | StringTokenizer tok = new StringTokenizer(qoplist, ","); |
michael@0 | 249 | while (tok.hasMoreTokens()) { |
michael@0 | 250 | String variant = tok.nextToken().trim(); |
michael@0 | 251 | if (variant.equals("auth")) { |
michael@0 | 252 | qop = QOP_AUTH; |
michael@0 | 253 | break; |
michael@0 | 254 | } |
michael@0 | 255 | } |
michael@0 | 256 | } else { |
michael@0 | 257 | qop = QOP_MISSING; |
michael@0 | 258 | } |
michael@0 | 259 | |
michael@0 | 260 | if (qop == QOP_UNKNOWN) { |
michael@0 | 261 | throw new AuthenticationException("None of the qop methods is supported: " + qoplist); |
michael@0 | 262 | } |
michael@0 | 263 | |
michael@0 | 264 | // If an algorithm is not specified, default to MD5. |
michael@0 | 265 | if (algorithm == null) { |
michael@0 | 266 | algorithm = "MD5"; |
michael@0 | 267 | } |
michael@0 | 268 | // If an charset is not specified, default to ISO-8859-1. |
michael@0 | 269 | String charset = getParameter("charset"); |
michael@0 | 270 | if (charset == null) { |
michael@0 | 271 | charset = "ISO-8859-1"; |
michael@0 | 272 | } |
michael@0 | 273 | |
michael@0 | 274 | String digAlg = algorithm; |
michael@0 | 275 | if (digAlg.equalsIgnoreCase("MD5-sess")) { |
michael@0 | 276 | digAlg = "MD5"; |
michael@0 | 277 | } |
michael@0 | 278 | |
michael@0 | 279 | MessageDigest digester; |
michael@0 | 280 | try { |
michael@0 | 281 | digester = createMessageDigest(digAlg); |
michael@0 | 282 | } catch (UnsupportedDigestAlgorithmException ex) { |
michael@0 | 283 | throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg); |
michael@0 | 284 | } |
michael@0 | 285 | |
michael@0 | 286 | String uname = credentials.getUserPrincipal().getName(); |
michael@0 | 287 | String pwd = credentials.getPassword(); |
michael@0 | 288 | |
michael@0 | 289 | if (nonce.equals(this.lastNonce)) { |
michael@0 | 290 | nounceCount++; |
michael@0 | 291 | } else { |
michael@0 | 292 | nounceCount = 1; |
michael@0 | 293 | cnonce = null; |
michael@0 | 294 | lastNonce = nonce; |
michael@0 | 295 | } |
michael@0 | 296 | StringBuilder sb = new StringBuilder(256); |
michael@0 | 297 | Formatter formatter = new Formatter(sb, Locale.US); |
michael@0 | 298 | formatter.format("%08x", nounceCount); |
michael@0 | 299 | String nc = sb.toString(); |
michael@0 | 300 | |
michael@0 | 301 | if (cnonce == null) { |
michael@0 | 302 | cnonce = createCnonce(); |
michael@0 | 303 | } |
michael@0 | 304 | |
michael@0 | 305 | a1 = null; |
michael@0 | 306 | a2 = null; |
michael@0 | 307 | // 3.2.2.2: Calculating digest |
michael@0 | 308 | if (algorithm.equalsIgnoreCase("MD5-sess")) { |
michael@0 | 309 | // H( unq(username-value) ":" unq(realm-value) ":" passwd ) |
michael@0 | 310 | // ":" unq(nonce-value) |
michael@0 | 311 | // ":" unq(cnonce-value) |
michael@0 | 312 | |
michael@0 | 313 | // calculated one per session |
michael@0 | 314 | sb.setLength(0); |
michael@0 | 315 | sb.append(uname).append(':').append(realm).append(':').append(pwd); |
michael@0 | 316 | String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset))); |
michael@0 | 317 | sb.setLength(0); |
michael@0 | 318 | sb.append(checksum).append(':').append(nonce).append(':').append(cnonce); |
michael@0 | 319 | a1 = sb.toString(); |
michael@0 | 320 | } else { |
michael@0 | 321 | // unq(username-value) ":" unq(realm-value) ":" passwd |
michael@0 | 322 | sb.setLength(0); |
michael@0 | 323 | sb.append(uname).append(':').append(realm).append(':').append(pwd); |
michael@0 | 324 | a1 = sb.toString(); |
michael@0 | 325 | } |
michael@0 | 326 | |
michael@0 | 327 | String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset))); |
michael@0 | 328 | |
michael@0 | 329 | if (qop == QOP_AUTH) { |
michael@0 | 330 | // Method ":" digest-uri-value |
michael@0 | 331 | a2 = method + ':' + uri; |
michael@0 | 332 | } else if (qop == QOP_AUTH_INT) { |
michael@0 | 333 | // Method ":" digest-uri-value ":" H(entity-body) |
michael@0 | 334 | //TODO: calculate entity hash if entity is repeatable |
michael@0 | 335 | throw new AuthenticationException("qop-int method is not suppported"); |
michael@0 | 336 | } else { |
michael@0 | 337 | a2 = method + ':' + uri; |
michael@0 | 338 | } |
michael@0 | 339 | |
michael@0 | 340 | String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset))); |
michael@0 | 341 | |
michael@0 | 342 | // 3.2.2.1 |
michael@0 | 343 | |
michael@0 | 344 | String digestValue; |
michael@0 | 345 | if (qop == QOP_MISSING) { |
michael@0 | 346 | sb.setLength(0); |
michael@0 | 347 | sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2); |
michael@0 | 348 | digestValue = sb.toString(); |
michael@0 | 349 | } else { |
michael@0 | 350 | sb.setLength(0); |
michael@0 | 351 | sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':') |
michael@0 | 352 | .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth") |
michael@0 | 353 | .append(':').append(hasha2); |
michael@0 | 354 | digestValue = sb.toString(); |
michael@0 | 355 | } |
michael@0 | 356 | |
michael@0 | 357 | String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue))); |
michael@0 | 358 | |
michael@0 | 359 | CharArrayBuffer buffer = new CharArrayBuffer(128); |
michael@0 | 360 | if (isProxy()) { |
michael@0 | 361 | buffer.append(AUTH.PROXY_AUTH_RESP); |
michael@0 | 362 | } else { |
michael@0 | 363 | buffer.append(AUTH.WWW_AUTH_RESP); |
michael@0 | 364 | } |
michael@0 | 365 | buffer.append(": Digest "); |
michael@0 | 366 | |
michael@0 | 367 | List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20); |
michael@0 | 368 | params.add(new BasicNameValuePair("username", uname)); |
michael@0 | 369 | params.add(new BasicNameValuePair("realm", realm)); |
michael@0 | 370 | params.add(new BasicNameValuePair("nonce", nonce)); |
michael@0 | 371 | params.add(new BasicNameValuePair("uri", uri)); |
michael@0 | 372 | params.add(new BasicNameValuePair("response", digest)); |
michael@0 | 373 | |
michael@0 | 374 | if (qop != QOP_MISSING) { |
michael@0 | 375 | params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth")); |
michael@0 | 376 | params.add(new BasicNameValuePair("nc", nc)); |
michael@0 | 377 | params.add(new BasicNameValuePair("cnonce", cnonce)); |
michael@0 | 378 | } |
michael@0 | 379 | if (algorithm != null) { |
michael@0 | 380 | params.add(new BasicNameValuePair("algorithm", algorithm)); |
michael@0 | 381 | } |
michael@0 | 382 | if (opaque != null) { |
michael@0 | 383 | params.add(new BasicNameValuePair("opaque", opaque)); |
michael@0 | 384 | } |
michael@0 | 385 | |
michael@0 | 386 | for (int i = 0; i < params.size(); i++) { |
michael@0 | 387 | BasicNameValuePair param = params.get(i); |
michael@0 | 388 | if (i > 0) { |
michael@0 | 389 | buffer.append(", "); |
michael@0 | 390 | } |
michael@0 | 391 | boolean noQuotes = "nc".equals(param.getName()) || "qop".equals(param.getName()); |
michael@0 | 392 | BasicHeaderValueFormatter.DEFAULT.formatNameValuePair(buffer, param, !noQuotes); |
michael@0 | 393 | } |
michael@0 | 394 | return new BufferedHeader(buffer); |
michael@0 | 395 | } |
michael@0 | 396 | |
michael@0 | 397 | String getCnonce() { |
michael@0 | 398 | return cnonce; |
michael@0 | 399 | } |
michael@0 | 400 | |
michael@0 | 401 | String getA1() { |
michael@0 | 402 | return a1; |
michael@0 | 403 | } |
michael@0 | 404 | |
michael@0 | 405 | String getA2() { |
michael@0 | 406 | return a2; |
michael@0 | 407 | } |
michael@0 | 408 | |
michael@0 | 409 | /** |
michael@0 | 410 | * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long |
michael@0 | 411 | * <CODE>String</CODE> according to RFC 2617. |
michael@0 | 412 | * |
michael@0 | 413 | * @param binaryData array containing the digest |
michael@0 | 414 | * @return encoded MD5, or <CODE>null</CODE> if encoding failed |
michael@0 | 415 | */ |
michael@0 | 416 | private static String encode(byte[] binaryData) { |
michael@0 | 417 | int n = binaryData.length; |
michael@0 | 418 | char[] buffer = new char[n * 2]; |
michael@0 | 419 | for (int i = 0; i < n; i++) { |
michael@0 | 420 | int low = (binaryData[i] & 0x0f); |
michael@0 | 421 | int high = ((binaryData[i] & 0xf0) >> 4); |
michael@0 | 422 | buffer[i * 2] = HEXADECIMAL[high]; |
michael@0 | 423 | buffer[(i * 2) + 1] = HEXADECIMAL[low]; |
michael@0 | 424 | } |
michael@0 | 425 | |
michael@0 | 426 | return new String(buffer); |
michael@0 | 427 | } |
michael@0 | 428 | |
michael@0 | 429 | |
michael@0 | 430 | /** |
michael@0 | 431 | * Creates a random cnonce value based on the current time. |
michael@0 | 432 | * |
michael@0 | 433 | * @return The cnonce value as String. |
michael@0 | 434 | */ |
michael@0 | 435 | public static String createCnonce() { |
michael@0 | 436 | SecureRandom rnd = new SecureRandom(); |
michael@0 | 437 | byte[] tmp = new byte[8]; |
michael@0 | 438 | rnd.nextBytes(tmp); |
michael@0 | 439 | return encode(tmp); |
michael@0 | 440 | } |
michael@0 | 441 | |
michael@0 | 442 | } |