blob: 5568a8d465ac2c6137b99d2831301dd6b83b0837 [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.accumulo.core.crypto;
import static org.apache.accumulo.core.crypto.CryptoUtils.getFileDecrypter;
import static org.apache.accumulo.core.file.rfile.RFileTest.getAccumuloConfig;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import org.apache.accumulo.core.classloader.ClassLoaderUtil;
import org.apache.accumulo.core.client.Scanner;
import org.apache.accumulo.core.client.rfile.RFile;
import org.apache.accumulo.core.client.rfile.RFileWriter;
import org.apache.accumulo.core.client.summary.Summarizer;
import org.apache.accumulo.core.client.summary.SummarizerConfiguration;
import org.apache.accumulo.core.client.summary.Summary;
import org.apache.accumulo.core.conf.AccumuloConfiguration;
import org.apache.accumulo.core.conf.ConfigurationCopy;
import org.apache.accumulo.core.conf.DefaultConfiguration;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.crypto.CryptoServiceFactory.ClassloaderType;
import org.apache.accumulo.core.crypto.streams.NoFlushOutputStream;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.spi.crypto.AESCryptoService;
import org.apache.accumulo.core.spi.crypto.CryptoEnvironment;
import org.apache.accumulo.core.spi.crypto.CryptoEnvironment.Scope;
import org.apache.accumulo.core.spi.crypto.CryptoService;
import org.apache.accumulo.core.spi.crypto.CryptoService.CryptoException;
import org.apache.accumulo.core.spi.crypto.FileDecrypter;
import org.apache.accumulo.core.spi.crypto.FileEncrypter;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.junit.BeforeClass;
import org.junit.Test;
import com.google.common.collect.Iterables;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
public class CryptoTest {
public static final int MARKER_INT = 0xCADEFEDD;
public static final String MARKER_STRING = "1 2 3 4 5 6 7 8 a b c d e f g h ";
public static final String CRYPTO_ON_CONF = "ON";
public static final String CRYPTO_OFF_CONF = "OFF";
public static final String keyPath =
System.getProperty("user.dir") + "/target/CryptoTest-testkeyfile";
public static final String emptyKeyPath =
System.getProperty("user.dir") + "/target/CryptoTest-emptykeyfile";
private static Configuration hadoopConf = new Configuration();
@BeforeClass
public static void setupKeyFiles() throws Exception {
FileSystem fs = FileSystem.getLocal(hadoopConf);
Path aesPath = new Path(keyPath);
try (FSDataOutputStream out = fs.create(aesPath)) {
out.writeUTF("sixteenbytekey"); // 14 + 2 from writeUTF
}
try (FSDataOutputStream out = fs.create(new Path(emptyKeyPath))) {
// auto close after creating
assertNotNull(out);
}
}
@Test
public void simpleGCMTest() throws Exception {
AccumuloConfiguration conf = getAccumuloConfig(CRYPTO_ON_CONF);
CryptoService cs = new AESCryptoService();
cs.init(conf.getAllPropertiesWithPrefix(Property.INSTANCE_CRYPTO_PREFIX));
CryptoEnvironment encEnv = new CryptoEnvironmentImpl(Scope.RFILE, null);
FileEncrypter encrypter = cs.getFileEncrypter(encEnv);
byte[] params = encrypter.getDecryptionParameters();
assertNotNull(params);
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dataOut = new DataOutputStream(out);
CryptoUtils.writeParams(params, dataOut);
OutputStream encrypted = encrypter.encryptStream(dataOut);
assertNotNull(encrypted);
DataOutputStream cipherOut = new DataOutputStream(encrypted);
cipherOut.writeUTF(MARKER_STRING);
cipherOut.close();
dataOut.close();
encrypted.close();
out.close();
byte[] cipherText = out.toByteArray();
// decrypt
ByteArrayInputStream in = new ByteArrayInputStream(cipherText);
FileDecrypter decrypter = getFileDecrypter(cs, Scope.RFILE, new DataInputStream(in));
DataInputStream decrypted = new DataInputStream(decrypter.decryptStream(in));
String plainText = decrypted.readUTF();
decrypted.close();
in.close();
assertEquals(MARKER_STRING, new String(plainText));
}
@Test
public void testAESCryptoServiceWAL() throws Exception {
AESCryptoService cs = new AESCryptoService();
byte[] resultingBytes = encrypt(cs, Scope.WAL, CRYPTO_ON_CONF);
String stringifiedBytes = Arrays.toString(resultingBytes);
String stringifiedMarkerBytes = getStringifiedBytes(null, MARKER_STRING, MARKER_INT);
assertNotEquals(stringifiedBytes, stringifiedMarkerBytes);
decrypt(resultingBytes, Scope.WAL, CRYPTO_ON_CONF);
}
@Test
public void testAESCryptoServiceRFILE() throws Exception {
AESCryptoService cs = new AESCryptoService();
byte[] resultingBytes = encrypt(cs, Scope.RFILE, CRYPTO_ON_CONF);
String stringifiedBytes = Arrays.toString(resultingBytes);
String stringifiedMarkerBytes = getStringifiedBytes(null, MARKER_STRING, MARKER_INT);
assertNotEquals(stringifiedBytes, stringifiedMarkerBytes);
decrypt(resultingBytes, Scope.RFILE, CRYPTO_ON_CONF);
}
@Test
public void testNoEncryptionWAL() throws Exception {
CryptoService cs = CryptoServiceFactory.newDefaultInstance();
byte[] encryptedBytes = encrypt(cs, Scope.WAL, CRYPTO_OFF_CONF);
String stringifiedBytes = Arrays.toString(encryptedBytes);
String stringifiedMarkerBytes =
getStringifiedBytes("U+1F47B".getBytes(), MARKER_STRING, MARKER_INT);
assertEquals(stringifiedBytes, stringifiedMarkerBytes);
decrypt(encryptedBytes, Scope.WAL, CRYPTO_OFF_CONF);
}
@Test
public void testNoEncryptionRFILE() throws Exception {
CryptoService cs = CryptoServiceFactory.newDefaultInstance();
byte[] encryptedBytes = encrypt(cs, Scope.RFILE, CRYPTO_OFF_CONF);
String stringifiedBytes = Arrays.toString(encryptedBytes);
String stringifiedMarkerBytes =
getStringifiedBytes("U+1F47B".getBytes(), MARKER_STRING, MARKER_INT);
assertEquals(stringifiedBytes, stringifiedMarkerBytes);
decrypt(encryptedBytes, Scope.RFILE, CRYPTO_OFF_CONF);
}
@Test
public void testRFileEncrypted() throws Exception {
AccumuloConfiguration cryptoOnConf = getAccumuloConfig(CRYPTO_ON_CONF);
FileSystem fs = FileSystem.getLocal(hadoopConf);
ArrayList<Key> keys = testData();
SummarizerConfiguration sumConf =
SummarizerConfiguration.builder(KeyCounter.class.getName()).build();
String file = "target/testFile1.rf";
fs.delete(new Path(file), true);
try (RFileWriter writer = RFile.newWriter().to(file).withFileSystem(fs)
.withTableProperties(cryptoOnConf).withSummarizers(sumConf).build()) {
Value empty = new Value(new byte[] {});
writer.startDefaultLocalityGroup();
for (Key key : keys) {
writer.append(key, empty);
}
}
Scanner iter =
RFile.newScanner().from(file).withFileSystem(fs).withTableProperties(cryptoOnConf).build();
ArrayList<Key> keysRead = new ArrayList<>();
iter.forEach(e -> keysRead.add(e.getKey()));
assertEquals(keys, keysRead);
Collection<Summary> summaries =
RFile.summaries().from(file).withFileSystem(fs).withTableProperties(cryptoOnConf).read();
Summary summary = Iterables.getOnlyElement(summaries);
assertEquals(keys.size(), (long) summary.getStatistics().get("keys"));
assertEquals(1, summary.getStatistics().size());
assertEquals(0, summary.getFileStatistics().getInaccurate());
assertEquals(1, summary.getFileStatistics().getTotal());
}
@Test
// This test is to ensure when Crypto is configured that it can read unencrypted files
public void testReadNoCryptoWithCryptoConfigured() throws Exception {
AccumuloConfiguration cryptoOffConf = getAccumuloConfig(CRYPTO_OFF_CONF);
AccumuloConfiguration cryptoOnConf = getAccumuloConfig(CRYPTO_ON_CONF);
FileSystem fs = FileSystem.getLocal(hadoopConf);
ArrayList<Key> keys = testData();
String file = "target/testFile2.rf";
fs.delete(new Path(file), true);
try (RFileWriter writer =
RFile.newWriter().to(file).withFileSystem(fs).withTableProperties(cryptoOffConf).build()) {
Value empty = new Value(new byte[] {});
writer.startDefaultLocalityGroup();
for (Key key : keys) {
writer.append(key, empty);
}
}
Scanner iter =
RFile.newScanner().from(file).withFileSystem(fs).withTableProperties(cryptoOnConf).build();
ArrayList<Key> keysRead = new ArrayList<>();
iter.forEach(e -> keysRead.add(e.getKey()));
assertEquals(keys, keysRead);
}
@Test
public void testMissingConfigProperties() throws ReflectiveOperationException {
ConfigurationCopy aconf = new ConfigurationCopy(DefaultConfiguration.getInstance());
Configuration conf = new Configuration(false);
for (Map.Entry<String,String> e : conf) {
aconf.set(e.getKey(), e.getValue());
}
aconf.set(Property.INSTANCE_CRYPTO_SERVICE,
"org.apache.accumulo.core.spi.crypto.AESCryptoService");
String configuredClass = aconf.get(Property.INSTANCE_CRYPTO_SERVICE.getKey());
Class<? extends CryptoService> clazz =
ClassLoaderUtil.loadClass(configuredClass, CryptoService.class);
CryptoService cs = clazz.getDeclaredConstructor().newInstance();
assertEquals(AESCryptoService.class, cs.getClass());
assertThrows(NullPointerException.class,
() -> cs.init(aconf.getAllPropertiesWithPrefix(Property.TABLE_PREFIX)));
}
@Test
public void testAESKeyUtilsGeneratesKey() throws NoSuchAlgorithmException,
NoSuchProviderException, NoSuchPaddingException, InvalidKeyException {
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "SUN");
// verify valid key sizes (corresponds to 128, 192, and 256 bits)
for (int i : new int[] {16, 24, 32}) {
verifyKeySizeForCBC(sr, i);
}
// verify invalid key sizes
for (int i : new int[] {1, 2, 8, 11, 15, 64, 128}) {
assertThrows(InvalidKeyException.class, () -> verifyKeySizeForCBC(sr, i));
}
}
// this has to be a separate method, for spotbugs, because spotbugs annotation doesn't seem to
// apply to the lambda inline
@SuppressFBWarnings(value = "CIPHER_INTEGRITY", justification = "CBC is being tested")
private void verifyKeySizeForCBC(SecureRandom sr, int sizeInBytes)
throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
java.security.Key key = AESCryptoService.generateKey(sr, sizeInBytes);
Cipher.getInstance("AES/CBC/NoPadding").init(Cipher.ENCRYPT_MODE, key);
}
@Test
public void testAESKeyUtilsWrapAndUnwrap()
throws NoSuchAlgorithmException, NoSuchProviderException {
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "SUN");
java.security.Key kek = AESCryptoService.generateKey(sr, 16);
java.security.Key fek = AESCryptoService.generateKey(sr, 16);
byte[] wrapped = AESCryptoService.wrapKey(fek, kek);
assertFalse(Arrays.equals(fek.getEncoded(), wrapped));
java.security.Key unwrapped = AESCryptoService.unwrapKey(wrapped, kek);
assertEquals(unwrapped, fek);
}
@Test
public void testAESKeyUtilsFailUnwrapWithWrongKEK()
throws NoSuchAlgorithmException, NoSuchProviderException {
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "SUN");
java.security.Key kek = AESCryptoService.generateKey(sr, 16);
java.security.Key fek = AESCryptoService.generateKey(sr, 16);
byte[] wrongBytes = kek.getEncoded();
wrongBytes[0]++;
java.security.Key wrongKek = new SecretKeySpec(wrongBytes, "AES");
byte[] wrapped = AESCryptoService.wrapKey(fek, kek);
assertThrows(CryptoException.class, () -> AESCryptoService.unwrapKey(wrapped, wrongKek));
}
@Test
public void testAESKeyUtilsLoadKekFromUri() throws IOException {
java.security.Key fileKey = AESCryptoService.loadKekFromUri(keyPath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeUTF("sixteenbytekey");
SecretKeySpec handKey = new SecretKeySpec(baos.toByteArray(), "AES");
assertEquals(fileKey, handKey);
}
@Test
public void testAESKeyUtilsLoadKekFromUriInvalidUri() {
assertThrows(CryptoException.class, () -> AESCryptoService.loadKekFromUri(
System.getProperty("user.dir") + "/target/CryptoTest-testkeyfile-doesnt-exist"));
}
@Test
public void testAESKeyUtilsLoadKekFromEmptyFile() {
assertThrows(CryptoException.class, () -> AESCryptoService.loadKekFromUri(emptyKeyPath));
}
private ArrayList<Key> testData() {
ArrayList<Key> keys = new ArrayList<>();
keys.add(new Key("a", "cf", "cq"));
keys.add(new Key("a1", "cf", "cq"));
keys.add(new Key("a2", "cf", "cq"));
keys.add(new Key("a3", "cf", "cq"));
return keys;
}
private <C extends CryptoService> byte[] encrypt(C cs, Scope scope, String configFile)
throws Exception {
AccumuloConfiguration conf = getAccumuloConfig(configFile);
cs.init(conf.getAllPropertiesWithPrefix(Property.INSTANCE_CRYPTO_PREFIX));
CryptoEnvironmentImpl env = new CryptoEnvironmentImpl(scope, null);
FileEncrypter encrypter = cs.getFileEncrypter(env);
byte[] params = encrypter.getDecryptionParameters();
assertNotNull("CryptoService returned null FileEncrypter", encrypter);
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dataOut = new DataOutputStream(out);
CryptoUtils.writeParams(params, dataOut);
DataOutputStream encrypted =
new DataOutputStream(encrypter.encryptStream(new NoFlushOutputStream(dataOut)));
assertNotNull(encrypted);
encrypted.writeUTF(MARKER_STRING);
encrypted.writeInt(MARKER_INT);
encrypted.close();
dataOut.close();
out.close();
return out.toByteArray();
}
private void decrypt(byte[] resultingBytes, Scope scope, String configFile) throws Exception {
try (DataInputStream dataIn = new DataInputStream(new ByteArrayInputStream(resultingBytes))) {
AccumuloConfiguration conf = getAccumuloConfig(configFile);
CryptoService cs = CryptoServiceFactory.newInstance(conf, ClassloaderType.JAVA);
FileDecrypter decrypter = getFileDecrypter(cs, scope, dataIn);
try (DataInputStream decrypted = new DataInputStream(decrypter.decryptStream(dataIn))) {
String markerString = decrypted.readUTF();
int markerInt = decrypted.readInt();
assertEquals(MARKER_STRING, markerString);
assertEquals(MARKER_INT, markerInt);
}
}
}
private String getStringifiedBytes(byte[] params, String s, int i) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dataOut = new DataOutputStream(out);
if (params != null) {
dataOut.writeInt(params.length);
dataOut.write(params);
}
dataOut.writeUTF(s);
dataOut.writeInt(i);
dataOut.close();
byte[] stringMarkerBytes = out.toByteArray();
return Arrays.toString(stringMarkerBytes);
}
// simple counter to just make sure crypto works with summaries
public static class KeyCounter implements Summarizer {
@Override
public Collector collector(SummarizerConfiguration sc) {
return new Collector() {
long keys = 0;
@Override
public void accept(Key k, Value v) {
if (!k.isDeleted())
keys++;
}
@Override
public void summarize(StatisticConsumer sc) {
sc.accept("keys", keys);
}
};
}
@Override
public Combiner combiner(SummarizerConfiguration sc) {
return (m1, m2) -> m2.forEach((k, v) -> m1.merge(k, v, Long::sum));
}
}
}