blob: 78e6a85f34ebb83b40475bb3024f96225b3c2ad7 [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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.orc;
import org.apache.orc.impl.HadoopShims;
import org.apache.orc.impl.KeyProvider;
import org.apache.orc.impl.LocalKey;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
/**
* This is an in-memory implementation of {@link KeyProvider}.
*
* The primary use of this class is for when the user doesn't have a
* Hadoop KMS running and wishes to use encryption. It is also useful for
* testing.
*
* The local keys for this class are encrypted/decrypted using the cipher
* in CBC/NoPadding mode and a constant IV. Since the key is random, the
* constant IV is not a problem.
*
* This class is not thread safe.
*/
public class InMemoryKeystore implements KeyProvider {
/**
* Support AES 256 ?
*/
public static final boolean SUPPORTS_AES_256;
static {
try {
SUPPORTS_AES_256 = Cipher.getMaxAllowedKeyLength("AES") >= 256;
} catch (final NoSuchAlgorithmException e) {
throw new IllegalArgumentException("Unknown algorithm", e);
}
}
private final Random random;
/**
* A map that stores the 'keyName@version'
* and 'metadata + material' mapping.
*/
private final TreeMap<String, KeyVersion> keys = new TreeMap<>();
/**
* A map from the keyName (without version) to the currentVersion.
*/
private final Map<String, Integer> currentVersion = new HashMap<>();
/**
* Create a new InMemoryKeystore.
*/
public InMemoryKeystore() {
this(new SecureRandom());
}
/**
* Create an InMemoryKeystore with the given random generator.
* Except for testing, this must be a SecureRandom.
*/
public InMemoryKeystore(Random random) {
this.random = random;
}
/**
* 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.
*/
private static String buildVersionName(final String name,
final int version) {
return name + "@" + version;
}
/**
* Get the list of key names from the key provider.
*
* @return a list of key names
*/
@Override
public List<String> getKeyNames() {
return new ArrayList<>(currentVersion.keySet());
}
/**
* Get the current metadata for a given key. This is used when encrypting
* new data.
*
* @param keyName the name of a key
* @return metadata for the current version of the key
*/
@Override
public HadoopShims.KeyMetadata getCurrentKeyVersion(final String keyName) {
String versionName = buildVersionName(keyName, currentVersion.get(keyName));
if (keys.containsKey(versionName)) {
return keys.get(versionName);
} else {
throw new IllegalArgumentException("Unknown key " + keyName);
}
}
/**
* Create a local key for the given key version.
*
* @param key the master key version
* @return the local key's material
*/
@Override
public LocalKey createLocalKey(final HadoopShims.KeyMetadata key) {
final String keyVersion = buildVersionName(key.getKeyName(), key.getVersion());
if (!keys.containsKey(keyVersion)) {
throw new IllegalArgumentException("Unknown key " + key);
}
final KeyVersion secret = keys.get(keyVersion);
final EncryptionAlgorithm algorithm = secret.getAlgorithm();
byte[] encryptedKey = new byte[algorithm.keyLength()];
random.nextBytes(encryptedKey);
byte[] iv = new byte[algorithm.getIvLength()];
System.arraycopy(encryptedKey, 0, iv, 0, iv.length);
Cipher localCipher = algorithm.createCipher();
try {
localCipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(secret.getMaterial(),
algorithm.getAlgorithm()), new IvParameterSpec(iv));
} catch (final InvalidKeyException e) {
throw new IllegalStateException(
"ORC bad encryption key for " + keyVersion, e);
} catch (final InvalidAlgorithmParameterException e) {
throw new IllegalStateException(
"ORC bad encryption parameter for " + keyVersion, e);
}
try {
byte[] decryptedKey = localCipher.doFinal(encryptedKey);
return new LocalKey(algorithm, decryptedKey, encryptedKey);
} catch (final IllegalBlockSizeException e) {
throw new IllegalStateException(
"ORC bad block size for " + keyVersion, e);
} catch (final BadPaddingException e) {
throw new IllegalStateException(
"ORC bad padding for " + keyVersion, e);
}
}
/**
* Create a local key for the given key version and initialization vector.
* Given a probabilistically unique iv, it will generate a unique key
* with the master key at the specified version. This allows the encryption
* to use this local key for the encryption and decryption without ever
* having access to the master key.
* <p>
* This uses KeyProviderCryptoExtension.decryptEncryptedKey with a fixed key
* of the appropriate length.
*
* @param key the master key version
* @param encryptedKey the unique initialization vector
* @return the local key's material
*/
@Override
public Key decryptLocalKey(HadoopShims.KeyMetadata key,
byte[] encryptedKey) {
final String keyVersion = buildVersionName(key.getKeyName(), key.getVersion());
if (!keys.containsKey(keyVersion)) {
return null;
}
final KeyVersion secret = keys.get(keyVersion);
final EncryptionAlgorithm algorithm = secret.getAlgorithm();
byte[] iv = new byte[algorithm.getIvLength()];
System.arraycopy(encryptedKey, 0, iv, 0, iv.length);
Cipher localCipher = algorithm.createCipher();
try {
localCipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(secret.getMaterial(),
algorithm.getAlgorithm()), new IvParameterSpec(iv));
} catch (final InvalidKeyException e) {
throw new IllegalStateException(
"ORC bad encryption key for " + keyVersion, e);
} catch (final InvalidAlgorithmParameterException e) {
throw new IllegalStateException(
"ORC bad encryption parameter for " + keyVersion, e);
}
try {
byte[] decryptedKey = localCipher.doFinal(encryptedKey);
return new SecretKeySpec(decryptedKey, algorithm.getAlgorithm());
} catch (final IllegalBlockSizeException e) {
throw new IllegalStateException(
"ORC bad block size for " + keyVersion, e);
} catch (final BadPaddingException e) {
throw new IllegalStateException(
"ORC bad padding for " + keyVersion, e);
}
}
@Override
public HadoopShims.KeyProviderKind getKind() {
return HadoopShims.KeyProviderKind.HADOOP;
}
/**
* Function that takes care of adding a new key.<br>
* A new key can be added only if:
* <ul>
* <li>This is a new key and no prior key version exist.</li>
* <li>If the key exists (has versions), then the new version to be added should be greater than
* the version that already exists.</li>
* </ul>
*
* @param keyName Name of the key to be added
* @param algorithm Algorithm used
* @param masterKey Master key
* @return this
*/
public InMemoryKeystore addKey(String keyName, EncryptionAlgorithm algorithm,
byte[] masterKey) throws IOException {
return addKey(keyName, 0, algorithm, masterKey);
}
/**
* Function that takes care of adding a new key.<br>
* A new key can be added only if:
* <ul>
* <li>This is a new key and no prior key version exist.</li>
* <li>If the key exists (has versions), then the new version to be added should be greater than
* the version that already exists.</li>
* </ul>
*
* @param keyName Name of the key to be added
* @param version Key Version
* @param algorithm Algorithm used
* @param masterKey Master key
* @return this
*/
public InMemoryKeystore addKey(String keyName, int version,
EncryptionAlgorithm algorithm,
byte[] masterKey) throws IOException {
/* Test weather platform supports the algorithm */
if (!SUPPORTS_AES_256 && (algorithm != EncryptionAlgorithm.AES_CTR_128)) {
algorithm = EncryptionAlgorithm.AES_CTR_128;
}
final byte[] buffer = new byte[algorithm.keyLength()];
if (algorithm.keyLength() > masterKey.length) {
System.arraycopy(masterKey, 0, buffer, 0, masterKey.length);
/* fill with zeros */
Arrays.fill(buffer, masterKey.length, buffer.length - 1, (byte) 0);
} else {
System.arraycopy(masterKey, 0, buffer, 0, algorithm.keyLength());
}
final KeyVersion key = new KeyVersion(keyName, version, algorithm,
buffer);
/* Check whether the key is already present and has a smaller version */
if (currentVersion.get(keyName) != null &&
currentVersion.get(keyName) >= version) {
throw new IOException(String
.format("Key %s with equal or higher version %d already exists",
keyName, version));
}
keys.put(buildVersionName(keyName, version), key);
currentVersion.put(keyName, version);
return this;
}
/**
* This class contains the meta-data and the material for the key.
*/
static class KeyVersion extends HadoopShims.KeyMetadata {
private final byte[] material;
public KeyVersion(final String keyName, final int version,
final EncryptionAlgorithm algorithm, final byte[] material) {
super(keyName, version, algorithm);
this.material = material;
}
/**
* Get the material for the key
*
* @return the material
*/
private byte[] getMaterial() {
return material;
}
}
}