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 org.apache.commons.io.IOUtils;
022import org.apache.hadoop.classification.InterfaceAudience;
023import org.apache.hadoop.classification.InterfaceAudience.Private;
024import org.apache.hadoop.conf.Configuration;
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.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import com.google.common.annotations.VisibleForTesting;
035
036import javax.crypto.spec.SecretKeySpec;
037
038import java.io.IOException;
039import java.io.InputStream;
040import java.io.ObjectInputStream;
041import java.io.ObjectOutputStream;
042import java.io.Serializable;
043import java.net.URI;
044import java.net.URL;
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://[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
087public 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}