| /** |
| * 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.hadoop.security.alias; |
| |
| import org.apache.hadoop.classification.InterfaceAudience; |
| import org.apache.hadoop.conf.Configuration; |
| import org.apache.hadoop.fs.CommonConfigurationKeysPublic; |
| import org.apache.hadoop.fs.Path; |
| import org.apache.hadoop.security.ProviderUtils; |
| |
| import com.google.common.base.Charsets; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import javax.crypto.spec.SecretKeySpec; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.URI; |
| import java.security.GeneralSecurityException; |
| import java.security.KeyStore; |
| import java.security.KeyStoreException; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.UnrecoverableKeyException; |
| import java.security.cert.CertificateException; |
| import java.util.ArrayList; |
| import java.util.Enumeration; |
| import java.util.List; |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReadWriteLock; |
| import java.util.concurrent.locks.ReentrantReadWriteLock; |
| |
| /** |
| * Abstract class for implementing credential providers that are based on |
| * Java Keystores as the underlying credential store. |
| * |
| * The password for the keystore is taken from the HADOOP_CREDSTORE_PASSWORD |
| * environment variable with a default of 'none'. |
| * |
| * It is expected that for access to credential protected resource to copy the |
| * creds from the original provider into the job's Credentials object, which is |
| * accessed via the UserProvider. Therefore, these providers won't be directly |
| * used by MapReduce tasks. |
| */ |
| @InterfaceAudience.Private |
| public abstract class AbstractJavaKeyStoreProvider extends CredentialProvider { |
| public static final Logger LOG = LoggerFactory.getLogger( |
| AbstractJavaKeyStoreProvider.class); |
| public static final String CREDENTIAL_PASSWORD_ENV_VAR = |
| "HADOOP_CREDSTORE_PASSWORD"; |
| public static final String CREDENTIAL_PASSWORD_FILE_KEY = |
| CommonConfigurationKeysPublic. |
| HADOOP_SECURITY_CREDENTIAL_PASSWORD_FILE_KEY; |
| public static final String CREDENTIAL_PASSWORD_DEFAULT = "none"; |
| |
| private Path path; |
| private final URI uri; |
| private KeyStore keyStore; |
| private char[] password = null; |
| private boolean changed = false; |
| private Lock readLock; |
| private Lock writeLock; |
| private final Configuration conf; |
| |
| protected AbstractJavaKeyStoreProvider(URI uri, Configuration conf) |
| throws IOException { |
| this.uri = uri; |
| this.conf = conf; |
| initFileSystem(uri); |
| locateKeystore(); |
| ReadWriteLock lock = new ReentrantReadWriteLock(true); |
| readLock = lock.readLock(); |
| writeLock = lock.writeLock(); |
| } |
| |
| protected Configuration getConf() { |
| return conf; |
| } |
| |
| public Path getPath() { |
| return path; |
| } |
| |
| public void setPath(Path p) { |
| this.path = p; |
| } |
| |
| public char[] getPassword() { |
| return password; |
| } |
| |
| public void setPassword(char[] pass) { |
| this.password = pass; |
| } |
| |
| public boolean isChanged() { |
| return changed; |
| } |
| |
| public void setChanged(boolean chg) { |
| this.changed = chg; |
| } |
| |
| public Lock getReadLock() { |
| return readLock; |
| } |
| |
| public void setReadLock(Lock rl) { |
| this.readLock = rl; |
| } |
| |
| public Lock getWriteLock() { |
| return writeLock; |
| } |
| |
| public void setWriteLock(Lock wl) { |
| this.writeLock = wl; |
| } |
| |
| public URI getUri() { |
| return uri; |
| } |
| |
| public KeyStore getKeyStore() { |
| return keyStore; |
| } |
| |
| protected final String getPathAsString() { |
| return getPath().toString(); |
| } |
| |
| protected abstract String getSchemeName(); |
| |
| protected abstract OutputStream getOutputStreamForKeystore() |
| throws IOException; |
| |
| protected abstract boolean keystoreExists() throws IOException; |
| |
| protected abstract InputStream getInputStreamForFile() throws IOException; |
| |
| protected abstract void createPermissions(String perms) throws IOException; |
| |
| protected abstract void stashOriginalFilePermissions() throws IOException; |
| |
| protected void initFileSystem(URI keystoreUri) |
| throws IOException { |
| path = ProviderUtils.unnestUri(keystoreUri); |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("backing jks path initialized to " + path); |
| } |
| } |
| |
| @Override |
| public CredentialEntry getCredentialEntry(String alias) |
| throws IOException { |
| readLock.lock(); |
| try { |
| SecretKeySpec key = null; |
| try { |
| if (!keyStore.containsAlias(alias)) { |
| return null; |
| } |
| key = (SecretKeySpec) keyStore.getKey(alias, password); |
| } catch (KeyStoreException e) { |
| throw new IOException("Can't get credential " + alias + " from " |
| + getPathAsString(), e); |
| } catch (NoSuchAlgorithmException e) { |
| throw new IOException("Can't get algorithm for credential " + alias |
| + " from " + getPathAsString(), e); |
| } catch (UnrecoverableKeyException e) { |
| throw new IOException("Can't recover credential " + alias + " from " |
| + getPathAsString(), e); |
| } |
| return new CredentialEntry(alias, bytesToChars(key.getEncoded())); |
| } finally { |
| readLock.unlock(); |
| } |
| } |
| |
| public static char[] bytesToChars(byte[] bytes) throws IOException { |
| String pass; |
| pass = new String(bytes, Charsets.UTF_8); |
| return pass.toCharArray(); |
| } |
| |
| @Override |
| public List<String> getAliases() throws IOException { |
| readLock.lock(); |
| try { |
| ArrayList<String> list = new ArrayList<String>(); |
| String alias = null; |
| try { |
| Enumeration<String> e = keyStore.aliases(); |
| while (e.hasMoreElements()) { |
| alias = e.nextElement(); |
| list.add(alias); |
| } |
| } catch (KeyStoreException e) { |
| throw new IOException("Can't get alias " + alias + " from " |
| + getPathAsString(), e); |
| } |
| return list; |
| } finally { |
| readLock.unlock(); |
| } |
| } |
| |
| @Override |
| public CredentialEntry createCredentialEntry(String alias, char[] credential) |
| throws IOException { |
| writeLock.lock(); |
| try { |
| if (keyStore.containsAlias(alias)) { |
| throw new IOException("Credential " + alias + " already exists in " |
| + this); |
| } |
| return innerSetCredential(alias, credential); |
| } catch (KeyStoreException e) { |
| throw new IOException("Problem looking up credential " + alias + " in " |
| + this, e); |
| } finally { |
| writeLock.unlock(); |
| } |
| } |
| |
| @Override |
| public void deleteCredentialEntry(String name) throws IOException { |
| writeLock.lock(); |
| try { |
| try { |
| if (keyStore.containsAlias(name)) { |
| keyStore.deleteEntry(name); |
| } else { |
| throw new IOException("Credential " + name + " does not exist in " |
| + this); |
| } |
| } catch (KeyStoreException e) { |
| throw new IOException("Problem removing " + name + " from " + this, e); |
| } |
| changed = true; |
| } finally { |
| writeLock.unlock(); |
| } |
| } |
| |
| CredentialEntry innerSetCredential(String alias, char[] material) |
| throws IOException { |
| writeLock.lock(); |
| try { |
| keyStore.setKeyEntry(alias, |
| new SecretKeySpec(new String(material).getBytes("UTF-8"), "AES"), |
| password, null); |
| } catch (KeyStoreException e) { |
| throw new IOException("Can't store credential " + alias + " in " + this, |
| e); |
| } finally { |
| writeLock.unlock(); |
| } |
| changed = true; |
| return new CredentialEntry(alias, material); |
| } |
| |
| @Override |
| public void flush() throws IOException { |
| writeLock.lock(); |
| try { |
| if (!changed) { |
| LOG.debug("Keystore hasn't changed, returning."); |
| return; |
| } |
| LOG.debug("Writing out keystore."); |
| try (OutputStream out = getOutputStreamForKeystore()) { |
| keyStore.store(out, password); |
| } catch (KeyStoreException e) { |
| throw new IOException("Can't store keystore " + this, e); |
| } catch (NoSuchAlgorithmException e) { |
| throw new IOException("No such algorithm storing keystore " + this, e); |
| } catch (CertificateException e) { |
| throw new IOException("Certificate exception storing keystore " + this, |
| e); |
| } |
| changed = false; |
| } finally { |
| writeLock.unlock(); |
| } |
| } |
| |
| /** |
| * Open up and initialize the keyStore. |
| * |
| * @throws IOException If there is a problem reading the password file |
| * or a problem reading the keystore. |
| */ |
| private void locateKeystore() throws IOException { |
| try { |
| password = ProviderUtils.locatePassword(CREDENTIAL_PASSWORD_ENV_VAR, |
| conf.get(CREDENTIAL_PASSWORD_FILE_KEY)); |
| if (password == null) { |
| password = CREDENTIAL_PASSWORD_DEFAULT.toCharArray(); |
| } |
| KeyStore ks; |
| ks = KeyStore.getInstance("jceks"); |
| if (keystoreExists()) { |
| stashOriginalFilePermissions(); |
| try (InputStream in = getInputStreamForFile()) { |
| ks.load(in, password); |
| } |
| } else { |
| createPermissions("600"); |
| // required to create an empty keystore. *sigh* |
| ks.load(null, password); |
| } |
| keyStore = ks; |
| } catch (KeyStoreException e) { |
| throw new IOException("Can't create keystore", e); |
| } catch (GeneralSecurityException e) { |
| throw new IOException("Can't load keystore " + getPathAsString(), e); |
| } |
| } |
| |
| @Override |
| public boolean needsPassword() throws IOException { |
| return (null == ProviderUtils.locatePassword(CREDENTIAL_PASSWORD_ENV_VAR, |
| conf.get(CREDENTIAL_PASSWORD_FILE_KEY))); |
| |
| } |
| |
| @Override |
| public String noPasswordWarning() { |
| return ProviderUtils.noPasswordWarning(CREDENTIAL_PASSWORD_ENV_VAR, |
| CREDENTIAL_PASSWORD_FILE_KEY); |
| } |
| |
| @Override |
| public String noPasswordError() { |
| return ProviderUtils.noPasswordError(CREDENTIAL_PASSWORD_ENV_VAR, |
| CREDENTIAL_PASSWORD_FILE_KEY); |
| } |
| |
| @Override |
| public String toString() { |
| return uri.toString(); |
| } |
| } |