001/* 002 * Copyright 2007-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2018 Ping Identity Corporation 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.Debug; 037import com.unboundid.util.DebugType; 038import com.unboundid.util.InternalUseOnly; 039import com.unboundid.util.NotMutable; 040import com.unboundid.util.StaticUtils; 041import com.unboundid.util.ThreadSafety; 042import com.unboundid.util.ThreadSafetyLevel; 043import com.unboundid.util.Validator; 044 045import static com.unboundid.ldap.sdk.LDAPMessages.*; 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 Validator.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 Validator.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 Validator.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 Validator.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 Validator.ensureNotNull(authenticationID, password); 239 240 this.authenticationID = authenticationID; 241 this.password = password; 242 243 unhandledCallbackMessages = new ArrayList<>(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 318 try 319 { 320 final String[] mechanisms = { CRAMMD5_MECHANISM_NAME }; 321 saslClient = Sasl.createSaslClient(mechanisms, null, "ldap", 322 connection.getConnectedAddress(), null, 323 this); 324 } 325 catch (final Exception e) 326 { 327 Debug.debugException(e); 328 throw new LDAPException(ResultCode.LOCAL_ERROR, 329 ERR_CRAMMD5_CANNOT_CREATE_SASL_CLIENT.get( 330 StaticUtils.getExceptionMessage(e)), 331 e); 332 } 333 334 final SASLHelper helper = new SASLHelper(this, connection, 335 CRAMMD5_MECHANISM_NAME, saslClient, getControls(), 336 getResponseTimeoutMillis(connection), unhandledCallbackMessages); 337 338 try 339 { 340 return helper.processSASLBind(); 341 } 342 finally 343 { 344 messageID = helper.getMessageID(); 345 } 346 } 347 348 349 350 /** 351 * {@inheritDoc} 352 */ 353 @Override() 354 public CRAMMD5BindRequest getRebindRequest(final String host, final int port) 355 { 356 return new CRAMMD5BindRequest(authenticationID, password, getControls()); 357 } 358 359 360 361 /** 362 * Handles any necessary callbacks required for SASL authentication. 363 * 364 * @param callbacks The set of callbacks to be handled. 365 */ 366 @InternalUseOnly() 367 @Override() 368 public void handle(final Callback[] callbacks) 369 { 370 for (final Callback callback : callbacks) 371 { 372 if (callback instanceof NameCallback) 373 { 374 ((NameCallback) callback).setName(authenticationID); 375 } 376 else if (callback instanceof PasswordCallback) 377 { 378 ((PasswordCallback) callback).setPassword( 379 password.stringValue().toCharArray()); 380 } 381 else 382 { 383 // This is an unexpected callback. 384 if (Debug.debugEnabled(DebugType.LDAP)) 385 { 386 Debug.debug(Level.WARNING, DebugType.LDAP, 387 "Unexpected CRAM-MD5 SASL callback of type " + 388 callback.getClass().getName()); 389 } 390 391 unhandledCallbackMessages.add(ERR_CRAMMD5_UNEXPECTED_CALLBACK.get( 392 callback.getClass().getName())); 393 } 394 } 395 } 396 397 398 399 /** 400 * {@inheritDoc} 401 */ 402 @Override() 403 public int getLastMessageID() 404 { 405 return messageID; 406 } 407 408 409 410 /** 411 * {@inheritDoc} 412 */ 413 @Override() 414 public CRAMMD5BindRequest duplicate() 415 { 416 return duplicate(getControls()); 417 } 418 419 420 421 /** 422 * {@inheritDoc} 423 */ 424 @Override() 425 public CRAMMD5BindRequest duplicate(final Control[] controls) 426 { 427 final CRAMMD5BindRequest bindRequest = 428 new CRAMMD5BindRequest(authenticationID, password, controls); 429 bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null)); 430 return bindRequest; 431 } 432 433 434 435 /** 436 * {@inheritDoc} 437 */ 438 @Override() 439 public void toString(final StringBuilder buffer) 440 { 441 buffer.append("CRAMMD5BindRequest(authenticationID='"); 442 buffer.append(authenticationID); 443 buffer.append('\''); 444 445 final Control[] controls = getControls(); 446 if (controls.length > 0) 447 { 448 buffer.append(", controls={"); 449 for (int i=0; i < controls.length; i++) 450 { 451 if (i > 0) 452 { 453 buffer.append(", "); 454 } 455 456 buffer.append(controls[i]); 457 } 458 buffer.append('}'); 459 } 460 461 buffer.append(')'); 462 } 463 464 465 466 /** 467 * {@inheritDoc} 468 */ 469 @Override() 470 public void toCode(final List<String> lineList, final String requestID, 471 final int indentSpaces, final boolean includeProcessing) 472 { 473 // Create the request variable. 474 final ArrayList<ToCodeArgHelper> constructorArgs = new ArrayList<>(3); 475 constructorArgs.add(ToCodeArgHelper.createString(authenticationID, 476 "Authentication ID")); 477 constructorArgs.add(ToCodeArgHelper.createString("---redacted-password---", 478 "Bind Password")); 479 480 final Control[] controls = getControls(); 481 if (controls.length > 0) 482 { 483 constructorArgs.add(ToCodeArgHelper.createControlArray(controls, 484 "Bind Controls")); 485 } 486 487 ToCodeHelper.generateMethodCall(lineList, indentSpaces, 488 "CRAMMD5BindRequest", requestID + "Request", 489 "new CRAMMD5BindRequest", constructorArgs); 490 491 492 // Add lines for processing the request and obtaining the result. 493 if (includeProcessing) 494 { 495 // Generate a string with the appropriate indent. 496 final StringBuilder buffer = new StringBuilder(); 497 for (int i=0; i < indentSpaces; i++) 498 { 499 buffer.append(' '); 500 } 501 final String indent = buffer.toString(); 502 503 lineList.add(""); 504 lineList.add(indent + "try"); 505 lineList.add(indent + '{'); 506 lineList.add(indent + " BindResult " + requestID + 507 "Result = connection.bind(" + requestID + "Request);"); 508 lineList.add(indent + " // The bind was processed successfully."); 509 lineList.add(indent + '}'); 510 lineList.add(indent + "catch (LDAPException e)"); 511 lineList.add(indent + '{'); 512 lineList.add(indent + " // The bind failed. Maybe the following will " + 513 "help explain why."); 514 lineList.add(indent + " // Note that the connection is now likely in " + 515 "an unauthenticated state."); 516 lineList.add(indent + " ResultCode resultCode = e.getResultCode();"); 517 lineList.add(indent + " String message = e.getMessage();"); 518 lineList.add(indent + " String matchedDN = e.getMatchedDN();"); 519 lineList.add(indent + " String[] referralURLs = e.getReferralURLs();"); 520 lineList.add(indent + " Control[] responseControls = " + 521 "e.getResponseControls();"); 522 lineList.add(indent + '}'); 523 } 524 } 525}