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}