001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.security.alias;
020
021import org.apache.commons.io.Charsets;
022import org.apache.commons.io.IOUtils;
023import org.apache.hadoop.classification.InterfaceAudience;
024import org.apache.hadoop.conf.Configuration;
025import org.apache.hadoop.fs.FSDataInputStream;
026import org.apache.hadoop.fs.FSDataOutputStream;
027import org.apache.hadoop.fs.FileStatus;
028import org.apache.hadoop.fs.FileSystem;
029import org.apache.hadoop.fs.Path;
030import org.apache.hadoop.fs.permission.FsPermission;
031import org.apache.hadoop.security.ProviderUtils;
032
033import javax.crypto.spec.SecretKeySpec;
034import java.io.IOException;
035import java.io.InputStream;
036import java.net.URI;
037import java.net.URL;
038import java.security.KeyStore;
039import java.security.KeyStoreException;
040import java.security.NoSuchAlgorithmException;
041import java.security.UnrecoverableKeyException;
042import java.security.cert.CertificateException;
043import java.util.ArrayList;
044import java.util.Enumeration;
045import java.util.HashMap;
046import java.util.List;
047import java.util.Map;
048import java.util.concurrent.locks.Lock;
049import java.util.concurrent.locks.ReadWriteLock;
050import java.util.concurrent.locks.ReentrantReadWriteLock;
051
052/**
053 * CredentialProvider based on Java's KeyStore file format. The file may be 
054 * stored in any Hadoop FileSystem using the following name mangling:
055 *  jceks://hdfs@nn1.example.com/my/creds.jceks -> hdfs://nn1.example.com/my/creds.jceks
056 *  jceks://file/home/larry/creds.jceks -> file:///home/larry/creds.jceks
057 *
058 * The password for the keystore is taken from the HADOOP_CREDSTORE_PASSWORD
059 * environment variable with a default of 'none'.
060 *
061 * It is expected that for access to credential protected resource to copy the 
062 * creds from the original provider into the job's Credentials object, which is
063 * accessed via the UserProvider. Therefore, this provider won't be directly 
064 * used by MapReduce tasks.
065 */
066@InterfaceAudience.Private
067public class JavaKeyStoreProvider extends CredentialProvider {
068  public static final String SCHEME_NAME = "jceks";
069  public static final String CREDENTIAL_PASSWORD_NAME =
070      "HADOOP_CREDSTORE_PASSWORD";
071  public static final String KEYSTORE_PASSWORD_FILE_KEY =
072      "hadoop.security.credstore.java-keystore-provider.password-file";
073  public static final String KEYSTORE_PASSWORD_DEFAULT = "none";
074
075  private final URI uri;
076  private final Path path;
077  private final FileSystem fs;
078  private final FsPermission permissions;
079  private final KeyStore keyStore;
080  private char[] password = null;
081  private boolean changed = false;
082  private Lock readLock;
083  private Lock writeLock;
084
085  private final Map<String, CredentialEntry> cache = new HashMap<String, CredentialEntry>();
086
087  private JavaKeyStoreProvider(URI uri, Configuration conf) throws IOException {
088    this.uri = uri;
089    path = ProviderUtils.unnestUri(uri);
090    fs = path.getFileSystem(conf);
091    // Get the password from the user's environment
092    if (System.getenv().containsKey(CREDENTIAL_PASSWORD_NAME)) {
093      password = System.getenv(CREDENTIAL_PASSWORD_NAME).toCharArray();
094    }
095    // if not in ENV get check for file
096    if (password == null) {
097      String pwFile = conf.get(KEYSTORE_PASSWORD_FILE_KEY);
098      if (pwFile != null) {
099        ClassLoader cl = Thread.currentThread().getContextClassLoader();
100        URL pwdFile = cl.getResource(pwFile);
101        if (pwdFile != null) {
102          try (InputStream is = pwdFile.openStream()) {
103            password = IOUtils.toString(is).trim().toCharArray();
104          }
105        }
106      }
107    }
108    if (password == null) {
109      password = KEYSTORE_PASSWORD_DEFAULT.toCharArray();
110    }
111
112    try {
113      keyStore = KeyStore.getInstance(SCHEME_NAME);
114      if (fs.exists(path)) {
115        // save off permissions in case we need to
116        // rewrite the keystore in flush()
117        FileStatus s = fs.getFileStatus(path);
118        permissions = s.getPermission();
119
120        try (FSDataInputStream in = fs.open(path)) {
121          keyStore.load(in, password);
122        }
123      } else {
124        permissions = new FsPermission("700");
125        // required to create an empty keystore. *sigh*
126        keyStore.load(null, password);
127      }
128    } catch (KeyStoreException e) {
129      throw new IOException("Can't create keystore", e);
130    } catch (NoSuchAlgorithmException e) {
131      throw new IOException("Can't load keystore " + path, e);
132    } catch (CertificateException e) {
133      throw new IOException("Can't load keystore " + path, e);
134    }
135    ReadWriteLock lock = new ReentrantReadWriteLock(true);
136    readLock = lock.readLock();
137    writeLock = lock.writeLock();
138  }
139
140  @Override
141  public CredentialEntry getCredentialEntry(String alias) throws IOException {
142    readLock.lock();
143    try {
144      SecretKeySpec key = null;
145      try {
146        if (cache.containsKey(alias)) {
147          return cache.get(alias);
148        }
149        if (!keyStore.containsAlias(alias)) {
150          return null;
151        }
152        key = (SecretKeySpec) keyStore.getKey(alias, password);
153      } catch (KeyStoreException e) {
154        throw new IOException("Can't get credential " + alias + " from " +
155                              path, e);
156      } catch (NoSuchAlgorithmException e) {
157        throw new IOException("Can't get algorithm for credential " + alias + " from " +
158                              path, e);
159      } catch (UnrecoverableKeyException e) {
160        throw new IOException("Can't recover credential " + alias + " from " + path, e);
161      }
162      return new CredentialEntry(alias, bytesToChars(key.getEncoded()));
163    } 
164    finally {
165      readLock.unlock();
166    }
167  }
168  
169  public static char[] bytesToChars(byte[] bytes) {
170    String pass = new String(bytes, Charsets.UTF_8);
171    return pass.toCharArray();
172  }
173
174  @Override
175  public List<String> getAliases() throws IOException {
176    readLock.lock();
177    try {
178      ArrayList<String> list = new ArrayList<String>();
179      String alias = null;
180      try {
181        Enumeration<String> e = keyStore.aliases();
182        while (e.hasMoreElements()) {
183           alias = e.nextElement();
184           list.add(alias);
185        }
186      } catch (KeyStoreException e) {
187        throw new IOException("Can't get alias " + alias + " from " + path, e);
188      }
189      return list;
190    }
191    finally {
192      readLock.unlock();
193    }
194  }
195
196  @Override
197  public CredentialEntry createCredentialEntry(String alias, char[] credential)
198      throws IOException {
199    writeLock.lock();
200    try {
201      if (keyStore.containsAlias(alias) || cache.containsKey(alias)) {
202        throw new IOException("Credential " + alias + " already exists in " + this);
203      }
204      return innerSetCredential(alias, credential);
205    } catch (KeyStoreException e) {
206      throw new IOException("Problem looking up credential " + alias + " in " + this,
207          e);
208    } finally {
209      writeLock.unlock();
210    }
211  }
212
213  @Override
214  public void deleteCredentialEntry(String name) throws IOException {
215    writeLock.lock();
216    try {
217      try {
218        if (keyStore.containsAlias(name)) {
219          keyStore.deleteEntry(name);
220        }
221        else {
222          throw new IOException("Credential " + name + " does not exist in " + this);
223        }
224      } catch (KeyStoreException e) {
225        throw new IOException("Problem removing " + name + " from " +
226            this, e);
227      }
228      cache.remove(name);
229      changed = true;
230    }
231    finally {
232      writeLock.unlock();
233    }
234  }
235
236  CredentialEntry innerSetCredential(String alias, char[] material)
237      throws IOException {
238    writeLock.lock();
239    try {
240      keyStore.setKeyEntry(alias, new SecretKeySpec(
241          new String(material).getBytes("UTF-8"), "AES"),
242          password, null);
243    } catch (KeyStoreException e) {
244      throw new IOException("Can't store credential " + alias + " in " + this,
245          e);
246    } finally {
247      writeLock.unlock();
248    }
249    changed = true;
250    return new CredentialEntry(alias, material);
251  }
252
253  @Override
254  public void flush() throws IOException {
255    writeLock.lock();
256    try {
257      if (!changed) {
258        return;
259      }
260      // write out the keystore
261      try (FSDataOutputStream out = FileSystem.create(fs, path, permissions)) {
262        keyStore.store(out, password);
263      } catch (KeyStoreException e) {
264        throw new IOException("Can't store keystore " + this, e);
265      } catch (NoSuchAlgorithmException e) {
266        throw new IOException("No such algorithm storing keystore " + this, e);
267      } catch (CertificateException e) {
268        throw new IOException("Certificate exception storing keystore " + this,
269            e);
270      }
271      changed = false;
272    }
273    finally {
274      writeLock.unlock();
275    }
276  }
277
278  @Override
279  public String toString() {
280    return uri.toString();
281  }
282
283  /**
284   * The factory to create JksProviders, which is used by the ServiceLoader.
285   */
286  public static class Factory extends CredentialProviderFactory {
287    @Override
288    public CredentialProvider createProvider(URI providerName,
289                                      Configuration conf) throws IOException {
290      if (SCHEME_NAME.equals(providerName.getScheme())) {
291        return new JavaKeyStoreProvider(providerName, conf);
292      }
293      return null;
294    }
295  }
296}