blob: 448469b99c97ad3d7afff7c2d90db575b76c9798 [file] [log] [blame]
/*
* 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 org.apache.hadoop.crypto.key;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.security.Key;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.conf.Configuration;
import org.codehaus.jackson.map.ObjectMapper;
import org.apache.hadoop.fs.Path;
import org.apache.ranger.credentialapi.CredentialReader;
import org.apache.ranger.kms.dao.DaoManager;
import org.apache.log4j.Logger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@InterfaceAudience.Private
public class RangerKeyStoreProvider extends KeyProvider{
static final Logger logger = Logger.getLogger(RangerKeyStoreProvider.class);
public static final String SCHEME_NAME = "dbks";
public static final String KMS_CONFIG_DIR = "kms.config.dir";
public static final String DBKS_SITE_XML = "dbks-site.xml";
public static final String ENCRYPTION_KEY = "ranger.db.encrypt.key.password";
private static final String KEY_METADATA = "KeyMetadata";
private static final String CREDENTIAL_PATH = "ranger.ks.jpa.jdbc.credential.provider.path";
private static final String MK_CREDENTIAL_ALIAS = "ranger.ks.masterkey.credential.alias";
private static final String DB_CREDENTIAL_ALIAS = "ranger.ks.jpa.jdbc.credential.alias";
private static final String DB_PASSWORD = "ranger.ks.jpa.jdbc.password";
private static final String HSM_ENABLED = "ranger.ks.hsm.enabled";
private static final String HSM_PARTITION_PASSWORD_ALIAS = "ranger.ks.hsm.partition.password.alias";
private static final String HSM_PARTITION_PASSWORD = "ranger.ks.hsm.partition.password";
private static final String KEYSECURE_ENABLED = "ranger.kms.keysecure.enabled";
private static final String KEYSECURE_USERNAME = "ranger.kms.keysecure.login.username";
private static final String KEYSECURE_PASSWORD_ALIAS = "ranger.kms.keysecure.login.password.alias";
private static final String KEYSECURE_PASSWORD = "ranger.kms.keysecure.login.password";
private static final String KEYSECURE_LOGIN = "ranger.kms.keysecure.login";
private final RangerKeyStore dbStore;
private char[] masterKey;
private boolean changed = false;
private final Map<String, Metadata> cache = new HashMap<String, Metadata>();
private DaoManager daoManager;
private Lock readLock;
public RangerKeyStoreProvider(Configuration conf) throws Throwable {
super(conf);
conf = getDBKSConf();
getFromJceks(conf, CREDENTIAL_PATH, MK_CREDENTIAL_ALIAS, ENCRYPTION_KEY);
getFromJceks(conf, CREDENTIAL_PATH, DB_CREDENTIAL_ALIAS, DB_PASSWORD);
getFromJceks(conf, CREDENTIAL_PATH, HSM_PARTITION_PASSWORD_ALIAS,
HSM_PARTITION_PASSWORD);
RangerKMSDB rangerKMSDB = new RangerKMSDB(conf);
daoManager = rangerKMSDB.getDaoManager();
RangerKMSMKI rangerMasterKey = null;
String password = conf.get(ENCRYPTION_KEY);
if (password == null || password.trim().equals("")
|| password.trim().equals("_")
|| password.trim().equals("crypted")) {
throw new IOException("Master Key Jceks does not exists");
}
if (StringUtils.isEmpty(conf.get(HSM_ENABLED))
|| conf.get(HSM_ENABLED).equalsIgnoreCase("false")) {
rangerMasterKey = new RangerMasterKey(daoManager);
} else {
rangerMasterKey = new RangerHSM(conf);
String partitionPasswd = conf.get(HSM_PARTITION_PASSWORD);
if (partitionPasswd == null || partitionPasswd.trim().equals("")
|| partitionPasswd.trim().equals("_")
|| partitionPasswd.trim().equals("crypted")) {
throw new IOException("Partition Password doesn't exists");
}
}
if (conf != null && StringUtils.isNotEmpty(conf.get(KEYSECURE_ENABLED))
&& conf.get(KEYSECURE_ENABLED).equalsIgnoreCase("true")) {
getFromJceks(conf, CREDENTIAL_PATH, KEYSECURE_PASSWORD_ALIAS, KEYSECURE_PASSWORD);
String keySecureLoginCred = conf.get(KEYSECURE_USERNAME).trim() + ":" + conf.get(KEYSECURE_PASSWORD);
conf.set(KEYSECURE_LOGIN, keySecureLoginCred);
rangerMasterKey = new RangerSafenetKeySecure(conf);
dbStore = new RangerKeyStore(daoManager);
// generate master key on key secure server
rangerMasterKey.generateMasterKey(password);
try {
masterKey = rangerMasterKey.getMasterKey(password)
.toCharArray();
} catch (Exception ex) {
throw new Exception("Error while getting Safenet KeySecure master key " + ex);
}
} else {
dbStore = new RangerKeyStore(daoManager);
rangerMasterKey.generateMasterKey(password);
// code to retrieve rangerMasterKey password
try {
masterKey = rangerMasterKey.getMasterKey(password)
.toCharArray();
} catch (Exception ex) {
throw new Exception("Error while getting Ranger Master key " + ex);
}
}
reloadKeys();
ReadWriteLock lock = new ReentrantReadWriteLock(true);
readLock = lock.readLock();
}
public static Configuration getDBKSConf() {
Configuration newConfig = getConfiguration(true, DBKS_SITE_XML);
getFromJceks(newConfig,CREDENTIAL_PATH, MK_CREDENTIAL_ALIAS, ENCRYPTION_KEY);
getFromJceks(newConfig,CREDENTIAL_PATH, DB_CREDENTIAL_ALIAS, DB_PASSWORD);
return newConfig;
}
static Configuration getConfiguration(boolean loadHadoopDefaults,
String ... resources) {
Configuration conf = new Configuration(loadHadoopDefaults);
String confDir = System.getProperty(KMS_CONFIG_DIR);
if (confDir != null) {
try {
Path confPath = new Path(confDir);
if (!confPath.isUriPathAbsolute()) {
throw new RuntimeException("System property '" + KMS_CONFIG_DIR +
"' must be an absolute path: " + confDir);
}
for (String resource : resources) {
conf.addResource(new URL("file://" + new Path(confDir, resource).toUri()));
}
} catch (MalformedURLException ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
} else {
for (String resource : resources) {
conf.addResource(resource);
}
}
return conf;
}
private void loadKeys(char[] masterKey) throws NoSuchAlgorithmException, CertificateException, IOException {
dbStore.engineLoad(null, masterKey);
}
@Override
public KeyVersion createKey(String name, byte[] material, Options options)
throws IOException {
reloadKeys();
if (dbStore.engineContainsAlias(name) || cache.containsKey(name)) {
throw new IOException("Key " + name + " already exists");
}
Metadata meta = new Metadata(options.getCipher(), options.getBitLength(),
options.getDescription(), options.getAttributes(), new Date(), 1);
if (options.getBitLength() != 8 * material.length) {
throw new IOException("Wrong key length. Required " +
options.getBitLength() + ", but got " + (8 * material.length));
}
cache.put(name, meta);
String versionName = buildVersionName(name, 0);
return innerSetKeyVersion(name, versionName, material, meta.getCipher(), meta.getBitLength(), meta.getDescription(), meta.getVersions(), meta.getAttributes());
}
KeyVersion innerSetKeyVersion(String name, String versionName, byte[] material, String cipher, int bitLength, String description, int version, Map<String, String> attributes) throws IOException {
try {
ObjectMapper om = new ObjectMapper();
String attribute = om.writeValueAsString(attributes);
dbStore.addKeyEntry(versionName, new SecretKeySpec(material, cipher), masterKey, cipher, bitLength, description, version, attribute);
} catch (KeyStoreException e) {
throw new IOException("Can't store key " + versionName,e);
}
changed = true;
return new KeyVersion(name, versionName, material);
}
@Override
public void deleteKey(String name) throws IOException {
reloadKeys();
Metadata meta = getMetadata(name);
if (meta == null) {
throw new IOException("Key " + name + " does not exist");
}
for(int v=0; v < meta.getVersions(); ++v) {
String versionName = buildVersionName(name, v);
try {
if (dbStore.engineContainsAlias(versionName)) {
dbStore.engineDeleteEntry(versionName);
}
} catch (KeyStoreException e) {
throw new IOException("Problem removing " + versionName, e);
}
}
try {
if (dbStore.engineContainsAlias(name)) {
dbStore.engineDeleteEntry(name);
}
} catch (KeyStoreException e) {
throw new IOException("Problem removing " + name + " from " + this, e);
}
cache.remove(name);
changed = true;
}
@Override
public void flush() throws IOException {
try {
if (!changed) {
return;
}
// put all of the updates into the db
for(Map.Entry<String, Metadata> entry: cache.entrySet()) {
try {
Metadata metadata = entry.getValue();
ObjectMapper om = new ObjectMapper();
String attributes = om.writeValueAsString(metadata.getAttributes());
dbStore.addKeyEntry(entry.getKey(), new KeyMetadata(metadata), masterKey, metadata.getAlgorithm(), metadata.getBitLength(), metadata.getDescription(), metadata.getVersions(), attributes);
} catch (KeyStoreException e) {
throw new IOException("Can't set metadata key " + entry.getKey(),e );
}
}
try {
dbStore.engineStore(null, masterKey);
reloadKeys();
} catch (NoSuchAlgorithmException e) {
throw new IOException("No such algorithm storing key", e);
} catch (CertificateException e) {
throw new IOException("Certificate exception storing key", e);
}
changed = false;
}catch (IOException ioe) {
cache.clear();
reloadKeys();
throw ioe;
}
}
@Override
public KeyVersion getKeyVersion(String versionName) throws IOException {
readLock.lock();
try {
SecretKeySpec key = null;
try {
if (!dbStore.engineContainsAlias(versionName)) {
dbStore.engineLoad(null, masterKey);
if (!dbStore.engineContainsAlias(versionName)) {
return null;
}
}
key = (SecretKeySpec) dbStore.engineGetKey(versionName,
masterKey);
} catch (NoSuchAlgorithmException e) {
throw new IOException("Can't get algorithm for key " + key, e);
} catch (UnrecoverableKeyException e) {
throw new IOException("Can't recover key " + key, e);
} catch (CertificateException e) {
throw new IOException("Certificate exception storing key", e);
}
if (key == null) {
return null;
} else {
return new KeyVersion(getBaseName(versionName), versionName,
key.getEncoded());
}
} finally {
readLock.unlock();
}
}
@Override
public List<KeyVersion> getKeyVersions(String name) throws IOException {
List<KeyVersion> list = new ArrayList<KeyVersion>();
Metadata km = getMetadata(name);
if (km != null) {
int latestVersion = km.getVersions();
KeyVersion v = null;
String versionName = null;
for (int i = 0; i < latestVersion; i++) {
versionName = buildVersionName(name, i);
v = getKeyVersion(versionName);
if (v != null) {
list.add(v);
}
}
}
return list;
}
@Override
public List<String> getKeys() throws IOException {
ArrayList<String> list = new ArrayList<String>();
String alias = null;
reloadKeys();
Enumeration<String> e = dbStore.engineAliases();
while (e.hasMoreElements()) {
alias = e.nextElement();
// only include the metadata key names in the list of names
if (!alias.contains("@")) {
list.add(alias);
}
}
return list;
}
@Override
public Metadata getMetadata(String name) throws IOException {
try {
readLock.lock();
if (cache.containsKey(name)) {
Metadata meta = cache.get(name);
return meta;
}
try {
if (!dbStore.engineContainsAlias(name)) {
dbStore.engineLoad(null, masterKey);
if (!dbStore.engineContainsAlias(name)) {
return null;
}
}
Key key = dbStore.engineGetKey(name, masterKey);
if(key != null){
Metadata meta = ((KeyMetadata) key).metadata;
cache.put(name, meta);
return meta;
}
} catch (NoSuchAlgorithmException e) {
throw new IOException("Can't get algorithm for " + name, e);
} catch (UnrecoverableKeyException e) {
throw new IOException("Can't recover key for " + name, e);
}
return null;
}
catch(Exception e){
throw new IOException("Please try again ", e);
}
finally {
readLock.unlock();
}
}
@Override
public KeyVersion rollNewVersion(String name, byte[] material)throws IOException {
reloadKeys();
Metadata meta = getMetadata(name);
if (meta == null) {
throw new IOException("Key " + name + " not found");
}
if (meta.getBitLength() != 8 * material.length) {
throw new IOException("Wrong key length. Required " + meta.getBitLength() + ", but got " + (8 * material.length));
}
int nextVersion = meta.addVersion();
String versionName = buildVersionName(name, nextVersion);
return innerSetKeyVersion(name, versionName, material, meta.getCipher(), meta.getBitLength(), meta.getDescription(), meta.getVersions(), meta.getAttributes());
}
private static void getFromJceks(Configuration conf, String path, String alias, String key){
//update credential from keystore
if(conf!=null){
String pathValue=conf.get(path);
String aliasValue=conf.get(alias);
if(pathValue!=null && aliasValue!=null){
String xaDBPassword=CredentialReader.getDecryptedString(pathValue.trim(),aliasValue.trim());
if(xaDBPassword!=null&& !xaDBPassword.trim().isEmpty() &&
!xaDBPassword.trim().equalsIgnoreCase("none")){
conf.set(key, xaDBPassword);
}else{
logger.info("Credential keystore password not applied for KMS; clear text password shall be applicable");
}
}
}
}
private void reloadKeys() throws IOException {
try {
cache.clear();
loadKeys(masterKey);
} catch (NoSuchAlgorithmException e) {
throw new IOException("Can't load Keys");
}catch(CertificateException e){
throw new IOException("Can't load Keys");
}
}
/**
* The factory to create JksProviders, which is used by the ServiceLoader.
*/
public static class Factory extends KeyProviderFactory {
@Override
public KeyProvider createProvider(URI providerName,
Configuration conf) throws IOException {
try {
if (SCHEME_NAME.equals(providerName.getScheme())) {
return new RangerKeyStoreProvider(conf);
}
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
}
/**
* An adapter between a KeyStore Key and our Metadata. This is used to store
* the metadata in a KeyStore even though isn't really a key.
*/
public static class KeyMetadata implements Key, Serializable {
private Metadata metadata;
private final static long serialVersionUID = 8405872419967874451L;
private KeyMetadata(Metadata meta) {
this.metadata = meta;
}
@Override
public String getAlgorithm() {
return metadata.getCipher();
}
@Override
public String getFormat() {
return KEY_METADATA;
}
@Override
public byte[] getEncoded() {
return new byte[0];
}
private void writeObject(ObjectOutputStream out) throws IOException {
byte[] serialized = metadata.serialize();
out.writeInt(serialized.length);
out.write(serialized);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
byte[] buf = new byte[in.readInt()];
in.readFully(buf);
metadata = new Metadata(buf);
}
}
}