001/* 002 * Copyright 2019-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2019-2024 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) 2019-2024 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.security.MessageDigest; 041import java.util.List; 042import java.util.logging.Level; 043import javax.crypto.Mac; 044import javax.crypto.spec.SecretKeySpec; 045 046import com.unboundid.asn1.ASN1OctetString; 047import com.unboundid.util.CryptoHelper; 048import com.unboundid.util.Debug; 049import com.unboundid.util.DebugType; 050import com.unboundid.util.Extensible; 051import com.unboundid.util.NotNull; 052import com.unboundid.util.Nullable; 053import com.unboundid.util.ThreadSafety; 054import com.unboundid.util.ThreadSafetyLevel; 055import com.unboundid.util.Validator; 056 057import static com.unboundid.ldap.sdk.LDAPMessages.*; 058 059 060 061/** 062 * This class provides the basis for bind requests that use the salted 063 * challenge-response authentication mechanism (SCRAM) described in 064 * <A HREF="http://www.ietf.org/rfc/rfc5802.txt">RFC 5802</A> and updated in 065 * <A HREF="https://tools.ietf.org/html/rfc7677">RFC 7677</A>. Subclasses 066 * should extend this class to provide support for specific algorithms. 067 * <BR><BR> 068 * Note that this implementation does not support the PLUS variants of these 069 * algorithms, which requires channel binding support. 070 */ 071@Extensible() 072@ThreadSafety(level= ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE) 073public abstract class SCRAMBindRequest 074 extends SASLBindRequest 075{ 076 /** 077 * The serial version UID for this serializable class. 078 */ 079 private static final long serialVersionUID = -1141722265190138366L; 080 081 082 083 // The password for this bind request. 084 @NotNull private final ASN1OctetString password; 085 086 // The username for this bind request. 087 @NotNull private final String username; 088 089 090 091 /** 092 * Creates a new SCRAM bind request with the provided information. 093 * 094 * @param username The username for this bind request. It must not be 095 * {@code null} or empty. 096 * @param password The password for this bind request. It must not be 097 * {@code null} or empty. 098 * @param controls The set of controls to include in the bind request. It 099 * may be {@code null} or empty if no controls are needed. 100 */ 101 public SCRAMBindRequest(@NotNull final String username, 102 @NotNull final ASN1OctetString password, 103 @Nullable final Control... controls) 104 { 105 super(controls); 106 107 Validator.ensureNotNullOrEmpty(username, 108 "SCRAMBindRequest.username must not be null or empty"); 109 Validator.ensureTrue( 110 ((password != null) && (password.getValueLength() > 0)), 111 "SCRAMBindRequest.password must not be null or empty"); 112 113 this.username = username; 114 this.password = password; 115 } 116 117 118 119 /** 120 * Retrieves the username for this bind request. 121 * 122 * @return The password for this bind request. 123 */ 124 @NotNull() 125 public final String getUsername() 126 { 127 return username; 128 } 129 130 131 132 /** 133 * Retrieves the password for this bind request, as a string. 134 * 135 * @return The password for this bind request, as a string. 136 */ 137 @NotNull() 138 public final String getPasswordString() 139 { 140 return password.stringValue(); 141 } 142 143 144 145 /** 146 * Retrieves the bytes that comprise the password for this bind request. 147 * 148 * @return The bytes that comprise the password for this bind request. 149 */ 150 @NotNull() 151 public final byte[] getPasswordBytes() 152 { 153 return password.getValue(); 154 } 155 156 157 158 /** 159 * Retrieves the name of the digest algorithm that will be used in the 160 * authentication processing. 161 * 162 * @return The name of the digest algorithm that will be used in the 163 * authentication processing. 164 */ 165 @NotNull() 166 protected abstract String getDigestAlgorithmName(); 167 168 169 170 /** 171 * Retrieves the name of the MAC algorithm that will be used in the 172 * authentication processing. 173 * 174 * @return The name of the MAC algorithm that will be used in the 175 * authentication processing. 176 */ 177 @NotNull() 178 protected abstract String getMACAlgorithmName(); 179 180 181 182 /** 183 * {@inheritDoc} 184 */ 185 @Override() 186 @NotNull() 187 protected final BindResult process(@NotNull final LDAPConnection connection, 188 final int depth) 189 throws LDAPException 190 { 191 setReferralDepth(depth); 192 193 // Generate the client first message and send it to the server. 194 final SCRAMClientFirstMessage clientFirstMessage = 195 new SCRAMClientFirstMessage(this); 196 if (Debug.debugEnabled()) 197 { 198 Debug.debug(Level.INFO, DebugType.LDAP, 199 "Sending " + getSASLMechanismName() + " client first message " + 200 clientFirstMessage); 201 } 202 203 final BindResult serverFirstResult = sendBindRequest(connection, null, 204 new ASN1OctetString(clientFirstMessage.getClientFirstMessage()), 205 getControls(), getResponseTimeoutMillis(connection)); 206 207 208 // If the result code from the server first result is anything other than 209 // SASL_BIND_IN_PROGRESS, then return that result as a failure. 210 if (serverFirstResult.getResultCode() != ResultCode.SASL_BIND_IN_PROGRESS) 211 { 212 return serverFirstResult; 213 } 214 215 216 // Parse the server first result, and use it to compute the client final 217 // message. 218 final SCRAMServerFirstMessage serverFirstMessage = 219 new SCRAMServerFirstMessage(this, clientFirstMessage, 220 serverFirstResult); 221 if (Debug.debugEnabled()) 222 { 223 Debug.debug(Level.INFO, DebugType.LDAP, 224 "Received " + getSASLMechanismName() + " server first message " + 225 serverFirstMessage); 226 } 227 228 final SCRAMClientFinalMessage clientFinalMessage = 229 new SCRAMClientFinalMessage(this, clientFirstMessage, 230 serverFirstMessage); 231 if (Debug.debugEnabled()) 232 { 233 Debug.debug(Level.INFO, DebugType.LDAP, 234 "Sending " + getSASLMechanismName() + " client final message " + 235 clientFinalMessage); 236 } 237 238 239 // Send the server final bind request to the server and get the result. 240 // We don't care what the result code was, because the server final message 241 // processing will handle both success and failure. 242 final BindResult serverFinalResult = sendBindRequest(connection, null, 243 new ASN1OctetString(clientFinalMessage.getClientFinalMessage()), 244 getControls(), getResponseTimeoutMillis(connection)); 245 246 final SCRAMServerFinalMessage serverFinalMessage = 247 new SCRAMServerFinalMessage(this, clientFirstMessage, 248 clientFinalMessage, serverFinalResult); 249 if (Debug.debugEnabled()) 250 { 251 Debug.debug(Level.INFO, DebugType.LDAP, 252 "Received " + getSASLMechanismName() + " server final message " + 253 serverFinalMessage); 254 } 255 256 257 // If we've gotten here, then the bind was successful. Return the server 258 // final result. 259 return serverFinalResult; 260 } 261 262 263 264 /** 265 * Computes a MAC of the provided data with the given key. 266 * 267 * @param key The bytes to use as the key for the MAC. 268 * @param data The data for which to generate the MAC. 269 * 270 * @return The MAC that was computed. 271 * 272 * @throws LDAPBindException If a problem is encountered while computing the 273 * MAC. 274 */ 275 @NotNull() 276 final byte[] mac(@NotNull final byte[] key, @NotNull final byte[] data) 277 throws LDAPBindException 278 { 279 return getMac(key).doFinal(data); 280 } 281 282 283 284 /** 285 * Retrieves a MAC generator for the provided key. 286 * 287 * @param key The bytes to use as the key for the MAC. 288 * 289 * @return The MAC generator. 290 * 291 * @throws LDAPBindException If a problem is encountered while obtaining the 292 * MAC generator. 293 */ 294 @NotNull() 295 final Mac getMac(@NotNull final byte[] key) 296 throws LDAPBindException 297 { 298 try 299 { 300 final Mac mac = CryptoHelper.getMAC(getMACAlgorithmName()); 301 final SecretKeySpec macKey = 302 new SecretKeySpec(key, getMACAlgorithmName()); 303 mac.init(macKey); 304 return mac; 305 } 306 catch (final Exception e) 307 { 308 Debug.debugException(e); 309 throw new LDAPBindException(new BindResult(-1, 310 ResultCode.LOCAL_ERROR, 311 ERR_SCRAM_BIND_REQUEST_CANNOT_GET_MAC.get(getSASLMechanismName(), 312 getMACAlgorithmName()), 313 null, null, null, null)); 314 } 315 } 316 317 318 319 /** 320 * Computes a message digest of the provided data with the given key. 321 * 322 * @param data The data for which to generate the digest. 323 * 324 * @return The digest that was computed. 325 * 326 * @throws LDAPBindException If a problem is encountered while computing the 327 * digest. 328 */ 329 @NotNull() 330 final byte[] digest(@NotNull final byte[] data) 331 throws LDAPBindException 332 { 333 try 334 { 335 final MessageDigest digest = 336 CryptoHelper.getMessageDigest(getDigestAlgorithmName()); 337 return digest.digest(data); 338 } 339 catch (final Exception e) 340 { 341 Debug.debugException(e); 342 throw new LDAPBindException(new BindResult(-1, 343 ResultCode.LOCAL_ERROR, 344 ERR_SCRAM_BIND_REQUEST_CANNOT_GET_DIGEST.get( 345 getSASLMechanismName(), getDigestAlgorithmName()), 346 null, null, null, null)); 347 } 348 } 349 350 351 352 /** 353 * {@inheritDoc} 354 */ 355 @Override() 356 @NotNull() 357 public abstract SCRAMBindRequest getRebindRequest( 358 @NotNull final String host, 359 final int port); 360 361 362 363 /** 364 * {@inheritDoc} 365 */ 366 @Override() 367 @NotNull() 368 public abstract SCRAMBindRequest duplicate(); 369 370 371 372 /** 373 * {@inheritDoc} 374 */ 375 @Override() 376 @NotNull() 377 public abstract SCRAMBindRequest duplicate(@NotNull final Control[] controls); 378 379 380 381 /** 382 * {@inheritDoc} 383 */ 384 @Override() 385 public abstract void toString(@NotNull final StringBuilder buffer); 386 387 388 389 /** 390 * {@inheritDoc} 391 */ 392 @Override() 393 public abstract void toCode(@NotNull final List<String> lineList, 394 @NotNull final String requestID, 395 final int indentSpaces, 396 final boolean includeProcessing); 397}