1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.hadoop.hbase.regionserver;
19
20 import static org.junit.Assert.*;
21
22 import java.io.IOException;
23 import java.security.Key;
24 import java.security.SecureRandom;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.List;
28
29 import javax.crypto.spec.SecretKeySpec;
30
31 import org.apache.commons.logging.Log;
32 import org.apache.commons.logging.LogFactory;
33 import org.apache.hadoop.conf.Configuration;
34 import org.apache.hadoop.fs.Path;
35 import org.apache.hadoop.hbase.HBaseTestingUtility;
36 import org.apache.hadoop.hbase.HColumnDescriptor;
37 import org.apache.hadoop.hbase.HConstants;
38 import org.apache.hadoop.hbase.HTableDescriptor;
39 import org.apache.hadoop.hbase.testclassification.MediumTests;
40 import org.apache.hadoop.hbase.TableName;
41 import org.apache.hadoop.hbase.Waiter.Predicate;
42 import org.apache.hadoop.hbase.client.HTable;
43 import org.apache.hadoop.hbase.client.Put;
44 import org.apache.hadoop.hbase.client.Table;
45 import org.apache.hadoop.hbase.io.crypto.Encryption;
46 import org.apache.hadoop.hbase.io.crypto.KeyProviderForTesting;
47 import org.apache.hadoop.hbase.io.crypto.aes.AES;
48 import org.apache.hadoop.hbase.io.hfile.CacheConfig;
49 import org.apache.hadoop.hbase.io.hfile.HFile;
50 import org.apache.hadoop.hbase.security.EncryptionUtil;
51 import org.apache.hadoop.hbase.security.User;
52 import org.apache.hadoop.hbase.util.Bytes;
53 import org.junit.AfterClass;
54 import org.junit.BeforeClass;
55 import org.junit.Test;
56 import org.junit.experimental.categories.Category;
57
58 @Category(MediumTests.class)
59 public class TestEncryptionKeyRotation {
60 private static final Log LOG = LogFactory.getLog(TestEncryptionKeyRotation.class);
61 private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
62 private static final Configuration conf = TEST_UTIL.getConfiguration();
63 private static final Key initialCFKey;
64 private static final Key secondCFKey;
65 static {
66
67 SecureRandom rng = new SecureRandom();
68 byte[] keyBytes = new byte[AES.KEY_LENGTH];
69 rng.nextBytes(keyBytes);
70 String algorithm =
71 conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES);
72 initialCFKey = new SecretKeySpec(keyBytes, algorithm);
73 rng.nextBytes(keyBytes);
74 secondCFKey = new SecretKeySpec(keyBytes, algorithm);
75 }
76
77 @BeforeClass
78 public static void setUp() throws Exception {
79 conf.setInt("hfile.format.version", 3);
80 conf.set(HConstants.CRYPTO_KEYPROVIDER_CONF_KEY, KeyProviderForTesting.class.getName());
81 conf.set(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "hbase");
82
83 conf.setBoolean("hbase.online.schema.update.enable", true);
84
85
86 TEST_UTIL.startMiniCluster(1);
87 }
88
89 @AfterClass
90 public static void tearDown() throws Exception {
91 TEST_UTIL.shutdownMiniCluster();
92 }
93
94 @Test
95 public void testCFKeyRotation() throws Exception {
96
97 HTableDescriptor htd = new HTableDescriptor(TableName.valueOf("default",
98 "testCFKeyRotation"));
99 HColumnDescriptor hcd = new HColumnDescriptor("cf");
100 String algorithm =
101 conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES);
102 hcd.setEncryptionType(algorithm);
103 hcd.setEncryptionKey(EncryptionUtil.wrapKey(conf, "hbase", initialCFKey));
104 htd.addFamily(hcd);
105
106
107 createTableAndFlush(htd);
108
109
110 final List<Path> initialPaths = findStorefilePaths(htd.getTableName());
111 assertTrue(initialPaths.size() > 0);
112 for (Path path: initialPaths) {
113 assertTrue("Store file " + path + " has incorrect key",
114 Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)));
115 }
116
117
118 hcd = htd.getFamily(Bytes.toBytes("cf"));
119 hcd.setEncryptionKey(EncryptionUtil.wrapKey(conf,
120 conf.get(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, User.getCurrent().getShortName()),
121 secondCFKey));
122 TEST_UTIL.getHBaseAdmin().modifyColumn(htd.getTableName(), hcd);
123 Thread.sleep(5000);
124
125
126 TEST_UTIL.getHBaseAdmin().majorCompact(htd.getTableName());
127 final List<Path> updatePaths = findCompactedStorefilePaths(htd.getTableName());
128 TEST_UTIL.waitFor(30000, 1000, true, new Predicate<Exception>() {
129 @Override
130 public boolean evaluate() throws Exception {
131
132
133 boolean found = false;
134 for (Path path: updatePaths) {
135 found = TEST_UTIL.getTestFileSystem().exists(path);
136 if (found) {
137 LOG.info("Found " + path);
138 break;
139 }
140 }
141 return !found;
142 }
143 });
144
145
146 Thread.sleep(1000);
147 waitForCompaction(htd.getTableName());
148 List<Path> pathsAfterCompaction = findStorefilePaths(htd.getTableName());
149 assertTrue(pathsAfterCompaction.size() > 0);
150 for (Path path: pathsAfterCompaction) {
151 assertTrue("Store file " + path + " has incorrect key",
152 Bytes.equals(secondCFKey.getEncoded(), extractHFileKey(path)));
153 }
154 List<Path> compactedPaths = findCompactedStorefilePaths(htd.getTableName());
155 assertTrue(compactedPaths.size() > 0);
156 for (Path path: compactedPaths) {
157 assertTrue("Store file " + path + " retains initial key",
158 Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)));
159 }
160 }
161
162 @Test
163 public void testMasterKeyRotation() throws Exception {
164
165 HTableDescriptor htd = new HTableDescriptor(TableName.valueOf("default",
166 "testMasterKeyRotation"));
167 HColumnDescriptor hcd = new HColumnDescriptor("cf");
168 String algorithm =
169 conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES);
170 hcd.setEncryptionType(algorithm);
171 hcd.setEncryptionKey(EncryptionUtil.wrapKey(conf, "hbase", initialCFKey));
172 htd.addFamily(hcd);
173
174
175 createTableAndFlush(htd);
176
177
178 List<Path> storeFilePaths = findStorefilePaths(htd.getTableName());
179 assertTrue(storeFilePaths.size() > 0);
180 for (Path path: storeFilePaths) {
181 assertTrue("Store file " + path + " has incorrect key",
182 Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)));
183 }
184
185
186 TEST_UTIL.shutdownMiniHBaseCluster();
187
188
189 conf.set(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "other");
190 conf.set(HConstants.CRYPTO_MASTERKEY_ALTERNATE_NAME_CONF_KEY, "hbase");
191
192
193 TEST_UTIL.startMiniHBaseCluster(1, 1);
194
195 TEST_UTIL.waitTableAvailable(htd.getName(), 5000);
196
197 storeFilePaths = findStorefilePaths(htd.getTableName());
198 assertTrue(storeFilePaths.size() > 0);
199 for (Path path: storeFilePaths) {
200 assertTrue("Store file " + path + " has incorrect key",
201 Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)));
202 }
203 }
204
205 private static void waitForCompaction(TableName tableName)
206 throws IOException, InterruptedException {
207 boolean compacted = false;
208 for (Region region : TEST_UTIL.getRSForFirstRegionInTable(tableName)
209 .getOnlineRegions(tableName)) {
210 for (Store store : region.getStores()) {
211 compacted = false;
212 while (!compacted) {
213 if (store.getStorefiles() != null) {
214 while (store.getStorefilesCount() != 1) {
215 Thread.sleep(100);
216 }
217 for (StoreFile storefile : store.getStorefiles()) {
218 if (!storefile.isCompactedAway()) {
219 compacted = true;
220 break;
221 }
222 Thread.sleep(100);
223 }
224 } else {
225 break;
226 }
227 }
228 }
229 }
230 }
231
232 private static List<Path> findStorefilePaths(TableName tableName) throws Exception {
233 List<Path> paths = new ArrayList<Path>();
234 for (Region region:
235 TEST_UTIL.getRSForFirstRegionInTable(tableName).getOnlineRegions(tableName)) {
236 for (Store store: region.getStores()) {
237 for (StoreFile storefile: store.getStorefiles()) {
238 paths.add(storefile.getPath());
239 }
240 }
241 }
242 return paths;
243 }
244
245 private static List<Path> findCompactedStorefilePaths(TableName tableName) throws Exception {
246 List<Path> paths = new ArrayList<Path>();
247 for (Region region:
248 TEST_UTIL.getRSForFirstRegionInTable(tableName).getOnlineRegions(tableName)) {
249 for (Store store : region.getStores()) {
250 Collection<StoreFile> compactedfiles =
251 ((HStore) store).getStoreEngine().getStoreFileManager().getCompactedfiles();
252 if (compactedfiles != null) {
253 for (StoreFile storefile : compactedfiles) {
254 paths.add(storefile.getPath());
255 }
256 }
257 }
258 }
259 return paths;
260 }
261
262 private void createTableAndFlush(HTableDescriptor htd) throws Exception {
263 HColumnDescriptor hcd = htd.getFamilies().iterator().next();
264
265 TEST_UTIL.getHBaseAdmin().createTable(htd);
266 TEST_UTIL.waitTableAvailable(htd.getName(), 5000);
267
268 Table table = new HTable(conf, htd.getTableName());
269 try {
270 table.put(new Put(Bytes.toBytes("testrow"))
271 .add(hcd.getName(), Bytes.toBytes("q"), Bytes.toBytes("value")));
272 } finally {
273 table.close();
274 }
275 TEST_UTIL.getHBaseAdmin().flush(htd.getTableName());
276 }
277
278 private static byte[] extractHFileKey(Path path) throws Exception {
279 HFile.Reader reader = HFile.createReader(TEST_UTIL.getTestFileSystem(), path,
280 new CacheConfig(conf), conf);
281 try {
282 reader.loadFileInfo();
283 Encryption.Context cryptoContext = reader.getFileContext().getEncryptionContext();
284 assertNotNull("Reader has a null crypto context", cryptoContext);
285 Key key = cryptoContext.getKey();
286 assertNotNull("Crypto context has no key", key);
287 return key.getEncoded();
288 } finally {
289 reader.close();
290 }
291 }
292
293 }