001/* 002 * Copyright 2007-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2017 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk; 022 023 024 025import java.util.ArrayList; 026import java.util.List; 027import java.util.logging.Level; 028import javax.security.auth.callback.Callback; 029import javax.security.auth.callback.CallbackHandler; 030import javax.security.auth.callback.NameCallback; 031import javax.security.auth.callback.PasswordCallback; 032import javax.security.sasl.Sasl; 033import javax.security.sasl.SaslClient; 034 035import com.unboundid.asn1.ASN1OctetString; 036import com.unboundid.util.DebugType; 037import com.unboundid.util.InternalUseOnly; 038import com.unboundid.util.NotMutable; 039import com.unboundid.util.ThreadSafety; 040import com.unboundid.util.ThreadSafetyLevel; 041 042import static com.unboundid.ldap.sdk.LDAPMessages.*; 043import static com.unboundid.util.Debug.*; 044import static com.unboundid.util.StaticUtils.*; 045import static com.unboundid.util.Validator.*; 046 047 048 049/** 050 * This class provides a SASL CRAM-MD5 bind request implementation as described 051 * in draft-ietf-sasl-crammd5. The CRAM-MD5 mechanism can be used to 052 * authenticate over an insecure channel without exposing the credentials 053 * (although it requires that the server have access to the clear-text 054 * password). It is similar to DIGEST-MD5, but does not provide as many 055 * options, and provides slightly weaker protection because the client does not 056 * contribute any of the random data used during bind processing. 057 * <BR><BR> 058 * Elements included in a CRAM-MD5 bind request include: 059 * <UL> 060 * <LI>Authentication ID -- A string which identifies the user that is 061 * attempting to authenticate. It should be an "authzId" value as 062 * described in section 5.2.1.8 of 063 * <A HREF="http://www.ietf.org/rfc/rfc4513.txt">RFC 4513</A>. That is, 064 * it should be either "dn:" followed by the distinguished name of the 065 * target user, or "u:" followed by the username. If the "u:" form is 066 * used, then the mechanism used to resolve the provided username to an 067 * entry may vary from server to server.</LI> 068 * <LI>Password -- The clear-text password for the target user.</LI> 069 * </UL> 070 * <H2>Example</H2> 071 * The following example demonstrates the process for performing a CRAM-MD5 072 * bind against a directory server with a username of "john.doe" and a password 073 * of "password": 074 * <PRE> 075 * CRAMMD5BindRequest bindRequest = 076 * new CRAMMD5BindRequest("u:john.doe", "password"); 077 * BindResult bindResult; 078 * try 079 * { 080 * bindResult = connection.bind(bindRequest); 081 * // If we get here, then the bind was successful. 082 * } 083 * catch (LDAPException le) 084 * { 085 * // The bind failed for some reason. 086 * bindResult = new BindResult(le.toLDAPResult()); 087 * ResultCode resultCode = le.getResultCode(); 088 * String errorMessageFromServer = le.getDiagnosticMessage(); 089 * } 090 * </PRE> 091 */ 092@NotMutable() 093@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 094public final class CRAMMD5BindRequest 095 extends SASLBindRequest 096 implements CallbackHandler 097{ 098 /** 099 * The name for the CRAM-MD5 SASL mechanism. 100 */ 101 public static final String CRAMMD5_MECHANISM_NAME = "CRAM-MD5"; 102 103 104 105 /** 106 * The serial version UID for this serializable class. 107 */ 108 private static final long serialVersionUID = -4556570436768136483L; 109 110 111 112 // The password for this bind request. 113 private final ASN1OctetString password; 114 115 // The message ID from the last LDAP message sent from this request. 116 private int messageID = -1; 117 118 // A list that will be updated with messages about any unhandled callbacks 119 // encountered during processing. 120 private final List<String> unhandledCallbackMessages; 121 122 // The authentication ID string for this bind request. 123 private final String authenticationID; 124 125 126 127 /** 128 * Creates a new SASL CRAM-MD5 bind request with the provided authentication 129 * ID and password. It will not include any controls. 130 * 131 * @param authenticationID The authentication ID for this bind request. It 132 * must not be {@code null}. 133 * @param password The password for this bind request. It must not 134 * be {@code null}. 135 */ 136 public CRAMMD5BindRequest(final String authenticationID, 137 final String password) 138 { 139 this(authenticationID, new ASN1OctetString(password), NO_CONTROLS); 140 141 ensureNotNull(password); 142 } 143 144 145 146 /** 147 * Creates a new SASL CRAM-MD5 bind request with the provided authentication 148 * ID and password. It will not include any controls. 149 * 150 * @param authenticationID The authentication ID for this bind request. It 151 * must not be {@code null}. 152 * @param password The password for this bind request. It must not 153 * be {@code null}. 154 */ 155 public CRAMMD5BindRequest(final String authenticationID, 156 final byte[] password) 157 { 158 this(authenticationID, new ASN1OctetString(password), NO_CONTROLS); 159 160 ensureNotNull(password); 161 } 162 163 164 165 /** 166 * Creates a new SASL CRAM-MD5 bind request with the provided authentication 167 * ID and password. It will not include any controls. 168 * 169 * @param authenticationID The authentication ID for this bind request. It 170 * must not be {@code null}. 171 * @param password The password for this bind request. It must not 172 * be {@code null}. 173 */ 174 public CRAMMD5BindRequest(final String authenticationID, 175 final ASN1OctetString password) 176 { 177 this(authenticationID, password, NO_CONTROLS); 178 } 179 180 181 182 /** 183 * Creates a new SASL CRAM-MD5 bind request with the provided authentication 184 * ID, password, and set of controls. 185 * 186 * @param authenticationID The authentication ID for this bind request. It 187 * must not be {@code null}. 188 * @param password The password for this bind request. It must not 189 * be {@code null}. 190 * @param controls The set of controls to include in the request. 191 */ 192 public CRAMMD5BindRequest(final String authenticationID, 193 final String password, final Control... controls) 194 { 195 this(authenticationID, new ASN1OctetString(password), controls); 196 197 ensureNotNull(password); 198 } 199 200 201 202 /** 203 * Creates a new SASL CRAM-MD5 bind request with the provided authentication 204 * ID, password, and set of controls. 205 * 206 * @param authenticationID The authentication ID for this bind request. It 207 * must not be {@code null}. 208 * @param password The password for this bind request. It must not 209 * be {@code null}. 210 * @param controls The set of controls to include in the request. 211 */ 212 public CRAMMD5BindRequest(final String authenticationID, 213 final byte[] password, final Control... controls) 214 { 215 this(authenticationID, new ASN1OctetString(password), controls); 216 217 ensureNotNull(password); 218 } 219 220 221 222 /** 223 * Creates a new SASL CRAM-MD5 bind request with the provided authentication 224 * ID, password, and set of controls. 225 * 226 * @param authenticationID The authentication ID for this bind request. It 227 * must not be {@code null}. 228 * @param password The password for this bind request. It must not 229 * be {@code null}. 230 * @param controls The set of controls to include in the request. 231 */ 232 public CRAMMD5BindRequest(final String authenticationID, 233 final ASN1OctetString password, 234 final Control... controls) 235 { 236 super(controls); 237 238 ensureNotNull(authenticationID, password); 239 240 this.authenticationID = authenticationID; 241 this.password = password; 242 243 unhandledCallbackMessages = new ArrayList<String>(5); 244 } 245 246 247 248 /** 249 * {@inheritDoc} 250 */ 251 @Override() 252 public String getSASLMechanismName() 253 { 254 return CRAMMD5_MECHANISM_NAME; 255 } 256 257 258 259 /** 260 * Retrieves the authentication ID for this bind request. 261 * 262 * @return The authentication ID for this bind request. 263 */ 264 public String getAuthenticationID() 265 { 266 return authenticationID; 267 } 268 269 270 271 /** 272 * Retrieves the string representation of the password for this bind request. 273 * 274 * @return The string representation of the password for this bind request. 275 */ 276 public String getPasswordString() 277 { 278 return password.stringValue(); 279 } 280 281 282 283 /** 284 * Retrieves the bytes that comprise the the password for this bind request. 285 * 286 * @return The bytes that comprise the password for this bind request. 287 */ 288 public byte[] getPasswordBytes() 289 { 290 return password.getValue(); 291 } 292 293 294 295 /** 296 * Sends this bind request to the target server over the provided connection 297 * and returns the corresponding response. 298 * 299 * @param connection The connection to use to send this bind request to the 300 * server and read the associated response. 301 * @param depth The current referral depth for this request. It should 302 * always be one for the initial request, and should only 303 * be incremented when following referrals. 304 * 305 * @return The bind response read from the server. 306 * 307 * @throws LDAPException If a problem occurs while sending the request or 308 * reading the response. 309 */ 310 @Override() 311 protected BindResult process(final LDAPConnection connection, final int depth) 312 throws LDAPException 313 { 314 unhandledCallbackMessages.clear(); 315 316 final SaslClient saslClient; 317 final String[] mechanisms = { CRAMMD5_MECHANISM_NAME }; 318 319 try 320 { 321 saslClient = Sasl.createSaslClient(mechanisms, null, "ldap", 322 connection.getConnectedAddress(), null, 323 this); 324 } 325 catch (Exception e) 326 { 327 debugException(e); 328 throw new LDAPException(ResultCode.LOCAL_ERROR, 329 ERR_CRAMMD5_CANNOT_CREATE_SASL_CLIENT.get(getExceptionMessage(e)), 330 e); 331 } 332 333 final SASLHelper helper = new SASLHelper(this, connection, 334 CRAMMD5_MECHANISM_NAME, saslClient, getControls(), 335 getResponseTimeoutMillis(connection), unhandledCallbackMessages); 336 337 try 338 { 339 return helper.processSASLBind(); 340 } 341 finally 342 { 343 messageID = helper.getMessageID(); 344 } 345 } 346 347 348 349 /** 350 * {@inheritDoc} 351 */ 352 @Override() 353 public CRAMMD5BindRequest getRebindRequest(final String host, final int port) 354 { 355 return new CRAMMD5BindRequest(authenticationID, password, getControls()); 356 } 357 358 359 360 /** 361 * Handles any necessary callbacks required for SASL authentication. 362 * 363 * @param callbacks The set of callbacks to be handled. 364 */ 365 @InternalUseOnly() 366 public void handle(final Callback[] callbacks) 367 { 368 for (final Callback callback : callbacks) 369 { 370 if (callback instanceof NameCallback) 371 { 372 ((NameCallback) callback).setName(authenticationID); 373 } 374 else if (callback instanceof PasswordCallback) 375 { 376 ((PasswordCallback) callback).setPassword( 377 password.stringValue().toCharArray()); 378 } 379 else 380 { 381 // This is an unexpected callback. 382 if (debugEnabled(DebugType.LDAP)) 383 { 384 debug(Level.WARNING, DebugType.LDAP, 385 "Unexpected CRAM-MD5 SASL callback of type " + 386 callback.getClass().getName()); 387 } 388 389 unhandledCallbackMessages.add(ERR_CRAMMD5_UNEXPECTED_CALLBACK.get( 390 callback.getClass().getName())); 391 } 392 } 393 } 394 395 396 397 /** 398 * {@inheritDoc} 399 */ 400 @Override() 401 public int getLastMessageID() 402 { 403 return messageID; 404 } 405 406 407 408 /** 409 * {@inheritDoc} 410 */ 411 @Override() 412 public CRAMMD5BindRequest duplicate() 413 { 414 return duplicate(getControls()); 415 } 416 417 418 419 /** 420 * {@inheritDoc} 421 */ 422 @Override() 423 public CRAMMD5BindRequest duplicate(final Control[] controls) 424 { 425 final CRAMMD5BindRequest bindRequest = 426 new CRAMMD5BindRequest(authenticationID, password, controls); 427 bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null)); 428 return bindRequest; 429 } 430 431 432 433 /** 434 * {@inheritDoc} 435 */ 436 @Override() 437 public void toString(final StringBuilder buffer) 438 { 439 buffer.append("CRAMMD5BindRequest(authenticationID='"); 440 buffer.append(authenticationID); 441 buffer.append('\''); 442 443 final Control[] controls = getControls(); 444 if (controls.length > 0) 445 { 446 buffer.append(", controls={"); 447 for (int i=0; i < controls.length; i++) 448 { 449 if (i > 0) 450 { 451 buffer.append(", "); 452 } 453 454 buffer.append(controls[i]); 455 } 456 buffer.append('}'); 457 } 458 459 buffer.append(')'); 460 } 461 462 463 464 /** 465 * {@inheritDoc} 466 */ 467 @Override() 468 public void toCode(final List<String> lineList, final String requestID, 469 final int indentSpaces, final boolean includeProcessing) 470 { 471 // Create the request variable. 472 final ArrayList<ToCodeArgHelper> constructorArgs = 473 new ArrayList<ToCodeArgHelper>(3); 474 constructorArgs.add(ToCodeArgHelper.createString(authenticationID, 475 "Authentication ID")); 476 constructorArgs.add(ToCodeArgHelper.createString("---redacted-password---", 477 "Bind Password")); 478 479 final Control[] controls = getControls(); 480 if (controls.length > 0) 481 { 482 constructorArgs.add(ToCodeArgHelper.createControlArray(controls, 483 "Bind Controls")); 484 } 485 486 ToCodeHelper.generateMethodCall(lineList, indentSpaces, 487 "CRAMMD5BindRequest", requestID + "Request", 488 "new CRAMMD5BindRequest", constructorArgs); 489 490 491 // Add lines for processing the request and obtaining the result. 492 if (includeProcessing) 493 { 494 // Generate a string with the appropriate indent. 495 final StringBuilder buffer = new StringBuilder(); 496 for (int i=0; i < indentSpaces; i++) 497 { 498 buffer.append(' '); 499 } 500 final String indent = buffer.toString(); 501 502 lineList.add(""); 503 lineList.add(indent + "try"); 504 lineList.add(indent + '{'); 505 lineList.add(indent + " BindResult " + requestID + 506 "Result = connection.bind(" + requestID + "Request);"); 507 lineList.add(indent + " // The bind was processed successfully."); 508 lineList.add(indent + '}'); 509 lineList.add(indent + "catch (LDAPException e)"); 510 lineList.add(indent + '{'); 511 lineList.add(indent + " // The bind failed. Maybe the following will " + 512 "help explain why."); 513 lineList.add(indent + " // Note that the connection is now likely in " + 514 "an unauthenticated state."); 515 lineList.add(indent + " ResultCode resultCode = e.getResultCode();"); 516 lineList.add(indent + " String message = e.getMessage();"); 517 lineList.add(indent + " String matchedDN = e.getMatchedDN();"); 518 lineList.add(indent + " String[] referralURLs = e.getReferralURLs();"); 519 lineList.add(indent + " Control[] responseControls = " + 520 "e.getResponseControls();"); 521 lineList.add(indent + '}'); 522 } 523 } 524}