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.security.Key;
23  import java.security.SecureRandom;
24  import java.util.ArrayList;
25  import java.util.List;
26  
27  import javax.crypto.spec.SecretKeySpec;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.apache.hadoop.conf.Configuration;
32  import org.apache.hadoop.fs.Path;
33  import org.apache.hadoop.hbase.HBaseTestingUtility;
34  import org.apache.hadoop.hbase.HColumnDescriptor;
35  import org.apache.hadoop.hbase.HConstants;
36  import org.apache.hadoop.hbase.HTableDescriptor;
37  import org.apache.hadoop.hbase.testclassification.MediumTests;
38  import org.apache.hadoop.hbase.TableName;
39  import org.apache.hadoop.hbase.Waiter.Predicate;
40  import org.apache.hadoop.hbase.client.HTable;
41  import org.apache.hadoop.hbase.client.Put;
42  import org.apache.hadoop.hbase.client.Table;
43  import org.apache.hadoop.hbase.io.crypto.Encryption;
44  import org.apache.hadoop.hbase.io.crypto.KeyProviderForTesting;
45  import org.apache.hadoop.hbase.io.crypto.aes.AES;
46  import org.apache.hadoop.hbase.io.hfile.CacheConfig;
47  import org.apache.hadoop.hbase.io.hfile.HFile;
48  import org.apache.hadoop.hbase.security.EncryptionUtil;
49  import org.apache.hadoop.hbase.security.User;
50  import org.apache.hadoop.hbase.util.Bytes;
51  import org.junit.AfterClass;
52  import org.junit.BeforeClass;
53  import org.junit.Test;
54  import org.junit.experimental.categories.Category;
55  
56  @Category(MediumTests.class)
57  public class TestEncryptionKeyRotation {
58    private static final Log LOG = LogFactory.getLog(TestEncryptionKeyRotation.class);
59    private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
60    private static final Configuration conf = TEST_UTIL.getConfiguration();
61    private static final Key initialCFKey;
62    private static final Key secondCFKey;
63    static {
64      // Create the test encryption keys
65      SecureRandom rng = new SecureRandom();
66      byte[] keyBytes = new byte[AES.KEY_LENGTH];
67      rng.nextBytes(keyBytes);
68      String algorithm =
69          conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES);
70      initialCFKey = new SecretKeySpec(keyBytes, algorithm);
71      rng.nextBytes(keyBytes);
72      secondCFKey = new SecretKeySpec(keyBytes, algorithm);
73    }
74  
75    @BeforeClass
76    public static void setUp() throws Exception {
77      conf.setInt("hfile.format.version", 3);
78      conf.set(HConstants.CRYPTO_KEYPROVIDER_CONF_KEY, KeyProviderForTesting.class.getName());
79      conf.set(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "hbase");
80      // Enable online schema updates
81      conf.setBoolean("hbase.online.schema.update.enable", true);
82  
83      // Start the minicluster
84      TEST_UTIL.startMiniCluster(1);
85    }
86  
87    @AfterClass
88    public static void tearDown() throws Exception {
89      TEST_UTIL.shutdownMiniCluster();
90    }
91  
92    @Test
93    public void testCFKeyRotation() throws Exception {
94      // Create the table schema
95      HTableDescriptor htd = new HTableDescriptor(TableName.valueOf("default",
96        "testCFKeyRotation"));
97      HColumnDescriptor hcd = new HColumnDescriptor("cf");
98      String algorithm =
99          conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES);
100     hcd.setEncryptionType(algorithm);
101     hcd.setEncryptionKey(EncryptionUtil.wrapKey(conf, "hbase", initialCFKey));
102     htd.addFamily(hcd);
103 
104     // Create the table and some on disk files
105     createTableAndFlush(htd);
106 
107     // Verify we have store file(s) with the initial key
108     final List<Path> initialPaths = findStorefilePaths(htd.getTableName());
109     assertTrue(initialPaths.size() > 0);
110     for (Path path: initialPaths) {
111       assertTrue("Store file " + path + " has incorrect key",
112         Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)));
113     }
114 
115     // Update the schema with a new encryption key
116     hcd = htd.getFamily(Bytes.toBytes("cf"));
117     hcd.setEncryptionKey(EncryptionUtil.wrapKey(conf,
118       conf.get(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, User.getCurrent().getShortName()),
119       secondCFKey));
120     TEST_UTIL.getHBaseAdmin().modifyColumn(htd.getTableName(), hcd);
121     Thread.sleep(5000); // Need a predicate for online schema change
122 
123     // And major compact
124     TEST_UTIL.getHBaseAdmin().majorCompact(htd.getTableName());
125     TEST_UTIL.waitFor(30000, 1000, true, new Predicate<Exception>() {
126       @Override
127       public boolean evaluate() throws Exception {
128         // When compaction has finished, all of the original files will be
129         // gone
130         boolean found = false;
131         for (Path path: initialPaths) {
132           found = TEST_UTIL.getTestFileSystem().exists(path);
133           if (found) {
134             LOG.info("Found " + path);
135             break;
136           }
137         }
138         return !found;
139       }
140     });
141 
142     // Verify we have store file(s) with only the new key
143     List<Path> pathsAfterCompaction = findStorefilePaths(htd.getTableName());
144     assertTrue(pathsAfterCompaction.size() > 0);
145     for (Path path: pathsAfterCompaction) {
146       assertFalse("Store file " + path + " retains initial key",
147         Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)));
148       assertTrue("Store file " + path + " has incorrect key",
149         Bytes.equals(secondCFKey.getEncoded(), extractHFileKey(path)));
150     }
151   }
152 
153   @Test
154   public void testMasterKeyRotation() throws Exception {
155     // Create the table schema
156     HTableDescriptor htd = new HTableDescriptor(TableName.valueOf("default",
157       "testMasterKeyRotation"));
158     HColumnDescriptor hcd = new HColumnDescriptor("cf");
159     String algorithm =
160         conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES);
161     hcd.setEncryptionType(algorithm);
162     hcd.setEncryptionKey(EncryptionUtil.wrapKey(conf, "hbase", initialCFKey));
163     htd.addFamily(hcd);
164 
165     // Create the table and some on disk files
166     createTableAndFlush(htd);
167 
168     // Verify we have store file(s) with the initial key
169     List<Path> storeFilePaths = findStorefilePaths(htd.getTableName());
170     assertTrue(storeFilePaths.size() > 0);
171     for (Path path: storeFilePaths) {
172       assertTrue("Store file " + path + " has incorrect key",
173         Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)));
174     }
175 
176     // Now shut down the HBase cluster
177     TEST_UTIL.shutdownMiniHBaseCluster();
178 
179     // "Rotate" the master key
180     conf.set(HConstants.CRYPTO_MASTERKEY_NAME_CONF_KEY, "other");
181     conf.set(HConstants.CRYPTO_MASTERKEY_ALTERNATE_NAME_CONF_KEY, "hbase");
182 
183     // Start the cluster back up
184     TEST_UTIL.startMiniHBaseCluster(1, 1);
185     // Verify the table can still be loaded
186     TEST_UTIL.waitTableAvailable(htd.getName(), 5000);
187     // Double check that the store file keys can be unwrapped
188     storeFilePaths = findStorefilePaths(htd.getTableName());
189     assertTrue(storeFilePaths.size() > 0);
190     for (Path path: storeFilePaths) {
191       assertTrue("Store file " + path + " has incorrect key",
192         Bytes.equals(initialCFKey.getEncoded(), extractHFileKey(path)));
193     }
194   }
195 
196   private static List<Path> findStorefilePaths(TableName tableName) throws Exception {
197     List<Path> paths = new ArrayList<Path>();
198     for (Region region:
199         TEST_UTIL.getRSForFirstRegionInTable(tableName).getOnlineRegions(tableName)) {
200       for (Store store: region.getStores()) {
201         for (StoreFile storefile: store.getStorefiles()) {
202           paths.add(storefile.getPath());
203         }
204       }
205     }
206     return paths;
207   }
208 
209   private void createTableAndFlush(HTableDescriptor htd) throws Exception {
210     HColumnDescriptor hcd = htd.getFamilies().iterator().next();
211     // Create the test table
212     TEST_UTIL.getHBaseAdmin().createTable(htd);
213     TEST_UTIL.waitTableAvailable(htd.getName(), 5000);
214     // Create a store file
215     Table table = new HTable(conf, htd.getTableName());
216     try {
217       table.put(new Put(Bytes.toBytes("testrow"))
218         .add(hcd.getName(), Bytes.toBytes("q"), Bytes.toBytes("value")));
219     } finally {
220       table.close();
221     }
222     TEST_UTIL.getHBaseAdmin().flush(htd.getTableName());
223   }
224 
225   private static byte[] extractHFileKey(Path path) throws Exception {
226     HFile.Reader reader = HFile.createReader(TEST_UTIL.getTestFileSystem(), path,
227       new CacheConfig(conf), conf);
228     try {
229       reader.loadFileInfo();
230       Encryption.Context cryptoContext = reader.getFileContext().getEncryptionContext();
231       assertNotNull("Reader has a null crypto context", cryptoContext);
232       Key key = cryptoContext.getKey();
233       assertNotNull("Crypto context has no key", key);
234       return key.getEncoded();
235     } finally {
236       reader.close();
237     }
238   }
239 
240 }