Java tutorial
/** * 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.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; 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; } @Override 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(); } @Override public boolean equals(Object rhs) { if (this == rhs) { return true; } if (rhs == null || getClass() != rhs.getClass()) { return false; } final KeyVersion kv = (KeyVersion) rhs; return new EqualsBuilder().append(name, kv.name).append(versionName, kv.versionName) .append(material, kv.material).isEquals(); } @Override public int hashCode() { return new HashCodeBuilder().append(name).append(versionName).append(material).toHashCode(); } } /** * 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; } public Map<String, String> getAttributes() { return (attributes == null) ? Collections.emptyMap() : 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; } public Map<String, String> getAttributes() { return (attributes == null) ? Collections.emptyMap() : 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); } /** * Can be used by implementing classes to invalidate the caches. This could be * used after rollNewVersion to provide a strong guarantee to return the new * version of the given key. * * @param name the basename of the key * @throws IOException */ public void invalidateCache(String name) throws IOException { // NOP } /** * 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; } }