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    
019    package org.apache.hadoop.security.alias;
020    
021    import org.apache.commons.io.IOUtils;
022    import org.apache.hadoop.classification.InterfaceAudience;
023    import org.apache.hadoop.conf.Configuration;
024    import org.apache.hadoop.fs.FSDataOutputStream;
025    import org.apache.hadoop.fs.FileStatus;
026    import org.apache.hadoop.fs.FileSystem;
027    import org.apache.hadoop.fs.Path;
028    import org.apache.hadoop.fs.permission.FsPermission;
029    import org.apache.hadoop.security.ProviderUtils;
030    
031    import javax.crypto.spec.SecretKeySpec;
032    import java.io.IOException;
033    import java.io.InputStream;
034    import java.net.URI;
035    import java.net.URL;
036    import java.security.KeyStore;
037    import java.security.KeyStoreException;
038    import java.security.NoSuchAlgorithmException;
039    import java.security.UnrecoverableKeyException;
040    import java.security.cert.CertificateException;
041    import java.util.ArrayList;
042    import java.util.Enumeration;
043    import java.util.HashMap;
044    import java.util.List;
045    import java.util.Map;
046    import java.util.concurrent.locks.Lock;
047    import java.util.concurrent.locks.ReadWriteLock;
048    import java.util.concurrent.locks.ReentrantReadWriteLock;
049    
050    /**
051     * CredentialProvider based on Java's KeyStore file format. The file may be 
052     * stored in any Hadoop FileSystem using the following name mangling:
053     *  jceks://hdfs@nn1.example.com/my/creds.jceks -> hdfs://nn1.example.com/my/creds.jceks
054     *  jceks://file/home/larry/creds.jceks -> file:///home/larry/creds.jceks
055     *
056     * The password for the keystore is taken from the HADOOP_CREDSTORE_PASSWORD
057     * environment variable with a default of 'none'.
058     *
059     * It is expected that for access to credential protected resource to copy the 
060     * creds from the original provider into the job's Credentials object, which is
061     * accessed via the UserProvider. Therefore, this provider won't be directly 
062     * used by MapReduce tasks.
063     */
064    @InterfaceAudience.Private
065    public class JavaKeyStoreProvider extends CredentialProvider {
066      public static final String SCHEME_NAME = "jceks";
067      public static final String CREDENTIAL_PASSWORD_NAME =
068          "HADOOP_CREDSTORE_PASSWORD";
069      public static final String KEYSTORE_PASSWORD_FILE_KEY =
070          "hadoop.security.credstore.java-keystore-provider.password-file";
071      public static final String KEYSTORE_PASSWORD_DEFAULT = "none";
072    
073      private final URI uri;
074      private final Path path;
075      private final FileSystem fs;
076      private final FsPermission permissions;
077      private final KeyStore keyStore;
078      private char[] password = null;
079      private boolean changed = false;
080      private Lock readLock;
081      private Lock writeLock;
082    
083      private final Map<String, CredentialEntry> cache = new HashMap<String, CredentialEntry>();
084    
085      private JavaKeyStoreProvider(URI uri, Configuration conf) throws IOException {
086        this.uri = uri;
087        path = ProviderUtils.unnestUri(uri);
088        fs = path.getFileSystem(conf);
089        // Get the password from the user's environment
090        if (System.getenv().containsKey(CREDENTIAL_PASSWORD_NAME)) {
091          password = System.getenv(CREDENTIAL_PASSWORD_NAME).toCharArray();
092        }
093        // if not in ENV get check for file
094        if (password == null) {
095          String pwFile = conf.get(KEYSTORE_PASSWORD_FILE_KEY);
096          if (pwFile != null) {
097            ClassLoader cl = Thread.currentThread().getContextClassLoader();
098            URL pwdFile = cl.getResource(pwFile);
099            if (pwdFile != null) {
100              InputStream is = pwdFile.openStream();
101              try {
102                password = IOUtils.toString(is).trim().toCharArray();
103              } finally {
104                is.close();
105              }
106            }
107          }
108        }
109        if (password == null) {
110          password = KEYSTORE_PASSWORD_DEFAULT.toCharArray();
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            keyStore.load(fs.open(path), password);
121          } else {
122            permissions = new FsPermission("700");
123            // required to create an empty keystore. *sigh*
124            keyStore.load(null, password);
125          }
126        } catch (KeyStoreException e) {
127          throw new IOException("Can't create keystore", e);
128        } catch (NoSuchAlgorithmException e) {
129          throw new IOException("Can't load keystore " + path, e);
130        } catch (CertificateException e) {
131          throw new IOException("Can't load keystore " + path, e);
132        }
133        ReadWriteLock lock = new ReentrantReadWriteLock(true);
134        readLock = lock.readLock();
135        writeLock = lock.writeLock();
136      }
137    
138      @Override
139      public CredentialEntry getCredentialEntry(String alias) throws IOException {
140        readLock.lock();
141        try {
142          SecretKeySpec key = null;
143          try {
144            if (cache.containsKey(alias)) {
145              return cache.get(alias);
146            }
147            if (!keyStore.containsAlias(alias)) {
148              return null;
149            }
150            key = (SecretKeySpec) keyStore.getKey(alias, password);
151          } catch (KeyStoreException e) {
152            throw new IOException("Can't get credential " + alias + " from " +
153                                  path, e);
154          } catch (NoSuchAlgorithmException e) {
155            throw new IOException("Can't get algorithm for credential " + alias + " from " +
156                                  path, e);
157          } catch (UnrecoverableKeyException e) {
158            throw new IOException("Can't recover credential " + alias + " from " + path, e);
159          }
160          return new CredentialEntry(alias, bytesToChars(key.getEncoded()));
161        } 
162        finally {
163          readLock.unlock();
164        }
165      }
166      
167      public static char[] bytesToChars(byte[] bytes) {
168        String pass = new String(bytes);
169        return pass.toCharArray();
170      }
171    
172      @Override
173      public List<String> getAliases() throws IOException {
174        readLock.lock();
175        try {
176          ArrayList<String> list = new ArrayList<String>();
177          String alias = null;
178          try {
179            Enumeration<String> e = keyStore.aliases();
180            while (e.hasMoreElements()) {
181               alias = e.nextElement();
182               list.add(alias);
183            }
184          } catch (KeyStoreException e) {
185            throw new IOException("Can't get alias " + alias + " from " + path, e);
186          }
187          return list;
188        }
189        finally {
190          readLock.unlock();
191        }
192      }
193    
194      @Override
195      public CredentialEntry createCredentialEntry(String alias, char[] credential)
196          throws IOException {
197        writeLock.lock();
198        try {
199          if (keyStore.containsAlias(alias) || cache.containsKey(alias)) {
200            throw new IOException("Credential " + alias + " already exists in " + this);
201          }
202          return innerSetCredential(alias, credential);
203        } catch (KeyStoreException e) {
204          throw new IOException("Problem looking up credential " + alias + " in " + this,
205              e);
206        } finally {
207          writeLock.unlock();
208        }
209      }
210    
211      @Override
212      public void deleteCredentialEntry(String name) throws IOException {
213        writeLock.lock();
214        try {
215          try {
216            if (keyStore.containsAlias(name)) {
217              keyStore.deleteEntry(name);
218            }
219            else {
220              throw new IOException("Credential " + name + " does not exist in " + this);
221            }
222          } catch (KeyStoreException e) {
223            throw new IOException("Problem removing " + name + " from " +
224                this, e);
225          }
226          cache.remove(name);
227          changed = true;
228        }
229        finally {
230          writeLock.unlock();
231        }
232      }
233    
234      CredentialEntry innerSetCredential(String alias, char[] material)
235          throws IOException {
236        writeLock.lock();
237        try {
238          keyStore.setKeyEntry(alias, new SecretKeySpec(
239              new String(material).getBytes("UTF-8"), "AES"),
240              password, null);
241        } catch (KeyStoreException e) {
242          throw new IOException("Can't store credential " + alias + " in " + this,
243              e);
244        } finally {
245          writeLock.unlock();
246        }
247        changed = true;
248        return new CredentialEntry(alias, material);
249      }
250    
251      @Override
252      public void flush() throws IOException {
253        writeLock.lock();
254        try {
255          if (!changed) {
256            return;
257          }
258          // write out the keystore
259          FSDataOutputStream out = FileSystem.create(fs, path, permissions);
260          try {
261            keyStore.store(out, password);
262          } catch (KeyStoreException e) {
263            throw new IOException("Can't store keystore " + this, e);
264          } catch (NoSuchAlgorithmException e) {
265            throw new IOException("No such algorithm storing keystore " + this, e);
266          } catch (CertificateException e) {
267            throw new IOException("Certificate exception storing keystore " + this,
268                e);
269          }
270          out.close();
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    }