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