blob: 68ecd08747079eca2419677a587c6efe3b97aca8 [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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
import javax.crypto.KeyGenerator;
import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_CRYPTO_JCEKS_KEY_SERIALFILTER;
/**
* A provider of secret key material for Hadoop applications. Provides an
* abstraction to separate key storage from users of encryption. It
* is intended to support getting or storing keys in a variety of ways,
* including third party bindings.
* <P/>
* <code>KeyProvider</code> implementations must be thread safe.
*/
@InterfaceAudience.Public
@InterfaceStability.Unstable
public abstract class KeyProvider {
public static final String DEFAULT_CIPHER_NAME =
CommonConfigurationKeysPublic.HADOOP_SECURITY_KEY_DEFAULT_CIPHER_KEY;
public static final String DEFAULT_CIPHER =
CommonConfigurationKeysPublic.HADOOP_SECURITY_KEY_DEFAULT_CIPHER_DEFAULT;
public static final String DEFAULT_BITLENGTH_NAME =
CommonConfigurationKeysPublic.HADOOP_SECURITY_KEY_DEFAULT_BITLENGTH_KEY;
public static final int DEFAULT_BITLENGTH = CommonConfigurationKeysPublic.
HADOOP_SECURITY_KEY_DEFAULT_BITLENGTH_DEFAULT;
public static final String JCEKS_KEY_SERIALFILTER_DEFAULT =
"java.lang.Enum;"
+ "java.security.KeyRep;"
+ "java.security.KeyRep$Type;"
+ "javax.crypto.spec.SecretKeySpec;"
+ "org.apache.hadoop.crypto.key.JavaKeyStoreProvider$KeyMetadata;"
+ "!*";
public static final String JCEKS_KEY_SERIAL_FILTER = "jceks.key.serialFilter";
private final Configuration conf;
/**
* The combination of both the key version name and the key material.
*/
public static class KeyVersion {
private final String name;
private final String versionName;
private final byte[] material;
protected KeyVersion(String name, String versionName,
byte[] material) {
this.name = name == null ? null : name.intern();
this.versionName = versionName == null ? null : versionName.intern();
this.material = material;
}
public String getName() {
return name;
}
public String getVersionName() {
return versionName;
}
public byte[] getMaterial() {
return material;
}
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("key(");
buf.append(versionName);
buf.append(")=");
if (material == null) {
buf.append("null");
} else {
for(byte b: material) {
buf.append(' ');
int right = b & 0xff;
if (right < 0x10) {
buf.append('0');
}
buf.append(Integer.toHexString(right));
}
}
return buf.toString();
}
}
/**
* Key metadata that is associated with the key.
*/
public static class Metadata {
private final static String CIPHER_FIELD = "cipher";
private final static String BIT_LENGTH_FIELD = "bitLength";
private final static String CREATED_FIELD = "created";
private final static String DESCRIPTION_FIELD = "description";
private final static String VERSIONS_FIELD = "versions";
private final static String ATTRIBUTES_FIELD = "attributes";
private final String cipher;
private final int bitLength;
private final String description;
private final Date created;
private int versions;
private Map<String, String> attributes;
protected Metadata(String cipher, int bitLength, String description,
Map<String, String> attributes, Date created, int versions) {
this.cipher = cipher;
this.bitLength = bitLength;
this.description = description;
this.attributes = (attributes == null || attributes.isEmpty())
? null : attributes;
this.created = created;
this.versions = versions;
}
public String toString() {
final StringBuilder metaSB = new StringBuilder();
metaSB.append("cipher: ").append(cipher).append(", ");
metaSB.append("length: ").append(bitLength).append(", ");
metaSB.append("description: ").append(description).append(", ");
metaSB.append("created: ").append(created).append(", ");
metaSB.append("version: ").append(versions).append(", ");
metaSB.append("attributes: ");
if ((attributes != null) && !attributes.isEmpty()) {
for (Map.Entry<String, String> attribute : attributes.entrySet()) {
metaSB.append("[");
metaSB.append(attribute.getKey());
metaSB.append("=");
metaSB.append(attribute.getValue());
metaSB.append("], ");
}
metaSB.deleteCharAt(metaSB.length() - 2); // remove last ', '
} else {
metaSB.append("null");
}
return metaSB.toString();
}
public String getDescription() {
return description;
}
public Date getCreated() {
return created;
}
public String getCipher() {
return cipher;
}
@SuppressWarnings("unchecked")
public Map<String, String> getAttributes() {
return (attributes == null) ? Collections.EMPTY_MAP : attributes;
}
/**
* Get the algorithm from the cipher.
* @return the algorithm name
*/
public String getAlgorithm() {
int slash = cipher.indexOf('/');
if (slash == - 1) {
return cipher;
} else {
return cipher.substring(0, slash);
}
}
public int getBitLength() {
return bitLength;
}
public int getVersions() {
return versions;
}
protected int addVersion() {
return versions++;
}
/**
* Serialize the metadata to a set of bytes.
* @return the serialized bytes
* @throws IOException
*/
protected byte[] serialize() throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
JsonWriter writer = new JsonWriter(
new OutputStreamWriter(buffer, StandardCharsets.UTF_8));
try {
writer.beginObject();
if (cipher != null) {
writer.name(CIPHER_FIELD).value(cipher);
}
if (bitLength != 0) {
writer.name(BIT_LENGTH_FIELD).value(bitLength);
}
if (created != null) {
writer.name(CREATED_FIELD).value(created.getTime());
}
if (description != null) {
writer.name(DESCRIPTION_FIELD).value(description);
}
if (attributes != null && attributes.size() > 0) {
writer.name(ATTRIBUTES_FIELD).beginObject();
for (Map.Entry<String, String> attribute : attributes.entrySet()) {
writer.name(attribute.getKey()).value(attribute.getValue());
}
writer.endObject();
}
writer.name(VERSIONS_FIELD).value(versions);
writer.endObject();
writer.flush();
} finally {
writer.close();
}
return buffer.toByteArray();
}
/**
* Deserialize a new metadata object from a set of bytes.
* @param bytes the serialized metadata
* @throws IOException
*/
protected Metadata(byte[] bytes) throws IOException {
String cipher = null;
int bitLength = 0;
Date created = null;
int versions = 0;
String description = null;
Map<String, String> attributes = null;
JsonReader reader =
new JsonReader(new InputStreamReader(new ByteArrayInputStream(bytes),
StandardCharsets.UTF_8));
try {
reader.beginObject();
while (reader.hasNext()) {
String field = reader.nextName();
if (CIPHER_FIELD.equals(field)) {
cipher = reader.nextString();
} else if (BIT_LENGTH_FIELD.equals(field)) {
bitLength = reader.nextInt();
} else if (CREATED_FIELD.equals(field)) {
created = new Date(reader.nextLong());
} else if (VERSIONS_FIELD.equals(field)) {
versions = reader.nextInt();
} else if (DESCRIPTION_FIELD.equals(field)) {
description = reader.nextString();
} else if (ATTRIBUTES_FIELD.equalsIgnoreCase(field)) {
reader.beginObject();
attributes = new HashMap<String, String>();
while (reader.hasNext()) {
attributes.put(reader.nextName(), reader.nextString());
}
reader.endObject();
}
}
reader.endObject();
} finally {
reader.close();
}
this.cipher = cipher;
this.bitLength = bitLength;
this.created = created;
this.description = description;
this.attributes = attributes;
this.versions = versions;
}
}
/**
* Options when creating key objects.
*/
public static class Options {
private String cipher;
private int bitLength;
private String description;
private Map<String, String> attributes;
public Options(Configuration conf) {
cipher = conf.get(DEFAULT_CIPHER_NAME, DEFAULT_CIPHER);
bitLength = conf.getInt(DEFAULT_BITLENGTH_NAME, DEFAULT_BITLENGTH);
}
public Options setCipher(String cipher) {
this.cipher = cipher;
return this;
}
public Options setBitLength(int bitLength) {
this.bitLength = bitLength;
return this;
}
public Options setDescription(String description) {
this.description = description;
return this;
}
public Options setAttributes(Map<String, String> attributes) {
if (attributes != null) {
if (attributes.containsKey(null)) {
throw new IllegalArgumentException("attributes cannot have a NULL key");
}
this.attributes = new HashMap<String, String>(attributes);
}
return this;
}
public String getCipher() {
return cipher;
}
public int getBitLength() {
return bitLength;
}
public String getDescription() {
return description;
}
@SuppressWarnings("unchecked")
public Map<String, String> getAttributes() {
return (attributes == null) ? Collections.EMPTY_MAP : attributes;
}
@Override
public String toString() {
return "Options{" +
"cipher='" + cipher + '\'' +
", bitLength=" + bitLength +
", description='" + description + '\'' +
", attributes=" + attributes +
'}';
}
}
/**
* Constructor.
*
* @param conf configuration for the provider
*/
public KeyProvider(Configuration conf) {
this.conf = new Configuration(conf);
// Added for HADOOP-15473. Configured serialFilter property fixes
// java.security.UnrecoverableKeyException in JDK 8u171.
if(System.getProperty(JCEKS_KEY_SERIAL_FILTER) == null) {
String serialFilter =
conf.get(HADOOP_SECURITY_CRYPTO_JCEKS_KEY_SERIALFILTER,
JCEKS_KEY_SERIALFILTER_DEFAULT);
System.setProperty(JCEKS_KEY_SERIAL_FILTER, serialFilter);
}
}
/**
* Return the provider configuration.
*
* @return the provider configuration
*/
public Configuration getConf() {
return conf;
}
/**
* A helper function to create an options object.
* @param conf the configuration to use
* @return a new options object
*/
public static Options options(Configuration conf) {
return new Options(conf);
}
/**
* Indicates whether this provider represents a store
* that is intended for transient use - such as the UserProvider
* is. These providers are generally used to provide access to
* keying material rather than for long term storage.
* @return true if transient, false otherwise
*/
public boolean isTransient() {
return false;
}
/**
* Get the key material for a specific version of the key. This method is used
* when decrypting data.
* @param versionName the name of a specific version of the key
* @return the key material
* @throws IOException
*/
public abstract KeyVersion getKeyVersion(String versionName
) throws IOException;
/**
* Get the key names for all keys.
* @return the list of key names
* @throws IOException
*/
public abstract List<String> getKeys() throws IOException;
/**
* Get key metadata in bulk.
* @param names the names of the keys to get
* @throws IOException
*/
public Metadata[] getKeysMetadata(String... names) throws IOException {
Metadata[] result = new Metadata[names.length];
for (int i=0; i < names.length; ++i) {
result[i] = getMetadata(names[i]);
}
return result;
}
/**
* Get the key material for all versions of a specific key name.
* @return the list of key material
* @throws IOException
*/
public abstract List<KeyVersion> getKeyVersions(String name) throws IOException;
/**
* Get the current version of the key, which should be used for encrypting new
* data.
* @param name the base name of the key
* @return the version name of the current version of the key or null if the
* key version doesn't exist
* @throws IOException
*/
public KeyVersion getCurrentKey(String name) throws IOException {
Metadata meta = getMetadata(name);
if (meta == null) {
return null;
}
return getKeyVersion(buildVersionName(name, meta.getVersions() - 1));
}
/**
* Get metadata about the key.
* @param name the basename of the key
* @return the key's metadata or null if the key doesn't exist
* @throws IOException
*/
public abstract Metadata getMetadata(String name) throws IOException;
/**
* Create a new key. The given key must not already exist.
* @param name the base name of the key
* @param material the key material for the first version of the key.
* @param options the options for the new key.
* @return the version name of the first version of the key.
* @throws IOException
*/
public abstract KeyVersion createKey(String name, byte[] material,
Options options) throws IOException;
/**
* Get the algorithm from the cipher.
*
* @return the algorithm name
*/
private String getAlgorithm(String cipher) {
int slash = cipher.indexOf('/');
if (slash == -1) {
return cipher;
} else {
return cipher.substring(0, slash);
}
}
/**
* Generates a key material.
*
* @param size length of the key.
* @param algorithm algorithm to use for generating the key.
* @return the generated key.
* @throws NoSuchAlgorithmException
*/
protected byte[] generateKey(int size, String algorithm)
throws NoSuchAlgorithmException {
algorithm = getAlgorithm(algorithm);
KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
keyGenerator.init(size);
byte[] key = keyGenerator.generateKey().getEncoded();
return key;
}
/**
* Create a new key generating the material for it.
* The given key must not already exist.
* <p/>
* This implementation generates the key material and calls the
* {@link #createKey(String, byte[], Options)} method.
*
* @param name the base name of the key
* @param options the options for the new key.
* @return the version name of the first version of the key.
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public KeyVersion createKey(String name, Options options)
throws NoSuchAlgorithmException, IOException {
byte[] material = generateKey(options.getBitLength(), options.getCipher());
return createKey(name, material, options);
}
/**
* Delete the given key.
* @param name the name of the key to delete
* @throws IOException
*/
public abstract void deleteKey(String name) throws IOException;
/**
* Roll a new version of the given key.
* @param name the basename of the key
* @param material the new key material
* @return the name of the new version of the key
* @throws IOException
*/
public abstract KeyVersion rollNewVersion(String name,
byte[] material
) throws IOException;
/**
* Can be used by implementing classes to close any resources
* that require closing
*/
public void close() throws IOException {
// NOP
}
/**
* Roll a new version of the given key generating the material for it.
* <p/>
* This implementation generates the key material and calls the
* {@link #rollNewVersion(String, byte[])} method.
*
* @param name the basename of the key
* @return the name of the new version of the key
* @throws IOException
*/
public KeyVersion rollNewVersion(String name) throws NoSuchAlgorithmException,
IOException {
Metadata meta = getMetadata(name);
if (meta == null) {
throw new IOException("Can't find Metadata for key " + name);
}
byte[] material = generateKey(meta.getBitLength(), meta.getCipher());
return rollNewVersion(name, material);
}
/**
* Ensures that any changes to the keys are written to persistent store.
* @throws IOException
*/
public abstract void flush() throws IOException;
/**
* Split the versionName in to a base name. Converts "/aaa/bbb/3" to
* "/aaa/bbb".
* @param versionName the version name to split
* @return the base name of the key
* @throws IOException
*/
public static String getBaseName(String versionName) throws IOException {
int div = versionName.lastIndexOf('@');
if (div == -1) {
throw new IOException("No version in key path " + versionName);
}
return versionName.substring(0, div);
}
/**
* Build a version string from a basename and version number. Converts
* "/aaa/bbb" and 3 to "/aaa/bbb@3".
* @param name the basename of the key
* @param version the version of the key
* @return the versionName of the key.
*/
protected static String buildVersionName(String name, int version) {
return name + "@" + version;
}
/**
* Find the provider with the given key.
* @param providerList the list of providers
* @param keyName the key name we are looking for
* @return the KeyProvider that has the key
*/
public static KeyProvider findProvider(List<KeyProvider> providerList,
String keyName) throws IOException {
for(KeyProvider provider: providerList) {
if (provider.getMetadata(keyName) != null) {
return provider;
}
}
throw new IOException("Can't find KeyProvider for key " + keyName);
}
/**
* Does this provider require a password? This means that a password is
* required for normal operation, and it has not been found through normal
* means. If true, the password should be provided by the caller using
* setPassword().
* @return Whether or not the provider requires a password
* @throws IOException
*/
public boolean needsPassword() throws IOException {
return false;
}
/**
* If a password for the provider is needed, but is not provided, this will
* return a warning and instructions for supplying said password to the
* provider.
* @return A warning and instructions for supplying the password
*/
public String noPasswordWarning() {
return null;
}
/**
* If a password for the provider is needed, but is not provided, this will
* return an error message and instructions for supplying said password to
* the provider.
* @return An error message and instructions for supplying the password
*/
public String noPasswordError() {
return null;
}
}