/**
* 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;
}
}
}