View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
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      // Create the test encryption keys
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      // Enable online schema updates
83      conf.setBoolean("hbase.online.schema.update.enable", true);
84  
85      // Start the minicluster
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      // Create the table schema
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     // Create the table and some on disk files
107     createTableAndFlush(htd);
108 
109     // Verify we have store file(s) with the initial key
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     // Update the schema with a new encryption key
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); // Need a predicate for online schema change
124 
125     // And major compact
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         // When compaction has finished, all of the original files will be
132         // gone
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     // Verify we have store file(s) with only the new key
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     // Create the table schema
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     // Create the table and some on disk files
175     createTableAndFlush(htd);
176 
177     // Verify we have store file(s) with the initial key
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     // Now shut down the HBase cluster
186     TEST_UTIL.shutdownMiniHBaseCluster();
187 
188     // "Rotate" the master key
189     conf.set(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "other");
190     conf.set(HConstants.CRYPTO_MASTERKEY_ALTERNATE_NAME_CONF_KEY, "hbase");
191 
192     // Start the cluster back up
193     TEST_UTIL.startMiniHBaseCluster(1, 1);
194     // Verify the table can still be loaded
195     TEST_UTIL.waitTableAvailable(htd.getName(), 5000);
196     // Double check that the store file keys can be unwrapped
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     // Create the test table
265     TEST_UTIL.getHBaseAdmin().createTable(htd);
266     TEST_UTIL.waitTableAvailable(htd.getName(), 5000);
267     // Create a store file
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 }