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