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}