| /* |
| * Copyright (c) 2003, 2012, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package com.sun.security.sasl.digest; |
| |
| import java.security.NoSuchAlgorithmException; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.UnsupportedEncodingException; |
| import java.util.StringTokenizer; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Arrays; |
| |
| import java.util.logging.Level; |
| |
| import javax.security.sasl.*; |
| import javax.security.auth.callback.*; |
| |
| /** |
| * An implementation of the DIGEST-MD5 server SASL mechanism. |
| * (<a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</a>) |
| * <p> |
| * The DIGEST-MD5 SASL mechanism specifies two modes of authentication. |
| * <ul><li>Initial Authentication |
| * <li>Subsequent Authentication - optional, (currently not supported) |
| * </ul> |
| * |
| * Required callbacks: |
| * - RealmCallback |
| * used as key by handler to fetch password |
| * - NameCallback |
| * used as key by handler to fetch password |
| * - PasswordCallback |
| * handler must enter password for username/realm supplied |
| * - AuthorizeCallback |
| * handler must verify that authid/authzids are allowed and set |
| * authorized ID to be the canonicalized authzid (if applicable). |
| * |
| * Environment properties that affect the implementation: |
| * javax.security.sasl.qop: |
| * specifies list of qops; default is "auth"; typically, caller should set |
| * this to "auth, auth-int, auth-conf". |
| * javax.security.sasl.strength |
| * specifies low/medium/high strength of encryption; default is all available |
| * ciphers [high,medium,low]; high means des3 or rc4 (128); medium des or |
| * rc4-56; low is rc4-40. |
| * javax.security.sasl.maxbuf |
| * specifies max receive buf size; default is 65536 |
| * javax.security.sasl.sendmaxbuffer |
| * specifies max send buf size; default is 65536 (min of this and client's max |
| * recv size) |
| * |
| * com.sun.security.sasl.digest.utf8: |
| * "true" means to use UTF-8 charset; "false" to use ISO-8859-1 encoding; |
| * default is "true". |
| * com.sun.security.sasl.digest.realm: |
| * space-separated list of realms; default is server name (fqdn parameter) |
| * |
| * @author Rosanna Lee |
| */ |
| |
| final class DigestMD5Server extends DigestMD5Base implements SaslServer { |
| private static final String MY_CLASS_NAME = DigestMD5Server.class.getName(); |
| |
| private static final String UTF8_DIRECTIVE = "charset=utf-8,"; |
| private static final String ALGORITHM_DIRECTIVE = "algorithm=md5-sess"; |
| |
| /* |
| * Always expect nonce count value to be 1 because we support only |
| * initial authentication. |
| */ |
| private static final int NONCE_COUNT_VALUE = 1; |
| |
| /* "true" means use UTF8; "false" ISO 8859-1; default is "true" */ |
| private static final String UTF8_PROPERTY = |
| "com.sun.security.sasl.digest.utf8"; |
| |
| /* List of space-separated realms used for authentication */ |
| private static final String REALM_PROPERTY = |
| "com.sun.security.sasl.digest.realm"; |
| |
| /* Directives encountered in responses sent by the client. */ |
| private static final String[] DIRECTIVE_KEY = { |
| "username", // exactly once |
| "realm", // exactly once if sent by server |
| "nonce", // exactly once |
| "cnonce", // exactly once |
| "nonce-count", // atmost once; default is 00000001 |
| "qop", // atmost once; default is "auth" |
| "digest-uri", // atmost once; (default?) |
| "response", // exactly once |
| "maxbuf", // atmost once; default is 65536 |
| "charset", // atmost once; default is ISO-8859-1 |
| "cipher", // exactly once if qop is "auth-conf" |
| "authzid", // atmost once; default is none |
| "auth-param", // >= 0 times (ignored) |
| }; |
| |
| /* Indices into DIRECTIVE_KEY */ |
| private static final int USERNAME = 0; |
| private static final int REALM = 1; |
| private static final int NONCE = 2; |
| private static final int CNONCE = 3; |
| private static final int NONCE_COUNT = 4; |
| private static final int QOP = 5; |
| private static final int DIGEST_URI = 6; |
| private static final int RESPONSE = 7; |
| private static final int MAXBUF = 8; |
| private static final int CHARSET = 9; |
| private static final int CIPHER = 10; |
| private static final int AUTHZID = 11; |
| private static final int AUTH_PARAM = 12; |
| |
| /* Server-generated/supplied information */ |
| private String specifiedQops; |
| private byte[] myCiphers; |
| private List<String> serverRealms; |
| |
| DigestMD5Server(String protocol, String serverName, Map<String, ?> props, |
| CallbackHandler cbh) throws SaslException { |
| super(props, MY_CLASS_NAME, 1, |
| protocol + "/" + (serverName==null?"*":serverName), |
| cbh); |
| |
| serverRealms = new ArrayList<String>(); |
| |
| useUTF8 = true; // default |
| |
| if (props != null) { |
| specifiedQops = (String) props.get(Sasl.QOP); |
| if ("false".equals((String) props.get(UTF8_PROPERTY))) { |
| useUTF8 = false; |
| logger.log(Level.FINE, "DIGEST80:Server supports ISO-Latin-1"); |
| } |
| |
| String realms = (String) props.get(REALM_PROPERTY); |
| if (realms != null) { |
| StringTokenizer parser = new StringTokenizer(realms, ", \t\n"); |
| int tokenCount = parser.countTokens(); |
| String token = null; |
| for (int i = 0; i < tokenCount; i++) { |
| token = parser.nextToken(); |
| logger.log(Level.FINE, "DIGEST81:Server supports realm {0}", |
| token); |
| serverRealms.add(token); |
| } |
| } |
| } |
| |
| encoding = (useUTF8 ? "UTF8" : "8859_1"); |
| |
| // By default, use server name as realm |
| if (serverRealms.isEmpty()) { |
| if (serverName == null) { |
| throw new SaslException( |
| "A realm must be provided in props or serverName"); |
| } else { |
| serverRealms.add(serverName); |
| } |
| } |
| } |
| |
| public byte[] evaluateResponse(byte[] response) throws SaslException { |
| if (response.length > MAX_RESPONSE_LENGTH) { |
| throw new SaslException( |
| "DIGEST-MD5: Invalid digest response length. Got: " + |
| response.length + " Expected < " + MAX_RESPONSE_LENGTH); |
| } |
| |
| byte[] challenge; |
| switch (step) { |
| case 1: |
| if (response.length != 0) { |
| throw new SaslException( |
| "DIGEST-MD5 must not have an initial response"); |
| } |
| |
| /* Generate first challenge */ |
| String supportedCiphers = null; |
| if ((allQop&PRIVACY_PROTECTION) != 0) { |
| myCiphers = getPlatformCiphers(); |
| StringBuilder sb = new StringBuilder(); |
| |
| // myCipher[i] is a byte that indicates whether CIPHER_TOKENS[i] |
| // is supported |
| for (int i = 0; i < CIPHER_TOKENS.length; i++) { |
| if (myCiphers[i] != 0) { |
| if (sb.length() > 0) { |
| sb.append(','); |
| } |
| sb.append(CIPHER_TOKENS[i]); |
| } |
| } |
| supportedCiphers = sb.toString(); |
| } |
| |
| try { |
| challenge = generateChallenge(serverRealms, specifiedQops, |
| supportedCiphers); |
| |
| step = 3; |
| return challenge; |
| } catch (UnsupportedEncodingException e) { |
| throw new SaslException( |
| "DIGEST-MD5: Error encoding challenge", e); |
| } catch (IOException e) { |
| throw new SaslException( |
| "DIGEST-MD5: Error generating challenge", e); |
| } |
| |
| // Step 2 is performed by client |
| |
| case 3: |
| /* Validates client's response and generate challenge: |
| * response-auth = "rspauth" "=" response-value |
| */ |
| try { |
| byte[][] responseVal = parseDirectives(response, DIRECTIVE_KEY, |
| null, REALM); |
| challenge = validateClientResponse(responseVal); |
| } catch (SaslException e) { |
| throw e; |
| } catch (UnsupportedEncodingException e) { |
| throw new SaslException( |
| "DIGEST-MD5: Error validating client response", e); |
| } finally { |
| step = 0; // Set to invalid state |
| } |
| |
| completed = true; |
| |
| /* Initialize SecurityCtx implementation */ |
| if (integrity && privacy) { |
| secCtx = new DigestPrivacy(false /* not client */); |
| } else if (integrity) { |
| secCtx = new DigestIntegrity(false /* not client */); |
| } |
| |
| return challenge; |
| |
| default: |
| // No other possible state |
| throw new SaslException("DIGEST-MD5: Server at illegal state"); |
| } |
| } |
| |
| /** |
| * Generates challenge to be sent to client. |
| * digest-challenge = |
| * 1#( realm | nonce | qop-options | stale | maxbuf | charset |
| * algorithm | cipher-opts | auth-param ) |
| * |
| * realm = "realm" "=" <"> realm-value <"> |
| * realm-value = qdstr-val |
| * nonce = "nonce" "=" <"> nonce-value <"> |
| * nonce-value = qdstr-val |
| * qop-options = "qop" "=" <"> qop-list <"> |
| * qop-list = 1#qop-value |
| * qop-value = "auth" | "auth-int" | "auth-conf" | |
| * token |
| * stale = "stale" "=" "true" |
| * maxbuf = "maxbuf" "=" maxbuf-value |
| * maxbuf-value = 1*DIGIT |
| * charset = "charset" "=" "utf-8" |
| * algorithm = "algorithm" "=" "md5-sess" |
| * cipher-opts = "cipher" "=" <"> 1#cipher-value <"> |
| * cipher-value = "3des" | "des" | "rc4-40" | "rc4" | |
| * "rc4-56" | token |
| * auth-param = token "=" ( token | quoted-string ) |
| */ |
| private byte[] generateChallenge(List<String> realms, String qopStr, |
| String cipherStr) throws UnsupportedEncodingException, IOException { |
| ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| |
| // Realms (>= 0) |
| for (int i = 0; realms != null && i < realms.size(); i++) { |
| out.write("realm=\"".getBytes(encoding)); |
| writeQuotedStringValue(out, realms.get(i).getBytes(encoding)); |
| out.write('"'); |
| out.write(','); |
| } |
| |
| // Nonce - required (1) |
| out.write(("nonce=\"").getBytes(encoding)); |
| nonce = generateNonce(); |
| writeQuotedStringValue(out, nonce); |
| out.write('"'); |
| out.write(','); |
| |
| // QOP - optional (1) [default: auth] |
| // qop="auth,auth-conf,auth-int" |
| if (qopStr != null) { |
| out.write(("qop=\"").getBytes(encoding)); |
| // Check for quotes in case of non-standard qop options |
| writeQuotedStringValue(out, qopStr.getBytes(encoding)); |
| out.write('"'); |
| out.write(','); |
| } |
| |
| // maxbuf - optional (1) [default: 65536] |
| if (recvMaxBufSize != DEFAULT_MAXBUF) { |
| out.write(("maxbuf=\"" + recvMaxBufSize + "\",").getBytes(encoding)); |
| } |
| |
| // charset - optional (1) [default: ISO 8859_1] |
| if (useUTF8) { |
| out.write(UTF8_DIRECTIVE.getBytes(encoding)); |
| } |
| |
| if (cipherStr != null) { |
| out.write("cipher=\"".getBytes(encoding)); |
| // Check for quotes in case of custom ciphers |
| writeQuotedStringValue(out, cipherStr.getBytes(encoding)); |
| out.write('"'); |
| out.write(','); |
| } |
| |
| // algorithm - required (1) |
| out.write(ALGORITHM_DIRECTIVE.getBytes(encoding)); |
| |
| return out.toByteArray(); |
| } |
| |
| /** |
| * Validates client's response. |
| * digest-response = 1#( username | realm | nonce | cnonce | |
| * nonce-count | qop | digest-uri | response | |
| * maxbuf | charset | cipher | authzid | |
| * auth-param ) |
| * |
| * username = "username" "=" <"> username-value <"> |
| * username-value = qdstr-val |
| * cnonce = "cnonce" "=" <"> cnonce-value <"> |
| * cnonce-value = qdstr-val |
| * nonce-count = "nc" "=" nc-value |
| * nc-value = 8LHEX |
| * qop = "qop" "=" qop-value |
| * digest-uri = "digest-uri" "=" <"> digest-uri-value <"> |
| * digest-uri-value = serv-type "/" host [ "/" serv-name ] |
| * serv-type = 1*ALPHA |
| * host = 1*( ALPHA | DIGIT | "-" | "." ) |
| * serv-name = host |
| * response = "response" "=" response-value |
| * response-value = 32LHEX |
| * LHEX = "0" | "1" | "2" | "3" | |
| * "4" | "5" | "6" | "7" | |
| * "8" | "9" | "a" | "b" | |
| * "c" | "d" | "e" | "f" |
| * cipher = "cipher" "=" cipher-value |
| * authzid = "authzid" "=" <"> authzid-value <"> |
| * authzid-value = qdstr-val |
| * sets: |
| * negotiatedQop |
| * negotiatedCipher |
| * negotiatedRealm |
| * negotiatedStrength |
| * digestUri (checked and set to clients to account for case diffs) |
| * sendMaxBufSize |
| * authzid (gotten from callback) |
| * @return response-value ('rspauth') for client to validate |
| */ |
| private byte[] validateClientResponse(byte[][] responseVal) |
| throws SaslException, UnsupportedEncodingException { |
| |
| /* CHARSET: optional atmost once */ |
| if (responseVal[CHARSET] != null) { |
| // The client should send this directive only if the server has |
| // indicated it supports UTF-8. |
| if (!useUTF8 || |
| !"utf-8".equals(new String(responseVal[CHARSET], encoding))) { |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| "violation. Incompatible charset value: " + |
| new String(responseVal[CHARSET])); |
| } |
| } |
| |
| // maxbuf: atmost once |
| int clntMaxBufSize = |
| (responseVal[MAXBUF] == null) ? DEFAULT_MAXBUF |
| : Integer.parseInt(new String(responseVal[MAXBUF], encoding)); |
| |
| // Max send buf size is min of client's max recv buf size and |
| // server's max send buf size |
| sendMaxBufSize = ((sendMaxBufSize == 0) ? clntMaxBufSize : |
| Math.min(sendMaxBufSize, clntMaxBufSize)); |
| |
| /* username: exactly once */ |
| String username; |
| if (responseVal[USERNAME] != null) { |
| username = new String(responseVal[USERNAME], encoding); |
| logger.log(Level.FINE, "DIGEST82:Username: {0}", username); |
| } else { |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| "violation. Missing username."); |
| } |
| |
| /* realm: exactly once if sent by server */ |
| negotiatedRealm = ((responseVal[REALM] != null) ? |
| new String(responseVal[REALM], encoding) : ""); |
| logger.log(Level.FINE, "DIGEST83:Client negotiated realm: {0}", |
| negotiatedRealm); |
| |
| if (!serverRealms.contains(negotiatedRealm)) { |
| // Server had sent at least one realm |
| // Check that response is one of these |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| "violation. Nonexistent realm: " + negotiatedRealm); |
| } |
| // Else, client specified realm was one of server's or server had none |
| |
| /* nonce: exactly once */ |
| if (responseVal[NONCE] == null) { |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| "violation. Missing nonce."); |
| } |
| byte[] nonceFromClient = responseVal[NONCE]; |
| if (!Arrays.equals(nonceFromClient, nonce)) { |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| "violation. Mismatched nonce."); |
| } |
| |
| /* cnonce: exactly once */ |
| if (responseVal[CNONCE] == null) { |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| "violation. Missing cnonce."); |
| } |
| byte[] cnonce = responseVal[CNONCE]; |
| |
| /* nonce-count: atmost once */ |
| if (responseVal[NONCE_COUNT] != null && |
| NONCE_COUNT_VALUE != Integer.parseInt( |
| new String(responseVal[NONCE_COUNT], encoding), 16)) { |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| "violation. Nonce count does not match: " + |
| new String(responseVal[NONCE_COUNT])); |
| } |
| |
| /* qop: atmost once; default is "auth" */ |
| negotiatedQop = ((responseVal[QOP] != null) ? |
| new String(responseVal[QOP], encoding) : "auth"); |
| |
| logger.log(Level.FINE, "DIGEST84:Client negotiated qop: {0}", |
| negotiatedQop); |
| |
| // Check that QOP is one sent by server |
| byte cQop; |
| switch (negotiatedQop) { |
| case "auth": |
| cQop = NO_PROTECTION; |
| break; |
| case "auth-int": |
| cQop = INTEGRITY_ONLY_PROTECTION; |
| integrity = true; |
| rawSendSize = sendMaxBufSize - 16; |
| break; |
| case "auth-conf": |
| cQop = PRIVACY_PROTECTION; |
| integrity = privacy = true; |
| rawSendSize = sendMaxBufSize - 26; |
| break; |
| default: |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| "violation. Invalid QOP: " + negotiatedQop); |
| } |
| if ((cQop&allQop) == 0) { |
| throw new SaslException("DIGEST-MD5: server does not support " + |
| " qop: " + negotiatedQop); |
| } |
| |
| if (privacy) { |
| negotiatedCipher = ((responseVal[CIPHER] != null) ? |
| new String(responseVal[CIPHER], encoding) : null); |
| if (negotiatedCipher == null) { |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| "violation. No cipher specified."); |
| } |
| |
| int foundCipher = -1; |
| logger.log(Level.FINE, "DIGEST85:Client negotiated cipher: {0}", |
| negotiatedCipher); |
| |
| // Check that cipher is one that we offered |
| for (int j = 0; j < CIPHER_TOKENS.length; j++) { |
| if (negotiatedCipher.equals(CIPHER_TOKENS[j]) && |
| myCiphers[j] != 0) { |
| foundCipher = j; |
| break; |
| } |
| } |
| if (foundCipher == -1) { |
| throw new SaslException("DIGEST-MD5: server does not " + |
| "support cipher: " + negotiatedCipher); |
| } |
| // Set negotiatedStrength |
| if ((CIPHER_MASKS[foundCipher]&HIGH_STRENGTH) != 0) { |
| negotiatedStrength = "high"; |
| } else if ((CIPHER_MASKS[foundCipher]&MEDIUM_STRENGTH) != 0) { |
| negotiatedStrength = "medium"; |
| } else { |
| // assume default low |
| negotiatedStrength = "low"; |
| } |
| |
| logger.log(Level.FINE, "DIGEST86:Negotiated strength: {0}", |
| negotiatedStrength); |
| } |
| |
| // atmost once |
| String digestUriFromResponse = ((responseVal[DIGEST_URI]) != null ? |
| new String(responseVal[DIGEST_URI], encoding) : null); |
| |
| if (digestUriFromResponse != null) { |
| logger.log(Level.FINE, "DIGEST87:digest URI: {0}", |
| digestUriFromResponse); |
| } |
| |
| // serv-type "/" host [ "/" serv-name ] |
| // e.g.: smtp/mail3.example.com/example.com |
| // e.g.: ftp/ftp.example.com |
| // e.g.: ldap/ldapserver.example.com |
| |
| // host should match one of service's configured service names |
| // Check against digest URI that mech was created with |
| |
| if (uriMatches(digestUri, digestUriFromResponse)) { |
| digestUri = digestUriFromResponse; // account for case-sensitive diffs |
| } else { |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| "violation. Mismatched URI: " + digestUriFromResponse + |
| "; expecting: " + digestUri); |
| } |
| |
| // response: exactly once |
| byte[] responseFromClient = responseVal[RESPONSE]; |
| if (responseFromClient == null) { |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| " violation. Missing response."); |
| } |
| |
| // authzid: atmost once |
| byte[] authzidBytes; |
| String authzidFromClient = ((authzidBytes=responseVal[AUTHZID]) != null? |
| new String(authzidBytes, encoding) : username); |
| |
| if (authzidBytes != null) { |
| logger.log(Level.FINE, "DIGEST88:Authzid: {0}", |
| new String(authzidBytes)); |
| } |
| |
| // Ignore auth-param |
| |
| // Get password need to generate verifying response |
| char[] passwd; |
| try { |
| // Realm and Name callbacks are used to provide info |
| RealmCallback rcb = new RealmCallback("DIGEST-MD5 realm: ", |
| negotiatedRealm); |
| NameCallback ncb = new NameCallback("DIGEST-MD5 authentication ID: ", |
| username); |
| |
| // PasswordCallback is used to collect info |
| PasswordCallback pcb = |
| new PasswordCallback("DIGEST-MD5 password: ", false); |
| |
| cbh.handle(new Callback[] {rcb, ncb, pcb}); |
| passwd = pcb.getPassword(); |
| pcb.clearPassword(); |
| |
| } catch (UnsupportedCallbackException e) { |
| throw new SaslException( |
| "DIGEST-MD5: Cannot perform callback to acquire password", e); |
| |
| } catch (IOException e) { |
| throw new SaslException( |
| "DIGEST-MD5: IO error acquiring password", e); |
| } |
| |
| if (passwd == null) { |
| throw new SaslException( |
| "DIGEST-MD5: cannot acquire password for " + username + |
| " in realm : " + negotiatedRealm); |
| } |
| |
| try { |
| // Validate response value sent by client |
| byte[] expectedResponse; |
| |
| try { |
| expectedResponse = generateResponseValue("AUTHENTICATE", |
| digestUri, negotiatedQop, username, negotiatedRealm, |
| passwd, nonce /* use own nonce */, |
| cnonce, NONCE_COUNT_VALUE, authzidBytes); |
| |
| } catch (NoSuchAlgorithmException e) { |
| throw new SaslException( |
| "DIGEST-MD5: problem duplicating client response", e); |
| } catch (IOException e) { |
| throw new SaslException( |
| "DIGEST-MD5: problem duplicating client response", e); |
| } |
| |
| if (!Arrays.equals(responseFromClient, expectedResponse)) { |
| throw new SaslException("DIGEST-MD5: digest response format " + |
| "violation. Mismatched response."); |
| } |
| |
| // Ensure that authzid mapping is OK |
| try { |
| AuthorizeCallback acb = |
| new AuthorizeCallback(username, authzidFromClient); |
| cbh.handle(new Callback[]{acb}); |
| |
| if (acb.isAuthorized()) { |
| authzid = acb.getAuthorizedID(); |
| } else { |
| throw new SaslException("DIGEST-MD5: " + username + |
| " is not authorized to act as " + authzidFromClient); |
| } |
| } catch (SaslException e) { |
| throw e; |
| } catch (UnsupportedCallbackException e) { |
| throw new SaslException( |
| "DIGEST-MD5: Cannot perform callback to check authzid", e); |
| } catch (IOException e) { |
| throw new SaslException( |
| "DIGEST-MD5: IO error checking authzid", e); |
| } |
| |
| return generateResponseAuth(username, passwd, cnonce, |
| NONCE_COUNT_VALUE, authzidBytes); |
| } finally { |
| // Clear password |
| for (int i = 0; i < passwd.length; i++) { |
| passwd[i] = 0; |
| } |
| } |
| } |
| |
| private static boolean uriMatches(String thisUri, String incomingUri) { |
| // Full match |
| if (thisUri.equalsIgnoreCase(incomingUri)) { |
| return true; |
| } |
| // Unbound match |
| if (thisUri.endsWith("/*")) { |
| int protoAndSlash = thisUri.length() - 1; |
| String thisProtoAndSlash = thisUri.substring(0, protoAndSlash); |
| String incomingProtoAndSlash = incomingUri.substring(0, protoAndSlash); |
| return thisProtoAndSlash.equalsIgnoreCase(incomingProtoAndSlash); |
| } |
| return false; |
| } |
| |
| /** |
| * Server sends a message formatted as follows: |
| * response-auth = "rspauth" "=" response-value |
| * where response-value is calculated as above, using the values sent in |
| * step two, except that if qop is "auth", then A2 is |
| * |
| * A2 = { ":", digest-uri-value } |
| * |
| * And if qop is "auth-int" or "auth-conf" then A2 is |
| * |
| * A2 = { ":", digest-uri-value, ":00000000000000000000000000000000" } |
| * |
| * Clears password afterwards. |
| */ |
| private byte[] generateResponseAuth(String username, char[] passwd, |
| byte[] cnonce, int nonceCount, byte[] authzidBytes) throws SaslException { |
| |
| // Construct response value |
| |
| try { |
| byte[] responseValue = generateResponseValue("", |
| digestUri, negotiatedQop, username, negotiatedRealm, |
| passwd, nonce, cnonce, nonceCount, authzidBytes); |
| |
| byte[] challenge = new byte[responseValue.length + 8]; |
| System.arraycopy("rspauth=".getBytes(encoding), 0, challenge, 0, 8); |
| System.arraycopy(responseValue, 0, challenge, 8, |
| responseValue.length ); |
| |
| return challenge; |
| |
| } catch (NoSuchAlgorithmException e) { |
| throw new SaslException("DIGEST-MD5: problem generating response", e); |
| } catch (IOException e) { |
| throw new SaslException("DIGEST-MD5: problem generating response", e); |
| } |
| } |
| |
| public String getAuthorizationID() { |
| if (completed) { |
| return authzid; |
| } else { |
| throw new IllegalStateException( |
| "DIGEST-MD5 server negotiation not complete"); |
| } |
| } |
| } |