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}