J. Duke | 319a3b9 | 2007-12-01 00:00:00 +0000 | [diff] [blame^] | 1 | /* |
| 2 | * Copyright 2003-2004 Sun Microsystems, Inc. All Rights Reserved. |
| 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| 4 | * |
| 5 | * This code is free software; you can redistribute it and/or modify it |
| 6 | * under the terms of the GNU General Public License version 2 only, as |
| 7 | * published by the Free Software Foundation. Sun designates this |
| 8 | * particular file as subject to the "Classpath" exception as provided |
| 9 | * by Sun in the LICENSE file that accompanied this code. |
| 10 | * |
| 11 | * This code is distributed in the hope that it will be useful, but WITHOUT |
| 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 14 | * version 2 for more details (a copy is included in the LICENSE file that |
| 15 | * accompanied this code). |
| 16 | * |
| 17 | * You should have received a copy of the GNU General Public License version |
| 18 | * 2 along with this work; if not, write to the Free Software Foundation, |
| 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| 20 | * |
| 21 | * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, |
| 22 | * CA 95054 USA or visit www.sun.com if you need additional information or |
| 23 | * have any questions. |
| 24 | */ |
| 25 | |
| 26 | package com.sun.security.sasl; |
| 27 | |
| 28 | import javax.security.sasl.*; |
| 29 | import javax.security.auth.callback.*; |
| 30 | import java.util.Random; |
| 31 | import java.util.Map; |
| 32 | import java.io.IOException; |
| 33 | import java.io.UnsupportedEncodingException; |
| 34 | import java.security.NoSuchAlgorithmException; |
| 35 | |
| 36 | import java.util.logging.Logger; |
| 37 | import java.util.logging.Level; |
| 38 | |
| 39 | /** |
| 40 | * Implements the CRAM-MD5 SASL server-side mechanism. |
| 41 | * (<A HREF="ftp://ftp.isi.edu/in-notes/rfc2195.txt">RFC 2195</A>). |
| 42 | * CRAM-MD5 has no initial response. |
| 43 | * |
| 44 | * client <---- M={random, timestamp, server-fqdn} ------- server |
| 45 | * client ----- {username HMAC_MD5(pw, M)} --------------> server |
| 46 | * |
| 47 | * CallbackHandler must be able to handle the following callbacks: |
| 48 | * - NameCallback: default name is name of user for whom to get password |
| 49 | * - PasswordCallback: must fill in password; if empty, no pw |
| 50 | * - AuthorizeCallback: must setAuthorized() and canonicalized authorization id |
| 51 | * - auth id == authzid, but needed to get canonicalized authzid |
| 52 | * |
| 53 | * @author Rosanna Lee |
| 54 | */ |
| 55 | final class CramMD5Server extends CramMD5Base implements SaslServer { |
| 56 | private String fqdn; |
| 57 | private byte[] challengeData = null; |
| 58 | private String authzid; |
| 59 | private CallbackHandler cbh; |
| 60 | |
| 61 | /** |
| 62 | * Creates a SASL mechanism with client credentials that it needs |
| 63 | * to participate in CRAM-MD5 authentication exchange with the server. |
| 64 | * |
| 65 | * @param authID A non-null string representing the principal |
| 66 | * being authenticated. |
| 67 | * |
| 68 | * @param pw A non-null String or byte[] |
| 69 | * containing the password. If it is an array, it is first cloned. |
| 70 | */ |
| 71 | CramMD5Server(String protocol, String serverFqdn, Map props, |
| 72 | CallbackHandler cbh) throws SaslException { |
| 73 | if (serverFqdn == null) { |
| 74 | throw new SaslException( |
| 75 | "CRAM-MD5: fully qualified server name must be specified"); |
| 76 | } |
| 77 | |
| 78 | fqdn = serverFqdn; |
| 79 | this.cbh = cbh; |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * Generates challenge based on response sent by client. |
| 84 | * |
| 85 | * CRAM-MD5 has no initial response. |
| 86 | * First call generates challenge. |
| 87 | * Second call verifies client response. If authentication fails, throws |
| 88 | * SaslException. |
| 89 | * |
| 90 | * @param responseData A non-null byte array containing the response |
| 91 | * data from the client. |
| 92 | * @return A non-null byte array containing the challenge to be sent to |
| 93 | * the client for the first call; null when 2nd call is successful. |
| 94 | * @throws SaslException If authentication fails. |
| 95 | */ |
| 96 | public byte[] evaluateResponse(byte[] responseData) |
| 97 | throws SaslException { |
| 98 | |
| 99 | // See if we've been here before |
| 100 | if (completed) { |
| 101 | throw new IllegalStateException( |
| 102 | "CRAM-MD5 authentication already completed"); |
| 103 | } |
| 104 | |
| 105 | if (aborted) { |
| 106 | throw new IllegalStateException( |
| 107 | "CRAM-MD5 authentication previously aborted due to error"); |
| 108 | } |
| 109 | |
| 110 | try { |
| 111 | if (challengeData == null) { |
| 112 | if (responseData.length != 0) { |
| 113 | aborted = true; |
| 114 | throw new SaslException( |
| 115 | "CRAM-MD5 does not expect any initial response"); |
| 116 | } |
| 117 | |
| 118 | // Generate challenge {random, timestamp, fqdn} |
| 119 | Random random = new Random(); |
| 120 | long rand = random.nextLong(); |
| 121 | long timestamp = System.currentTimeMillis(); |
| 122 | |
| 123 | StringBuffer buf = new StringBuffer(); |
| 124 | buf.append('<'); |
| 125 | buf.append(rand); |
| 126 | buf.append('.'); |
| 127 | buf.append(timestamp); |
| 128 | buf.append('@'); |
| 129 | buf.append(fqdn); |
| 130 | buf.append('>'); |
| 131 | String challengeStr = buf.toString(); |
| 132 | |
| 133 | logger.log(Level.FINE, |
| 134 | "CRAMSRV01:Generated challenge: {0}", challengeStr); |
| 135 | |
| 136 | challengeData = challengeStr.getBytes("UTF8"); |
| 137 | return challengeData.clone(); |
| 138 | |
| 139 | } else { |
| 140 | // Examine response to see if correctly encrypted challengeData |
| 141 | if(logger.isLoggable(Level.FINE)) { |
| 142 | logger.log(Level.FINE, |
| 143 | "CRAMSRV02:Received response: {0}", |
| 144 | new String(responseData, "UTF8")); |
| 145 | } |
| 146 | |
| 147 | // Extract username from response |
| 148 | int ulen = 0; |
| 149 | for (int i = 0; i < responseData.length; i++) { |
| 150 | if (responseData[i] == ' ') { |
| 151 | ulen = i; |
| 152 | break; |
| 153 | } |
| 154 | } |
| 155 | if (ulen == 0) { |
| 156 | aborted = true; |
| 157 | throw new SaslException( |
| 158 | "CRAM-MD5: Invalid response; space missing"); |
| 159 | } |
| 160 | String username = new String(responseData, 0, ulen, "UTF8"); |
| 161 | |
| 162 | logger.log(Level.FINE, |
| 163 | "CRAMSRV03:Extracted username: {0}", username); |
| 164 | |
| 165 | // Get user's password |
| 166 | NameCallback ncb = |
| 167 | new NameCallback("CRAM-MD5 authentication ID: ", username); |
| 168 | PasswordCallback pcb = |
| 169 | new PasswordCallback("CRAM-MD5 password: ", false); |
| 170 | cbh.handle(new Callback[]{ncb,pcb}); |
| 171 | char pwChars[] = pcb.getPassword(); |
| 172 | if (pwChars == null || pwChars.length == 0) { |
| 173 | // user has no password; OK to disclose to server |
| 174 | aborted = true; |
| 175 | throw new SaslException( |
| 176 | "CRAM-MD5: username not found: " + username); |
| 177 | } |
| 178 | pcb.clearPassword(); |
| 179 | String pwStr = new String(pwChars); |
| 180 | for (int i = 0; i < pwChars.length; i++) { |
| 181 | pwChars[i] = 0; |
| 182 | } |
| 183 | pw = pwStr.getBytes("UTF8"); |
| 184 | |
| 185 | // Generate a keyed-MD5 digest from the user's password and |
| 186 | // original challenge. |
| 187 | String digest = HMAC_MD5(pw, challengeData); |
| 188 | |
| 189 | logger.log(Level.FINE, |
| 190 | "CRAMSRV04:Expecting digest: {0}", digest); |
| 191 | |
| 192 | // clear pw when we no longer need it |
| 193 | clearPassword(); |
| 194 | |
| 195 | // Check whether digest is as expected |
| 196 | byte [] expectedDigest = digest.getBytes("UTF8"); |
| 197 | int digestLen = responseData.length - ulen - 1; |
| 198 | if (expectedDigest.length != digestLen) { |
| 199 | aborted = true; |
| 200 | throw new SaslException("Invalid response"); |
| 201 | } |
| 202 | int j = 0; |
| 203 | for (int i = ulen + 1; i < responseData.length ; i++) { |
| 204 | if (expectedDigest[j++] != responseData[i]) { |
| 205 | aborted = true; |
| 206 | throw new SaslException("Invalid response"); |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | // All checks out, use AuthorizeCallback to canonicalize name |
| 211 | AuthorizeCallback acb = new AuthorizeCallback(username, username); |
| 212 | cbh.handle(new Callback[]{acb}); |
| 213 | if (acb.isAuthorized()) { |
| 214 | authzid = acb.getAuthorizedID(); |
| 215 | } else { |
| 216 | // Not authorized |
| 217 | aborted = true; |
| 218 | throw new SaslException( |
| 219 | "CRAM-MD5: user not authorized: " + username); |
| 220 | } |
| 221 | |
| 222 | logger.log(Level.FINE, |
| 223 | "CRAMSRV05:Authorization id: {0}", authzid); |
| 224 | |
| 225 | completed = true; |
| 226 | return null; |
| 227 | } |
| 228 | } catch (UnsupportedEncodingException e) { |
| 229 | aborted = true; |
| 230 | throw new SaslException("UTF8 not available on platform", e); |
| 231 | } catch (NoSuchAlgorithmException e) { |
| 232 | aborted = true; |
| 233 | throw new SaslException("MD5 algorithm not available on platform", e); |
| 234 | } catch (UnsupportedCallbackException e) { |
| 235 | aborted = true; |
| 236 | throw new SaslException("CRAM-MD5 authentication failed", e); |
| 237 | } catch (SaslException e) { |
| 238 | throw e; // rethrow |
| 239 | } catch (IOException e) { |
| 240 | aborted = true; |
| 241 | throw new SaslException("CRAM-MD5 authentication failed", e); |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | public String getAuthorizationID() { |
| 246 | if (completed) { |
| 247 | return authzid; |
| 248 | } else { |
| 249 | throw new IllegalStateException( |
| 250 | "CRAM-MD5 authentication not completed"); |
| 251 | } |
| 252 | } |
| 253 | } |