001/* 002 * Copyright 2019-2022 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2019-2022 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-2022 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 // Generate the client first message and send it to the server. 192 final SCRAMClientFirstMessage clientFirstMessage = 193 new SCRAMClientFirstMessage(this); 194 if (Debug.debugEnabled()) 195 { 196 Debug.debug(Level.INFO, DebugType.LDAP, 197 "Sending " + getSASLMechanismName() + " client first message " + 198 clientFirstMessage); 199 } 200 201 final BindResult serverFirstResult = sendBindRequest(connection, null, 202 new ASN1OctetString(clientFirstMessage.getClientFirstMessage()), 203 getControls(), getResponseTimeoutMillis(connection)); 204 205 206 // If the result code from the server first result is anything other than 207 // SASL_BIND_IN_PROGRESS, then return that result as a failure. 208 if (serverFirstResult.getResultCode() != ResultCode.SASL_BIND_IN_PROGRESS) 209 { 210 return serverFirstResult; 211 } 212 213 214 // Parse the server first result, and use it to compute the client final 215 // message. 216 final SCRAMServerFirstMessage serverFirstMessage = 217 new SCRAMServerFirstMessage(this, clientFirstMessage, 218 serverFirstResult); 219 if (Debug.debugEnabled()) 220 { 221 Debug.debug(Level.INFO, DebugType.LDAP, 222 "Received " + getSASLMechanismName() + " server first message " + 223 serverFirstMessage); 224 } 225 226 final SCRAMClientFinalMessage clientFinalMessage = 227 new SCRAMClientFinalMessage(this, clientFirstMessage, 228 serverFirstMessage); 229 if (Debug.debugEnabled()) 230 { 231 Debug.debug(Level.INFO, DebugType.LDAP, 232 "Sending " + getSASLMechanismName() + " client final message " + 233 clientFinalMessage); 234 } 235 236 237 // Send the server final bind request to the server and get the result. 238 // We don't care what the result code was, because the server final message 239 // processing will handle both success and failure. 240 final BindResult serverFinalResult = sendBindRequest(connection, null, 241 new ASN1OctetString(clientFinalMessage.getClientFinalMessage()), 242 getControls(), getResponseTimeoutMillis(connection)); 243 244 final SCRAMServerFinalMessage serverFinalMessage = 245 new SCRAMServerFinalMessage(this, clientFirstMessage, 246 clientFinalMessage, serverFinalResult); 247 if (Debug.debugEnabled()) 248 { 249 Debug.debug(Level.INFO, DebugType.LDAP, 250 "Received " + getSASLMechanismName() + " server final message " + 251 serverFinalMessage); 252 } 253 254 255 // If we've gotten here, then the bind was successful. Return the server 256 // final result. 257 return serverFinalResult; 258 } 259 260 261 262 /** 263 * Computes a MAC of the provided data with the given key. 264 * 265 * @param key The bytes to use as the key for the MAC. 266 * @param data The data for which to generate the MAC. 267 * 268 * @return The MAC that was computed. 269 * 270 * @throws LDAPBindException If a problem is encountered while computing the 271 * MAC. 272 */ 273 @NotNull() 274 final byte[] mac(@NotNull final byte[] key, @NotNull final byte[] data) 275 throws LDAPBindException 276 { 277 return getMac(key).doFinal(data); 278 } 279 280 281 282 /** 283 * Retrieves a MAC generator for the provided key. 284 * 285 * @param key The bytes to use as the key for the MAC. 286 * 287 * @return The MAC generator. 288 * 289 * @throws LDAPBindException If a problem is encountered while obtaining the 290 * MAC generator. 291 */ 292 @NotNull() 293 final Mac getMac(@NotNull final byte[] key) 294 throws LDAPBindException 295 { 296 try 297 { 298 final Mac mac = CryptoHelper.getMAC(getMACAlgorithmName()); 299 final SecretKeySpec macKey = 300 new SecretKeySpec(key, getMACAlgorithmName()); 301 mac.init(macKey); 302 return mac; 303 } 304 catch (final Exception e) 305 { 306 Debug.debugException(e); 307 throw new LDAPBindException(new BindResult(-1, 308 ResultCode.LOCAL_ERROR, 309 ERR_SCRAM_BIND_REQUEST_CANNOT_GET_MAC.get(getSASLMechanismName(), 310 getMACAlgorithmName()), 311 null, null, null, null)); 312 } 313 } 314 315 316 317 /** 318 * Computes a message digest of the provided data with the given key. 319 * 320 * @param data The data for which to generate the digest. 321 * 322 * @return The digest that was computed. 323 * 324 * @throws LDAPBindException If a problem is encountered while computing the 325 * digest. 326 */ 327 @NotNull() 328 final byte[] digest(@NotNull final byte[] data) 329 throws LDAPBindException 330 { 331 try 332 { 333 final MessageDigest digest = 334 CryptoHelper.getMessageDigest(getDigestAlgorithmName()); 335 return digest.digest(data); 336 } 337 catch (final Exception e) 338 { 339 Debug.debugException(e); 340 throw new LDAPBindException(new BindResult(-1, 341 ResultCode.LOCAL_ERROR, 342 ERR_SCRAM_BIND_REQUEST_CANNOT_GET_DIGEST.get( 343 getSASLMechanismName(), getDigestAlgorithmName()), 344 null, null, null, null)); 345 } 346 } 347 348 349 350 /** 351 * {@inheritDoc} 352 */ 353 @Override() 354 @NotNull() 355 public abstract SCRAMBindRequest getRebindRequest( 356 @NotNull final String host, 357 final int port); 358 359 360 361 /** 362 * {@inheritDoc} 363 */ 364 @Override() 365 @NotNull() 366 public abstract SCRAMBindRequest duplicate(); 367 368 369 370 /** 371 * {@inheritDoc} 372 */ 373 @Override() 374 @NotNull() 375 public abstract SCRAMBindRequest duplicate(@NotNull final Control[] controls); 376 377 378 379 /** 380 * {@inheritDoc} 381 */ 382 @Override() 383 public abstract void toString(@NotNull final StringBuilder buffer); 384 385 386 387 /** 388 * {@inheritDoc} 389 */ 390 @Override() 391 public abstract void toCode(@NotNull final List<String> lineList, 392 @NotNull final String requestID, 393 final int indentSpaces, 394 final boolean includeProcessing); 395}