| /** |
| * 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.hbase.regionserver; |
| |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import java.io.IOException; |
| import java.security.Key; |
| import java.security.SecureRandom; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import javax.crypto.spec.SecretKeySpec; |
| import org.apache.hadoop.conf.Configuration; |
| import org.apache.hadoop.fs.Path; |
| import org.apache.hadoop.hbase.HBaseClassTestRule; |
| import org.apache.hadoop.hbase.HBaseTestingUtility; |
| import org.apache.hadoop.hbase.HConstants; |
| import org.apache.hadoop.hbase.TableName; |
| import org.apache.hadoop.hbase.Waiter; |
| import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; |
| import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; |
| import org.apache.hadoop.hbase.client.CompactionState; |
| import org.apache.hadoop.hbase.client.Put; |
| import org.apache.hadoop.hbase.client.Table; |
| import org.apache.hadoop.hbase.client.TableDescriptor; |
| import org.apache.hadoop.hbase.client.TableDescriptorBuilder; |
| import org.apache.hadoop.hbase.io.crypto.Encryption; |
| import org.apache.hadoop.hbase.io.crypto.KeyProviderForTesting; |
| import org.apache.hadoop.hbase.io.crypto.aes.AES; |
| import org.apache.hadoop.hbase.io.hfile.CacheConfig; |
| import org.apache.hadoop.hbase.io.hfile.HFile; |
| import org.apache.hadoop.hbase.security.EncryptionUtil; |
| import org.apache.hadoop.hbase.security.User; |
| import org.apache.hadoop.hbase.testclassification.MediumTests; |
| import org.apache.hadoop.hbase.testclassification.RegionServerTests; |
| import org.apache.hadoop.hbase.util.Bytes; |
| import org.junit.AfterClass; |
| import org.junit.BeforeClass; |
| import org.junit.ClassRule; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.experimental.categories.Category; |
| import org.junit.rules.TestName; |
| |
| @Category({RegionServerTests.class, MediumTests.class}) |
| public class TestEncryptionKeyRotation { |
| |
| @ClassRule |
| public static final HBaseClassTestRule CLASS_RULE = |
| HBaseClassTestRule.forClass(TestEncryptionKeyRotation.class); |
| |
| private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); |
| private static final Configuration conf = TEST_UTIL.getConfiguration(); |
| private static final Key initialCFKey; |
| private static final Key secondCFKey; |
| |
| @Rule |
| public TestName name = new TestName(); |
| |
| static { |
| // Create the test encryption keys |
| SecureRandom rng = new SecureRandom(); |
| byte[] keyBytes = new byte[AES.KEY_LENGTH]; |
| rng.nextBytes(keyBytes); |
| String algorithm = |
| conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES); |
| initialCFKey = new SecretKeySpec(keyBytes, algorithm); |
| rng.nextBytes(keyBytes); |
| secondCFKey = new SecretKeySpec(keyBytes, algorithm); |
| } |
| |
| @BeforeClass |
| public static void setUp() throws Exception { |
| conf.setInt("hfile.format.version", 3); |
| conf.set(HConstants.CRYPTO_KEYPROVIDER_CONF_KEY, KeyProviderForTesting.class.getName()); |
| conf.set(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "hbase"); |
| |
| // Start the minicluster |
| TEST_UTIL.startMiniCluster(1); |
| } |
| |
| @AfterClass |
| public static void tearDown() throws Exception { |
| TEST_UTIL.shutdownMiniCluster(); |
| } |
| |
| @Test |
| public void testCFKeyRotation() throws Exception { |
| // Create the table schema |
| TableDescriptorBuilder tableDescriptorBuilder = |
| TableDescriptorBuilder.newBuilder(TableName.valueOf("default", name.getMethodName())); |
| ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder = |
| ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("cf")); |
| String algorithm = |
| conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES); |
| columnFamilyDescriptorBuilder.setEncryptionType(algorithm); |
| columnFamilyDescriptorBuilder.setEncryptionKey(EncryptionUtil.wrapKey(conf, "hbase", |
| initialCFKey)); |
| tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptorBuilder.build()); |
| TableDescriptor tableDescriptor = tableDescriptorBuilder.build(); |
| |
| // Create the table and some on disk files |
| createTableAndFlush(tableDescriptor); |
| |
| // Verify we have store file(s) with the initial key |
| final List<Path> initialPaths = findStorefilePaths(tableDescriptor.getTableName()); |
| assertTrue(initialPaths.size() > 0); |
| for (Path path: initialPaths) { |
| assertTrue("Store file " + path + " has incorrect key", |
| Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path))); |
| } |
| |
| // Update the schema with a new encryption key |
| columnFamilyDescriptorBuilder.setEncryptionKey(EncryptionUtil.wrapKey(conf, |
| conf.get(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, User.getCurrent().getShortName()), |
| secondCFKey)); |
| TEST_UTIL.getAdmin().modifyColumnFamily(tableDescriptor.getTableName(), |
| columnFamilyDescriptorBuilder.build()); |
| Thread.sleep(5000); // Need a predicate for online schema change |
| |
| // And major compact |
| TEST_UTIL.getAdmin().majorCompact(tableDescriptor.getTableName()); |
| // waiting for the major compaction to complete |
| TEST_UTIL.waitFor(30000, new Waiter.Predicate<IOException>() { |
| @Override |
| public boolean evaluate() throws IOException { |
| return TEST_UTIL.getAdmin().getCompactionState(tableDescriptor |
| .getTableName()) == CompactionState.NONE; |
| } |
| }); |
| List<Path> pathsAfterCompaction = findStorefilePaths(tableDescriptor.getTableName()); |
| assertTrue(pathsAfterCompaction.size() > 0); |
| for (Path path: pathsAfterCompaction) { |
| assertTrue("Store file " + path + " has incorrect key", |
| Bytes.equals(secondCFKey.getEncoded(), extractHFileKey(path))); |
| } |
| List<Path> compactedPaths = findCompactedStorefilePaths(tableDescriptor.getTableName()); |
| assertTrue(compactedPaths.size() > 0); |
| for (Path path: compactedPaths) { |
| assertTrue("Store file " + path + " retains initial key", |
| Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path))); |
| } |
| } |
| |
| @Test |
| public void testMasterKeyRotation() throws Exception { |
| // Create the table schema |
| TableDescriptorBuilder tableDescriptorBuilder = |
| TableDescriptorBuilder.newBuilder(TableName.valueOf("default", name.getMethodName())); |
| ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder = |
| ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("cf")); |
| String algorithm = |
| conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES); |
| columnFamilyDescriptorBuilder.setEncryptionType(algorithm); |
| columnFamilyDescriptorBuilder.setEncryptionKey( |
| EncryptionUtil.wrapKey(conf, "hbase", initialCFKey)); |
| tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptorBuilder.build()); |
| TableDescriptor tableDescriptor = tableDescriptorBuilder.build(); |
| |
| // Create the table and some on disk files |
| createTableAndFlush(tableDescriptor); |
| |
| // Verify we have store file(s) with the initial key |
| List<Path> storeFilePaths = findStorefilePaths(tableDescriptor.getTableName()); |
| assertTrue(storeFilePaths.size() > 0); |
| for (Path path: storeFilePaths) { |
| assertTrue("Store file " + path + " has incorrect key", |
| Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path))); |
| } |
| |
| // Now shut down the HBase cluster |
| TEST_UTIL.shutdownMiniHBaseCluster(); |
| |
| // "Rotate" the master key |
| conf.set(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "other"); |
| conf.set(HConstants.CRYPTO_MASTERKEY_ALTERNATE_NAME_CONF_KEY, "hbase"); |
| |
| // Start the cluster back up |
| TEST_UTIL.startMiniHBaseCluster(); |
| // Verify the table can still be loaded |
| TEST_UTIL.waitTableAvailable(tableDescriptor.getTableName(), 5000); |
| // Double check that the store file keys can be unwrapped |
| storeFilePaths = findStorefilePaths(tableDescriptor.getTableName()); |
| assertTrue(storeFilePaths.size() > 0); |
| for (Path path: storeFilePaths) { |
| assertTrue("Store file " + path + " has incorrect key", |
| Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path))); |
| } |
| } |
| |
| private static List<Path> findStorefilePaths(TableName tableName) throws Exception { |
| List<Path> paths = new ArrayList<>(); |
| for (Region region : TEST_UTIL.getRSForFirstRegionInTable(tableName) |
| .getRegions(tableName)) { |
| for (HStore store : ((HRegion) region).getStores()) { |
| for (HStoreFile storefile : store.getStorefiles()) { |
| paths.add(storefile.getPath()); |
| } |
| } |
| } |
| return paths; |
| } |
| |
| private static List<Path> findCompactedStorefilePaths(TableName tableName) throws Exception { |
| List<Path> paths = new ArrayList<>(); |
| for (Region region : TEST_UTIL.getRSForFirstRegionInTable(tableName) |
| .getRegions(tableName)) { |
| for (HStore store : ((HRegion) region).getStores()) { |
| Collection<HStoreFile> compactedfiles = |
| store.getStoreEngine().getStoreFileManager().getCompactedfiles(); |
| if (compactedfiles != null) { |
| for (HStoreFile storefile : compactedfiles) { |
| paths.add(storefile.getPath()); |
| } |
| } |
| } |
| } |
| return paths; |
| } |
| |
| private void createTableAndFlush(TableDescriptor tableDescriptor) throws Exception { |
| ColumnFamilyDescriptor cfd = tableDescriptor.getColumnFamilies()[0]; |
| // Create the test table |
| TEST_UTIL.getAdmin().createTable(tableDescriptor); |
| TEST_UTIL.waitTableAvailable(tableDescriptor.getTableName(), 5000); |
| // Create a store file |
| Table table = TEST_UTIL.getConnection().getTable(tableDescriptor.getTableName()); |
| try { |
| table.put(new Put(Bytes.toBytes("testrow")) |
| .addColumn(cfd.getName(), Bytes.toBytes("q"), Bytes.toBytes("value"))); |
| } finally { |
| table.close(); |
| } |
| TEST_UTIL.getAdmin().flush(tableDescriptor.getTableName()); |
| } |
| |
| private static byte[] extractHFileKey(Path path) throws Exception { |
| HFile.Reader reader = HFile.createReader(TEST_UTIL.getTestFileSystem(), path, |
| new CacheConfig(conf), true, conf); |
| try { |
| Encryption.Context cryptoContext = reader.getFileContext().getEncryptionContext(); |
| assertNotNull("Reader has a null crypto context", cryptoContext); |
| Key key = cryptoContext.getKey(); |
| assertNotNull("Crypto context has no key", key); |
| return key.getEncoded(); |
| } finally { |
| reader.close(); |
| } |
| } |
| |
| } |