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