/*
* 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.flume.channel.file.encryption;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import org.apache.flume.ChannelException;
import org.apache.flume.FlumeException;
import org.apache.flume.channel.file.FileChannelConfiguration;
import org.apache.flume.channel.file.TestFileChannelBase;
import org.apache.flume.channel.file.TestUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.apache.flume.channel.file.TestUtils.compareInputAndOut;
import static org.apache.flume.channel.file.TestUtils.consumeChannel;
import static org.apache.flume.channel.file.TestUtils.fillChannel;
import static org.apache.flume.channel.file.TestUtils.putEvents;
import static org.apache.flume.channel.file.TestUtils.takeEvents;
public class TestFileChannelEncryption extends TestFileChannelBase {
protected static final Logger LOGGER =
LoggerFactory.getLogger(TestFileChannelEncryption.class);
private File keyStoreFile;
private File keyStorePasswordFile;
private Map<String, File> keyAliasPassword;
@Before
public void setup() throws Exception {
super.setup();
keyStorePasswordFile = new File(baseDir, "keyStorePasswordFile");
Files.write("keyStorePassword", keyStorePasswordFile, Charsets.UTF_8);
keyStoreFile = new File(baseDir, "keyStoreFile");
Assert.assertTrue(keyStoreFile.createNewFile());
keyAliasPassword = Maps.newHashMap();
keyAliasPassword.putAll(EncryptionTestUtils.configureTestKeyStore(baseDir, keyStoreFile));
}
@After
public void teardown() {
super.teardown();
}
private Map<String, String> getOverrides() throws Exception {
Map<String, String> overrides = Maps.newHashMap();
overrides.put(FileChannelConfiguration.CAPACITY, String.valueOf(100));
overrides.put(FileChannelConfiguration.TRANSACTION_CAPACITY, String.valueOf(100));
return overrides;
}
private Map<String, String> getOverridesForEncryption() throws Exception {
Map<String, String> overrides = getOverrides();
Map<String, String> encryptionProps =
EncryptionTestUtils.configureForKeyStore(keyStoreFile,
keyStorePasswordFile,
keyAliasPassword);
encryptionProps.put(EncryptionConfiguration.KEY_PROVIDER,
KeyProviderType.JCEKSFILE.name());
encryptionProps.put(EncryptionConfiguration.CIPHER_PROVIDER,
CipherProviderType.AESCTRNOPADDING.name());
encryptionProps.put(EncryptionConfiguration.ACTIVE_KEY, "key-1");
for (String key : encryptionProps.keySet()) {
overrides.put(EncryptionConfiguration.ENCRYPTION_PREFIX + "." + key,
encryptionProps.get(key));
}
return overrides;
}
/**
* Test fails without FLUME-1565
*/
@Test
public void testThreadedConsume() throws Exception {
int numThreads = 20;
Map<String, String> overrides = getOverridesForEncryption();
overrides.put(FileChannelConfiguration.CAPACITY, String.valueOf(10000));
overrides.put(FileChannelConfiguration.TRANSACTION_CAPACITY,
String.valueOf(100));
channel = createFileChannel(overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
Executor executor = Executors.newFixedThreadPool(numThreads);
Set<String> in = fillChannel(channel, "threaded-consume");
final AtomicBoolean error = new AtomicBoolean(false);
final CountDownLatch startLatch = new CountDownLatch(numThreads);
final CountDownLatch stopLatch = new CountDownLatch(numThreads);
final Set<String> out = Collections.synchronizedSet(new HashSet<String>());
for (int i = 0; i < numThreads; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
startLatch.countDown();
startLatch.await();
out.addAll(takeEvents(channel, 10));
} catch (Throwable t) {
error.set(true);
LOGGER.error("Error in take thread", t);
} finally {
stopLatch.countDown();
}
}
});
}
stopLatch.await();
Assert.assertFalse(error.get());
compareInputAndOut(in, out);
}
@Test
public void testThreadedProduce() throws Exception {
int numThreads = 20;
Map<String, String> overrides = getOverridesForEncryption();
overrides.put(FileChannelConfiguration.CAPACITY, String.valueOf(10000));
overrides.put(FileChannelConfiguration.TRANSACTION_CAPACITY,
String.valueOf(100));
channel = createFileChannel(overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
Executor executor = Executors.newFixedThreadPool(numThreads);
final AtomicBoolean error = new AtomicBoolean(false);
final CountDownLatch startLatch = new CountDownLatch(numThreads);
final CountDownLatch stopLatch = new CountDownLatch(numThreads);
final Set<String> in = Collections.synchronizedSet(new HashSet<String>());
for (int i = 0; i < numThreads; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
startLatch.countDown();
startLatch.await();
in.addAll(putEvents(channel, "thread-produce", 10, 10000, true));
} catch (Throwable t) {
error.set(true);
LOGGER.error("Error in put thread", t);
} finally {
stopLatch.countDown();
}
}
});
}
stopLatch.await();
Set<String> out = consumeChannel(channel);
Assert.assertFalse(error.get());
compareInputAndOut(in, out);
}
@Test
public void testConfiguration() throws Exception {
Map<String, String> overrides = Maps.newHashMap();
overrides.put("encryption.activeKey", "key-1");
overrides.put("encryption.cipherProvider", "AESCTRNOPADDING");
overrides.put("encryption.keyProvider", "JCEKSFILE");
overrides.put("encryption.keyProvider.keyStoreFile",
keyStoreFile.getAbsolutePath());
overrides.put("encryption.keyProvider.keyStorePasswordFile",
keyStorePasswordFile.getAbsolutePath());
overrides.put("encryption.keyProvider.keys", "key-0 key-1");
overrides.put("encryption.keyProvider.keys.key-0.passwordFile",
keyAliasPassword.get("key-0").getAbsolutePath());
channel = createFileChannel(overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
Set<String> in = fillChannel(channel, "restart");
channel.stop();
channel = TestUtils.createFileChannel(checkpointDir.getAbsolutePath(),
dataDir, overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
Set<String> out = consumeChannel(channel);
compareInputAndOut(in, out);
}
@Test
public void testBasicEncyrptionDecryption() throws Exception {
Map<String, String> overrides = getOverridesForEncryption();
channel = createFileChannel(overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
Set<String> in = fillChannel(channel, "restart");
channel.stop();
channel = TestUtils.createFileChannel(checkpointDir.getAbsolutePath(),
dataDir, overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
Set<String> out = consumeChannel(channel);
compareInputAndOut(in, out);
}
@Test
public void testEncryptedChannelWithoutEncryptionConfigFails() throws Exception {
Map<String, String> overrides = getOverridesForEncryption();
channel = createFileChannel(overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
fillChannel(channel, "will-not-restart");
channel.stop();
Map<String, String> noEncryptionOverrides = getOverrides();
channel = createFileChannel(noEncryptionOverrides);
channel.start();
if (channel.isOpen()) {
try {
takeEvents(channel, 1, 1);
Assert.fail("Channel was opened and take did not throw exception");
} catch (ChannelException ex) {
// expected
}
}
}
@Test
public void testUnencyrptedAndEncryptedLogs() throws Exception {
Map<String, String> noEncryptionOverrides = getOverrides();
channel = createFileChannel(noEncryptionOverrides);
channel.start();
Assert.assertTrue(channel.isOpen());
Set<String> in = fillChannel(channel, "unencrypted-and-encrypted");
int numEventsToRemove = in.size() / 2;
for (int i = 0; i < numEventsToRemove; i++) {
Assert.assertTrue(in.removeAll(takeEvents(channel, 1, 1)));
}
// now we have logs with no encryption and the channel is half full
channel.stop();
Map<String, String> overrides = getOverridesForEncryption();
channel = createFileChannel(overrides);
channel.start();
Assert.assertTrue(channel.isOpen());
in.addAll(fillChannel(channel, "unencrypted-and-encrypted"));
Set<String> out = consumeChannel(channel);
compareInputAndOut(in, out);
}
@Test
public void testBadKeyProviderInvalidValue() throws Exception {
Map<String, String> overrides = getOverridesForEncryption();
overrides.put(Joiner.on(".").join(EncryptionConfiguration.ENCRYPTION_PREFIX,
EncryptionConfiguration.KEY_PROVIDER),
"invalid");
try {
channel = createFileChannel(overrides);
Assert.fail();
} catch (FlumeException ex) {
Assert.assertEquals("java.lang.ClassNotFoundException: invalid", ex.getMessage());
}
}
@Test
public void testBadKeyProviderInvalidClass() throws Exception {
Map<String, String> overrides = getOverridesForEncryption();
overrides.put(Joiner.on(".").join(EncryptionConfiguration.ENCRYPTION_PREFIX,
EncryptionConfiguration.KEY_PROVIDER),
String.class.getName());
try {
channel = createFileChannel(overrides);
Assert.fail();
} catch (FlumeException ex) {
Assert.assertEquals("Unable to instantiate Builder from java.lang.String", ex.getMessage());
}
}
@Test
public void testBadCipherProviderInvalidValue() throws Exception {
Map<String, String> overrides = getOverridesForEncryption();
overrides.put(Joiner.on(".").join(EncryptionConfiguration.ENCRYPTION_PREFIX,
EncryptionConfiguration.CIPHER_PROVIDER),
"invalid");
channel = createFileChannel(overrides);
channel.start();
Assert.assertFalse(channel.isOpen());
}
@Test
public void testBadCipherProviderInvalidClass() throws Exception {
Map<String, String> overrides = getOverridesForEncryption();
overrides.put(Joiner.on(".").join(EncryptionConfiguration.ENCRYPTION_PREFIX,
EncryptionConfiguration.CIPHER_PROVIDER), String.class.getName());
channel = createFileChannel(overrides);
channel.start();
Assert.assertFalse(channel.isOpen());
}
@Test
public void testMissingKeyStoreFile() throws Exception {
Map<String, String> overrides = getOverridesForEncryption();
overrides.put(Joiner.on(".").join(EncryptionConfiguration.ENCRYPTION_PREFIX,
EncryptionConfiguration.KEY_PROVIDER,
EncryptionConfiguration.JCE_FILE_KEY_STORE_FILE),
"/path/does/not/exist");
try {
channel = createFileChannel(overrides);
Assert.fail();
} catch (RuntimeException ex) {
Assert.assertTrue("Exception message is incorrect: " + ex.getMessage(),
ex.getMessage().startsWith("java.io.FileNotFoundException: /path/does/not/exist "));
}
}
@Test
public void testMissingKeyStorePasswordFile() throws Exception {
Map<String, String> overrides = getOverridesForEncryption();
overrides.put(Joiner.on(".").join(EncryptionConfiguration.ENCRYPTION_PREFIX,
EncryptionConfiguration.KEY_PROVIDER,
EncryptionConfiguration.JCE_FILE_KEY_STORE_PASSWORD_FILE),
"/path/does/not/exist");
try {
channel = createFileChannel(overrides);
Assert.fail();
} catch (RuntimeException ex) {
Assert.assertTrue("Exception message is incorrect: " + ex.getMessage(),
ex.getMessage().startsWith("java.io.FileNotFoundException: /path/does/not/exist "));
}
}
@Test
public void testBadKeyStorePassword() throws Exception {
Files.write("invalid", keyStorePasswordFile, Charsets.UTF_8);
Map<String, String> overrides = getOverridesForEncryption();
try {
channel = TestUtils.createFileChannel(checkpointDir.getAbsolutePath(),
dataDir, overrides);
Assert.fail();
} catch (RuntimeException ex) {
Assert.assertEquals("java.io.IOException: Keystore was tampered with, or " +
"password was incorrect", ex.getMessage());
}
}
@Test
public void testBadKeyAlias() throws Exception {
Map<String, String> overrides = getOverridesForEncryption();
overrides.put(EncryptionConfiguration.ENCRYPTION_PREFIX + "." +
EncryptionConfiguration.ACTIVE_KEY, "invalid");
channel = TestUtils.createFileChannel(checkpointDir.getAbsolutePath(),
dataDir, overrides);
channel.start();
Assert.assertFalse(channel.isOpen());
}
}