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