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