/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package gobblin.crypto; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.EnumSet; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import lombok.extern.slf4j.Slf4j; /** * A CredentialStore that uses Java's Keystore implementation to store and retrieve keys. */ @Slf4j public class JCEKSKeystoreCredentialStore implements CredentialStore { public final static String TAG = "java"; private final KeyStore ks; private final char[] password; private final Path path; private final FileSystem fs; /** * Options that can be used while instantiating a new keystore */ public enum CreationOptions { /** * Create an empty keystore if one can't be found. Otherwise an exception will be thrown. */ CREATE_IF_MISSING } /** * Instantiate a new keystore at the given path protected by a password. * @param path Path to find the keystore * @param passwordStr Password the keystore is protected with * @throws IOException If the keystore cannot be loaded because the password is wrong or the file has been corrupted. * @throws IllegalArgumentException If the keystore does not exist at the given path. */ public JCEKSKeystoreCredentialStore(String path, String passwordStr) throws IOException { this(path, passwordStr, EnumSet.noneOf(CreationOptions.class)); } /** * Instantiate a new keystore at the given path protected by a password. * @param path Path to find the keystore * @param passwordStr Password the keystore is protected with * @param options Flags for keystore creation * @throws IOException If the keystore cannot be loaded because of a corrupt file or the password is wrong * @throws IllegalArgumentException If CREATE_IF_MISSING is not present in options and the keystore does not exist * at the given path. */ public JCEKSKeystoreCredentialStore(String path, String passwordStr, EnumSet<CreationOptions> options) throws IOException { this(new Path(path), passwordStr, options); } /** * Instantiate a new keystore at the given path protected by a password. * @param path Path to find the keystore * @param passwordStr Password the keystore is protected with * @param options Flags for keystore creation * @throws IOException If the keystore cannot be loaded because of a corrupt file or the password is wrong * @throws IllegalArgumentException If CREATE_IF_MISSING is not present in options and the keystore does not exist * at the given path. */ public JCEKSKeystoreCredentialStore(Path path, String passwordStr, EnumSet<CreationOptions> options) throws IOException { try { this.ks = KeyStore.getInstance("JCEKS"); this.password = passwordStr.toCharArray(); this.path = path; this.fs = path.getFileSystem(new Configuration()); if (!fs.exists(path)) { if (options.contains(CreationOptions.CREATE_IF_MISSING)) { log.info("No keystore found at " + path + ", creating from scratch"); ks.load(null, password); } else { throw new IllegalArgumentException("Keystore " + path + " does not exist"); } } else { try (InputStream fis = fs.open(path)) { ks.load(fis, password); log.info("Successfully loaded keystore from " + path); } } } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException e) { throw new IllegalStateException("Unexpected failure initializing keystore", e); } } @Override public byte[] getEncodedKey(String id) { try { Key k = ks.getKey(id, password); return (k == null) ? null : k.getEncoded(); } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { log.warn("Error trying to decode key " + id, e); return null; } } @Override public Map<String, byte[]> getAllEncodedKeys() { Map<String, byte[]> ret = new HashMap<>(); try { Enumeration<String> aliases = ks.aliases(); while (aliases.hasMoreElements()) { String key = aliases.nextElement(); try { if (ks.isKeyEntry(key)) { ret.put(key, getEncodedKey(key)); } } catch (KeyStoreException e) { log.warn("Error trying to decode key id " + key + ", not returning in list", e); } } } catch (KeyStoreException e) { log.warn("Error retrieving all aliases in keystore; treating as empty", e); return ret; } return ret; } /** * Generate a set of AES keys for the store. The key ids will simple be (startOffset ... startOffset + numKeys). * @param numKeys Number of keys to generate * @param startOffset ID to start generating keys with * @throws IOException If there is an error serializing the keystore back to disk * @throws KeyStoreException If there is an error serializing the keystore back to disk */ public void generateAesKeys(int numKeys, int startOffset) throws IOException, KeyStoreException { for (int i = 1; i <= numKeys; i++) { SecretKey key = generateKey(); ks.setEntry(String.valueOf(i + startOffset), new KeyStore.SecretKeyEntry(key), new KeyStore.PasswordProtection(password)); } saveKeystore(); } private SecretKey generateKey() { SecureRandom r = new SecureRandom(); byte[] keyBytes = new byte[16]; r.nextBytes(keyBytes); return new SecretKeySpec(keyBytes, "AES"); } private void saveKeystore() throws IOException { try (OutputStream fOs = fs.create(path, true)) { ks.store(fOs, password); } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException e) { throw new IOException("Error serializing keystore", e); } } }