001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.regionserver; 019 020import static org.junit.Assert.assertNotNull; 021import static org.junit.Assert.assertTrue; 022 023import java.io.IOException; 024import java.security.Key; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.List; 028import javax.crypto.spec.SecretKeySpec; 029import org.apache.hadoop.conf.Configuration; 030import org.apache.hadoop.fs.Path; 031import org.apache.hadoop.hbase.HBaseClassTestRule; 032import org.apache.hadoop.hbase.HBaseTestingUtility; 033import org.apache.hadoop.hbase.HColumnDescriptor; 034import org.apache.hadoop.hbase.HConstants; 035import org.apache.hadoop.hbase.HTableDescriptor; 036import org.apache.hadoop.hbase.TableName; 037import org.apache.hadoop.hbase.Waiter; 038import org.apache.hadoop.hbase.client.CompactionState; 039import org.apache.hadoop.hbase.client.Put; 040import org.apache.hadoop.hbase.client.Table; 041import org.apache.hadoop.hbase.io.crypto.Encryption; 042import org.apache.hadoop.hbase.io.crypto.KeyProviderForTesting; 043import org.apache.hadoop.hbase.io.crypto.aes.AES; 044import org.apache.hadoop.hbase.io.hfile.CacheConfig; 045import org.apache.hadoop.hbase.io.hfile.HFile; 046import org.apache.hadoop.hbase.security.EncryptionUtil; 047import org.apache.hadoop.hbase.security.User; 048import org.apache.hadoop.hbase.testclassification.MediumTests; 049import org.apache.hadoop.hbase.testclassification.RegionServerTests; 050import org.apache.hadoop.hbase.util.Bytes; 051import org.junit.AfterClass; 052import org.junit.BeforeClass; 053import org.junit.ClassRule; 054import org.junit.Rule; 055import org.junit.Test; 056import org.junit.experimental.categories.Category; 057import org.junit.rules.TestName; 058import org.slf4j.Logger; 059import org.slf4j.LoggerFactory; 060 061@Category({ RegionServerTests.class, MediumTests.class }) 062public class TestEncryptionKeyRotation { 063 064 @ClassRule 065 public static final HBaseClassTestRule CLASS_RULE = 066 HBaseClassTestRule.forClass(TestEncryptionKeyRotation.class); 067 068 private static final Logger LOG = LoggerFactory.getLogger(TestEncryptionKeyRotation.class); 069 private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); 070 private static final Configuration conf = TEST_UTIL.getConfiguration(); 071 private static final Key initialCFKey; 072 private static final Key secondCFKey; 073 074 @Rule 075 public TestName name = new TestName(); 076 077 static { 078 // Create the test encryption keys 079 byte[] keyBytes = new byte[AES.KEY_LENGTH]; 080 Bytes.secureRandom(keyBytes); 081 String algorithm = conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES); 082 initialCFKey = new SecretKeySpec(keyBytes, algorithm); 083 Bytes.secureRandom(keyBytes); 084 secondCFKey = new SecretKeySpec(keyBytes, algorithm); 085 } 086 087 @BeforeClass 088 public static void setUp() throws Exception { 089 conf.setInt("hfile.format.version", 3); 090 conf.set(HConstants.CRYPTO_KEYPROVIDER_CONF_KEY, KeyProviderForTesting.class.getName()); 091 conf.set(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "hbase"); 092 093 // Start the minicluster 094 TEST_UTIL.startMiniCluster(1); 095 } 096 097 @AfterClass 098 public static void tearDown() throws Exception { 099 TEST_UTIL.shutdownMiniCluster(); 100 } 101 102 @Test 103 public void testCFKeyRotation() throws Exception { 104 // Create the table schema 105 HTableDescriptor htd = new HTableDescriptor(TableName.valueOf("default", name.getMethodName())); 106 HColumnDescriptor hcd = new HColumnDescriptor("cf"); 107 String algorithm = conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES); 108 hcd.setEncryptionType(algorithm); 109 hcd.setEncryptionKey(EncryptionUtil.wrapKey(conf, "hbase", initialCFKey)); 110 htd.addFamily(hcd); 111 112 // Create the table and some on disk files 113 createTableAndFlush(htd); 114 115 // Verify we have store file(s) with the initial key 116 final List<Path> initialPaths = findStorefilePaths(htd.getTableName()); 117 assertTrue(initialPaths.size() > 0); 118 for (Path path : initialPaths) { 119 assertTrue("Store file " + path + " has incorrect key", 120 Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path))); 121 } 122 123 // Update the schema with a new encryption key 124 hcd = htd.getFamily(Bytes.toBytes("cf")); 125 hcd.setEncryptionKey(EncryptionUtil.wrapKey(conf, 126 conf.get(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, User.getCurrent().getShortName()), 127 secondCFKey)); 128 TEST_UTIL.getAdmin().modifyColumnFamily(htd.getTableName(), hcd); 129 Thread.sleep(5000); // Need a predicate for online schema change 130 131 // And major compact 132 TEST_UTIL.getAdmin().majorCompact(htd.getTableName()); 133 // waiting for the major compaction to complete 134 TEST_UTIL.waitFor(30000, new Waiter.Predicate<IOException>() { 135 @Override 136 public boolean evaluate() throws IOException { 137 return TEST_UTIL.getAdmin().getCompactionState(htd.getTableName()) == CompactionState.NONE; 138 } 139 }); 140 List<Path> pathsAfterCompaction = findStorefilePaths(htd.getTableName()); 141 assertTrue(pathsAfterCompaction.size() > 0); 142 for (Path path : pathsAfterCompaction) { 143 assertTrue("Store file " + path + " has incorrect key", 144 Bytes.equals(secondCFKey.getEncoded(), extractHFileKey(path))); 145 } 146 List<Path> compactedPaths = findCompactedStorefilePaths(htd.getTableName()); 147 assertTrue(compactedPaths.size() > 0); 148 for (Path path : compactedPaths) { 149 assertTrue("Store file " + path + " retains initial key", 150 Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path))); 151 } 152 } 153 154 @Test 155 public void testMasterKeyRotation() throws Exception { 156 // Create the table schema 157 HTableDescriptor htd = new HTableDescriptor(TableName.valueOf("default", name.getMethodName())); 158 HColumnDescriptor hcd = new HColumnDescriptor("cf"); 159 String algorithm = conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES); 160 hcd.setEncryptionType(algorithm); 161 hcd.setEncryptionKey(EncryptionUtil.wrapKey(conf, "hbase", initialCFKey)); 162 htd.addFamily(hcd); 163 164 // Create the table and some on disk files 165 createTableAndFlush(htd); 166 167 // Verify we have store file(s) with the initial key 168 List<Path> storeFilePaths = findStorefilePaths(htd.getTableName()); 169 assertTrue(storeFilePaths.size() > 0); 170 for (Path path : storeFilePaths) { 171 assertTrue("Store file " + path + " has incorrect key", 172 Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path))); 173 } 174 175 // Now shut down the HBase cluster 176 TEST_UTIL.shutdownMiniHBaseCluster(); 177 178 // "Rotate" the master key 179 conf.set(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "other"); 180 conf.set(HConstants.CRYPTO_MASTERKEY_ALTERNATE_NAME_CONF_KEY, "hbase"); 181 182 // Start the cluster back up 183 TEST_UTIL.startMiniHBaseCluster(); 184 // Verify the table can still be loaded 185 TEST_UTIL.waitTableAvailable(htd.getTableName(), 5000); 186 // Double check that the store file keys can be unwrapped 187 storeFilePaths = findStorefilePaths(htd.getTableName()); 188 assertTrue(storeFilePaths.size() > 0); 189 for (Path path : storeFilePaths) { 190 assertTrue("Store file " + path + " has incorrect key", 191 Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path))); 192 } 193 } 194 195 private static List<Path> findStorefilePaths(TableName tableName) throws Exception { 196 List<Path> paths = new ArrayList<>(); 197 for (Region region : TEST_UTIL.getRSForFirstRegionInTable(tableName).getRegions(tableName)) { 198 for (HStore store : ((HRegion) region).getStores()) { 199 for (HStoreFile storefile : store.getStorefiles()) { 200 paths.add(storefile.getPath()); 201 } 202 } 203 } 204 return paths; 205 } 206 207 private static List<Path> findCompactedStorefilePaths(TableName tableName) throws Exception { 208 List<Path> paths = new ArrayList<>(); 209 for (Region region : TEST_UTIL.getRSForFirstRegionInTable(tableName).getRegions(tableName)) { 210 for (HStore store : ((HRegion) region).getStores()) { 211 Collection<HStoreFile> compactedfiles = 212 store.getStoreEngine().getStoreFileManager().getCompactedfiles(); 213 if (compactedfiles != null) { 214 for (HStoreFile storefile : compactedfiles) { 215 paths.add(storefile.getPath()); 216 } 217 } 218 } 219 } 220 return paths; 221 } 222 223 private void createTableAndFlush(HTableDescriptor htd) throws Exception { 224 HColumnDescriptor hcd = htd.getFamilies().iterator().next(); 225 // Create the test table 226 TEST_UTIL.getAdmin().createTable(htd); 227 TEST_UTIL.waitTableAvailable(htd.getTableName(), 5000); 228 // Create a store file 229 Table table = TEST_UTIL.getConnection().getTable(htd.getTableName()); 230 try { 231 table.put(new Put(Bytes.toBytes("testrow")).addColumn(hcd.getName(), Bytes.toBytes("q"), 232 Bytes.toBytes("value"))); 233 } finally { 234 table.close(); 235 } 236 TEST_UTIL.getAdmin().flush(htd.getTableName()); 237 } 238 239 private static byte[] extractHFileKey(Path path) throws Exception { 240 HFile.Reader reader = 241 HFile.createReader(TEST_UTIL.getTestFileSystem(), path, new CacheConfig(conf), true, conf); 242 try { 243 Encryption.Context cryptoContext = reader.getFileContext().getEncryptionContext(); 244 assertNotNull("Reader has a null crypto context", cryptoContext); 245 Key key = cryptoContext.getKey(); 246 assertNotNull("Crypto context has no key", key); 247 return key.getEncoded(); 248 } finally { 249 reader.close(); 250 } 251 } 252 253}