/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2011-2012 profiq s.r.o.
 *      Portions Copyright 2011-2015 ForgeRock AS.
 */
package org.opends.server.plugins;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.*;

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ModificationType;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.meta.PluginCfgDefn;
import org.opends.server.admin.std.meta.SambaPasswordPluginCfgDefn.PwdSyncPolicy;
import org.opends.server.admin.std.server.SambaPasswordPluginCfg;
import org.opends.server.api.plugin.DirectoryServerPlugin;
import org.opends.server.api.plugin.PluginResult;
import org.opends.server.api.plugin.PluginType;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.opends.server.controls.LDAPAssertionRequestControl;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyOperation;
import org.opends.server.extensions.PasswordModifyExtendedOperation;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.ldap.LDAPFilter;
import org.opends.server.types.*;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.types.operation.PostOperationExtendedOperation;
import org.opends.server.types.operation.PreOperationModifyOperation;

import static org.opends.messages.PluginMessages.*;
import static org.opends.server.util.StaticUtils.*;

/**
 * The Samba password synchronization plugin implementation class.
 * <p>
 * This plugin synchronizes the userPassword attribute with the Samba password
 * attribute(s) for all entries containing the specified Samba object class.
 * <p>
 * It handles clear-text userPassword modify operations and password modify
 * extended operations. It does not cover the case of using pre-encoded
 * password.
 */
public final class SambaPasswordPlugin extends
    DirectoryServerPlugin<SambaPasswordPluginCfg> implements
    ConfigurationChangeListener<SambaPasswordPluginCfg>
{

  /**
   * The implementation of this algorithm has been derived from BouncyCastle.org
   * whose license can be found at http://www.bouncycastle.org/licence.html:
   * <p>
   * Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle
   * (http://www.bouncycastle.org)
   * <p>
   * Permission is hereby granted, free of charge, to any person obtaining a
   * copy of this software and associated documentation files (the "Software"),
   * to deal in the Software without restriction, including without limitation
   * the rights to use, copy, modify, merge, publish, distribute, sublicense,
   * and/or sell copies of the Software, and to permit persons to whom the
   * Software is furnished to do so, subject to the following conditions:
   * <p>
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   * <p>
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
   * DEALINGS IN THE SOFTWARE.
   */
  static final class MD4MessageDigest extends MessageDigest
  {
    /** Class is package private for testing. */
    private final byte[] xBuf = new byte[4];
    private int xBufOff;
    private long byteCount;

    private static final int DIGEST_LENGTH = 16;
    /** IV's. */
    private int H1, H2, H3, H4;
    private final int[] X = new int[16];
    private int xOff;

    /** Round 1 left rotates. */
    private static final int S11 = 3;
    private static final int S12 = 7;
    private static final int S13 = 11;
    private static final int S14 = 19;

    /** Round 2 left rotates. */
    private static final int S21 = 3;
    private static final int S22 = 5;
    private static final int S23 = 9;
    private static final int S24 = 13;

    /** Round 3 left rotates. */
    private static final int S31 = 3;
    private static final int S32 = 9;
    private static final int S33 = 11;
    private static final int S34 = 15;



    /**
     * Creates a new MD4 message digest algorithm.
     */
    MD4MessageDigest()
    {
      super("MD4");
      engineReset();
    }



    /** {@inheritDoc} */
    @Override
    public byte[] engineDigest()
    {
      final byte[] digestBytes = new byte[DIGEST_LENGTH];
      finish();
      unpackWord(H1, digestBytes, 0);
      unpackWord(H2, digestBytes, 4);
      unpackWord(H3, digestBytes, 8);
      unpackWord(H4, digestBytes, 12);
      engineReset();
      return digestBytes;
    }



    /** {@inheritDoc} */
    @Override
    public void engineReset()
    {
      byteCount = 0;
      xBufOff = 0;
      for (int i = 0; i < xBuf.length; i++)
      {
        xBuf[i] = 0;
      }

      H1 = 0x67452301;
      H2 = 0xefcdab89;
      H3 = 0x98badcfe;
      H4 = 0x10325476;
      xOff = 0;
      for (int i = 0; i != X.length; i++)
      {
        X[i] = 0;
      }
    }



    /** {@inheritDoc} */
    @Override
    public void engineUpdate(final byte input)
    {
      xBuf[xBufOff++] = input;
      if (xBufOff == xBuf.length)
      {
        processWord(xBuf, 0);
        xBufOff = 0;
      }
      byteCount++;
    }



    /** {@inheritDoc} */
    @Override
    public void engineUpdate(final byte[] in, int inOff, int len)
    {
      //
      // fill the current word
      //
      while (xBufOff != 0 && len > 0)
      {
        update(in[inOff]);

        inOff++;
        len--;
      }

      //
      // process whole words.
      //
      while (len > xBuf.length)
      {
        processWord(in, inOff);

        inOff += xBuf.length;
        len -= xBuf.length;
        byteCount += xBuf.length;
      }

      //
      // load in the remainder.
      //
      while (len > 0)
      {
        update(in[inOff]);

        inOff++;
        len--;
      }
    }



    /**
     * F, G, H and I are the basic MD4 functions.
     */
    private int F(final int u, final int v, final int w)
    {
      return (u & v) | (~u & w);
    }



    private void finish()
    {
      final long bitLength = byteCount << 3;

      //
      // add the pad bytes.
      //
      engineUpdate((byte) 128);
      while (xBufOff != 0)
      {
        engineUpdate((byte) 0);
      }
      processLength(bitLength);
      processBlock();
    }



    private int G(final int u, final int v, final int w)
    {
      return (u & v) | (u & w) | (v & w);
    }



    private int H(final int u, final int v, final int w)
    {
      return u ^ v ^ w;
    }



    private void processBlock()
    {
      int a = H1;
      int b = H2;
      int c = H3;
      int d = H4;

      //
      // Round 1 - F cycle, 16 times.
      //
      a = rotateLeft(a + F(b, c, d) + X[0], S11);
      d = rotateLeft(d + F(a, b, c) + X[1], S12);
      c = rotateLeft(c + F(d, a, b) + X[2], S13);
      b = rotateLeft(b + F(c, d, a) + X[3], S14);
      a = rotateLeft(a + F(b, c, d) + X[4], S11);
      d = rotateLeft(d + F(a, b, c) + X[5], S12);
      c = rotateLeft(c + F(d, a, b) + X[6], S13);
      b = rotateLeft(b + F(c, d, a) + X[7], S14);
      a = rotateLeft(a + F(b, c, d) + X[8], S11);
      d = rotateLeft(d + F(a, b, c) + X[9], S12);
      c = rotateLeft(c + F(d, a, b) + X[10], S13);
      b = rotateLeft(b + F(c, d, a) + X[11], S14);
      a = rotateLeft(a + F(b, c, d) + X[12], S11);
      d = rotateLeft(d + F(a, b, c) + X[13], S12);
      c = rotateLeft(c + F(d, a, b) + X[14], S13);
      b = rotateLeft(b + F(c, d, a) + X[15], S14);

      //
      // Round 2 - G cycle, 16 times.
      //
      a = rotateLeft(a + G(b, c, d) + X[0] + 0x5a827999, S21);
      d = rotateLeft(d + G(a, b, c) + X[4] + 0x5a827999, S22);
      c = rotateLeft(c + G(d, a, b) + X[8] + 0x5a827999, S23);
      b = rotateLeft(b + G(c, d, a) + X[12] + 0x5a827999, S24);
      a = rotateLeft(a + G(b, c, d) + X[1] + 0x5a827999, S21);
      d = rotateLeft(d + G(a, b, c) + X[5] + 0x5a827999, S22);
      c = rotateLeft(c + G(d, a, b) + X[9] + 0x5a827999, S23);
      b = rotateLeft(b + G(c, d, a) + X[13] + 0x5a827999, S24);
      a = rotateLeft(a + G(b, c, d) + X[2] + 0x5a827999, S21);
      d = rotateLeft(d + G(a, b, c) + X[6] + 0x5a827999, S22);
      c = rotateLeft(c + G(d, a, b) + X[10] + 0x5a827999, S23);
      b = rotateLeft(b + G(c, d, a) + X[14] + 0x5a827999, S24);
      a = rotateLeft(a + G(b, c, d) + X[3] + 0x5a827999, S21);
      d = rotateLeft(d + G(a, b, c) + X[7] + 0x5a827999, S22);
      c = rotateLeft(c + G(d, a, b) + X[11] + 0x5a827999, S23);
      b = rotateLeft(b + G(c, d, a) + X[15] + 0x5a827999, S24);

      //
      // Round 3 - H cycle, 16 times.
      //
      a = rotateLeft(a + H(b, c, d) + X[0] + 0x6ed9eba1, S31);
      d = rotateLeft(d + H(a, b, c) + X[8] + 0x6ed9eba1, S32);
      c = rotateLeft(c + H(d, a, b) + X[4] + 0x6ed9eba1, S33);
      b = rotateLeft(b + H(c, d, a) + X[12] + 0x6ed9eba1, S34);
      a = rotateLeft(a + H(b, c, d) + X[2] + 0x6ed9eba1, S31);
      d = rotateLeft(d + H(a, b, c) + X[10] + 0x6ed9eba1, S32);
      c = rotateLeft(c + H(d, a, b) + X[6] + 0x6ed9eba1, S33);
      b = rotateLeft(b + H(c, d, a) + X[14] + 0x6ed9eba1, S34);
      a = rotateLeft(a + H(b, c, d) + X[1] + 0x6ed9eba1, S31);
      d = rotateLeft(d + H(a, b, c) + X[9] + 0x6ed9eba1, S32);
      c = rotateLeft(c + H(d, a, b) + X[5] + 0x6ed9eba1, S33);
      b = rotateLeft(b + H(c, d, a) + X[13] + 0x6ed9eba1, S34);
      a = rotateLeft(a + H(b, c, d) + X[3] + 0x6ed9eba1, S31);
      d = rotateLeft(d + H(a, b, c) + X[11] + 0x6ed9eba1, S32);
      c = rotateLeft(c + H(d, a, b) + X[7] + 0x6ed9eba1, S33);
      b = rotateLeft(b + H(c, d, a) + X[15] + 0x6ed9eba1, S34);

      H1 += a;
      H2 += b;
      H3 += c;
      H4 += d;

      //
      // reset the offset and clean out the word buffer.
      //
      xOff = 0;
      for (int i = 0; i != X.length; i++)
      {
        X[i] = 0;
      }
    }



    private void processLength(final long bitLength)
    {
      if (xOff > 14)
      {
        processBlock();
      }

      X[14] = (int) (bitLength & 0xffffffff);
      X[15] = (int) (bitLength >>> 32);
    }



    private void processWord(final byte[] in, final int inOff)
    {
      X[xOff++] = (in[inOff] & 0xff) | ((in[inOff + 1] & 0xff) << 8)
          | ((in[inOff + 2] & 0xff) << 16) | ((in[inOff + 3] & 0xff) << 24);

      if (xOff == 16)
      {
        processBlock();
      }
    }



    /*
     * rotate int x left n bits.
     */
    private int rotateLeft(final int x, final int n)
    {
      return (x << n) | (x >>> 32 - n);
    }



    private void unpackWord(final int word, final byte[] out, final int outOff)
    {
      out[outOff] = (byte) word;
      out[outOff + 1] = (byte) (word >>> 8);
      out[outOff + 2] = (byte) (word >>> 16);
      out[outOff + 3] = (byte) (word >>> 24);
    }
  }



  /**
   * Plugin configuration object.
   */
  private SambaPasswordPluginCfg config;

  /** The name of the Samba LanMan password attribute. */
  private static final String SAMBA_LM_PASSWORD_ATTRIBUTE_NAME =
    "sambaLMPassword";

  /** The name of the Samba NT password attribute. */
  private static final String SAMBA_NT_PASSWORD_ATTRIBUTE_NAME =
    "sambaNTPassword";

  /** The name of the Samba account object class. */
  private static final String SAMBA_SAM_ACCOUNT_OC_NAME = "sambaSAMAccount";

  /** The name of the Samba last password change attribute. */
  private static final String SAMBA_PWD_LAST_SET_NAME = "sambaPwdLastSet";

  /** Debug tracer object to log debugging information. */
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** Password Modify Extended Operation OID. */
  private static final String PWMOD_EXTOP_OID = "1.3.6.1.4.1.4203.1.11.1";

  /** Magic string to be used as salt. */
  private static final String MAGIC_STR = "KGS!@#$%";

  /** Default timestamp provider implementation. */
  private static final TimeStampProvider DEFAULT_TIMESTAMP_PROVIDER =
  new TimeStampProvider()
  {
    @Override
    public long getCurrentTime()
    {
      return System.currentTimeMillis() / 1000L;
    }
  };

  /** Use the default implementation of the timestamp provider... by default. */
  private TimeStampProvider timeStampProvider = DEFAULT_TIMESTAMP_PROVIDER;


  /**
   * Add the parity to the 56-bit key converting it to 64-bit key.
   *
   * @param key56
   *          56-bit key.
   * @return 64-bit key.
   */
  private static byte[] addParity(final byte[] key56)
  {
    final byte[] key64 = new byte[8];
    final int[] key7 = new int[7];
    final int[] key8 = new int[8];

    for (int i = 0; i < 7; i++)
    {
      key7[i] = key56[i] & 0xFF;
    }

    key8[0] = key7[0];
    key8[1] = ((key7[0] << 7) & 0xFF) | (key7[1] >> 1);
    key8[2] = ((key7[1] << 6) & 0xFF) | (key7[2] >> 2);
    key8[3] = ((key7[2] << 5) & 0xFF) | (key7[3] >> 3);
    key8[4] = ((key7[3] << 4) & 0xFF) | (key7[4] >> 4);
    key8[5] = ((key7[4] << 3) & 0xFF) | (key7[5] >> 5);
    key8[6] = ((key7[5] << 2) & 0xFF) | (key7[6] >> 6);
    key8[7] = key7[6] << 1;

    for (int i = 0; i < 8; i++)
    {
      key64[i] = (byte) setOddParity(key8[i]);
    }

    return key64;

  }



  /**
   * Create a LanMan hash from a clear-text password.
   *
   * @param password
   *          Clear-text password.
   * @return Hex string version of the hash based on the clear-text password.
   * @throws UnsupportedEncodingException
   *           if the <code>US-ASCII</code> coding is not available.
   * @throws NoSuchAlgorithmException
   *           if the algorithm does not exist for the used provider.
   * @throws InvalidKeyException
   *           if the key is inappropriate to initialize the cipher.
   * @throws NoSuchPaddingException
   *           if the padding scheme is not available.
   * @throws IllegalBlockSizeException
   *           if this encryption algorithm is unable to process the input data
   *           provided
   * @throws BadPaddingException
   *           if this cipher is in decryption mode, and (un)padding has been
   *           requested, but the decrypted data is not bounded by the
   *           appropriate padding bytes
   */
  private static String lmHash(final String password)
      throws UnsupportedEncodingException, NoSuchAlgorithmException,
      InvalidKeyException, NoSuchPaddingException, IllegalBlockSizeException,
      BadPaddingException
  {
    // Password has to be OEM encoded and in upper case
    final byte[] oemPass = password.toUpperCase().getBytes("US-ASCII");

    // It shouldn't be longer then 14 bytes
    int length = 14;
    if (oemPass.length < length)
    {
      length = oemPass.length;
    }

    // The password should be divided into two 7-byte keys
    final byte[] key1 = new byte[7];
    final byte[] key2 = new byte[7];
    if (length <= 7)
    {
      System.arraycopy(oemPass, 0, key1, 0, length);
    }
    else
    {
      System.arraycopy(oemPass, 0, key1, 0, 7);
      System.arraycopy(oemPass, 7, key2, 0, length - 7);
    }

    // We create two DES keys using key1 and key2 to on the magic string
    final SecretKey lowKey = new SecretKeySpec(addParity(key1), "DES");
    final SecretKey highKey = new SecretKeySpec(addParity(key2), "DES");
    final Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
    des.init(Cipher.ENCRYPT_MODE, lowKey);
    final byte[] lowHash = des.doFinal(MAGIC_STR.getBytes());
    des.init(Cipher.ENCRYPT_MODE, highKey);
    final byte[] highHash = des.doFinal(MAGIC_STR.getBytes());

    // We finally merge hashes and return them to the client

    final byte[] lmHash = new byte[16];
    System.arraycopy(lowHash, 0, lmHash, 0, 8);
    System.arraycopy(highHash, 0, lmHash, 8, 8);
    return toLowerCase(bytesToHexNoSpace(lmHash));
  }



  /**
   * Creates a NTLM hash from a clear-text password.
   *
   * @param password
   *          Clear text password.
   * @return Returns a NTLM hash.
   * @throws NoSuchProviderException
   *           if the BouncyCastle provider does not load
   * @throws NoSuchAlgorithmException
   *           if the MD4 algorithm is not found
   * @throws UnsupportedEncodingException
   *           if the encoding <code>UnicodeLittleUnmarked</code> is not
   *           supported.
   */
  private static String ntHash(final String password)
      throws NoSuchProviderException, UnsupportedEncodingException,
      NoSuchAlgorithmException
  {
    final byte[] unicodePassword = password.getBytes("UnicodeLittleUnmarked");
    final MessageDigest md4 = new MD4MessageDigest();
    return toLowerCase(bytesToHexNoSpace(md4.digest(unicodePassword)));
  }



  /**
   * Set the parity bit for an integer.
   *
   * @param integer
   *          to add the parity bit for.
   * @return integer with the parity bit set.
   */
  private static int setOddParity(final int parity)
  {
    final boolean hasEvenBits = (parity >>> 7 ^ parity >>> 6
                               ^ parity >>> 5 ^ parity >>> 4
                               ^ parity >>> 3 ^ parity >>> 2
                               ^ ((parity >>> 1) & 0x01)) == 0;
    if (hasEvenBits)
    {
      return parity | 0x01;
    }
    else
    {
      return parity & 0xFE;
    }
  }



  /**
   * Default constructor.
   */
  public SambaPasswordPlugin()
  {
    super();
  }



  /** {@inheritDoc} */
  @Override
  public ConfigChangeResult applyConfigurationChange(
      final SambaPasswordPluginCfg newConfig)
  {

    // No validation required and no restart required.
    config = newConfig;

    return new ConfigChangeResult();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostOperation doPostOperation(
      final PostOperationExtendedOperation extendedOperation)
  {
    /*
     * If the operation is not Password Modify Extended Operation then skip this
     * operation.
     */
    if (!extendedOperation.getRequestOID().equals(PWMOD_EXTOP_OID))
    {
      return PluginResult.PostOperation.continueOperationProcessing();
    }

    /*
     * If the operation has not been successful then ignore the operation.
     */
    if (extendedOperation.getResultCode() != ResultCode.SUCCESS)
    {
      return PluginResult.PostOperation.continueOperationProcessing();
    }

    /*
     * Verify if the operation has been initiated by what was defined as Samba
     * administrative user. If so, we will skip this operation to avoid double
     * synchronization of Samba attributes.
     */
    final DN authDN = extendedOperation.getAuthorizationDN();
    final DN sambaAdminDN = config.getSambaAdministratorDN();
    if (sambaAdminDN != null
        && !sambaAdminDN.isRootDN()
        && authDN.equals(sambaAdminDN))
    {
      if (logger.isTraceEnabled())
      {
        logger.trace("This operation will be skipped because"
            + " it was performed by Samba admin user: " + sambaAdminDN);
      }
      return PluginResult.PostOperation.continueOperationProcessing();
    }

    // Get the name of the entry and clear passwords from the operation
    // attachments.
    final DN dn = (DN) extendedOperation
        .getAttachment(PasswordModifyExtendedOperation.AUTHZ_DN_ATTACHMENT);
    if (dn == null)
    {
      // The attachment is missing which should never happen.
      if (logger.isTraceEnabled())
      {
        logger.trace("SambaPasswordPlugin: missing DN attachment");
      }
      return PluginResult.PostOperation.continueOperationProcessing();
    }

    final String password = extendedOperation.getAttachment(
        PasswordModifyExtendedOperation.CLEAR_PWD_ATTACHMENT).toString();
    if (password == null)
    {
      if (logger.isTraceEnabled())
      {
        logger.trace("SambaPasswordPlugin: skipping syncing "
            + "pre-encoded password");
      }
      return PluginResult.PostOperation.continueOperationProcessing();
    }

    @SuppressWarnings("unchecked")
    final List<ByteString> encPasswords = (List<ByteString>) extendedOperation
        .getAttachment(PasswordModifyExtendedOperation.ENCODED_PWD_ATTACHMENT);

    try
    {
      // Before proceeding make sure this entry has samba object class.
      final Entry entry = DirectoryServer.getEntry(dn);
      if (!isSynchronizable(entry))
      {
        if (logger.isTraceEnabled())
        {
          logger.trace("The entry is not Samba object.");
        }
        return PluginResult.PostOperation.continueOperationProcessing();
      }

      /*
       * Make an internal connection to process the password modification. It
       * will not trigger this plugin again with the pre-operation modify since
       * the password passed would be encoded hence the pre operation part would
       * skip it.
       */
      final InternalClientConnection connection = InternalClientConnection
          .getRootConnection();

      final List<Modification> modifications = getModifications(password);

      // Use an assertion control to avoid race conditions since extended
      // operation post-ops are done outside of the write lock.
      List<Control> controls = null;
      if (!encPasswords.isEmpty())
      {
        final AttributeType pwdAttribute = (AttributeType) extendedOperation
            .getAttachment(
                PasswordModifyExtendedOperation.PWD_ATTRIBUTE_ATTACHMENT);
        final LDAPFilter filter = RawFilter.createEqualityFilter(
            pwdAttribute.getNameOrOID(), encPasswords.get(0));
        final Control assertionControl = new LDAPAssertionRequestControl(true,
            filter);
        controls = Collections.singletonList(assertionControl);
      }

      final ModifyOperation modifyOperation = connection.processModify(dn,
          modifications, controls);

      if (logger.isTraceEnabled())
      {
        logger.trace("rc=%s", modifyOperation.getResultCode());
      }
    }
    catch (final DirectoryException e)
    {
      /*
       * This exception occurs if there is a problem while retrieving the entry.
       * This should never happen as we are processing the post-operation which
       * succeeded so the entry has to exist if we have reached this point.
       */
      logger.traceException(e);
    }

    return PluginResult.PostOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation doPreOperation(
      final PreOperationModifyOperation modifyOperation)
  {
    /*
     * If the passwords are changed in clear text they will be available with
     * the getNewPasswords() method. If they are encoded the method would return
     * null. The list of passwords should not be modified.
     */
    final List<ByteString> passwords = modifyOperation.getNewPasswords();

    /*
     * If the password list is not empty, we can be sure the current operation
     * is the one that applies to our case: - it's a modify operation on
     * userPassword attribute - it's replaces or adds new userPassword attribute
     * value. If it doesn't then we skip this modify operation.
     */
    if (passwords == null)
    {
      return PluginResult.PreOperation.continueOperationProcessing();
    }

    // Skip synchronization operations.
    if (modifyOperation.isSynchronizationOperation())
    {
      if (logger.isTraceEnabled())
      {
        logger.trace("Synchronization operation. Skipping.");
      }
      return PluginResult.PreOperation.continueOperationProcessing();
    }

    /*
     * Verify if the operation has been initiated by the Samba administrative
     * user. If so, we will skip this operation to avoid double synchronization
     * of Samba attributes.
     */
    final DN authDN = modifyOperation.getAuthorizationDN();
    final DN sambaAdminDN = config.getSambaAdministratorDN();
    if (sambaAdminDN != null
        && !sambaAdminDN.isRootDN()
        && authDN.equals(sambaAdminDN))
    {
      if (logger.isTraceEnabled())
      {
        logger.trace("This operation will be skipped because"
            + " it was performed by Samba admin user: " + sambaAdminDN);
      }
      return PluginResult.PreOperation.continueOperationProcessing();
    }

    /*
     * Before proceeding with the modification, we have to make sure this entry
     * is indeed a Samba object.
     */
    if (!isSynchronizable(modifyOperation.getCurrentEntry()))
    {
      if (logger.isTraceEnabled())
      {
        logger.trace("Skipping '%s' because it does not have Samba object class.", modifyOperation.getEntryDN());
      }
      return PluginResult.PreOperation.continueOperationProcessing();
    }

    /*
     * Proceed with processing: add a new modification to the current modify
     * operation, so they could be executed at the same time.
     */
    processModification(modifyOperation, passwords);

    // Continue plugin processing.
    return PluginResult.PreOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public void initializePlugin(final Set<PluginType> pluginTypes,
      final SambaPasswordPluginCfg configuration) throws ConfigException,
      InitializationException
  {

    // Verify config parameters.
    final LinkedList<LocalizableMessage> messages = new LinkedList<>();
    if (!isConfigurationAcceptable(configuration, messages))
    {
      for (final LocalizableMessage m : messages)
      {
        logger.error(m);
      }
      throw new ConfigException(messages.poll());
    }

    // Register the configuration change listener.
    configuration.addSambaPasswordChangeListener(this);

    // Save the configuration.
    this.config = configuration;
  }



  /**
   * Verifies if the plugin configuration is acceptable.
   *
   * @param configuration
   *          The plugin configuration.
   * @param unacceptableReasons
   *          Reasons why the configuration is not acceptable.
   * @return Returns <code>true</code> for the correct configuration and
   *         <code>false</code> for the incorrect one.
   */
  public boolean isConfigurationAcceptable(
      final SambaPasswordPluginCfg configuration,
      final List<LocalizableMessage> unacceptableReasons)
  {
    return isConfigurationChangeAcceptable(configuration, unacceptableReasons);
  }



  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationChangeAcceptable(
      final SambaPasswordPluginCfg newConfig, final List<LocalizableMessage> messages)
  {
    /*
     * The plugin implements only postoperationmodify and postoperationextended
     * plugin types. The rest should be rejected.
     */

    final SortedSet<PluginCfgDefn.PluginType> pluginTypes = newConfig
        .getPluginType();
    for (final PluginCfgDefn.PluginType t : pluginTypes)
    {
      switch (t)
      {
      case PREOPERATIONMODIFY:
      case POSTOPERATIONEXTENDED:
        break;
      default:
        messages.add(ERR_PLUGIN_SAMBA_SYNC_INVALID_PLUGIN_TYPE.get(t));
        return false;
      }
    }

    return true;
  }



  /**
   * Creates the modifications to modify Samba password attributes. It uses
   * clear-text password and encodes it with the appropriate algorithm for it's
   * respective type (NTLM or LanMan); then it wraps it in the modifications to
   * be added to the modify operation.
   *
   * @param password
   *          New password which is to be encoded for Samba.
   * @return Returns a list of modifications, or null if a problem occurs.
   */
  private List<Modification> getModifications(final String password)
  {
    ArrayList<Modification> modifications = new ArrayList<>();
    try
    {
      if (config.getPwdSyncPolicy().contains(PwdSyncPolicy.SYNC_NT_PASSWORD))
      {
        final Attribute attribute = Attributes.create(
            SAMBA_NT_PASSWORD_ATTRIBUTE_NAME, ntHash(password));
        modifications.add(new Modification(ModificationType.REPLACE, attribute));
      }

      if (config.getPwdSyncPolicy().contains(PwdSyncPolicy.SYNC_LM_PASSWORD))
      {
        final Attribute attribute = Attributes.create(
            SAMBA_LM_PASSWORD_ATTRIBUTE_NAME, lmHash(password));
        modifications.add(new Modification(ModificationType.REPLACE, attribute));
      }
      final Attribute pwdLastSet = Attributes.create(
        SAMBA_PWD_LAST_SET_NAME,
        String.valueOf(timeStampProvider.getCurrentTime()));
      modifications.add(new Modification(ModificationType.REPLACE, pwdLastSet));
    }
    catch (final Exception e)
    {
      logger.info(ERR_PLUGIN_SAMBA_SYNC_ENCODING, e.getMessage(), e);
      modifications = null;
    }

    return modifications;
  }



  /**
   * Verify if the target entry contains pre-defined Samba object class.
   *
   * @param entry
   *          The entry being modified.
   * @return Returns true if the entry has Samba object class, otherwise returns
   *         false.
   */
  private boolean isSynchronizable(final Entry entry)
  {
    final Schema schema = DirectoryServer.getSchema();
    final ObjectClass sambaOc = schema
        .getObjectClass(toLowerCase(SAMBA_SAM_ACCOUNT_OC_NAME));
    return sambaOc != null && entry.hasObjectClass(sambaOc);
  }



  /**
   * Adds modifications for the configured Samba password attributes to the
   * current modify operation.
   *
   * @param modifyOperation
   *          Current modify operation which will be modified to add Samba
   *          password attribute changes.
   * @param passwords
   *          List of userPassword clear-text attribute values to be hashed for
   *          Samba
   */
  private void processModification(
      final PreOperationModifyOperation modifyOperation,
      final List<ByteString> passwords)
  {
    // Get the last password (in case there is more then one).
    final String password = passwords.get(passwords.size() - 1).toString();
    try
    {
      // Generate the necessary modifications.
      for (final Modification modification : getModifications(password))
      {
        modifyOperation.addModification(modification);
      }
    }
    catch (final DirectoryException e)
    {
      logger.info(ERR_PLUGIN_SAMBA_SYNC_MODIFICATION_PROCESSING, e.getMessage(), e);
    }
  }

  /**
   * Timestamp provider interface. Intended primarily for testing purposes.
   */
  static interface TimeStampProvider
  {
    /**
     * Generates a custom time stamp.
     *
     * @return  A timestamp in UNIX format.
     */
    long getCurrentTime();
  }

  /**
   * Use custom timestamp provider. Intended primarily for testing purposes.
   *
   * @param timeStampProvider Provider object that implements the
   * TimeStampProvider interface.
   */
  void setTimeStampProvider(TimeStampProvider timeStampProvider)
  {
    this.timeStampProvider = (timeStampProvider == null)
                             ? DEFAULT_TIMESTAMP_PROVIDER : timeStampProvider;
  }
}