HBase Cell Level Encryption Test Plan - cto-bdt-qa/bdt-qa GitHub Wiki
HBase Cell Level Encryption is a feature that provides a mechanism to encrypt/decrypt HBase Cell. Also, it provides mechanisms to protect users' secrets such as keys. Please note that the feature is mainly implemented at client side. So, it should be able to co-work with 'HBase Transparent Encryption' perfectly. Besides, the cell data should be encrypted/decrypted at the client side.
To users, the feature only provides API. Inserting/Searching data are not transparent to end users, they should use specific APIs.
- Key store is the only key provider for the feature.
- Key store is unprotected by any extra mechanisms.
- Only AES is supported.
Before using the feature, we should provide HBase with some extra configurations.
Encryption keys are the secret used to encrypt users' data. Keys are stored in JKS/JCEKS.
Key provider is a mechanism to provide master key. Currently, all the master keys are stored in JKS or JCEKS.
<property> <name>hbase.client.crypto.keyprovider</name> <value>org.apache.hadoop.io.crypto.KeyStoreKeyProvider</value></property>
We need to provide HBase with JKS location, key store type and key store password.
<property> <name>hbase.client.crypto.keyprovider.parameters</name> <value><![CDATA[keyStoreUrl=file:///path/to/hbase/client/conf/hbase.jks&keyStoreType=JCEKS&password=password]]></value></property>
Note that, XML characters in the value should be escaped, so that the string value can be used as simple text in hbase-site.xml:
- & -> &
e.g. keyStoreUrl=file:///tmp/hbase_cell.keystore&keyStoreType=JCEKS&password=123456
We should define a statute to tell HBase how to use keys explicitly. The master key is a table-level key. That means each HBase user can specify a master key for a table. The master key should be used by the user who specified key only.
<property> <name>hbase.crypto.user.key.mapping</name> <value>%t-%u</value></property>
%t represents table name and %u means user name. E.g. According to the above statute, if user A wants to specify his/her master key for table B. The key alias must be B-A.
You can use Java key store tool to generate your keystore.
E.g. Create a JCEKS whose password is 123456 and import a key named B-A to the keystore.
keytool -keystore /path/to/hbase/client/conf/keystore.jks \ -storetype jceks -storepass 123456 \ -genseckey -keyalg AES -keysize 256 \ -alias B-A
EncryptedHTable is the user programming interface just like HTable. It provides extra mechenism for encryption. There are 4 approaches to instantiate it.
Approach 1
EncryptedHTable htable = new EncryptedHTable(TestContext.HBASE_CONF, Bytes.toBytes(TEST_TBL_1));
Approach 2
EncryptedHTable htable = new EncryptedHTable(TestContext.HBASE_CONF, Bytes.toBytes(TEST_TBL_1), POOL);
Create a simple thread pool first, then, instantiate EncryptedHTable
private static final ExecutorService POOL = Executors.newCachedThreadPool();
Approach 1
EncryptedHTable htable = new EncryptedHTable(TestContext.HBASE_CONF, Bytes.toBytes("tbl_1"), POOL);
Approach 2
HConnection hConn = HConnectionManager.createConnection(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(Bytes.toBytes("tbl_1"), hConn, POOL);
The feature provides a factory to instantiate and release EncryptedHTables.
EncryptedHTableFactory factory = new EncryptedHTableFactory();HTableInterface htable = factory.createHTableInterface(TestContext.HBASE_CONF, Bytes.toByte("tbl_1"));
factory.releaseHTableInterface(Bytes.toByte("tbl_1"));
- Write single record:
Write normal data
final Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);final byte[] data = Bytes.toBytes("value");EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);final byte[] rowKey = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowKey).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data));htable.close();
Write data twice
the previous data will be overwritten
final Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);final byte[] data = Bytes.toBytes("value");final byte[] data2 = Bytes.toBytes("value2");EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);final byte[] rowKey = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowKey).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data));htable.put(new Put(rowKey).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data2));htable.close();
Write big data
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);final byte[] data = Bytes.toBytes("qwertyuiopasdfghjklzxcvbnm1234567890qwertyuiop[]asdfgh"+ "jkl;'zxcvbnm,.1234567890-=qwertyuiopasdfghjklzxcvbnm1234567890qwertyuiop[]asdfgh"+ "jkl;'zxcvbnm,.1234567890-=qwertyuiopasdfghjklzxcvbnm1234567890qwertyuiop[]asdfgh"+ "jkl;'zxcvbnm,.1234567890-=qwertyuiopasdfghjklzxcvbnm1234567890qwertyuiop[]asdfgh"+ "jkl;'zxcvbnm,.1234567890-=qwertyuiopasdfghjklzxcvbnm1234567890qwertyuiop[]asdfgh"+ "jkl;'zxcvbnm,.1234567890-=qwertyuiopasdfghjklzxcvbnm1234567890qwertyuiop[]asdfgh"+ "jkl;'zxcvbnm,.1234567890-=" + 23454801);htable = new EncryptedHTable(conf, TEST_TBL_1);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data));
Write empty data
will get empty data when get the value
final Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);final byte[] data = Bytes.toBytes("");EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);final byte[] rowKey = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowKey).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data));htable.close();
Write null data
will get null when get the value
final Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);final byte[] data = null;EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);final byte[] rowKey = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowKey).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data));htable.close();
- Write multiple records:
final Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);Puts puts = HBaseUtils.generatePut(10, TEST_TBL_1_CF2, TEST_TBL_1_QUALIFIER1);htable.put(puts.puts);
- Check and write:
Atomically checks if a row/family/qualifier value matches the expected value. If it does, it adds the put. If the passed value is null, the check is for the lack of column.
Check is passed and write a new data
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);final byte[] data = Bytes.toBytes("value");final byte[] data2 = Bytes.toBytes("value2");htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1,TEST_TBL_1_QUALIFIER1, data));if (!htable.checkAndPut(rowId, TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data,new Put(rowId).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data2))) {throw new RuntimeException("Check failed!");}
Check is not passed and won't write a new data
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);final byte[] data = Bytes.toBytes("value");final byte[] data2 = Bytes.toBytes("value2");htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1,TEST_TBL_1_QUALIFIER1, data));if (htable.checkAndPut(rowId, TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data2, new Put(rowId).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data2))) {throw new RuntimeException("Check failed!");}
Check will also be failed if there's no data
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);final byte[] data = Bytes.toBytes("value");final byte[] data2 = Bytes.toBytes("value2");htable = new EncryptedHTable(conf, TEST_TBL_1);htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());if (htable.checkAndPut(rowId, TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data, new Put(rowId).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data2))) {throw new RuntimeException("Check failed!");}
- Write by mutateRow:
Performs multiple mutations atomically on a single row.
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());RowMutations rmadd = new RowMutations(rowId);rmadd.add(new Put(rowId).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data));rmadd.add(new Put(rowId).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER2, data));htable.mutateRow(rmadd);
- Increase the value
Increments one or more columns within a single row. The amount to increment the cell can be either positive or negative. If overflow the maximun value, it will cycle to the minimun value(example: 9223372036854775807 + 1 = -9223372036854775808).
Increment one or more columns within a single row
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);final byte[] data = Bytes.toBytes(123L);final byte[] result = Bytes.toBytes(124L);htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1,TEST_TBL_1_QUALIFIER1, data));htable.increment(new Increment(rowId).addColumn(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1,1));
Increment a column value
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);final byte[] data = Bytes.toBytes(123L);final byte[] result = Bytes.toBytes(124L);htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1,TEST_TBL_1_QUALIFIER1, data));htable.incrementColumnValue(rowId, TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, 1);
Increment a negative amount
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);final byte[] data = Bytes.toBytes(123L);final byte[] result = Bytes.toBytes(-1L);htable = new EncryptedHTable(conf, TEST_TBL_1);htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data));htable.incrementColumnValue(rowId, TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, -124);
Overflow when increment
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);final byte[] data = Bytes.toBytes(9223372036854775807L);final byte[] result = Bytes.toBytes(-9223372036854775808L);htable = new EncryptedHTable(conf, TEST_TBL_1);htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data));htable.incrementColumnValue(rowId, TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, 1);
Increment a column value and write logs
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);final byte[] data = Bytes.toBytes(123L);final byte[] result = Bytes.toBytes(124L);htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1,TEST_TBL_1_QUALIFIER1, data));htable.incrementColumnValue(rowId, TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, 1, true);
- Append the string
Appends values to one or more columns within a single row.
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);final byte[] data = Bytes.toBytes("a");final byte[] data2 = Bytes.toBytes("b");htable = new EncryptedHTable(conf, TEST_TBL_1);htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1,TEST_TBL_1_QUALIFIER1, data));htable.append(new Append(rowId).add(TEST_TBL_1_CF1,TEST_TBL_1_QUALIFIER1, data2));
If append null, nothing will be done
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);final byte[] data = Bytes.toBytes("a");final byte[] data2 = null;htable = new EncryptedHTable(conf, TEST_TBL_1);htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1,TEST_TBL_1_QUALIFIER1, data));htable.append(new Append(rowId).add(TEST_TBL_1_CF1,TEST_TBL_1_QUALIFIER1, data2));
- Read data by Get:
byte[] rs = htable.get(new Get(rowKey)).getValue(TEST_TBL_1_CF1,TEST_TBL_1_QUALIFIER1);
- Scan by given family and column:
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);ResultScanner scanner = htable.getScanner(TEST_TBL_1_CF2,TEST_TBL_1_QUALIFIER1);
- Scan by given family:
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);ResultScanner scanner = htable.getScanner(TEST_TBL_1_CF2);
- Scan by specified object:
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);Scan scan = new Scan();scan.setMaxVersions(1);scan.addColumn(TEST_TBL_1_CF2, TEST_TBL_1_QUALIFIER1);ResultScanner scanner = htable.getScanner(scan);Iterator<Result> it = scanner.iterator();while (it.hasNext()) {Result rs = it.next();if (!IDS.contains(Bytes.toString(rs.getRow()))) {assertTrue("Test result is out of the expected data.",false);}else {KeyValue[] kvs = rs.raw();for (KeyValue kv : kvs) {if (Bytes.equals(Bytes.toBytes("value"), kv.getValue())) {assertTrue("Unexpected test result.", false);}}}}
nonexistent data won't be scanned
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);Scan scan = new Scan();scan.setMaxVersions(1);scan.addColumn(TEST_TBL_1_CF2, TEST_TBL_1_QUALIFIER1);ResultScanner scanner = htable.getScanner(scan);Iterator<Result> it = scanner.iterator();while (it.hasNext()) {Result rs = it.next();if (!IDS.contains(Bytes.toString(rs.getRow()))) {assertTrue("Test result is out of the expected data.",false);}else {KeyValue[] kvs = rs.raw();for (KeyValue kv : kvs) {if (Bytes.equals(Bytes.toBytes("value123"), kv.getValue())) {assertTrue("Unexpected test result.", false);}}}}
- Scan and return row:
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);Result rs = htable.getRowOrBefore(Bytes.toBytes(rowId),TEST_TBL_1_CF2);if (!Bytes.equals(Bytes.toBytes("value"),rs.getValue(TEST_TBL_1_CF2, TEST_TBL_1_QUALIFIER1))){throw new RuntimeException("Data is fetched & decrypted failed!");}
nonexistent data won't be scanned
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);Result rs = htable.getRowOrBefore(Bytes.toBytes(rowId),TEST_TBL_1_CF2);if (!Bytes.equals(Bytes.toBytes("value123"),rs.getValue(TEST_TBL_1_CF2, TEST_TBL_1_QUALIFIER1))){throw new RuntimeException("Data is fetched & decrypted failed!");}
- Check and delete:
Atomically checks if a row/family/qualifier value matches the expected value. If it does, it adds the delete. If the passed value is null, the check is for the lack of column.
Check is passed and delete the data
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1,TEST_TBL_1_QUALIFIER1, data));if (!htable.checkAndDelete(rowId, TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data,new Delete(rowId).deleteColumn(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1))) {throw new RuntimeException("Check failed!");}
Check is not passed and won't delete the data
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);final byte[] data = Bytes.toBytes("value");final byte[] data2 = Bytes.toBytes("value2");htable = new EncryptedHTable(conf, TEST_TBL_1);htable.addFamily(TEST_TBL_1_CF2);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());htable.put(new Put(rowId).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data));if (htable.checkAndDelete(rowId, TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data2,new Delete(rowId).deleteColumn(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1))) {throw new RuntimeException("Check failed!");}
- Delete by mutateRow:
Performs multiple mutations atomically on a single row.
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());RowMutations rmdelete = new RowMutations(rowId);rmdelete.add(new Delete(rowId).deleteColumn(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1));rmdelete.add(new Delete(rowId).deleteColumn(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER2));htable.mutateRow(rmdelete);
Method that does a batch call on Deletes, Gets, Puts, Increments, Appends and RowMutations.
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);final byte[] data1 = Bytes.toBytes("a");final byte[] data2 = Bytes.toBytes("b");final byte[] data3 = Bytes.toBytes(2L);final byte[] data4 = Bytes.toBytes(3L);final byte[] data = Bytes.toBytes("ab");htable = new EncryptedHTable(conf, TEST_TBL_1);byte[] rowId = Bytes.toBytes(UUID.randomUUID().toString());byte[] rowId2 = Bytes.toBytes(UUID.randomUUID().toString());byte[] rowId3 = Bytes.toBytes(UUID.randomUUID().toString());htable.addFamily(TEST_TBL_1_CF2);htable.put(new Put(rowId).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data1));htable.put(new Put(rowId3).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data3));List<Row> action = new ArrayList<Row>();Put put = new Put(rowId2).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data1);action.add(put);Append append = new Append(rowId).add(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, data2);action.add(append);Increment increment = new Increment(rowId3).addColumn( TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1, 1);action.add(increment);Get get = new Get(rowId).addColumn(TEST_TBL_1_CF1, TEST_TBL_1_QUALIFIER1);action.add(get);htable.batch(action);
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);htable.addFamily(Bytes.toBytes("cf1"));Collection<byte[]> family = htable.getFamilySet();
Add a family twice
When add a family already in the familyset, action is allowed but nothing will be done.
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);htable.addFamily(Bytes.toBytes("cf1"));htable.addFamily(Bytes.toBytes("cf1"));Collection<byte[]> family = htable.getFamilySet();
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);htable.removeFamily(Bytes.toBytes("cf1"));Collection<byte[]> family = htable.getFamilySet();
Remove a family twice
When remove a family not in the familyset, action is allowed but nothing will be done.
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);htable.removeFamily(Bytes.toBytes("cf1"));htable.removeFamily(Bytes.toBytes("cf1"));Collection<byte[]> family = htable.getFamilySet();
Configuration conf = HBaseConfiguration.create(TestContext.HBASE_CONF);EncryptedHTable htable = new EncryptedHTable(conf, TEST_TBL_1);List<byte[]> family = new ArrayList<byte[]>();family.add(TEST_TBL_1_CF1);family.add(TEST_TBL_1_CF2);htable.setFamilySet(family);
family = htable.getFamilySet();
Please configure hbase client crypto related configurations before running test cases. The keystore named hbase-cell.keystore has been put in resource dir of the test driver. The password of keystore is 123456 and store type is JCEKS.
Please execute the test driver with user root.
ant runner