content: /*
   * 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
   * <p/>
   * http://www.apache.org/licenses/LICENSE-2.0
   * <p/>
   * 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.security.encryption;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.nio.ByteBuffer;
  import java.nio.CharBuffer;
  import java.nio.charset.Charset;
  import java.security.Key;
  import java.security.KeyStore;
  import java.security.KeyStoreException;
  import java.security.NoSuchAlgorithmException;
  import java.security.UnrecoverableKeyException;
  import java.security.cert.CertificateException;
  import java.util.Arrays;
  import java.util.Enumeration;
  import java.util.HashSet;
  import java.util.Set;
  import java.util.concurrent.locks.Lock;
  import java.util.concurrent.locks.ReentrantLock;
  
  import javax.crypto.spec.SecretKeySpec;
  
  import org.apache.ambari.server.AmbariException;
  import org.apache.ambari.server.security.credential.Credential;
  import org.apache.ambari.server.security.credential.CredentialFactory;
  
  /**
   * AbstractCredentialStore is an abstract implementation of CredentialStore that loads and
   * stores @{link KeyStore} data. Implementations of this class, provide the input and output streams
   * used to read and write the data.
   */
  public abstract class AbstractCredentialStore implements CredentialStore {
    protected static final String DEFAULT_STORE_TYPE = "JCEKS";
  
    /**
     * A lock object to lock access to the credential store, allowing only one thread to access
     * the credential store at a time.
     */
    private final Lock lock = new ReentrantLock();
  
    /**
     * The MasterKeyService containing the key used to encrypt the KeyStore data
     */
    private MasterKeyService masterKeyService;
  
    /**
     * Adds a new credential to this CredentialStore
     * <p/>
     * The supplied key will be converted into UTF-8 bytes before being stored.
     * <p/>
     * This implementation is thread-safe, allowing one thread at a time to access the credential store.
     *
     * @param alias      a string declaring the alias (or name) of the credential
     * @param credential the credential to store
     * @throws AmbariException if an error occurs while storing the new credential
     */
    @Override
    public void addCredential(String alias, Credential credential) throws AmbariException {
      if ((alias == null) || alias.isEmpty()) {
        throw new IllegalArgumentException("Alias cannot be null or empty.");
      }
  
      lock.lock();
      try {
        KeyStore ks = loadCredentialStore();
        addCredential(ks, alias, credential);
        persistCredentialStore(ks);
      } finally {
        lock.unlock();
      }
    }
  
    /**
     * Retrieves the specified credential from this CredentialStore
     * <p/>
     * This implementation is thread-safe, allowing one thread at a time to access the credential store.
     *
     * @param alias a string declaring the alias (or name) of the credential
     * @return a Credential or null of not found
     * @throws AmbariException if an error occurs while retrieving the new credential
     */
    @Override
    public Credential getCredential(String alias) throws AmbariException {
      if (alias == null) {
        return null;
      } else {
        lock.lock();
        try {
          return getCredential(loadCredentialStore(), alias);
        } finally {
          lock.unlock();
        }
      }
    }
  
    /**
     * Removes the specified credential from this CredentialStore
     * <p/>
     * This implementation is thread-safe, allowing one thread at a time to access the credential store.
     *
     * @param alias a string declaring the alias (or name) of the credential
     * @throws AmbariException if an error occurs while removing the new credential
     */
    @Override
    public void removeCredential(String alias) throws AmbariException {
      if ((alias != null) && !alias.isEmpty()) {
        lock.lock();
        try {
          KeyStore ks = loadCredentialStore();
          if (ks != null) {
            try {
              ks.deleteEntry(alias);
              persistCredentialStore(ks);
            } catch (KeyStoreException e) {
              throw new AmbariException("Failed to delete the KeyStore entry - the key store may not have been initialized", e);
            }
          }
        } finally {
          lock.unlock();
        }
      }
    }
  
    /**
     * Returns a list of the alias names for the credentials stored in the CredentialStore
     * <p/>
     * This implementation is thread-safe, allowing one thread at a time to access the credential store.
     *
     * @return a Set of Strings representing alias names for the credentials stored in the CredentialStore
     * @throws AmbariException if an error occurs while searching forthe credential
     */
    @Override
    public Set<String> listCredentials() throws AmbariException {
      Set<String> credentials = null;
  
      lock.lock();
      try {
        KeyStore ks = loadCredentialStore();
        if (ks != null) {
          try {
            Enumeration<String> aliases = ks.aliases();
  
            if (aliases != null) {
              credentials = new HashSet<>();
              while (aliases.hasMoreElements()) {
                credentials.add(aliases.nextElement());
              }
            }
          } catch (KeyStoreException e) {
            throw new AmbariException("Failed to read KeyStore - the key store may not have been initialized", e);
          }
        }
      } finally {
        lock.unlock();
      }
  
      return credentials;
    }
  
    /**
     * Tests this CredentialStore for the existence of a credential with the specified alias
     * <p/>
     * This implementation is thread-safe, allowing one thread at a time to access the credential store.
     *
     * @param alias a string declaring the alias (or name) of the credential
     * @return true if the alias exists; otherwise false
     * @throws AmbariException if an error occurs while searching forthe credential
     */
    @Override
    public boolean containsCredential(String alias) throws AmbariException {
      boolean exists = false;
  
      if ((alias != null) && !alias.isEmpty()) {
        lock.lock();
        try {
          KeyStore ks = loadCredentialStore();
          if (ks != null) {
            try {
              exists = ks.containsAlias(alias);
            } catch (KeyStoreException e) {
              throw new AmbariException("Failed to search the KeyStore for the requested entry - the key store may not have been initialized", e);
            }
          }
        } finally {
          lock.unlock();
        }
      }
  
      return exists;
    }
  
    @Override
    public void setMasterKeyService(MasterKeyService masterKeyService) {
      this.masterKeyService = masterKeyService;
    }
  
    /**
     * Adds a new credential to the supplied KeyStore
     * <p/>
     * The supplied key will be converted into UTF-8 bytes before being stored.
     *
     * @param keyStore   the KeyStore
     * @param alias      a string declaring the alias (or name) of the credential
     * @param credential the credential to store
     * @throws AmbariException if an error occurs while storing the new credential
     */
    protected void addCredential(KeyStore keyStore, String alias, Credential credential) throws AmbariException {
      if (keyStore != null) {
        try {
          Key key;
          char[] value = (credential == null) ? null : credential.toValue();
  
          if ((value == null) || (value.length == 0)) {
            key = null;
          } else {
            key = new SecretKeySpec(toBytes(value), "AES");
          }
  
          keyStore.setKeyEntry(alias, key, masterKeyService.getMasterSecret(), null);
        } catch (KeyStoreException e) {
          throw new AmbariException("The key store has not been initialized", e);
        }
      }
    }
  
    /**
     * Retrieves the specified credential from a KeyStore
     *
     * @param keyStore the KeyStore
     * @param alias    a string declaring the alias (or name) of the credential
     * @return an array of chars containing the credential
     * @throws AmbariException if an error occurs while retrieving the new credential
     */
    protected Credential getCredential(KeyStore keyStore, String alias) throws AmbariException {
      char[] value = null;
  
      if (keyStore != null) {
        try {
          Key key = keyStore.getKey(alias, masterKeyService.getMasterSecret());
          if (key != null) {
            value = toChars(key.getEncoded());
          }
        } catch (UnrecoverableKeyException e) {
          throw new AmbariException("The key cannot be recovered (e.g., the given password is wrong)", e);
        } catch (KeyStoreException e) {
          throw new AmbariException("The key store has not been initialized", e);
        } catch (NoSuchAlgorithmException e) {
          throw new AmbariException(" if the algorithm for recovering the key cannot be found", e);
        }
      }
  
      return CredentialFactory.createCredential(value);
    }
  
    /**
     * Calls the implementation-specific facility to persist the KeyStore
     *
     * @param keyStore the KeyStore to persist
     * @throws AmbariException if an error occurs while persisting the key store data
     */
    protected abstract void persistCredentialStore(KeyStore keyStore) throws AmbariException;
  
    /**
     * Calls the implementation-specific facility to load the KeyStore
     *
     * @throws AmbariException if an error occurs while loading the key store data
     */
    protected abstract KeyStore loadCredentialStore() throws AmbariException;
  
  
    /**
     * Gets the lock object used to protect access to the credential store
     *
     * @return a Lock object
     */
    protected Lock getLock() {
      return lock;
    }
  
    /**
     * Loads a KeyStore from an InputStream
     * <p/>
     * Implementations are expected to call this to load the relevant KeyStore data from the
     * InputStream of some storage facility.
     *
     * @param inputStream  the InputStream to read the data from
     * @param keyStoreType the type of key store data expected
     * @return a new KeyStore instance with the loaded data
     * @throws AmbariException if an error occurs while loading the key store data from the InputStream
     */
    protected KeyStore loadKeyStore(InputStream inputStream, String keyStoreType) throws AmbariException {
      if (masterKeyService == null) {
        throw new AmbariException("Master Key Service is not set for this Credential store.");
      }
  
      KeyStore keyStore;
      try {
        keyStore = KeyStore.getInstance(keyStoreType);
      } catch (KeyStoreException e) {
        throw new AmbariException(String.format("No provider supports a key store implementation for the specified type: %s", keyStoreType), e);
      }
  
      try {
        keyStore.load(inputStream, masterKeyService.getMasterSecret());
      } catch (CertificateException e) {
        throw new AmbariException(String.format("One or more credentials from the key store could not be loaded: %s", e.getLocalizedMessage()), e);
      } catch (NoSuchAlgorithmException e) {
        throw new AmbariException(String.format("The algorithm used to check the integrity of the key store cannot be found: %s", e.getLocalizedMessage()), e);
      } catch (IOException e) {
        if (e.getCause() instanceof UnrecoverableKeyException) {
          throw new AmbariException(String.format("The password used to decrypt the key store is incorrect: %s", e.getLocalizedMessage()), e);
        } else {
          throw new AmbariException(String.format("Failed to read the key store: %s", e.getLocalizedMessage()), e);
        }
      }
  
      return keyStore;
    }
  
    /**
     * Writes a KeyStore to an OutputStream
     * <p/>
     * Implementations are expected to call this to write the relevant KeyStore data to the
     * OutputStream of some storage facility.
     *
     * @param keyStore     the KeyStore to write
     * @param outputStream the OutputStream to write the data into
     * @throws AmbariException if an error occurs while writing the key store data
     */
    protected void writeKeyStore(KeyStore keyStore, OutputStream outputStream) throws AmbariException {
      if (masterKeyService == null) {
        throw new AmbariException("Master Key Service is not set for this Credential store.");
      }
  
      try {
        keyStore.store(outputStream, masterKeyService.getMasterSecret());
      } catch (CertificateException e) {
        throw new AmbariException(String.format("A credential within in the key store data could not be stored: %s", e.getLocalizedMessage()), e);
      } catch (NoSuchAlgorithmException e) {
        throw new AmbariException(String.format("The appropriate data integrity algorithm could not be found: %s", e.getLocalizedMessage()), e);
      } catch (KeyStoreException e) {
        throw new AmbariException(String.format("The key store has not been initialized: %s", e.getLocalizedMessage()), e);
      } catch (IOException e) {
        throw new AmbariException(String.format("Failed to write the key store: %s", e.getLocalizedMessage()), e);
      }
    }
  
    /**
     * Converts an array of characters to an array of bytes by encoding each character into UTF-8 bytes.
     * <p/>
     * An attempt is made to clear out sensitive data by filling any buffers with 0's
     *
     * @param chars the array of chars to convert
     * @return an array of bytes, or null if the original array was null
     */
    protected byte[] toBytes(char[] chars) {
      if (chars == null) {
        return null;
      } else {
        CharBuffer charBuffer = CharBuffer.wrap(chars);
        ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer);
  
        byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
  
        // Clear sensitive data
        Arrays.fill(charBuffer.array(), '\u0000');
        Arrays.fill(byteBuffer.array(), (byte) 0);
  
        return bytes;
      }
    }
  
    /**
     * Converts an array of bytes to an array of character by decoding the bytes using the UTF-8
     * character set.
     * <p/>
     * An attempt is made to clear out sensitive data by filling any buffers with 0's
     *
     * @param bytes the array of bytes to convert
     * @return an array of chars, or null if the original array was null
     */
    protected char[] toChars(byte[] bytes) {
      if (bytes == null) {
        return null;
      } else {
        ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
        CharBuffer charBuffer = Charset.forName("UTF-8").decode(byteBuffer);
  
        char[] chars = Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit());
  
        // Clear sensitive data
        Arrays.fill(charBuffer.array(), '\u0000');
        Arrays.fill(byteBuffer.array(), (byte) 0);
  
        return chars;
      }
    }
  }
  