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.crypto.key; 020 021 import org.apache.commons.io.IOUtils; 022 import org.apache.hadoop.classification.InterfaceAudience; 023 import org.apache.hadoop.classification.InterfaceAudience.Private; 024 import org.apache.hadoop.conf.Configuration; 025 import org.apache.hadoop.fs.FSDataOutputStream; 026 import org.apache.hadoop.fs.FileStatus; 027 import org.apache.hadoop.fs.FileSystem; 028 import org.apache.hadoop.fs.Path; 029 import org.apache.hadoop.fs.permission.FsPermission; 030 import org.apache.hadoop.security.ProviderUtils; 031 import org.slf4j.Logger; 032 import org.slf4j.LoggerFactory; 033 034 import com.google.common.annotations.VisibleForTesting; 035 036 import javax.crypto.spec.SecretKeySpec; 037 038 import java.io.IOException; 039 import java.io.InputStream; 040 import java.io.ObjectInputStream; 041 import java.io.ObjectOutputStream; 042 import java.io.Serializable; 043 import java.net.URI; 044 import java.net.URL; 045 import java.security.Key; 046 import java.security.KeyStore; 047 import java.security.KeyStoreException; 048 import java.security.NoSuchAlgorithmException; 049 import java.security.UnrecoverableKeyException; 050 import java.security.cert.CertificateException; 051 import java.util.ArrayList; 052 import java.util.Date; 053 import java.util.Enumeration; 054 import java.util.HashMap; 055 import java.util.List; 056 import java.util.Map; 057 import java.util.concurrent.locks.Lock; 058 import java.util.concurrent.locks.ReadWriteLock; 059 import java.util.concurrent.locks.ReentrantReadWriteLock; 060 061 /** 062 * KeyProvider based on Java's KeyStore file format. The file may be stored in 063 * any Hadoop FileSystem using the following name mangling: 064 * jks://[email protected]/my/keys.jks -> hdfs://nn1.example.com/my/keys.jks 065 * jks://file/home/owen/keys.jks -> file:///home/owen/keys.jks 066 * <p/> 067 * If the <code>HADOOP_KEYSTORE_PASSWORD</code> environment variable is set, 068 * its value is used as the password for the keystore. 069 * <p/> 070 * If the <code>HADOOP_KEYSTORE_PASSWORD</code> environment variable is not set, 071 * the password for the keystore is read from file specified in the 072 * {@link #KEYSTORE_PASSWORD_FILE_KEY} configuration property. The password file 073 * is looked up in Hadoop's configuration directory via the classpath. 074 * <p/> 075 * <b>NOTE:</b> Make sure the password in the password file does not have an 076 * ENTER at the end, else it won't be valid for the Java KeyStore. 077 * <p/> 078 * If the environment variable, nor the property are not set, the password used 079 * is 'none'. 080 * <p/> 081 * It is expected for encrypted InputFormats and OutputFormats to copy the keys 082 * from the original provider into the job's Credentials object, which is 083 * accessed via the UserProvider. Therefore, this provider won't be used by 084 * MapReduce tasks. 085 */ 086 @InterfaceAudience.Private 087 public class JavaKeyStoreProvider extends KeyProvider { 088 private static final String KEY_METADATA = "KeyMetadata"; 089 private static Logger LOG = 090 LoggerFactory.getLogger(JavaKeyStoreProvider.class); 091 092 public static final String SCHEME_NAME = "jceks"; 093 094 public static final String KEYSTORE_PASSWORD_FILE_KEY = 095 "hadoop.security.keystore.java-keystore-provider.password-file"; 096 097 public static final String KEYSTORE_PASSWORD_ENV_VAR = 098 "HADOOP_KEYSTORE_PASSWORD"; 099 public static final char[] KEYSTORE_PASSWORD_DEFAULT = "none".toCharArray(); 100 101 private final URI uri; 102 private final Path path; 103 private final FileSystem fs; 104 private final FsPermission permissions; 105 private final KeyStore keyStore; 106 private char[] password; 107 private boolean changed = false; 108 private Lock readLock; 109 private Lock writeLock; 110 111 private final Map<String, Metadata> cache = new HashMap<String, Metadata>(); 112 113 @VisibleForTesting 114 JavaKeyStoreProvider(JavaKeyStoreProvider other) { 115 super(new Configuration()); 116 uri = other.uri; 117 path = other.path; 118 fs = other.fs; 119 permissions = other.permissions; 120 keyStore = other.keyStore; 121 password = other.password; 122 changed = other.changed; 123 readLock = other.readLock; 124 writeLock = other.writeLock; 125 } 126 127 private JavaKeyStoreProvider(URI uri, Configuration conf) throws IOException { 128 super(conf); 129 this.uri = uri; 130 path = ProviderUtils.unnestUri(uri); 131 fs = path.getFileSystem(conf); 132 // Get the password file from the conf, if not present from the user's 133 // environment var 134 if (System.getenv().containsKey(KEYSTORE_PASSWORD_ENV_VAR)) { 135 password = System.getenv(KEYSTORE_PASSWORD_ENV_VAR).toCharArray(); 136 } 137 if (password == null) { 138 String pwFile = conf.get(KEYSTORE_PASSWORD_FILE_KEY); 139 if (pwFile != null) { 140 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 141 URL pwdFile = cl.getResource(pwFile); 142 if (pwdFile == null) { 143 // Provided Password file does not exist 144 throw new IOException("Password file does not exists"); 145 } 146 if (pwdFile != null) { 147 InputStream is = pwdFile.openStream(); 148 try { 149 password = IOUtils.toString(is).trim().toCharArray(); 150 } finally { 151 is.close(); 152 } 153 } 154 } 155 } 156 if (password == null) { 157 password = KEYSTORE_PASSWORD_DEFAULT; 158 } 159 try { 160 Path oldPath = constructOldPath(path); 161 Path newPath = constructNewPath(path); 162 keyStore = KeyStore.getInstance(SCHEME_NAME); 163 FsPermission perm = null; 164 if (fs.exists(path)) { 165 // flush did not proceed to completion 166 // _NEW should not exist 167 if (fs.exists(newPath)) { 168 throw new IOException( 169 String.format("Keystore not loaded due to some inconsistency " 170 + "('%s' and '%s' should not exist together)!!", path, newPath)); 171 } 172 perm = tryLoadFromPath(path, oldPath); 173 } else { 174 perm = tryLoadIncompleteFlush(oldPath, newPath); 175 } 176 // Need to save off permissions in case we need to 177 // rewrite the keystore in flush() 178 permissions = perm; 179 } catch (KeyStoreException e) { 180 throw new IOException("Can't create keystore", e); 181 } catch (NoSuchAlgorithmException e) { 182 throw new IOException("Can't load keystore " + path, e); 183 } catch (CertificateException e) { 184 throw new IOException("Can't load keystore " + path, e); 185 } 186 ReadWriteLock lock = new ReentrantReadWriteLock(true); 187 readLock = lock.readLock(); 188 writeLock = lock.writeLock(); 189 } 190 191 /** 192 * Try loading from the user specified path, else load from the backup 193 * path in case Exception is not due to bad/wrong password 194 * @param path Actual path to load from 195 * @param backupPath Backup path (_OLD) 196 * @return The permissions of the loaded file 197 * @throws NoSuchAlgorithmException 198 * @throws CertificateException 199 * @throws IOException 200 */ 201 private FsPermission tryLoadFromPath(Path path, Path backupPath) 202 throws NoSuchAlgorithmException, CertificateException, 203 IOException { 204 FsPermission perm = null; 205 try { 206 perm = loadFromPath(path, password); 207 // Remove _OLD if exists 208 if (fs.exists(backupPath)) { 209 fs.delete(backupPath, true); 210 } 211 LOG.debug("KeyStore loaded successfully !!"); 212 } catch (IOException ioe) { 213 // If file is corrupted for some reason other than 214 // wrong password try the _OLD file if exits 215 if (!isBadorWrongPassword(ioe)) { 216 perm = loadFromPath(backupPath, password); 217 // Rename CURRENT to CORRUPTED 218 renameOrFail(path, new Path(path.toString() + "_CORRUPTED_" 219 + System.currentTimeMillis())); 220 renameOrFail(backupPath, path); 221 LOG.debug(String.format( 222 "KeyStore loaded successfully from '%s' since '%s'" 223 + "was corrupted !!", backupPath, path)); 224 } else { 225 throw ioe; 226 } 227 } 228 return perm; 229 } 230 231 /** 232 * The KeyStore might have gone down during a flush, In which case either the 233 * _NEW or _OLD files might exists. This method tries to load the KeyStore 234 * from one of these intermediate files. 235 * @param oldPath the _OLD file created during flush 236 * @param newPath the _NEW file created during flush 237 * @return The permissions of the loaded file 238 * @throws IOException 239 * @throws NoSuchAlgorithmException 240 * @throws CertificateException 241 */ 242 private FsPermission tryLoadIncompleteFlush(Path oldPath, Path newPath) 243 throws IOException, NoSuchAlgorithmException, CertificateException { 244 FsPermission perm = null; 245 // Check if _NEW exists (in case flush had finished writing but not 246 // completed the re-naming) 247 if (fs.exists(newPath)) { 248 perm = loadAndReturnPerm(newPath, oldPath); 249 } 250 // try loading from _OLD (An earlier Flushing MIGHT not have completed 251 // writing completely) 252 if ((perm == null) && fs.exists(oldPath)) { 253 perm = loadAndReturnPerm(oldPath, newPath); 254 } 255 // If not loaded yet, 256 // required to create an empty keystore. *sigh* 257 if (perm == null) { 258 keyStore.load(null, password); 259 LOG.debug("KeyStore initialized anew successfully !!"); 260 perm = new FsPermission("700"); 261 } 262 return perm; 263 } 264 265 private FsPermission loadAndReturnPerm(Path pathToLoad, Path pathToDelete) 266 throws NoSuchAlgorithmException, CertificateException, 267 IOException { 268 FsPermission perm = null; 269 try { 270 perm = loadFromPath(pathToLoad, password); 271 renameOrFail(pathToLoad, path); 272 LOG.debug(String.format("KeyStore loaded successfully from '%s'!!", 273 pathToLoad)); 274 if (fs.exists(pathToDelete)) { 275 fs.delete(pathToDelete, true); 276 } 277 } catch (IOException e) { 278 // Check for password issue : don't want to trash file due 279 // to wrong password 280 if (isBadorWrongPassword(e)) { 281 throw e; 282 } 283 } 284 return perm; 285 } 286 287 private boolean isBadorWrongPassword(IOException ioe) { 288 // As per documentation this is supposed to be the way to figure 289 // if password was correct 290 if (ioe.getCause() instanceof UnrecoverableKeyException) { 291 return true; 292 } 293 // Unfortunately that doesn't seem to work.. 294 // Workaround : 295 if ((ioe.getCause() == null) 296 && (ioe.getMessage() != null) 297 && ((ioe.getMessage().contains("Keystore was tampered")) || (ioe 298 .getMessage().contains("password was incorrect")))) { 299 return true; 300 } 301 return false; 302 } 303 304 private FsPermission loadFromPath(Path p, char[] password) 305 throws IOException, NoSuchAlgorithmException, CertificateException { 306 FileStatus s = fs.getFileStatus(p); 307 keyStore.load(fs.open(p), password); 308 return s.getPermission(); 309 } 310 311 private Path constructNewPath(Path path) { 312 Path newPath = new Path(path.toString() + "_NEW"); 313 return newPath; 314 } 315 316 private Path constructOldPath(Path path) { 317 Path oldPath = new Path(path.toString() + "_OLD"); 318 return oldPath; 319 } 320 321 @Override 322 public KeyVersion getKeyVersion(String versionName) throws IOException { 323 readLock.lock(); 324 try { 325 SecretKeySpec key = null; 326 try { 327 if (!keyStore.containsAlias(versionName)) { 328 return null; 329 } 330 key = (SecretKeySpec) keyStore.getKey(versionName, password); 331 } catch (KeyStoreException e) { 332 throw new IOException("Can't get key " + versionName + " from " + 333 path, e); 334 } catch (NoSuchAlgorithmException e) { 335 throw new IOException("Can't get algorithm for key " + key + " from " + 336 path, e); 337 } catch (UnrecoverableKeyException e) { 338 throw new IOException("Can't recover key " + key + " from " + path, e); 339 } 340 return new KeyVersion(getBaseName(versionName), versionName, key.getEncoded()); 341 } finally { 342 readLock.unlock(); 343 } 344 } 345 346 @Override 347 public List<String> getKeys() throws IOException { 348 readLock.lock(); 349 try { 350 ArrayList<String> list = new ArrayList<String>(); 351 String alias = null; 352 try { 353 Enumeration<String> e = keyStore.aliases(); 354 while (e.hasMoreElements()) { 355 alias = e.nextElement(); 356 // only include the metadata key names in the list of names 357 if (!alias.contains("@")) { 358 list.add(alias); 359 } 360 } 361 } catch (KeyStoreException e) { 362 throw new IOException("Can't get key " + alias + " from " + path, e); 363 } 364 return list; 365 } finally { 366 readLock.unlock(); 367 } 368 } 369 370 @Override 371 public List<KeyVersion> getKeyVersions(String name) throws IOException { 372 readLock.lock(); 373 try { 374 List<KeyVersion> list = new ArrayList<KeyVersion>(); 375 Metadata km = getMetadata(name); 376 if (km != null) { 377 int latestVersion = km.getVersions(); 378 KeyVersion v = null; 379 String versionName = null; 380 for (int i = 0; i < latestVersion; i++) { 381 versionName = buildVersionName(name, i); 382 v = getKeyVersion(versionName); 383 if (v != null) { 384 list.add(v); 385 } 386 } 387 } 388 return list; 389 } finally { 390 readLock.unlock(); 391 } 392 } 393 394 @Override 395 public Metadata getMetadata(String name) throws IOException { 396 readLock.lock(); 397 try { 398 if (cache.containsKey(name)) { 399 return cache.get(name); 400 } 401 try { 402 if (!keyStore.containsAlias(name)) { 403 return null; 404 } 405 Metadata meta = ((KeyMetadata) keyStore.getKey(name, password)).metadata; 406 cache.put(name, meta); 407 return meta; 408 } catch (KeyStoreException e) { 409 throw new IOException("Can't get metadata for " + name + 410 " from keystore " + path, e); 411 } catch (NoSuchAlgorithmException e) { 412 throw new IOException("Can't get algorithm for " + name + 413 " from keystore " + path, e); 414 } catch (UnrecoverableKeyException e) { 415 throw new IOException("Can't recover key for " + name + 416 " from keystore " + path, e); 417 } 418 } finally { 419 readLock.unlock(); 420 } 421 } 422 423 @Override 424 public KeyVersion createKey(String name, byte[] material, 425 Options options) throws IOException { 426 writeLock.lock(); 427 try { 428 try { 429 if (keyStore.containsAlias(name) || cache.containsKey(name)) { 430 throw new IOException("Key " + name + " already exists in " + this); 431 } 432 } catch (KeyStoreException e) { 433 throw new IOException("Problem looking up key " + name + " in " + this, 434 e); 435 } 436 Metadata meta = new Metadata(options.getCipher(), options.getBitLength(), 437 options.getDescription(), options.getAttributes(), new Date(), 1); 438 if (options.getBitLength() != 8 * material.length) { 439 throw new IOException("Wrong key length. Required " + 440 options.getBitLength() + ", but got " + (8 * material.length)); 441 } 442 cache.put(name, meta); 443 String versionName = buildVersionName(name, 0); 444 return innerSetKeyVersion(name, versionName, material, meta.getCipher()); 445 } finally { 446 writeLock.unlock(); 447 } 448 } 449 450 @Override 451 public void deleteKey(String name) throws IOException { 452 writeLock.lock(); 453 try { 454 Metadata meta = getMetadata(name); 455 if (meta == null) { 456 throw new IOException("Key " + name + " does not exist in " + this); 457 } 458 for(int v=0; v < meta.getVersions(); ++v) { 459 String versionName = buildVersionName(name, v); 460 try { 461 if (keyStore.containsAlias(versionName)) { 462 keyStore.deleteEntry(versionName); 463 } 464 } catch (KeyStoreException e) { 465 throw new IOException("Problem removing " + versionName + " from " + 466 this, e); 467 } 468 } 469 try { 470 if (keyStore.containsAlias(name)) { 471 keyStore.deleteEntry(name); 472 } 473 } catch (KeyStoreException e) { 474 throw new IOException("Problem removing " + name + " from " + this, e); 475 } 476 cache.remove(name); 477 changed = true; 478 } finally { 479 writeLock.unlock(); 480 } 481 } 482 483 KeyVersion innerSetKeyVersion(String name, String versionName, byte[] material, 484 String cipher) throws IOException { 485 try { 486 keyStore.setKeyEntry(versionName, new SecretKeySpec(material, cipher), 487 password, null); 488 } catch (KeyStoreException e) { 489 throw new IOException("Can't store key " + versionName + " in " + this, 490 e); 491 } 492 changed = true; 493 return new KeyVersion(name, versionName, material); 494 } 495 496 @Override 497 public KeyVersion rollNewVersion(String name, 498 byte[] material) throws IOException { 499 writeLock.lock(); 500 try { 501 Metadata meta = getMetadata(name); 502 if (meta == null) { 503 throw new IOException("Key " + name + " not found"); 504 } 505 if (meta.getBitLength() != 8 * material.length) { 506 throw new IOException("Wrong key length. Required " + 507 meta.getBitLength() + ", but got " + (8 * material.length)); 508 } 509 int nextVersion = meta.addVersion(); 510 String versionName = buildVersionName(name, nextVersion); 511 return innerSetKeyVersion(name, versionName, material, meta.getCipher()); 512 } finally { 513 writeLock.unlock(); 514 } 515 } 516 517 @Override 518 public void flush() throws IOException { 519 Path newPath = constructNewPath(path); 520 Path oldPath = constructOldPath(path); 521 Path resetPath = path; 522 writeLock.lock(); 523 try { 524 if (!changed) { 525 return; 526 } 527 // Might exist if a backup has been restored etc. 528 if (fs.exists(newPath)) { 529 renameOrFail(newPath, new Path(newPath.toString() 530 + "_ORPHANED_" + System.currentTimeMillis())); 531 } 532 if (fs.exists(oldPath)) { 533 renameOrFail(oldPath, new Path(oldPath.toString() 534 + "_ORPHANED_" + System.currentTimeMillis())); 535 } 536 // put all of the updates into the keystore 537 for(Map.Entry<String, Metadata> entry: cache.entrySet()) { 538 try { 539 keyStore.setKeyEntry(entry.getKey(), new KeyMetadata(entry.getValue()), 540 password, null); 541 } catch (KeyStoreException e) { 542 throw new IOException("Can't set metadata key " + entry.getKey(),e ); 543 } 544 } 545 546 // Save old File first 547 boolean fileExisted = backupToOld(oldPath); 548 if (fileExisted) { 549 resetPath = oldPath; 550 } 551 // write out the keystore 552 // Write to _NEW path first : 553 try { 554 writeToNew(newPath); 555 } catch (IOException ioe) { 556 // rename _OLD back to curent and throw Exception 557 revertFromOld(oldPath, fileExisted); 558 resetPath = path; 559 throw ioe; 560 } 561 // Rename _NEW to CURRENT and delete _OLD 562 cleanupNewAndOld(newPath, oldPath); 563 changed = false; 564 } catch (IOException ioe) { 565 resetKeyStoreState(resetPath); 566 throw ioe; 567 } finally { 568 writeLock.unlock(); 569 } 570 } 571 572 private void resetKeyStoreState(Path path) { 573 LOG.debug("Could not flush Keystore.." 574 + "attempting to reset to previous state !!"); 575 // 1) flush cache 576 cache.clear(); 577 // 2) load keyStore from previous path 578 try { 579 loadFromPath(path, password); 580 LOG.debug("KeyStore resetting to previously flushed state !!"); 581 } catch (Exception e) { 582 LOG.debug("Could not reset Keystore to previous state", e); 583 } 584 } 585 586 private void cleanupNewAndOld(Path newPath, Path oldPath) throws IOException { 587 // Rename _NEW to CURRENT 588 renameOrFail(newPath, path); 589 // Delete _OLD 590 if (fs.exists(oldPath)) { 591 fs.delete(oldPath, true); 592 } 593 } 594 595 protected void writeToNew(Path newPath) throws IOException { 596 FSDataOutputStream out = 597 FileSystem.create(fs, newPath, permissions); 598 try { 599 keyStore.store(out, password); 600 } catch (KeyStoreException e) { 601 throw new IOException("Can't store keystore " + this, e); 602 } catch (NoSuchAlgorithmException e) { 603 throw new IOException( 604 "No such algorithm storing keystore " + this, e); 605 } catch (CertificateException e) { 606 throw new IOException( 607 "Certificate exception storing keystore " + this, e); 608 } 609 out.close(); 610 } 611 612 protected boolean backupToOld(Path oldPath) 613 throws IOException { 614 boolean fileExisted = false; 615 if (fs.exists(path)) { 616 renameOrFail(path, oldPath); 617 fileExisted = true; 618 } 619 return fileExisted; 620 } 621 622 private void revertFromOld(Path oldPath, boolean fileExisted) 623 throws IOException { 624 if (fileExisted) { 625 renameOrFail(oldPath, path); 626 } 627 } 628 629 630 private void renameOrFail(Path src, Path dest) 631 throws IOException { 632 if (!fs.rename(src, dest)) { 633 throw new IOException("Rename unsuccessful : " 634 + String.format("'%s' to '%s'", src, dest)); 635 } 636 } 637 638 @Override 639 public String toString() { 640 return uri.toString(); 641 } 642 643 /** 644 * The factory to create JksProviders, which is used by the ServiceLoader. 645 */ 646 public static class Factory extends KeyProviderFactory { 647 @Override 648 public KeyProvider createProvider(URI providerName, 649 Configuration conf) throws IOException { 650 if (SCHEME_NAME.equals(providerName.getScheme())) { 651 return new JavaKeyStoreProvider(providerName, conf); 652 } 653 return null; 654 } 655 } 656 657 /** 658 * An adapter between a KeyStore Key and our Metadata. This is used to store 659 * the metadata in a KeyStore even though isn't really a key. 660 */ 661 public static class KeyMetadata implements Key, Serializable { 662 private Metadata metadata; 663 private final static long serialVersionUID = 8405872419967874451L; 664 665 private KeyMetadata(Metadata meta) { 666 this.metadata = meta; 667 } 668 669 @Override 670 public String getAlgorithm() { 671 return metadata.getCipher(); 672 } 673 674 @Override 675 public String getFormat() { 676 return KEY_METADATA; 677 } 678 679 @Override 680 public byte[] getEncoded() { 681 return new byte[0]; 682 } 683 684 private void writeObject(ObjectOutputStream out) throws IOException { 685 byte[] serialized = metadata.serialize(); 686 out.writeInt(serialized.length); 687 out.write(serialized); 688 } 689 690 private void readObject(ObjectInputStream in 691 ) throws IOException, ClassNotFoundException { 692 byte[] buf = new byte[in.readInt()]; 693 in.readFully(buf); 694 metadata = new Metadata(buf); 695 } 696 697 } 698 }