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.jupiter.api.Assertions.assertNotNull; 021import static org.junit.jupiter.api.Assertions.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.HBaseTestingUtil; 032import org.apache.hadoop.hbase.HConstants; 033import org.apache.hadoop.hbase.TableName; 034import org.apache.hadoop.hbase.Waiter; 035import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; 036import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 037import org.apache.hadoop.hbase.client.CompactionState; 038import org.apache.hadoop.hbase.client.Put; 039import org.apache.hadoop.hbase.client.Table; 040import org.apache.hadoop.hbase.client.TableDescriptor; 041import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 042import org.apache.hadoop.hbase.io.crypto.Encryption; 043import org.apache.hadoop.hbase.io.crypto.MockAesKeyProvider; 044import org.apache.hadoop.hbase.io.crypto.aes.AES; 045import org.apache.hadoop.hbase.io.hfile.CacheConfig; 046import org.apache.hadoop.hbase.io.hfile.HFile; 047import org.apache.hadoop.hbase.security.EncryptionUtil; 048import org.apache.hadoop.hbase.security.User; 049import org.apache.hadoop.hbase.testclassification.MediumTests; 050import org.apache.hadoop.hbase.testclassification.RegionServerTests; 051import org.apache.hadoop.hbase.util.Bytes; 052import org.junit.jupiter.api.AfterAll; 053import org.junit.jupiter.api.BeforeAll; 054import org.junit.jupiter.api.BeforeEach; 055import org.junit.jupiter.api.Tag; 056import org.junit.jupiter.api.Test; 057import org.junit.jupiter.api.TestInfo; 058 059@Tag(RegionServerTests.TAG) 060@Tag(MediumTests.TAG) 061public class TestEncryptionKeyRotation { 062 063 private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 064 private static final Configuration conf = TEST_UTIL.getConfiguration(); 065 private static final Key initialCFKey; 066 private static final Key secondCFKey; 067 private String name; 068 069 @BeforeEach 070 public void setTestName(TestInfo testInfo) { 071 this.name = testInfo.getTestMethod().get().getName(); 072 } 073 074 static { 075 // Create the test encryption keys 076 byte[] keyBytes = new byte[AES.KEY_LENGTH]; 077 Bytes.secureRandom(keyBytes); 078 String algorithm = conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES); 079 initialCFKey = new SecretKeySpec(keyBytes, algorithm); 080 Bytes.secureRandom(keyBytes); 081 secondCFKey = new SecretKeySpec(keyBytes, algorithm); 082 } 083 084 @BeforeAll 085 public static void setUp() throws Exception { 086 conf.setInt("hfile.format.version", 3); 087 conf.set(HConstants.CRYPTO_KEYPROVIDER_CONF_KEY, MockAesKeyProvider.class.getName()); 088 conf.set(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "hbase"); 089 090 // Start the minicluster 091 TEST_UTIL.startMiniCluster(1); 092 } 093 094 @AfterAll 095 public static void tearDown() throws Exception { 096 TEST_UTIL.shutdownMiniCluster(); 097 } 098 099 @Test 100 public void testCFKeyRotation() throws Exception { 101 // Create the table schema 102 TableDescriptorBuilder tableDescriptorBuilder = 103 TableDescriptorBuilder.newBuilder(TableName.valueOf("default", name)); 104 ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder = 105 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("cf")); 106 String algorithm = conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES); 107 columnFamilyDescriptorBuilder.setEncryptionType(algorithm); 108 columnFamilyDescriptorBuilder 109 .setEncryptionKey(EncryptionUtil.wrapKey(conf, "hbase", initialCFKey)); 110 tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptorBuilder.build()); 111 TableDescriptor tableDescriptor = tableDescriptorBuilder.build(); 112 113 // Create the table and some on disk files 114 createTableAndFlush(tableDescriptor); 115 116 // Verify we have store file(s) with the initial key 117 final List<Path> initialPaths = findStorefilePaths(tableDescriptor.getTableName()); 118 assertTrue(initialPaths.size() > 0); 119 for (Path path : initialPaths) { 120 assertTrue(Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)), 121 "Store file " + path + " has incorrect key"); 122 } 123 124 // Update the schema with a new encryption key 125 columnFamilyDescriptorBuilder.setEncryptionKey(EncryptionUtil.wrapKey(conf, 126 conf.get(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, User.getCurrent().getShortName()), 127 secondCFKey)); 128 TEST_UTIL.getAdmin().modifyColumnFamily(tableDescriptor.getTableName(), 129 columnFamilyDescriptorBuilder.build()); 130 Thread.sleep(5000); // Need a predicate for online schema change 131 132 // And major compact 133 TEST_UTIL.getAdmin().majorCompact(tableDescriptor.getTableName()); 134 // waiting for the major compaction to complete 135 TEST_UTIL.waitFor(30000, new Waiter.Predicate<IOException>() { 136 @Override 137 public boolean evaluate() throws IOException { 138 return TEST_UTIL.getAdmin().getCompactionState(tableDescriptor.getTableName()) 139 == CompactionState.NONE; 140 } 141 }); 142 List<Path> pathsAfterCompaction = findStorefilePaths(tableDescriptor.getTableName()); 143 assertTrue(pathsAfterCompaction.size() > 0); 144 for (Path path : pathsAfterCompaction) { 145 assertTrue(Bytes.equals(secondCFKey.getEncoded(), extractHFileKey(path)), 146 "Store file " + path + " has incorrect key"); 147 } 148 List<Path> compactedPaths = findCompactedStorefilePaths(tableDescriptor.getTableName()); 149 assertTrue(compactedPaths.size() > 0); 150 for (Path path : compactedPaths) { 151 assertTrue(Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)), 152 "Store file " + path + " retains initial key"); 153 } 154 } 155 156 @Test 157 public void testMasterKeyRotation() throws Exception { 158 // Create the table schema 159 TableDescriptorBuilder tableDescriptorBuilder = 160 TableDescriptorBuilder.newBuilder(TableName.valueOf("default", name)); 161 ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder = 162 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("cf")); 163 String algorithm = conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES); 164 columnFamilyDescriptorBuilder.setEncryptionType(algorithm); 165 columnFamilyDescriptorBuilder 166 .setEncryptionKey(EncryptionUtil.wrapKey(conf, "hbase", initialCFKey)); 167 tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptorBuilder.build()); 168 TableDescriptor tableDescriptor = tableDescriptorBuilder.build(); 169 170 // Create the table and some on disk files 171 createTableAndFlush(tableDescriptor); 172 173 // Verify we have store file(s) with the initial key 174 List<Path> storeFilePaths = findStorefilePaths(tableDescriptor.getTableName()); 175 assertTrue(storeFilePaths.size() > 0); 176 for (Path path : storeFilePaths) { 177 assertTrue(Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)), 178 "Store file " + path + " has incorrect key"); 179 } 180 181 // Now shut down the HBase cluster 182 TEST_UTIL.shutdownMiniHBaseCluster(); 183 184 // "Rotate" the master key 185 conf.set(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "other"); 186 conf.set(HConstants.CRYPTO_MASTERKEY_ALTERNATE_NAME_CONF_KEY, "hbase"); 187 188 // Start the cluster back up 189 TEST_UTIL.startMiniHBaseCluster(); 190 // Verify the table can still be loaded 191 TEST_UTIL.waitTableAvailable(tableDescriptor.getTableName(), 5000); 192 // Double check that the store file keys can be unwrapped 193 storeFilePaths = findStorefilePaths(tableDescriptor.getTableName()); 194 assertTrue(storeFilePaths.size() > 0); 195 for (Path path : storeFilePaths) { 196 assertTrue(Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)), 197 "Store file " + path + " has incorrect key"); 198 } 199 } 200 201 private static List<Path> findStorefilePaths(TableName tableName) throws Exception { 202 List<Path> paths = new ArrayList<>(); 203 for (Region region : TEST_UTIL.getRSForFirstRegionInTable(tableName).getRegions(tableName)) { 204 for (HStore store : ((HRegion) region).getStores()) { 205 for (HStoreFile storefile : store.getStorefiles()) { 206 paths.add(storefile.getPath()); 207 } 208 } 209 } 210 return paths; 211 } 212 213 private static List<Path> findCompactedStorefilePaths(TableName tableName) throws Exception { 214 List<Path> paths = new ArrayList<>(); 215 for (Region region : TEST_UTIL.getRSForFirstRegionInTable(tableName).getRegions(tableName)) { 216 for (HStore store : ((HRegion) region).getStores()) { 217 Collection<HStoreFile> compactedfiles = 218 store.getStoreEngine().getStoreFileManager().getCompactedfiles(); 219 if (compactedfiles != null) { 220 for (HStoreFile storefile : compactedfiles) { 221 paths.add(storefile.getPath()); 222 } 223 } 224 } 225 } 226 return paths; 227 } 228 229 private void createTableAndFlush(TableDescriptor tableDescriptor) throws Exception { 230 ColumnFamilyDescriptor cfd = tableDescriptor.getColumnFamilies()[0]; 231 // Create the test table 232 TEST_UTIL.getAdmin().createTable(tableDescriptor); 233 TEST_UTIL.waitTableAvailable(tableDescriptor.getTableName(), 5000); 234 // Create a store file 235 Table table = TEST_UTIL.getConnection().getTable(tableDescriptor.getTableName()); 236 try { 237 table.put(new Put(Bytes.toBytes("testrow")).addColumn(cfd.getName(), Bytes.toBytes("q"), 238 Bytes.toBytes("value"))); 239 } finally { 240 table.close(); 241 } 242 TEST_UTIL.getAdmin().flush(tableDescriptor.getTableName()); 243 } 244 245 private static byte[] extractHFileKey(Path path) throws Exception { 246 HFile.Reader reader = 247 HFile.createReader(TEST_UTIL.getTestFileSystem(), path, new CacheConfig(conf), true, conf); 248 try { 249 Encryption.Context cryptoContext = reader.getFileContext().getEncryptionContext(); 250 assertNotNull(cryptoContext, "Reader has a null crypto context"); 251 Key key = cryptoContext.getKey(); 252 assertNotNull(key, "Crypto context has no key"); 253 return key.getEncoded(); 254 } finally { 255 reader.close(); 256 } 257 } 258 259}