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 }