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