001    /*
002     * Copyright 2007-2013 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2013 UnboundID Corp.
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     */
021    package com.unboundid.ldap.sdk;
022    
023    
024    
025    import java.util.HashMap;
026    import java.util.logging.Level;
027    import javax.security.auth.callback.Callback;
028    import javax.security.auth.callback.CallbackHandler;
029    import javax.security.auth.callback.NameCallback;
030    import javax.security.auth.callback.PasswordCallback;
031    import javax.security.sasl.RealmCallback;
032    import javax.security.sasl.RealmChoiceCallback;
033    import javax.security.sasl.Sasl;
034    import javax.security.sasl.SaslClient;
035    
036    import com.unboundid.asn1.ASN1OctetString;
037    import com.unboundid.util.DebugType;
038    import com.unboundid.util.InternalUseOnly;
039    import com.unboundid.util.NotMutable;
040    import com.unboundid.util.ThreadSafety;
041    import com.unboundid.util.ThreadSafetyLevel;
042    
043    import static com.unboundid.ldap.sdk.LDAPMessages.*;
044    import static com.unboundid.util.Debug.*;
045    import static com.unboundid.util.StaticUtils.*;
046    import static com.unboundid.util.Validator.*;
047    
048    
049    
050    /**
051     * This class provides a SASL DIGEST-MD5 bind request implementation as
052     * described in <A HREF="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</A>.  The
053     * DIGEST-MD5 mechanism can be used to authenticate over an insecure channel
054     * without exposing the credentials (although it requires that the server have
055     * access to the clear-text password).  It is similar to CRAM-MD5, but provides
056     * better security by combining random data from both the client and the server,
057     * and allows for greater security and functionality, including the ability to
058     * specify an alternate authorization identity and the ability to use data
059     * integrity or confidentiality protection.  At present, however, this
060     * implementation may only be used for authentication, as it does not yet
061     * support integrity or confidentiality.
062     * <BR><BR>
063     * Elements included in a DIGEST-MD5 bind request include:
064     * <UL>
065     *   <LI>Authentication ID -- A string which identifies the user that is
066     *       attempting to authenticate.  It should be an "authzId" value as
067     *       described in section 5.2.1.8 of
068     *       <A HREF="http://www.ietf.org/rfc/rfc4513.txt">RFC 4513</A>.  That is,
069     *       it should be either "dn:" followed by the distinguished name of the
070     *       target user, or "u:" followed by the username.  If the "u:" form is
071     *       used, then the mechanism used to resolve the provided username to an
072     *       entry may vary from server to server.</LI>
073     *   <LI>Authorization ID -- An optional string which specifies an alternate
074     *       authorization identity that should be used for subsequent operations
075     *       requested on the connection.  Like the authentication ID, the
076     *       authorization ID should use the "authzId" syntax.</LI>
077     *   <LI>Realm -- An optional string which specifies the realm into which the
078     *       user should authenticate.</LI>
079     *   <LI>Password -- The clear-text password for the target user.</LI>
080     * </UL>
081     * <H2>Example</H2>
082     * The following example demonstrates the process for performing a DIGEST-MD5
083     * bind against a directory server with a username of "john.doe" and a password
084     * of "password":
085     * <PRE>
086     *   DIGESTMD5BindRequest bindRequest =
087     *        new DIGESTMD5BindRequest("u:john.doe", "password");
088     *   try
089     *   {
090     *     BindResult bindResult = connection.bind(bindRequest);
091     *     // If we get here, then the bind was successful.
092     *   }
093     *   catch (LDAPException le)
094     *   {
095     *     // The bind failed for some reason.
096     *   }
097     * </PRE>
098     */
099    @NotMutable()
100    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
101    public final class DIGESTMD5BindRequest
102           extends SASLBindRequest
103           implements CallbackHandler
104    {
105      /**
106       * The name for the DIGEST-MD5 SASL mechanism.
107       */
108      public static final String DIGESTMD5_MECHANISM_NAME = "DIGEST-MD5";
109    
110    
111    
112      /**
113       * The serial version UID for this serializable class.
114       */
115      private static final long serialVersionUID = 867592367640540593L;
116    
117    
118    
119      // The password for this bind request.
120      private final ASN1OctetString password;
121    
122      // The message ID from the last LDAP message sent from this request.
123      private int messageID = -1;
124    
125      // The authentication ID string for this bind request.
126      private final String authenticationID;
127    
128      // The authorization ID string for this bind request, if available.
129      private final String authorizationID;
130    
131      // The realm form this bind request, if available.
132      private final String realm;
133    
134    
135    
136      /**
137       * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
138       * ID and password.  It will not include an authorization ID, a realm, or any
139       * controls.
140       *
141       * @param  authenticationID  The authentication ID for this bind request.  It
142       *                           must not be {@code null}.
143       * @param  password          The password for this bind request.  It must not
144       *                           be {@code null}.
145       */
146      public DIGESTMD5BindRequest(final String authenticationID,
147                                  final String password)
148      {
149        this(authenticationID, null, new ASN1OctetString(password), null,
150             NO_CONTROLS);
151    
152        ensureNotNull(password);
153      }
154    
155    
156    
157      /**
158       * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
159       * ID and password.  It will not include an authorization ID, a realm, or any
160       * controls.
161       *
162       * @param  authenticationID  The authentication ID for this bind request.  It
163       *                           must not be {@code null}.
164       * @param  password          The password for this bind request.  It must not
165       *                           be {@code null}.
166       */
167      public DIGESTMD5BindRequest(final String authenticationID,
168                                  final byte[] password)
169      {
170        this(authenticationID, null, new ASN1OctetString(password), null,
171             NO_CONTROLS);
172    
173        ensureNotNull(password);
174      }
175    
176    
177    
178      /**
179       * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
180       * ID and password.  It will not include an authorization ID, a realm, or any
181       * controls.
182       *
183       * @param  authenticationID  The authentication ID for this bind request.  It
184       *                           must not be {@code null}.
185       * @param  password          The password for this bind request.  It must not
186       *                           be {@code null}.
187       */
188      public DIGESTMD5BindRequest(final String authenticationID,
189                                  final ASN1OctetString password)
190      {
191        this(authenticationID, null, password, null, NO_CONTROLS);
192      }
193    
194    
195    
196      /**
197       * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
198       * ID and password.  It will not include an authorization ID or any controls.
199       *
200       * @param  authenticationID  The authentication ID for this bind request.  It
201       *                           must not be {@code null}.
202       * @param  authorizationID   The authorization ID for this bind request.  It
203       *                           may be {@code null} if there will not be an
204       *                           alternate authorization identity.
205       * @param  password          The password for this bind request.  It must not
206       *                           be {@code null}.
207       * @param  realm             The realm to use for the authentication.  It may
208       *                           be {@code null} if the server supports a default
209       *                           realm.
210       * @param  controls          The set of controls to include in the request.
211       */
212      public DIGESTMD5BindRequest(final String authenticationID,
213                                  final String authorizationID,
214                                  final String password, final String realm,
215                                  final Control... controls)
216      {
217        this(authenticationID, authorizationID, new ASN1OctetString(password),
218             realm, controls);
219    
220        ensureNotNull(password);
221      }
222    
223    
224    
225      /**
226       * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
227       * ID and password.  It will not include an authorization ID or any controls.
228       *
229       * @param  authenticationID  The authentication ID for this bind request.  It
230       *                           must not be {@code null}.
231       * @param  authorizationID   The authorization ID for this bind request.  It
232       *                           may be {@code null} if there will not be an
233       *                           alternate authorization identity.
234       * @param  password          The password for this bind request.  It must not
235       *                           be {@code null}.
236       * @param  realm             The realm to use for the authentication.  It may
237       *                           be {@code null} if the server supports a default
238       *                           realm.
239       * @param  controls          The set of controls to include in the request.
240       */
241      public DIGESTMD5BindRequest(final String authenticationID,
242                                  final String authorizationID,
243                                  final byte[] password, final String realm,
244                                  final Control... controls)
245      {
246        this(authenticationID, authorizationID, new ASN1OctetString(password),
247             realm, controls);
248    
249        ensureNotNull(password);
250      }
251    
252    
253    
254      /**
255       * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
256       * ID and password.  It will not include an authorization ID or any controls.
257       *
258       * @param  authenticationID  The authentication ID for this bind request.  It
259       *                           must not be {@code null}.
260       * @param  authorizationID   The authorization ID for this bind request.  It
261       *                           may be {@code null} if there will not be an
262       *                           alternate authorization identity.
263       * @param  password          The password for this bind request.  It must not
264       *                           be {@code null}.
265       * @param  realm             The realm to use for the authentication.  It may
266       *                           be {@code null} if the server supports a default
267       *                           realm.
268       * @param  controls          The set of controls to include in the request.
269       */
270      public DIGESTMD5BindRequest(final String authenticationID,
271                                  final String authorizationID,
272                                  final ASN1OctetString password,
273                                  final String realm, final Control... controls)
274      {
275        super(controls);
276    
277        ensureNotNull(authenticationID, password);
278    
279        this.authenticationID = authenticationID;
280        this.authorizationID  = authorizationID;
281        this.password         = password;
282        this.realm            = realm;
283      }
284    
285    
286    
287      /**
288       * {@inheritDoc}
289       */
290      @Override()
291      public String getSASLMechanismName()
292      {
293        return DIGESTMD5_MECHANISM_NAME;
294      }
295    
296    
297    
298      /**
299       * Retrieves the authentication ID for this bind request.
300       *
301       * @return  The authentication ID for this bind request.
302       */
303      public String getAuthenticationID()
304      {
305        return authenticationID;
306      }
307    
308    
309    
310      /**
311       * Retrieves the authorization ID for this bind request, if any.
312       *
313       * @return  The authorization ID for this bind request, or {@code null} if
314       *          there should not be a separate authorization identity.
315       */
316      public String getAuthorizationID()
317      {
318        return authorizationID;
319      }
320    
321    
322    
323      /**
324       * Retrieves the string representation of the password for this bind request.
325       *
326       * @return  The string representation of the password for this bind request.
327       */
328      public String getPasswordString()
329      {
330        return password.stringValue();
331      }
332    
333    
334    
335      /**
336       * Retrieves the bytes that comprise the the password for this bind request.
337       *
338       * @return  The bytes that comprise the password for this bind request.
339       */
340      public byte[] getPasswordBytes()
341      {
342        return password.getValue();
343      }
344    
345    
346    
347      /**
348       * Retrieves the realm for this bind request, if any.
349       *
350       * @return  The realm for this bind request, or {@code null} if none was
351       *          defined and the server should use the default realm.
352       */
353      public String getRealm()
354      {
355        return realm;
356      }
357    
358    
359    
360      /**
361       * Sends this bind request to the target server over the provided connection
362       * and returns the corresponding response.
363       *
364       * @param  connection  The connection to use to send this bind request to the
365       *                     server and read the associated response.
366       * @param  depth       The current referral depth for this request.  It should
367       *                     always be one for the initial request, and should only
368       *                     be incremented when following referrals.
369       *
370       * @return  The bind response read from the server.
371       *
372       * @throws  LDAPException  If a problem occurs while sending the request or
373       *                         reading the response.
374       */
375      @Override()
376      protected BindResult process(final LDAPConnection connection, final int depth)
377                throws LDAPException
378      {
379        final String[] mechanisms = { DIGESTMD5_MECHANISM_NAME };
380    
381        final HashMap<String,Object> saslProperties = new HashMap<String,Object>();
382        saslProperties.put(Sasl.QOP, "auth");
383        saslProperties.put(Sasl.SERVER_AUTH, "false");
384    
385        final SaslClient saslClient;
386        try
387        {
388          saslClient = Sasl.createSaslClient(mechanisms, authorizationID, "ldap",
389                                             connection.getConnectedAddress(),
390                                             saslProperties, this);
391        }
392        catch (Exception e)
393        {
394          debugException(e);
395          throw new LDAPException(ResultCode.LOCAL_ERROR,
396               ERR_DIGESTMD5_CANNOT_CREATE_SASL_CLIENT.get(getExceptionMessage(e)),
397               e);
398        }
399    
400        final SASLHelper helper = new SASLHelper(this, connection,
401             DIGESTMD5_MECHANISM_NAME, saslClient, getControls(),
402             getResponseTimeoutMillis(connection));
403    
404        try
405        {
406          return helper.processSASLBind();
407        }
408        finally
409        {
410          messageID = helper.getMessageID();
411        }
412      }
413    
414    
415    
416      /**
417       * {@inheritDoc}
418       */
419      @Override()
420      public DIGESTMD5BindRequest getRebindRequest(final String host,
421                                                   final int port)
422      {
423        return new DIGESTMD5BindRequest(authenticationID, authorizationID, password,
424                                        realm, getControls());
425      }
426    
427    
428    
429      /**
430       * Handles any necessary callbacks required for SASL authentication.
431       *
432       * @param  callbacks  The set of callbacks to be handled.
433       */
434      @InternalUseOnly()
435      public void handle(final Callback[] callbacks)
436      {
437        for (final Callback callback : callbacks)
438        {
439          if (callback instanceof NameCallback)
440          {
441            ((NameCallback) callback).setName(authenticationID);
442          }
443          else if (callback instanceof PasswordCallback)
444          {
445            ((PasswordCallback) callback).setPassword(
446                 password.stringValue().toCharArray());
447          }
448          else if (callback instanceof RealmCallback)
449          {
450            if (realm != null)
451            {
452              ((RealmCallback) callback).setText(realm);
453            }
454          }
455          else if (callback instanceof RealmChoiceCallback)
456          {
457            if (realm != null)
458            {
459              final RealmChoiceCallback rcc = (RealmChoiceCallback) callback;
460              final String[] choices = rcc.getChoices();
461              for (int i=0; i < choices.length; i++)
462              {
463                if (choices[i].equals(realm))
464                {
465                  rcc.setSelectedIndex(i);
466                  break;
467                }
468              }
469            }
470          }
471          else
472          {
473            // This is an unexpected callback.
474            if (debugEnabled(DebugType.LDAP))
475            {
476              debug(Level.WARNING, DebugType.LDAP,
477                    "Unexpected DIGEST-MD5 SASL callback of type " +
478                    callback.getClass().getName());
479            }
480          }
481        }
482      }
483    
484    
485    
486      /**
487       * {@inheritDoc}
488       */
489      @Override()
490      public int getLastMessageID()
491      {
492        return messageID;
493      }
494    
495    
496    
497      /**
498       * {@inheritDoc}
499       */
500      @Override()
501      public DIGESTMD5BindRequest duplicate()
502      {
503        return duplicate(getControls());
504      }
505    
506    
507    
508      /**
509       * {@inheritDoc}
510       */
511      @Override()
512      public DIGESTMD5BindRequest duplicate(final Control[] controls)
513      {
514        final DIGESTMD5BindRequest bindRequest =
515             new DIGESTMD5BindRequest(authenticationID, authorizationID, password,
516                  realm, controls);
517        bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
518        return bindRequest;
519      }
520    
521    
522    
523      /**
524       * {@inheritDoc}
525       */
526      @Override()
527      public void toString(final StringBuilder buffer)
528      {
529        buffer.append("DIGESTMD5BindRequest(authenticationID='");
530        buffer.append(authenticationID);
531        buffer.append('\'');
532    
533        if (authorizationID != null)
534        {
535          buffer.append(", authorizationID='");
536          buffer.append(authorizationID);
537          buffer.append('\'');
538        }
539    
540        if (realm != null)
541        {
542          buffer.append(", realm='");
543          buffer.append(realm);
544          buffer.append('\'');
545        }
546    
547        final Control[] controls = getControls();
548        if (controls.length > 0)
549        {
550          buffer.append(", controls={");
551          for (int i=0; i < controls.length; i++)
552          {
553            if (i > 0)
554            {
555              buffer.append(", ");
556            }
557    
558            buffer.append(controls[i]);
559          }
560          buffer.append('}');
561        }
562    
563        buffer.append(')');
564      }
565    }