001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2011-2012 profiq s.r.o.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.plugins;
018
019import java.io.UnsupportedEncodingException;
020import java.security.InvalidKeyException;
021import java.security.MessageDigest;
022import java.security.NoSuchAlgorithmException;
023import java.security.NoSuchProviderException;
024import java.util.*;
025
026import javax.crypto.*;
027import javax.crypto.spec.SecretKeySpec;
028
029import org.forgerock.i18n.LocalizableMessage;
030import org.forgerock.i18n.slf4j.LocalizedLogger;
031import org.forgerock.opendj.ldap.ByteString;
032import org.forgerock.opendj.ldap.DN;
033import org.forgerock.opendj.ldap.ModificationType;
034import org.opends.server.admin.server.ConfigurationChangeListener;
035import org.opends.server.admin.std.meta.PluginCfgDefn;
036import org.opends.server.admin.std.meta.SambaPasswordPluginCfgDefn.PwdSyncPolicy;
037import org.opends.server.admin.std.server.SambaPasswordPluginCfg;
038import org.opends.server.api.plugin.DirectoryServerPlugin;
039import org.opends.server.api.plugin.PluginResult;
040import org.opends.server.api.plugin.PluginType;
041import org.forgerock.opendj.config.server.ConfigChangeResult;
042import org.forgerock.opendj.config.server.ConfigException;
043import org.opends.server.controls.LDAPAssertionRequestControl;
044import org.opends.server.core.DirectoryServer;
045import org.opends.server.core.ModifyOperation;
046import org.opends.server.extensions.PasswordModifyExtendedOperation;
047import org.opends.server.protocols.internal.InternalClientConnection;
048import org.opends.server.protocols.ldap.LDAPFilter;
049import org.forgerock.opendj.ldap.schema.AttributeType;
050import org.opends.server.types.*;
051import org.forgerock.opendj.ldap.ResultCode;
052import org.opends.server.types.operation.PostOperationExtendedOperation;
053import org.opends.server.types.operation.PreOperationModifyOperation;
054
055import static org.opends.messages.PluginMessages.*;
056import static org.opends.server.util.StaticUtils.*;
057
058/**
059 * The Samba password synchronization plugin implementation class.
060 * <p>
061 * This plugin synchronizes the userPassword attribute with the Samba password
062 * attribute(s) for all entries containing the specified Samba object class.
063 * <p>
064 * It handles clear-text userPassword modify operations and password modify
065 * extended operations. It does not cover the case of using pre-encoded
066 * password.
067 */
068public final class SambaPasswordPlugin extends
069    DirectoryServerPlugin<SambaPasswordPluginCfg> implements
070    ConfigurationChangeListener<SambaPasswordPluginCfg>
071{
072
073  /**
074   * The implementation of this algorithm has been derived from BouncyCastle.org
075   * whose license can be found at http://www.bouncycastle.org/licence.html:
076   * <p>
077   * Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle
078   * (http://www.bouncycastle.org)
079   * <p>
080   * Permission is hereby granted, free of charge, to any person obtaining a
081   * copy of this software and associated documentation files (the "Software"),
082   * to deal in the Software without restriction, including without limitation
083   * the rights to use, copy, modify, merge, publish, distribute, sublicense,
084   * and/or sell copies of the Software, and to permit persons to whom the
085   * Software is furnished to do so, subject to the following conditions:
086   * <p>
087   * The above copyright notice and this permission notice shall be included in
088   * all copies or substantial portions of the Software.
089   * <p>
090   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
091   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
092   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
093   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
094   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
095   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
096   * DEALINGS IN THE SOFTWARE.
097   */
098  static final class MD4MessageDigest extends MessageDigest
099  {
100    /** Class is package private for testing. */
101    private final byte[] xBuf = new byte[4];
102    private int xBufOff;
103    private long byteCount;
104
105    private static final int DIGEST_LENGTH = 16;
106    /** IV's. */
107    private int H1, H2, H3, H4;
108    private final int[] X = new int[16];
109    private int xOff;
110
111    /** Round 1 left rotates. */
112    private static final int S11 = 3;
113    private static final int S12 = 7;
114    private static final int S13 = 11;
115    private static final int S14 = 19;
116
117    /** Round 2 left rotates. */
118    private static final int S21 = 3;
119    private static final int S22 = 5;
120    private static final int S23 = 9;
121    private static final int S24 = 13;
122
123    /** Round 3 left rotates. */
124    private static final int S31 = 3;
125    private static final int S32 = 9;
126    private static final int S33 = 11;
127    private static final int S34 = 15;
128
129
130
131    /**
132     * Creates a new MD4 message digest algorithm.
133     */
134    MD4MessageDigest()
135    {
136      super("MD4");
137      engineReset();
138    }
139
140
141
142    /** {@inheritDoc} */
143    @Override
144    public byte[] engineDigest()
145    {
146      final byte[] digestBytes = new byte[DIGEST_LENGTH];
147      finish();
148      unpackWord(H1, digestBytes, 0);
149      unpackWord(H2, digestBytes, 4);
150      unpackWord(H3, digestBytes, 8);
151      unpackWord(H4, digestBytes, 12);
152      engineReset();
153      return digestBytes;
154    }
155
156
157
158    /** {@inheritDoc} */
159    @Override
160    public void engineReset()
161    {
162      byteCount = 0;
163      xBufOff = 0;
164      for (int i = 0; i < xBuf.length; i++)
165      {
166        xBuf[i] = 0;
167      }
168
169      H1 = 0x67452301;
170      H2 = 0xefcdab89;
171      H3 = 0x98badcfe;
172      H4 = 0x10325476;
173      xOff = 0;
174      for (int i = 0; i != X.length; i++)
175      {
176        X[i] = 0;
177      }
178    }
179
180
181
182    /** {@inheritDoc} */
183    @Override
184    public void engineUpdate(final byte input)
185    {
186      xBuf[xBufOff++] = input;
187      if (xBufOff == xBuf.length)
188      {
189        processWord(xBuf, 0);
190        xBufOff = 0;
191      }
192      byteCount++;
193    }
194
195
196
197    /** {@inheritDoc} */
198    @Override
199    public void engineUpdate(final byte[] in, int inOff, int len)
200    {
201      //
202      // fill the current word
203      //
204      while (xBufOff != 0 && len > 0)
205      {
206        update(in[inOff]);
207
208        inOff++;
209        len--;
210      }
211
212      //
213      // process whole words.
214      //
215      while (len > xBuf.length)
216      {
217        processWord(in, inOff);
218
219        inOff += xBuf.length;
220        len -= xBuf.length;
221        byteCount += xBuf.length;
222      }
223
224      //
225      // load in the remainder.
226      //
227      while (len > 0)
228      {
229        update(in[inOff]);
230
231        inOff++;
232        len--;
233      }
234    }
235
236
237
238    /**
239     * F, G, H and I are the basic MD4 functions.
240     */
241    private int F(final int u, final int v, final int w)
242    {
243      return (u & v) | (~u & w);
244    }
245
246
247
248    private void finish()
249    {
250      final long bitLength = byteCount << 3;
251
252      //
253      // add the pad bytes.
254      //
255      engineUpdate((byte) 128);
256      while (xBufOff != 0)
257      {
258        engineUpdate((byte) 0);
259      }
260      processLength(bitLength);
261      processBlock();
262    }
263
264
265
266    private int G(final int u, final int v, final int w)
267    {
268      return (u & v) | (u & w) | (v & w);
269    }
270
271
272
273    private int H(final int u, final int v, final int w)
274    {
275      return u ^ v ^ w;
276    }
277
278
279
280    private void processBlock()
281    {
282      int a = H1;
283      int b = H2;
284      int c = H3;
285      int d = H4;
286
287      //
288      // Round 1 - F cycle, 16 times.
289      //
290      a = rotateLeft(a + F(b, c, d) + X[0], S11);
291      d = rotateLeft(d + F(a, b, c) + X[1], S12);
292      c = rotateLeft(c + F(d, a, b) + X[2], S13);
293      b = rotateLeft(b + F(c, d, a) + X[3], S14);
294      a = rotateLeft(a + F(b, c, d) + X[4], S11);
295      d = rotateLeft(d + F(a, b, c) + X[5], S12);
296      c = rotateLeft(c + F(d, a, b) + X[6], S13);
297      b = rotateLeft(b + F(c, d, a) + X[7], S14);
298      a = rotateLeft(a + F(b, c, d) + X[8], S11);
299      d = rotateLeft(d + F(a, b, c) + X[9], S12);
300      c = rotateLeft(c + F(d, a, b) + X[10], S13);
301      b = rotateLeft(b + F(c, d, a) + X[11], S14);
302      a = rotateLeft(a + F(b, c, d) + X[12], S11);
303      d = rotateLeft(d + F(a, b, c) + X[13], S12);
304      c = rotateLeft(c + F(d, a, b) + X[14], S13);
305      b = rotateLeft(b + F(c, d, a) + X[15], S14);
306
307      //
308      // Round 2 - G cycle, 16 times.
309      //
310      a = rotateLeft(a + G(b, c, d) + X[0] + 0x5a827999, S21);
311      d = rotateLeft(d + G(a, b, c) + X[4] + 0x5a827999, S22);
312      c = rotateLeft(c + G(d, a, b) + X[8] + 0x5a827999, S23);
313      b = rotateLeft(b + G(c, d, a) + X[12] + 0x5a827999, S24);
314      a = rotateLeft(a + G(b, c, d) + X[1] + 0x5a827999, S21);
315      d = rotateLeft(d + G(a, b, c) + X[5] + 0x5a827999, S22);
316      c = rotateLeft(c + G(d, a, b) + X[9] + 0x5a827999, S23);
317      b = rotateLeft(b + G(c, d, a) + X[13] + 0x5a827999, S24);
318      a = rotateLeft(a + G(b, c, d) + X[2] + 0x5a827999, S21);
319      d = rotateLeft(d + G(a, b, c) + X[6] + 0x5a827999, S22);
320      c = rotateLeft(c + G(d, a, b) + X[10] + 0x5a827999, S23);
321      b = rotateLeft(b + G(c, d, a) + X[14] + 0x5a827999, S24);
322      a = rotateLeft(a + G(b, c, d) + X[3] + 0x5a827999, S21);
323      d = rotateLeft(d + G(a, b, c) + X[7] + 0x5a827999, S22);
324      c = rotateLeft(c + G(d, a, b) + X[11] + 0x5a827999, S23);
325      b = rotateLeft(b + G(c, d, a) + X[15] + 0x5a827999, S24);
326
327      //
328      // Round 3 - H cycle, 16 times.
329      //
330      a = rotateLeft(a + H(b, c, d) + X[0] + 0x6ed9eba1, S31);
331      d = rotateLeft(d + H(a, b, c) + X[8] + 0x6ed9eba1, S32);
332      c = rotateLeft(c + H(d, a, b) + X[4] + 0x6ed9eba1, S33);
333      b = rotateLeft(b + H(c, d, a) + X[12] + 0x6ed9eba1, S34);
334      a = rotateLeft(a + H(b, c, d) + X[2] + 0x6ed9eba1, S31);
335      d = rotateLeft(d + H(a, b, c) + X[10] + 0x6ed9eba1, S32);
336      c = rotateLeft(c + H(d, a, b) + X[6] + 0x6ed9eba1, S33);
337      b = rotateLeft(b + H(c, d, a) + X[14] + 0x6ed9eba1, S34);
338      a = rotateLeft(a + H(b, c, d) + X[1] + 0x6ed9eba1, S31);
339      d = rotateLeft(d + H(a, b, c) + X[9] + 0x6ed9eba1, S32);
340      c = rotateLeft(c + H(d, a, b) + X[5] + 0x6ed9eba1, S33);
341      b = rotateLeft(b + H(c, d, a) + X[13] + 0x6ed9eba1, S34);
342      a = rotateLeft(a + H(b, c, d) + X[3] + 0x6ed9eba1, S31);
343      d = rotateLeft(d + H(a, b, c) + X[11] + 0x6ed9eba1, S32);
344      c = rotateLeft(c + H(d, a, b) + X[7] + 0x6ed9eba1, S33);
345      b = rotateLeft(b + H(c, d, a) + X[15] + 0x6ed9eba1, S34);
346
347      H1 += a;
348      H2 += b;
349      H3 += c;
350      H4 += d;
351
352      //
353      // reset the offset and clean out the word buffer.
354      //
355      xOff = 0;
356      for (int i = 0; i != X.length; i++)
357      {
358        X[i] = 0;
359      }
360    }
361
362
363
364    private void processLength(final long bitLength)
365    {
366      if (xOff > 14)
367      {
368        processBlock();
369      }
370
371      X[14] = (int) (bitLength & 0xffffffff);
372      X[15] = (int) (bitLength >>> 32);
373    }
374
375
376
377    private void processWord(final byte[] in, final int inOff)
378    {
379      X[xOff++] = (in[inOff] & 0xff) | ((in[inOff + 1] & 0xff) << 8)
380          | ((in[inOff + 2] & 0xff) << 16) | ((in[inOff + 3] & 0xff) << 24);
381
382      if (xOff == 16)
383      {
384        processBlock();
385      }
386    }
387
388
389
390    /*
391     * rotate int x left n bits.
392     */
393    private int rotateLeft(final int x, final int n)
394    {
395      return (x << n) | (x >>> 32 - n);
396    }
397
398
399
400    private void unpackWord(final int word, final byte[] out, final int outOff)
401    {
402      out[outOff] = (byte) word;
403      out[outOff + 1] = (byte) (word >>> 8);
404      out[outOff + 2] = (byte) (word >>> 16);
405      out[outOff + 3] = (byte) (word >>> 24);
406    }
407  }
408
409
410
411  /**
412   * Plugin configuration object.
413   */
414  private SambaPasswordPluginCfg config;
415
416  /** The name of the Samba LanMan password attribute. */
417  private static final String SAMBA_LM_PASSWORD_ATTRIBUTE_NAME =
418    "sambaLMPassword";
419
420  /** The name of the Samba NT password attribute. */
421  private static final String SAMBA_NT_PASSWORD_ATTRIBUTE_NAME =
422    "sambaNTPassword";
423
424  /** The name of the Samba account object class. */
425  private static final String SAMBA_SAM_ACCOUNT_OC_NAME = "sambaSAMAccount";
426
427  /** The name of the Samba last password change attribute. */
428  private static final String SAMBA_PWD_LAST_SET_NAME = "sambaPwdLastSet";
429
430  /** Debug tracer object to log debugging information. */
431  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
432
433  /** Password Modify Extended Operation OID. */
434  private static final String PWMOD_EXTOP_OID = "1.3.6.1.4.1.4203.1.11.1";
435
436  /** Magic string to be used as salt. */
437  private static final String MAGIC_STR = "KGS!@#$%";
438
439  /** Default timestamp provider implementation. */
440  private static final TimeStampProvider DEFAULT_TIMESTAMP_PROVIDER =
441  new TimeStampProvider()
442  {
443    @Override
444    public long getCurrentTime()
445    {
446      return System.currentTimeMillis() / 1000L;
447    }
448  };
449
450  /** Use the default implementation of the timestamp provider... by default. */
451  private TimeStampProvider timeStampProvider = DEFAULT_TIMESTAMP_PROVIDER;
452
453
454  /**
455   * Add the parity to the 56-bit key converting it to 64-bit key.
456   *
457   * @param key56
458   *          56-bit key.
459   * @return 64-bit key.
460   */
461  private static byte[] addParity(final byte[] key56)
462  {
463    final byte[] key64 = new byte[8];
464    final int[] key7 = new int[7];
465    final int[] key8 = new int[8];
466
467    for (int i = 0; i < 7; i++)
468    {
469      key7[i] = key56[i] & 0xFF;
470    }
471
472    key8[0] = key7[0];
473    key8[1] = ((key7[0] << 7) & 0xFF) | (key7[1] >> 1);
474    key8[2] = ((key7[1] << 6) & 0xFF) | (key7[2] >> 2);
475    key8[3] = ((key7[2] << 5) & 0xFF) | (key7[3] >> 3);
476    key8[4] = ((key7[3] << 4) & 0xFF) | (key7[4] >> 4);
477    key8[5] = ((key7[4] << 3) & 0xFF) | (key7[5] >> 5);
478    key8[6] = ((key7[5] << 2) & 0xFF) | (key7[6] >> 6);
479    key8[7] = key7[6] << 1;
480
481    for (int i = 0; i < 8; i++)
482    {
483      key64[i] = (byte) setOddParity(key8[i]);
484    }
485
486    return key64;
487
488  }
489
490
491
492  /**
493   * Create a LanMan hash from a clear-text password.
494   *
495   * @param password
496   *          Clear-text password.
497   * @return Hex string version of the hash based on the clear-text password.
498   * @throws UnsupportedEncodingException
499   *           if the <code>US-ASCII</code> coding is not available.
500   * @throws NoSuchAlgorithmException
501   *           if the algorithm does not exist for the used provider.
502   * @throws InvalidKeyException
503   *           if the key is inappropriate to initialize the cipher.
504   * @throws NoSuchPaddingException
505   *           if the padding scheme is not available.
506   * @throws IllegalBlockSizeException
507   *           if this encryption algorithm is unable to process the input data
508   *           provided
509   * @throws BadPaddingException
510   *           if this cipher is in decryption mode, and (un)padding has been
511   *           requested, but the decrypted data is not bounded by the
512   *           appropriate padding bytes
513   */
514  private static String lmHash(final String password)
515      throws UnsupportedEncodingException, NoSuchAlgorithmException,
516      InvalidKeyException, NoSuchPaddingException, IllegalBlockSizeException,
517      BadPaddingException
518  {
519    // Password has to be OEM encoded and in upper case
520    final byte[] oemPass = password.toUpperCase().getBytes("US-ASCII");
521
522    // It shouldn't be longer then 14 bytes
523    int length = 14;
524    if (oemPass.length < length)
525    {
526      length = oemPass.length;
527    }
528
529    // The password should be divided into two 7-byte keys
530    final byte[] key1 = new byte[7];
531    final byte[] key2 = new byte[7];
532    if (length <= 7)
533    {
534      System.arraycopy(oemPass, 0, key1, 0, length);
535    }
536    else
537    {
538      System.arraycopy(oemPass, 0, key1, 0, 7);
539      System.arraycopy(oemPass, 7, key2, 0, length - 7);
540    }
541
542    // We create two DES keys using key1 and key2 to on the magic string
543    final SecretKey lowKey = new SecretKeySpec(addParity(key1), "DES");
544    final SecretKey highKey = new SecretKeySpec(addParity(key2), "DES");
545    final Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
546    des.init(Cipher.ENCRYPT_MODE, lowKey);
547    final byte[] lowHash = des.doFinal(MAGIC_STR.getBytes());
548    des.init(Cipher.ENCRYPT_MODE, highKey);
549    final byte[] highHash = des.doFinal(MAGIC_STR.getBytes());
550
551    // We finally merge hashes and return them to the client
552
553    final byte[] lmHash = new byte[16];
554    System.arraycopy(lowHash, 0, lmHash, 0, 8);
555    System.arraycopy(highHash, 0, lmHash, 8, 8);
556    return toLowerCase(bytesToHexNoSpace(lmHash));
557  }
558
559
560
561  /**
562   * Creates a NTLM hash from a clear-text password.
563   *
564   * @param password
565   *          Clear text password.
566   * @return Returns a NTLM hash.
567   * @throws NoSuchProviderException
568   *           if the BouncyCastle provider does not load
569   * @throws NoSuchAlgorithmException
570   *           if the MD4 algorithm is not found
571   * @throws UnsupportedEncodingException
572   *           if the encoding <code>UnicodeLittleUnmarked</code> is not
573   *           supported.
574   */
575  private static String ntHash(final String password)
576      throws NoSuchProviderException, UnsupportedEncodingException,
577      NoSuchAlgorithmException
578  {
579    final byte[] unicodePassword = password.getBytes("UnicodeLittleUnmarked");
580    final MessageDigest md4 = new MD4MessageDigest();
581    return toLowerCase(bytesToHexNoSpace(md4.digest(unicodePassword)));
582  }
583
584
585
586  /**
587   * Set the parity bit for an integer.
588   *
589   * @param integer
590   *          to add the parity bit for.
591   * @return integer with the parity bit set.
592   */
593  private static int setOddParity(final int parity)
594  {
595    final boolean hasEvenBits = (parity >>> 7 ^ parity >>> 6
596                               ^ parity >>> 5 ^ parity >>> 4
597                               ^ parity >>> 3 ^ parity >>> 2
598                               ^ ((parity >>> 1) & 0x01)) == 0;
599    if (hasEvenBits)
600    {
601      return parity | 0x01;
602    }
603    else
604    {
605      return parity & 0xFE;
606    }
607  }
608
609
610
611  /**
612   * Default constructor.
613   */
614  public SambaPasswordPlugin()
615  {
616    super();
617  }
618
619
620
621  /** {@inheritDoc} */
622  @Override
623  public ConfigChangeResult applyConfigurationChange(
624      final SambaPasswordPluginCfg newConfig)
625  {
626
627    // No validation required and no restart required.
628    config = newConfig;
629
630    return new ConfigChangeResult();
631  }
632
633
634
635  /** {@inheritDoc} */
636  @Override
637  public PluginResult.PostOperation doPostOperation(
638      final PostOperationExtendedOperation extendedOperation)
639  {
640    /*
641     * If the operation is not Password Modify Extended Operation then skip this
642     * operation.
643     */
644    if (!extendedOperation.getRequestOID().equals(PWMOD_EXTOP_OID))
645    {
646      return PluginResult.PostOperation.continueOperationProcessing();
647    }
648
649    /*
650     * If the operation has not been successful then ignore the operation.
651     */
652    if (extendedOperation.getResultCode() != ResultCode.SUCCESS)
653    {
654      return PluginResult.PostOperation.continueOperationProcessing();
655    }
656
657    /*
658     * Verify if the operation has been initiated by what was defined as Samba
659     * administrative user. If so, we will skip this operation to avoid double
660     * synchronization of Samba attributes.
661     */
662    final DN authDN = extendedOperation.getAuthorizationDN();
663    final DN sambaAdminDN = config.getSambaAdministratorDN();
664    if (sambaAdminDN != null
665        && !sambaAdminDN.isRootDN()
666        && authDN.equals(sambaAdminDN))
667    {
668      if (logger.isTraceEnabled())
669      {
670        logger.trace("This operation will be skipped because"
671            + " it was performed by Samba admin user: " + sambaAdminDN);
672      }
673      return PluginResult.PostOperation.continueOperationProcessing();
674    }
675
676    // Get the name of the entry and clear passwords from the operation
677    // attachments.
678    final DN dn = (DN) extendedOperation
679        .getAttachment(PasswordModifyExtendedOperation.AUTHZ_DN_ATTACHMENT);
680    if (dn == null)
681    {
682      // The attachment is missing which should never happen.
683      if (logger.isTraceEnabled())
684      {
685        logger.trace("SambaPasswordPlugin: missing DN attachment");
686      }
687      return PluginResult.PostOperation.continueOperationProcessing();
688    }
689
690    final String password = extendedOperation.getAttachment(
691        PasswordModifyExtendedOperation.CLEAR_PWD_ATTACHMENT).toString();
692    if (password == null)
693    {
694      if (logger.isTraceEnabled())
695      {
696        logger.trace("SambaPasswordPlugin: skipping syncing "
697            + "pre-encoded password");
698      }
699      return PluginResult.PostOperation.continueOperationProcessing();
700    }
701
702    @SuppressWarnings("unchecked")
703    final List<ByteString> encPasswords = (List<ByteString>) extendedOperation
704        .getAttachment(PasswordModifyExtendedOperation.ENCODED_PWD_ATTACHMENT);
705
706    try
707    {
708      // Before proceeding make sure this entry has samba object class.
709      final Entry entry = DirectoryServer.getEntry(dn);
710      if (!isSynchronizable(entry))
711      {
712        if (logger.isTraceEnabled())
713        {
714          logger.trace("The entry is not Samba object.");
715        }
716        return PluginResult.PostOperation.continueOperationProcessing();
717      }
718
719      /*
720       * Make an internal connection to process the password modification. It
721       * will not trigger this plugin again with the pre-operation modify since
722       * the password passed would be encoded hence the pre operation part would
723       * skip it.
724       */
725      final InternalClientConnection connection = InternalClientConnection
726          .getRootConnection();
727
728      final List<Modification> modifications = getModifications(password);
729
730      // Use an assertion control to avoid race conditions since extended
731      // operation post-ops are done outside of the write lock.
732      List<Control> controls = null;
733      if (!encPasswords.isEmpty())
734      {
735        final AttributeType pwdAttribute = (AttributeType) extendedOperation
736            .getAttachment(
737                PasswordModifyExtendedOperation.PWD_ATTRIBUTE_ATTACHMENT);
738        final LDAPFilter filter = RawFilter.createEqualityFilter(
739            pwdAttribute.getNameOrOID(), encPasswords.get(0));
740        final Control assertionControl = new LDAPAssertionRequestControl(true,
741            filter);
742        controls = Collections.singletonList(assertionControl);
743      }
744
745      final ModifyOperation modifyOperation = connection.processModify(dn,
746          modifications, controls);
747
748      if (logger.isTraceEnabled())
749      {
750        logger.trace("rc=%s", modifyOperation.getResultCode());
751      }
752    }
753    catch (final DirectoryException e)
754    {
755      /*
756       * This exception occurs if there is a problem while retrieving the entry.
757       * This should never happen as we are processing the post-operation which
758       * succeeded so the entry has to exist if we have reached this point.
759       */
760      logger.traceException(e);
761    }
762
763    return PluginResult.PostOperation.continueOperationProcessing();
764  }
765
766
767
768  /** {@inheritDoc} */
769  @Override
770  public PluginResult.PreOperation doPreOperation(
771      final PreOperationModifyOperation modifyOperation)
772  {
773    /*
774     * If the passwords are changed in clear text they will be available with
775     * the getNewPasswords() method. If they are encoded the method would return
776     * null. The list of passwords should not be modified.
777     */
778    final List<ByteString> passwords = modifyOperation.getNewPasswords();
779
780    /*
781     * If the password list is not empty, we can be sure the current operation
782     * is the one that applies to our case: - it's a modify operation on
783     * userPassword attribute - it's replaces or adds new userPassword attribute
784     * value. If it doesn't then we skip this modify operation.
785     */
786    if (passwords == null)
787    {
788      return PluginResult.PreOperation.continueOperationProcessing();
789    }
790
791    // Skip synchronization operations.
792    if (modifyOperation.isSynchronizationOperation())
793    {
794      if (logger.isTraceEnabled())
795      {
796        logger.trace("Synchronization operation. Skipping.");
797      }
798      return PluginResult.PreOperation.continueOperationProcessing();
799    }
800
801    /*
802     * Verify if the operation has been initiated by the Samba administrative
803     * user. If so, we will skip this operation to avoid double synchronization
804     * of Samba attributes.
805     */
806    final DN authDN = modifyOperation.getAuthorizationDN();
807    final DN sambaAdminDN = config.getSambaAdministratorDN();
808    if (sambaAdminDN != null
809        && !sambaAdminDN.isRootDN()
810        && authDN.equals(sambaAdminDN))
811    {
812      if (logger.isTraceEnabled())
813      {
814        logger.trace("This operation will be skipped because"
815            + " it was performed by Samba admin user: " + sambaAdminDN);
816      }
817      return PluginResult.PreOperation.continueOperationProcessing();
818    }
819
820    /*
821     * Before proceeding with the modification, we have to make sure this entry
822     * is indeed a Samba object.
823     */
824    if (!isSynchronizable(modifyOperation.getCurrentEntry()))
825    {
826      if (logger.isTraceEnabled())
827      {
828        logger.trace("Skipping '%s' because it does not have Samba object class.", modifyOperation.getEntryDN());
829      }
830      return PluginResult.PreOperation.continueOperationProcessing();
831    }
832
833    /*
834     * Proceed with processing: add a new modification to the current modify
835     * operation, so they could be executed at the same time.
836     */
837    processModification(modifyOperation, passwords);
838
839    // Continue plugin processing.
840    return PluginResult.PreOperation.continueOperationProcessing();
841  }
842
843
844
845  /** {@inheritDoc} */
846  @Override
847  public void initializePlugin(final Set<PluginType> pluginTypes,
848      final SambaPasswordPluginCfg configuration) throws ConfigException,
849      InitializationException
850  {
851
852    // Verify config parameters.
853    final LinkedList<LocalizableMessage> messages = new LinkedList<>();
854    if (!isConfigurationAcceptable(configuration, messages))
855    {
856      for (final LocalizableMessage m : messages)
857      {
858        logger.error(m);
859      }
860      throw new ConfigException(messages.poll());
861    }
862
863    // Register the configuration change listener.
864    configuration.addSambaPasswordChangeListener(this);
865
866    // Save the configuration.
867    this.config = configuration;
868  }
869
870
871
872  /**
873   * Verifies if the plugin configuration is acceptable.
874   *
875   * @param configuration
876   *          The plugin configuration.
877   * @param unacceptableReasons
878   *          Reasons why the configuration is not acceptable.
879   * @return Returns <code>true</code> for the correct configuration and
880   *         <code>false</code> for the incorrect one.
881   */
882  public boolean isConfigurationAcceptable(
883      final SambaPasswordPluginCfg configuration,
884      final List<LocalizableMessage> unacceptableReasons)
885  {
886    return isConfigurationChangeAcceptable(configuration, unacceptableReasons);
887  }
888
889
890
891  /** {@inheritDoc} */
892  @Override
893  public boolean isConfigurationChangeAcceptable(
894      final SambaPasswordPluginCfg newConfig, final List<LocalizableMessage> messages)
895  {
896    /*
897     * The plugin implements only postoperationmodify and postoperationextended
898     * plugin types. The rest should be rejected.
899     */
900
901    final SortedSet<PluginCfgDefn.PluginType> pluginTypes = newConfig
902        .getPluginType();
903    for (final PluginCfgDefn.PluginType t : pluginTypes)
904    {
905      switch (t)
906      {
907      case PREOPERATIONMODIFY:
908      case POSTOPERATIONEXTENDED:
909        break;
910      default:
911        messages.add(ERR_PLUGIN_SAMBA_SYNC_INVALID_PLUGIN_TYPE.get(t));
912        return false;
913      }
914    }
915
916    return true;
917  }
918
919
920
921  /**
922   * Creates the modifications to modify Samba password attributes. It uses
923   * clear-text password and encodes it with the appropriate algorithm for it's
924   * respective type (NTLM or LanMan); then it wraps it in the modifications to
925   * be added to the modify operation.
926   *
927   * @param password
928   *          New password which is to be encoded for Samba.
929   * @return Returns a list of modifications, or null if a problem occurs.
930   */
931  private List<Modification> getModifications(final String password)
932  {
933    ArrayList<Modification> modifications = new ArrayList<>();
934    try
935    {
936      if (config.getPwdSyncPolicy().contains(PwdSyncPolicy.SYNC_NT_PASSWORD))
937      {
938        final Attribute attribute = Attributes.create(
939            SAMBA_NT_PASSWORD_ATTRIBUTE_NAME, ntHash(password));
940        modifications.add(new Modification(ModificationType.REPLACE, attribute));
941      }
942
943      if (config.getPwdSyncPolicy().contains(PwdSyncPolicy.SYNC_LM_PASSWORD))
944      {
945        final Attribute attribute = Attributes.create(
946            SAMBA_LM_PASSWORD_ATTRIBUTE_NAME, lmHash(password));
947        modifications.add(new Modification(ModificationType.REPLACE, attribute));
948      }
949      final Attribute pwdLastSet = Attributes.create(
950        SAMBA_PWD_LAST_SET_NAME,
951        String.valueOf(timeStampProvider.getCurrentTime()));
952      modifications.add(new Modification(ModificationType.REPLACE, pwdLastSet));
953    }
954    catch (final Exception e)
955    {
956      logger.info(ERR_PLUGIN_SAMBA_SYNC_ENCODING, e.getMessage(), e);
957      modifications = null;
958    }
959
960    return modifications;
961  }
962
963
964
965  /**
966   * Verify if the target entry contains pre-defined Samba object class.
967   *
968   * @param entry
969   *          The entry being modified.
970   * @return Returns true if the entry has Samba object class, otherwise returns
971   *         false.
972   */
973  private boolean isSynchronizable(final Entry entry)
974  {
975    final Schema schema = DirectoryServer.getSchema();
976    final ObjectClass sambaOc = schema
977        .getObjectClass(toLowerCase(SAMBA_SAM_ACCOUNT_OC_NAME));
978    return sambaOc != null && entry.hasObjectClass(sambaOc);
979  }
980
981
982
983  /**
984   * Adds modifications for the configured Samba password attributes to the
985   * current modify operation.
986   *
987   * @param modifyOperation
988   *          Current modify operation which will be modified to add Samba
989   *          password attribute changes.
990   * @param passwords
991   *          List of userPassword clear-text attribute values to be hashed for
992   *          Samba
993   */
994  private void processModification(
995      final PreOperationModifyOperation modifyOperation,
996      final List<ByteString> passwords)
997  {
998    // Get the last password (in case there is more then one).
999    final String password = passwords.get(passwords.size() - 1).toString();
1000    try
1001    {
1002      // Generate the necessary modifications.
1003      for (final Modification modification : getModifications(password))
1004      {
1005        modifyOperation.addModification(modification);
1006      }
1007    }
1008    catch (final DirectoryException e)
1009    {
1010      logger.info(ERR_PLUGIN_SAMBA_SYNC_MODIFICATION_PROCESSING, e.getMessage(), e);
1011    }
1012  }
1013
1014  /**
1015   * Timestamp provider interface. Intended primarily for testing purposes.
1016   */
1017  static interface TimeStampProvider
1018  {
1019    /**
1020     * Generates a custom time stamp.
1021     *
1022     * @return  A timestamp in UNIX format.
1023     */
1024    long getCurrentTime();
1025  }
1026
1027  /**
1028   * Use custom timestamp provider. Intended primarily for testing purposes.
1029   *
1030   * @param timeStampProvider Provider object that implements the
1031   * TimeStampProvider interface.
1032   */
1033  void setTimeStampProvider(TimeStampProvider timeStampProvider)
1034  {
1035    this.timeStampProvider = (timeStampProvider == null)
1036                             ? DEFAULT_TIMESTAMP_PROVIDER : timeStampProvider;
1037  }
1038}