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