/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.ambari.server.serveraction.kerberos;

import org.apache.ambari.server.utils.ShellCommandUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory;
import org.apache.directory.server.kerberos.shared.keytab.Keytab;
import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry;
import org.apache.directory.shared.kerberos.KerberosTime;
import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
import org.apache.directory.shared.kerberos.components.EncryptionKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * KerberosOperationHandler is an abstract class providing basic implementations of common Kerberos
 * operations (like generating secure passwords) and placeholders for KDC-specific operations
 * (such as creating principals).
 */
public abstract class KerberosOperationHandler {
  private final static Logger LOG = LoggerFactory.getLogger(KerberosOperationHandler.class);

  private final static SecureRandom SECURE_RANDOM = new SecureRandom();

  /**
   * The number of characters to generate for a secure password
   */
  protected final static int SECURE_PASSWORD_LENGTH = 18;

  /**
   * Kerberos-env configuration property name: ldap_url
   */
  public final static String KERBEROS_ENV_LDAP_URL = "ldap_url";

  /**
   * Kerberos-env configuration property name: container_dn
   */
  public final static String KERBEROS_ENV_PRINCIPAL_CONTAINER_DN = "container_dn";

  /**
   * Kerberos-env configuration property name: create_attributes_template
   */
  public final static String KERBEROS_ENV_CREATE_ATTRIBUTES_TEMPLATE = "create_attributes_template";

  /**
   * Kerberos-env configuration property name: encryption_types
   */
  public final static String KERBEROS_ENV_ENCRYPTION_TYPES = "encryption_types";

  /**
   * Kerberos-env configuration property name: kdc_host
   */
  public final static String KERBEROS_ENV_KDC_HOST = "kdc_host";

  /**
   * Kerberos-env configuration property name: admin_server_host
   */
  public final static String KERBEROS_ENV_ADMIN_SERVER_HOST = "admin_server_host";

  /**
   * The set of available characters to use when generating a secure password
   */
  private final static char[] SECURE_PASSWORD_CHARS =
      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890?.!$%^*()-_+=~".toCharArray();

  /**
   * A Map of MIT KDC Encryption types to EncryptionType values.
   * <p/>
   * See http://web.mit.edu/kerberos/krb5-devel/doc/admin/conf_files/kdc_conf.html#encryption-types
   */
  private static final Map<String, Set<EncryptionType>> ENCRYPTION_TYPE_TRANSLATION_MAP = Collections.unmodifiableMap(
      new HashMap<String, Set<EncryptionType>>() {
        {
          // aes: The AES family: aes256-cts-hmac-sha1-96 and aes128-cts-hmac-sha1-96
          put("aes", EnumSet.of(EncryptionType.AES256_CTS_HMAC_SHA1_96, EncryptionType.AES128_CTS_HMAC_SHA1_96));

          // aes256-cts-hmac-sha1-96 aes256-cts:  AES-256	CTS mode with 96-bit SHA-1 HMAC
          put("aes256-cts-hmac-sha1-96", EnumSet.of(EncryptionType.AES256_CTS_HMAC_SHA1_96));
          put("aes256-cts", EnumSet.of(EncryptionType.AES256_CTS_HMAC_SHA1_96));
          put("aes-256", EnumSet.of(EncryptionType.AES256_CTS_HMAC_SHA1_96));

          // aes128-cts-hmac-sha1-96 aes128-cts AES-128:	CTS mode with 96-bit SHA-1 HMAC
          put("aes128-cts-hmac-sha1-96", EnumSet.of(EncryptionType.AES128_CTS_HMAC_SHA1_96));
          put("aes128-cts", EnumSet.of(EncryptionType.AES128_CTS_HMAC_SHA1_96));
          put("aes-128", EnumSet.of(EncryptionType.AES128_CTS_HMAC_SHA1_96));

          // rc4	The RC4 family: arcfour-hmac
          put("rc4", EnumSet.of(EncryptionType.RC4_HMAC));

          // arcfour-hmac rc4-hmac arcfour-hmac-md5:	RC4 with HMAC/MD5
          put("arcfour-hmac", EnumSet.of(EncryptionType.RC4_HMAC));
          put("rc4-hmac", EnumSet.of(EncryptionType.RC4_HMAC));
          put("arcfour-hmac-md5", EnumSet.of(EncryptionType.UNKNOWN));

          // arcfour-hmac-exp rc4-hmac-exp arcfour-hmac-md5-exp:	Exportable RC4 with HMAC/MD5 (weak)
          put("arcfour-hmac-exp", EnumSet.of(EncryptionType.RC4_HMAC_EXP));
          put("rc4-hmac-exp", EnumSet.of(EncryptionType.RC4_HMAC_EXP));
          put("arcfour-hmac-md5-exp", EnumSet.of(EncryptionType.UNKNOWN));

          // camellia 	The Camellia family: camellia256-cts-cmac and camellia128-cts-cmac
          put("camellia", EnumSet.of(EncryptionType.UNKNOWN));

          // camellia256-cts-cmac camellia256-cts:	Camellia-256 CTS mode with CMAC
          put("camellia256-cts-cmac", EnumSet.of(EncryptionType.UNKNOWN));
          put("camellia256-cts", EnumSet.of(EncryptionType.UNKNOWN));

          // camellia128-cts-cmac camellia128-cts:	Camellia-128 CTS mode with CMAC
          put("camellia128-cts-cmac", EnumSet.of(EncryptionType.UNKNOWN));
          put("camellia128-cts", EnumSet.of(EncryptionType.UNKNOWN));

          //des:	The DES family: des-cbc-crc, des-cbc-md5, and des-cbc-md4 (weak)
          put("des", EnumSet.of(EncryptionType.DES_CBC_CRC, EncryptionType.DES_CBC_MD5, EncryptionType.DES_CBC_MD4));

          // des-cbc-md4: DES cbc mode with RSA-MD4 (weak)
          put("des-cbc-md4", EnumSet.of(EncryptionType.DES_CBC_MD4));

          // des-cbc-md5:	DES cbc mode with RSA-MD5 (weak)
          put("des-cbc-md5", EnumSet.of(EncryptionType.DES_CBC_MD5));

          // des-cbc-crc:	DES cbc mode with CRC-32 (weak)
          put("des-cbc-crc", EnumSet.of(EncryptionType.DES_CBC_CRC));

          // des-cbc-raw: DES cbc mode raw (weak)
          put("des-cbc-raw", EnumSet.of(EncryptionType.UNKNOWN));

          // des-hmac-sha1: DES with HMAC/sha1 (weak)
          put("des-hmac-sha1", EnumSet.of(EncryptionType.UNKNOWN));

          // des3:	The triple DES family: des3-cbc-sha1
          put("des3", EnumSet.of(EncryptionType.DES3_CBC_SHA1_KD)); // Using DES3_CBC_SHA1_KD since DES3_CBC_SHA1 invalid key issues with KDC

          // des3-cbc-raw:	Triple DES cbc mode raw (weak)
          put("des3-cbc-raw", EnumSet.of(EncryptionType.UNKNOWN));

          // des3-cbc-sha1 des3-hmac-sha1 des3-cbc-sha1-kd:	Triple DES cbc mode with HMAC/sha1
          put("des3-cbc-sha1", EnumSet.of(EncryptionType.DES3_CBC_SHA1_KD)); // Using DES3_CBC_SHA1_KD since DES3_CBC_SHA1 invalid key issues with KDC
          put("des3-hmac-sha1", EnumSet.of(EncryptionType.UNKNOWN));
          put("des3-cbc-sha1-kd", EnumSet.of(EncryptionType.DES3_CBC_SHA1_KD));


        }
      }
  );

  /**
   * The default set of ciphers to use for creating keytab entries
   */
  private static final Set<EncryptionType> DEFAULT_CIPHERS = Collections.unmodifiableSet(
      new HashSet<EncryptionType>() {{
        add(EncryptionType.DES_CBC_MD5);
        add(EncryptionType.DES3_CBC_SHA1_KD);
        add(EncryptionType.RC4_HMAC);
        add(EncryptionType.AES128_CTS_HMAC_SHA1_96);
        add(EncryptionType.AES256_CTS_HMAC_SHA1_96);
      }});

  private KerberosCredential administratorCredentials = null;
  private String defaultRealm = null;
  private Set<EncryptionType> keyEncryptionTypes = new HashSet<EncryptionType>(DEFAULT_CIPHERS);
  private boolean open = false;

  /**
   * Create a secure (random) password using a secure random number generator and a set of (reasonable)
   * characters.
   *
   * @return a String containing the new password
   */
  public String createSecurePassword() {
    return createSecurePassword(SECURE_PASSWORD_LENGTH);
  }

  /**
   * Create a secure (random) password using a secure random number generator and a set of (reasonable)
   * characters.
   *
   * @param length an integer value declaring the length of the password to create,
   *               if <1, a default will be used.
   * @return a String containing the new password
   */
  public String createSecurePassword(int length) {
    StringBuilder passwordBuilder;

    // If the supplied length is less than 1 use the default value.
    if (length < 1) {
      length = SECURE_PASSWORD_LENGTH;
    }

    // Create a new StringBuilder and ensure its capacity is set for the length of the password to
    // be generated
    passwordBuilder = new StringBuilder(length);

    // For each character to be added to the password, (securely) generate a random number to pull
    // a random character from the character array
    for (int i = 0; i < length; i++) {
      passwordBuilder.append(SECURE_PASSWORD_CHARS[SECURE_RANDOM.nextInt(SECURE_PASSWORD_CHARS.length)]);
    }

    return passwordBuilder.toString();
  }

  /**
   * Prepares and creates resources to be used by this KerberosOperationHandler.
   * Implementation in this class is ignoring parameters ldapUrl and principalContainerDn and delegate to
   * <code>open(KerberosCredential administratorCredentials, String defaultRealm)</code>
   * Subclasses that want to use these parameters need to override this method.
   * <p/>
   * It is expected that this KerberosOperationHandler will not be used before this call.
   *
   * @param administratorCredentials a KerberosCredential containing the administrative credentials
   *                                 for the relevant KDC
   * @param defaultRealm             a String declaring the default Kerberos realm (or domain)
   * @param kerberosConfiguration    a Map of key/value pairs containing data from the kerberos-env configuration set
   */
  public abstract void open(KerberosCredential administratorCredentials, String defaultRealm, Map<String, String> kerberosConfiguration)
      throws KerberosOperationException;

  /**
   * Closes and cleans up any resources used by this KerberosOperationHandler
   * <p/>
   * It is expected that this KerberosOperationHandler will not be used after this call.
   */
  public abstract void close()
      throws KerberosOperationException;

  /**
   * Test to see if the specified principal exists in a previously configured KDC
   * <p/>
   * The implementation is specific to a particular type of KDC.
   *
   * @param principal a String containing the principal to test
   * @return true if the principal exists; false otherwise
   * @throws KerberosOperationException
   */
  public abstract boolean principalExists(String principal)
      throws KerberosOperationException;

  /**
   * Creates a new principal in a previously configured KDC
   * <p/>
   * The implementation is specific to a particular type of KDC.
   *
   * @param principal a String containing the principal to add
   * @param password  a String containing the password to use when creating the principal
   * @param service   a boolean value indicating whether the principal is to be created as a service principal or not
   * @return an Integer declaring the generated key number
   * @throws KerberosOperationException
   */
  public abstract Integer createPrincipal(String principal, String password, boolean service)
      throws KerberosOperationException;

  /**
   * Updates the password for an existing principal in a previously configured KDC
   * <p/>
   * The implementation is specific to a particular type of KDC.
   *
   * @param principal a String containing the principal to update
   * @param password  a String containing the password to set
   * @return an Integer declaring the new key number
   * @throws KerberosOperationException
   */
  public abstract Integer setPrincipalPassword(String principal, String password)
      throws KerberosOperationException;

  /**
   * Removes an existing principal in a previously configured KDC
   * <p/>
   * The implementation is specific to a particular type of KDC.
   *
   * @param principal a String containing the principal to remove
   * @return true if the principal was successfully removed; otherwise false
   * @throws KerberosOperationException
   */
  public abstract boolean removePrincipal(String principal)
      throws KerberosOperationException;

  /**
   * Tests to ensure the connection information and credentials allow for administrative
   * connectivity to the KDC
   *
   * @return true of successful; otherwise false
   * @throws KerberosOperationException if a failure occurs while testing the
   *                                    administrator credentials
   */
  public boolean testAdministratorCredentials() throws KerberosOperationException {
    if (!isOpen()) {
      throw new KerberosOperationException("This operation handler has not been opened");
    }

    KerberosCredential credentials = getAdministratorCredentials();
    if (credentials == null) {
      throw new KerberosOperationException("Missing KDC administrator credentials");
    } else {
      return principalExists(credentials.getPrincipal());
    }
  }

  /**
   * Create a keytab using the specified principal and password.
   *
   * @param principal a String containing the principal to test
   * @param password  a String containing the password to use when creating the principal
   * @param keyNumber a Integer indicating the key number for the keytab entries
   * @return the created Keytab
   * @throws KerberosOperationException
   */
  protected Keytab createKeytab(String principal, String password, Integer keyNumber)
      throws KerberosOperationException {

    if ((principal == null) || principal.isEmpty()) {
      throw new KerberosOperationException("Failed to create keytab file, missing principal");
    }

    if (password == null) {
      throw new KerberosOperationException(String.format("Failed to create keytab file for %s, missing password", principal));
    }

    Set<EncryptionType> ciphers = new HashSet<EncryptionType>(keyEncryptionTypes);
    List<KeytabEntry> keytabEntries = new ArrayList<KeytabEntry>();
    Keytab keytab = new Keytab();


    if (!ciphers.isEmpty()) {
      // Create a set of keys and relevant keytab entries
      Map<EncryptionType, EncryptionKey> keys = KerberosKeyFactory.getKerberosKeys(principal, password, ciphers);

      if (keys != null) {
        byte keyVersion = (keyNumber == null) ? 0 : keyNumber.byteValue();
        KerberosTime timestamp = new KerberosTime();

        for (EncryptionKey encryptionKey : keys.values()) {
          keytabEntries.add(new KeytabEntry(principal, 1, timestamp, keyVersion, encryptionKey));
        }

        keytab.setEntries(keytabEntries);
      }
    }

    return keytab;
  }

  /**
   * Create or append to a keytab file using keytab data from another keytab file.
   * <p/>
   * If the destination keytab file contains keytab data, that data will be merged with the new data
   * to create a composite set of keytab entries.
   *
   * @param sourceKeytabFile      a File containing the absolute path to the file with the keytab data to store
   * @param destinationKeytabFile a File containing the absolute path to where the keytab data is to be stored
   * @return true if the keytab file was successfully created; false otherwise
   * @throws KerberosOperationException
   * @see #createKeytabFile(org.apache.directory.server.kerberos.shared.keytab.Keytab, java.io.File)
   */
  protected boolean createKeytabFile(File sourceKeytabFile, File destinationKeytabFile)
      throws KerberosOperationException {
    return createKeytabFile(readKeytabFile(sourceKeytabFile), destinationKeytabFile);
  }

  /**
   * Create or append to a keytab file using the specified principal and password.
   * <p/>
   * If the destination keytab file contains keytab data, that data will be merged with the new data
   * to create a composite set of keytab entries.
   *
   * @param principal             a String containing the principal to test
   * @param password              a String containing the password to use when creating the principal
   * @param keyNumber             an Integer declaring the relevant key number to use for the keytabs entries
   * @param destinationKeytabFile a File containing the absolute path to where the keytab data is to be stored
   * @return true if the keytab file was successfully created; false otherwise
   * @throws KerberosOperationException
   * @see #createKeytabFile(org.apache.directory.server.kerberos.shared.keytab.Keytab, java.io.File)
   */
  protected boolean createKeytabFile(String principal, String password, Integer keyNumber, File destinationKeytabFile)
      throws KerberosOperationException {
    return createKeytabFile(createKeytab(principal, password, keyNumber), destinationKeytabFile);
  }

  /**
   * Create or append to a keytab file using the specified Keytab
   * <p/>
   * If the destination keytab file contains keytab data, that data will be merged with the new data
   * to create a composite set of keytab entries.
   *
   * @param keytab                the Keytab containing the data to add to the keytab file
   * @param destinationKeytabFile a File containing the absolute path to where the keytab data is to be stored
   * @return true if the keytab file was successfully created; false otherwise
   * @throws KerberosOperationException
   */
  protected boolean createKeytabFile(Keytab keytab, File destinationKeytabFile)
      throws KerberosOperationException {

    if (destinationKeytabFile == null) {
      throw new KerberosOperationException("The destination file path is null");
    }

    try {
      mergeKeytabs(readKeytabFile(destinationKeytabFile), keytab).write(destinationKeytabFile);
      return true;
    } catch (IOException e) {
      String message = "Failed to export keytab file";
      LOG.error(message, e);

      if (!destinationKeytabFile.delete()) {
        destinationKeytabFile.deleteOnExit();
      }

      throw new KerberosOperationException(message, e);
    }
  }

  /**
   * Merge the keytab data from one keytab with the keytab data from a different keytab.
   * <p/>
   * If similar key entries exist for the same principal, the updated values will be used
   *
   * @param keytab  a Keytab with the base keytab data
   * @param updates a Keytab containing the updated keytab data
   * @return a Keytab with the merged data
   */
  protected Keytab mergeKeytabs(Keytab keytab, Keytab updates) {
    List<KeytabEntry> keytabEntries = (keytab == null)
        ? Collections.<KeytabEntry>emptyList()
        : new ArrayList<KeytabEntry>(keytab.getEntries());
    List<KeytabEntry> updateEntries = (updates == null)
        ? Collections.<KeytabEntry>emptyList()
        : new ArrayList<KeytabEntry>(updates.getEntries());
    List<KeytabEntry> mergedEntries = new ArrayList<KeytabEntry>();

    if (keytabEntries.isEmpty()) {
      mergedEntries.addAll(updateEntries);
    } else if (updateEntries.isEmpty()) {
      mergedEntries.addAll(keytabEntries);
    } else {
      Iterator<KeytabEntry> iterator = keytabEntries.iterator();

      while (iterator.hasNext()) {
        KeytabEntry keytabEntry = iterator.next();

        for (KeytabEntry entry : updateEntries) {
          if (entry.getPrincipalName().equals(keytabEntry.getPrincipalName()) &&
              entry.getKey().getKeyType().equals(keytabEntry.getKey().getKeyType())) {
            iterator.remove();
            break;
          }
        }
      }

      mergedEntries.addAll(keytabEntries);
      mergedEntries.addAll(updateEntries);
    }

    Keytab mergedKeytab = new Keytab();
    mergedKeytab.setEntries(mergedEntries);
    return mergedKeytab;
  }

  /**
   * Reads a file containing keytab data into a new Keytab
   *
   * @param file A File containing the path to the file from which to read keytab data
   * @return a Keytab or null if the file was not readable
   */
  protected Keytab readKeytabFile(File file) {
    Keytab keytab;

    if (file.exists() && file.canRead() && (file.length() > 0)) {
      try {
        keytab = Keytab.read(file);
      } catch (IOException e) {
        // There was an issue reading in the existing keytab file... quietly assume no data
        keytab = null;
      }
    } else {
      keytab = null;
    }

    return keytab;
  }

  public KerberosCredential getAdministratorCredentials() {
    return administratorCredentials;
  }

  /**
   * Sets the administrator credentials for this KerberosOperationHandler.
   * <p/>
   * If the supplied {@link KerberosCredential} is not <code>null</code>, validates that the administrator
   * principal is not <code>null</code> or empty and that either the password or the keytab value
   * is not <code>null</code> or empty. If the credential value does not validate, then a
   * {@link KerberosAdminAuthenticationException} will be thrown.
   *
   * @param administratorCredentials the relevant KerberosCredential
   * @throws KerberosAdminAuthenticationException if the non-null KerberosCredential fails contain
   *                                              a non-empty principal and a non-empty password or
   *                                              keytab value.
   */
  public void setAdministratorCredentials(KerberosCredential administratorCredentials)
      throws KerberosAdminAuthenticationException {

    // Ensure the KerberosCredential is not null
    if (administratorCredentials == null) {
      throw new KerberosAdminAuthenticationException("The administrator credential must not be null");
    }

    String value;

    // Ensure the principal is not null or empty
    value = administratorCredentials.getPrincipal();
    if ((value == null) || value.isEmpty()) {
      throw new KerberosAdminAuthenticationException("Must specify a principal but it is null or empty");
    }

    // Ensure either the password or the keytab value is not null or empty
    value = administratorCredentials.getPassword();
    if ((value == null) || value.isEmpty()) {
      value = administratorCredentials.getKeytab();

      if ((value == null) || value.isEmpty()) {
        throw new KerberosAdminAuthenticationException("Must specify either a password or a keytab but both are null or empty");
      }
    }

    this.administratorCredentials = administratorCredentials;
  }

  public String getDefaultRealm() {
    return defaultRealm;
  }

  public void setDefaultRealm(String defaultRealm) {
    this.defaultRealm = defaultRealm;
  }

  /**
   * Gets the encryption algorithms used to encrypt keys in keytab entries
   *
   * @return a Set of EncryptionKey values indicating which algorithms are to be used when
   * encrypting keys for keytab entries.
   */
  public Set<EncryptionType> getKeyEncryptionTypes() {
    return keyEncryptionTypes;
  }

  /**
   * Sets the encryption algorithms to use to encrypt keys in keytab entries
   * <p/>
   * If set to <code>null</code> the default set of ciphers will be used.  See {@link #DEFAULT_CIPHERS}
   *
   * @param keyEncryptionTypes a Set of EncryptionKey values or null to indicate the default set
   */
  public void setKeyEncryptionTypes(Set<EncryptionType> keyEncryptionTypes) {
    this.keyEncryptionTypes = new HashSet<EncryptionType>(
        (keyEncryptionTypes == null)
            ? DEFAULT_CIPHERS
            : keyEncryptionTypes
    );
  }

  /**
   * Test this KerberosOperationHandler to see whether is was previously open or not
   *
   * @return a boolean value indicating whether this KerberosOperationHandler was open (true) or not (false)
   */
  public boolean isOpen() {
    return open;
  }

  /**
   * Sets whether this KerberosOperationHandler is open or not.
   *
   * @param open a boolean value indicating whether this KerberosOperationHandler was open (true) or not (false)
   */
  public void setOpen(boolean open) {
    this.open = open;
  }

  /**
   * Given base64-encoded keytab data, decode the String to binary data and write it to a (temporary)
   * file.
   * <p/>
   * Upon success, a new file is created.  The caller is expected to clean up this file when done
   * with it.
   *
   * @param keytabData a String containing base64-encoded keytab data
   * @return a File pointing to the decoded keytab file or null if not successful
   * @throws KerberosOperationException
   */
  protected File createKeytabFile(String keytabData)
      throws KerberosOperationException {
    boolean success = false;
    File tempFile = null;

    // Create a temporary file
    try {
      tempFile = File.createTempFile("temp", ".dat");
    } catch (IOException e) {
      LOG.error(String.format("Failed to create temporary keytab file: %s", e.getLocalizedMessage()), e);
    }

    if ((tempFile != null) && (keytabData != null)) {
      OutputStream fos = null;

      // Decoded the base64-encoded String and write it to the temporary file
      try {
        fos = new FileOutputStream(tempFile);
        fos.write(Base64.decodeBase64(keytabData));
        success = true;
      } catch (FileNotFoundException e) {
        String message = String.format("Failed to write to temporary keytab file %s: %s",
            tempFile.getAbsolutePath(), e.getLocalizedMessage());
        LOG.error(message, e);
        throw new KerberosOperationException(message, e);
      } catch (IOException e) {
        String message = String.format("Failed to write to temporary keytab file %s: %s",
            tempFile.getAbsolutePath(), e.getLocalizedMessage());
        LOG.error(message, e);
        throw new KerberosOperationException(message, e);
      } finally {
        if (fos != null) {
          try {
            fos.close();
          } catch (IOException e) {
            // Ignore this...
          }
        }

        // If there was an issue, clean up the file
        if (!success) {
          if (!tempFile.delete()) {
            tempFile.deleteOnExit();
          }

          tempFile = null;
        }
      }
    }

    return tempFile;
  }

  /**
   * Executes a shell command.
   * <p/>
   * See {@link org.apache.ambari.server.utils.ShellCommandUtil#runCommand(String[])}
   *
   * @param command an array of String value representing the command and its arguments
   * @return a ShellCommandUtil.Result declaring the result of the operation
   * @throws KerberosOperationException
   */
  protected ShellCommandUtil.Result executeCommand(String[] command)
      throws KerberosOperationException {

    if ((command == null) || (command.length == 0)) {
      return null;
    } else {
      try {
        return ShellCommandUtil.runCommand(command);
      } catch (IOException e) {
        String message = String.format("Failed to execute the command: %s", e.getLocalizedMessage());
        LOG.error(message, e);
        throw new KerberosOperationException(message, e);
      } catch (InterruptedException e) {
        String message = String.format("Failed to wait for the command to complete: %s", e.getLocalizedMessage());
        LOG.error(message, e);
        throw new KerberosOperationException(message, e);
      }
    }
  }

  /**
   * Given a principal, attempt to create a new DeconstructedPrincipal
   *
   * @param principal a String containing the principal to deconstruct
   * @return a DeconstructedPrincipal
   * @throws KerberosOperationException
   */
  protected DeconstructedPrincipal createDeconstructPrincipal(String principal) throws KerberosOperationException {
    try {
      return DeconstructedPrincipal.valueOf(principal, getDefaultRealm());
    } catch (IllegalArgumentException e) {
      throw new KerberosOperationException(e.getMessage(), e);
    }
  }

  /**
   * Given a cipher (or algorithm) name, attempts to translate it into an EncryptionType value.
   * <p/>
   * If a translation is not able to be made, {@link org.apache.directory.shared.kerberos.codec.types.EncryptionType#UNKNOWN}
   * is returned.
   *
   * @param name a String containing the name of the cipher to translate
   * @return an EncryptionType
   */
  protected Set<EncryptionType> translateEncryptionType(String name) {
    Set<EncryptionType> encryptionTypes = null;

    if ((name != null) && !name.isEmpty()) {
      encryptionTypes = ENCRYPTION_TYPE_TRANSLATION_MAP.get(name.toLowerCase());
    }

    return (encryptionTypes == null) ? Collections.<EncryptionType>emptySet() : encryptionTypes;
  }

  /**
   * Given a delimited set of encryption type names, attempts to translate into a set of EncryptionType
   * values.
   *
   * @param names     a String containing a delimited list of encryption type names
   * @param delimiter a String declaring the delimiter to use to split names, if null, " " is used.
   * @return a Set of EncryptionType values
   */
  protected Set<EncryptionType> translateEncryptionTypes(String names, String delimiter) {
    Set<EncryptionType> encryptionTypes = new HashSet<EncryptionType>();

    if ((names != null) && !names.isEmpty()) {
      for (String name : names.split((delimiter == null) ? "\\s+" : delimiter)) {
        encryptionTypes.addAll(translateEncryptionType(name.trim()));
      }
    }

    return encryptionTypes;
  }

  /**
   * Iterates through the characters in a string to escape special characters
   *
   * @param string             the String to process
   * @param charactersToEscape a Set of characters declaring the special characters to escape
   * @param escapeCharacter    a character to use for escaping
   * @return the string with escaped characters
   */
  protected String escapeCharacters(String string, Set<Character> charactersToEscape, Character escapeCharacter) {
    if ((string == null) || string.isEmpty() || (charactersToEscape == null) || charactersToEscape.isEmpty()) {
      return string;
    } else {
      StringBuilder builder = new StringBuilder();

      for (char character : string.toCharArray()) {
        if (charactersToEscape.contains(character)) {
          builder.append(escapeCharacter);
        }
        builder.append(character);
      }

      return builder.toString();
    }
  }
}
