Skip to content

Commit 44f8061

Browse files
committed
HBASE-29368 [Precursor] Add API surface and refactoring for key management feature
This commit prepares the codebase for the upcoming key management feature (HBASE-29368) by introducing the necessary API definitions, protocol buffer changes, and infrastructure refactoring. No functional changes are included; all implementation will follow in the feature PR. This precursor PR essentially extracts the API surface definitions and infrastructure refactoring from the main feature PR (apache#7421) to facilitate easier review. By separating the ~15k line feature PR into a smaller precursor containing interface definitions, protocol changes, and method signature updates, the subsequent feature PR will focus purely on implementation logic. API Surface Additions: * New interfaces: - KeymetaAdmin: Admin API for key management operations - Server methods for cache management (getManagedKeyDataCache, getSystemKeyCache) * Protocol buffer definitions: - ManagedKeys.proto: Definitions for managed key data and operations - Admin.proto: RPC methods for key management admin operations - Procedure.proto: Key rotation procedure support Infrastructure Refactoring: * Encryption context creation: - Moved createEncryptionContext from EncryptionUtil (client) to SecurityUtil (server) where it properly belongs, as it requires server-side resources - Added overloads to support future key encryption key (KEK) parameters * Method signature updates: - Added ManagedKeyDataCache and SystemKeyCache parameters to encryption-related methods throughout HRegion, HStore, HStoreFile, and HFile classes - Updated constructors and factory methods to thread cache references - All cache parameters are currently null/unused, enabling gradual feature rollout * New utility methods: - Encryption.encryptWithGivenKey() / decryptWithGivenKey(): Extract method refactoring to support both subject-based and KEK-based encryption - EncryptionUtil.wrapKey() / unwrapKey() overloads with KEK parameter - Bytes.add() 4-argument overload for concatenation Stub Infrastructure: * Blank place holder shells for some public data classes such as ManagedKeyData and KeymetaAdminClient * Stub implementations for key management services and caches that return null or throw UnsupportedOperationException, clearly documented as placeholders * New package org.apache.hadoop.hbase.keymeta for key management classes * Mock services updated to support new cache getter methods for testing Code Organization: * Procedure framework: Added support for region-level server name tracking to support future key rotation procedures * Testing infrastructure updated to support new constructor signatures All stub implementations clearly document they are placeholders for the upcoming feature PR. Existing encryption functionality remains unchanged and continues to work as before. Testing: * All existing tests pass (precursor introduces no functional changes) * Build completes successfully with new API surface * Backward compatibility maintained for non-key-management code paths
1 parent 9805ddc commit 44f8061

80 files changed

Lines changed: 1717 additions & 281 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,11 @@ linklint/
2525
**/*.log
2626
tmp
2727
**/.flattened-pom.xml
28+
.sw*
29+
.*.sw*
30+
ID
31+
filenametags
32+
tags
33+
.codegenie/
2834
.vscode/
2935
**/__pycache__

hbase-client/src/main/java/org/apache/hadoop/hbase/client/ColumnFamilyDescriptor.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ public interface ColumnFamilyDescriptor {
114114
/** Returns Return the raw crypto key attribute for the family, or null if not set */
115115
byte[] getEncryptionKey();
116116

117+
/** Returns the encryption key namespace for this family */
118+
String getEncryptionKeyNamespace();
119+
117120
/** Returns Return the encryption algorithm in use by this family */
118121
String getEncryptionType();
119122

hbase-client/src/main/java/org/apache/hadoop/hbase/client/ColumnFamilyDescriptorBuilder.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ public class ColumnFamilyDescriptorBuilder {
167167
@InterfaceAudience.Private
168168
public static final String ENCRYPTION_KEY = "ENCRYPTION_KEY";
169169
private static final Bytes ENCRYPTION_KEY_BYTES = new Bytes(Bytes.toBytes(ENCRYPTION_KEY));
170+
@InterfaceAudience.Private
171+
public static final String ENCRYPTION_KEY_NAMESPACE = "ENCRYPTION_KEY_NAMESPACE";
172+
private static final Bytes ENCRYPTION_KEY_NAMESPACE_BYTES =
173+
new Bytes(Bytes.toBytes(ENCRYPTION_KEY_NAMESPACE));
170174

171175
private static final boolean DEFAULT_MOB = false;
172176
@InterfaceAudience.Private
@@ -320,6 +324,7 @@ public static Map<String, String> getDefaultValues() {
320324
DEFAULT_VALUES.keySet().forEach(s -> RESERVED_KEYWORDS.add(new Bytes(Bytes.toBytes(s))));
321325
RESERVED_KEYWORDS.add(new Bytes(Bytes.toBytes(ENCRYPTION)));
322326
RESERVED_KEYWORDS.add(new Bytes(Bytes.toBytes(ENCRYPTION_KEY)));
327+
RESERVED_KEYWORDS.add(new Bytes(Bytes.toBytes(ENCRYPTION_KEY_NAMESPACE)));
323328
RESERVED_KEYWORDS.add(new Bytes(Bytes.toBytes(IS_MOB)));
324329
RESERVED_KEYWORDS.add(new Bytes(Bytes.toBytes(MOB_THRESHOLD)));
325330
RESERVED_KEYWORDS.add(new Bytes(Bytes.toBytes(MOB_COMPACT_PARTITION_POLICY)));
@@ -522,6 +527,11 @@ public ColumnFamilyDescriptorBuilder setEncryptionKey(final byte[] value) {
522527
return this;
523528
}
524529

530+
public ColumnFamilyDescriptorBuilder setEncryptionKeyNamespace(final String value) {
531+
desc.setEncryptionKeyNamespace(value);
532+
return this;
533+
}
534+
525535
public ColumnFamilyDescriptorBuilder setEncryptionType(String value) {
526536
desc.setEncryptionType(value);
527537
return this;
@@ -1337,6 +1347,20 @@ public ModifyableColumnFamilyDescriptor setEncryptionKey(byte[] keyBytes) {
13371347
return setValue(ENCRYPTION_KEY_BYTES, new Bytes(keyBytes));
13381348
}
13391349

1350+
@Override
1351+
public String getEncryptionKeyNamespace() {
1352+
return getStringOrDefault(ENCRYPTION_KEY_NAMESPACE_BYTES, Function.identity(), null);
1353+
}
1354+
1355+
/**
1356+
* Set the encryption key namespace attribute for the family
1357+
* @param keyNamespace the key namespace, or null to remove existing setting
1358+
* @return this (for chained invocation)
1359+
*/
1360+
public ModifyableColumnFamilyDescriptor setEncryptionKeyNamespace(String keyNamespace) {
1361+
return setValue(ENCRYPTION_KEY_NAMESPACE_BYTES, keyNamespace);
1362+
}
1363+
13401364
@Override
13411365
public long getMobThreshold() {
13421366
return getStringOrDefault(MOB_THRESHOLD_BYTES, Long::valueOf, DEFAULT_MOB_THRESHOLD);
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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.keymeta;
19+
20+
import java.io.IOException;
21+
import java.security.KeyException;
22+
import java.util.List;
23+
import org.apache.hadoop.hbase.client.Connection;
24+
import org.apache.hadoop.hbase.io.crypto.ManagedKeyData;
25+
import org.apache.yetus.audience.InterfaceAudience;
26+
27+
/**
28+
* STUB IMPLEMENTATION - Feature not yet complete. This class will be fully implemented in
29+
* HBASE-29368 feature PR.
30+
*/
31+
@InterfaceAudience.Public
32+
public class KeymetaAdminClient implements KeymetaAdmin {
33+
34+
public KeymetaAdminClient(Connection conn) throws IOException {
35+
// Stub constructor
36+
}
37+
38+
@Override
39+
public ManagedKeyData enableKeyManagement(byte[] keyCust, String keyNamespace)
40+
throws IOException, KeyException {
41+
throw new UnsupportedOperationException("KeymetaAdmin feature not yet implemented");
42+
}
43+
44+
@Override
45+
public List<ManagedKeyData> getManagedKeys(byte[] keyCust, String keyNamespace)
46+
throws IOException, KeyException {
47+
throw new UnsupportedOperationException("KeymetaAdmin feature not yet implemented");
48+
}
49+
50+
@Override
51+
public boolean rotateSTK() throws IOException {
52+
throw new UnsupportedOperationException("KeymetaAdmin feature not yet implemented");
53+
}
54+
55+
@Override
56+
public void ejectManagedKeyDataCacheEntry(byte[] keyCustodian, String keyNamespace,
57+
String keyMetadata) throws IOException {
58+
throw new UnsupportedOperationException("KeymetaAdmin feature not yet implemented");
59+
}
60+
61+
@Override
62+
public void clearManagedKeyDataCache() throws IOException {
63+
throw new UnsupportedOperationException("KeymetaAdmin feature not yet implemented");
64+
}
65+
66+
@Override
67+
public ManagedKeyData disableKeyManagement(byte[] keyCust, String keyNamespace)
68+
throws IOException, KeyException {
69+
throw new UnsupportedOperationException("KeymetaAdmin feature not yet implemented");
70+
}
71+
72+
@Override
73+
public ManagedKeyData disableManagedKey(byte[] keyCust, String keyNamespace,
74+
byte[] keyMetadataHash) throws IOException, KeyException {
75+
throw new UnsupportedOperationException("KeymetaAdmin feature not yet implemented");
76+
}
77+
78+
@Override
79+
public ManagedKeyData rotateManagedKey(byte[] keyCust, String keyNamespace)
80+
throws IOException, KeyException {
81+
throw new UnsupportedOperationException("KeymetaAdmin feature not yet implemented");
82+
}
83+
84+
@Override
85+
public void refreshManagedKeys(byte[] keyCust, String keyNamespace)
86+
throws IOException, KeyException {
87+
throw new UnsupportedOperationException("KeymetaAdmin feature not yet implemented");
88+
}
89+
}

hbase-client/src/main/java/org/apache/hadoop/hbase/security/EncryptionUtil.java

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.apache.commons.crypto.cipher.CryptoCipherFactory;
2828
import org.apache.hadoop.conf.Configuration;
2929
import org.apache.hadoop.hbase.HConstants;
30-
import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
3130
import org.apache.hadoop.hbase.io.crypto.Cipher;
3231
import org.apache.hadoop.hbase.io.crypto.Encryption;
3332
import org.apache.hadoop.hbase.io.crypto.aes.CryptoAES;
@@ -80,6 +79,21 @@ public static byte[] wrapKey(Configuration conf, byte[] key, String algorithm)
8079
* @return the encrypted key bytes
8180
*/
8281
public static byte[] wrapKey(Configuration conf, String subject, Key key) throws IOException {
82+
return wrapKey(conf, subject, key, null);
83+
}
84+
85+
/**
86+
* Protect a key by encrypting it with the secret key of the given subject or kek. The
87+
* configuration must be set up correctly for key alias resolution. Only one of the
88+
* {@code subject} or {@code kek} needs to be specified and the other one can be {@code null}.
89+
* @param conf configuration
90+
* @param subject subject key alias
91+
* @param key the key
92+
* @param kek the key encryption key
93+
* @return the encrypted key bytes
94+
*/
95+
public static byte[] wrapKey(Configuration conf, String subject, Key key, Key kek)
96+
throws IOException {
8397
// Wrap the key with the configured encryption algorithm.
8498
String algorithm = conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES);
8599
Cipher cipher = Encryption.getCipher(conf, algorithm);
@@ -100,8 +114,12 @@ public static byte[] wrapKey(Configuration conf, String subject, Key key) throws
100114
builder
101115
.setHash(UnsafeByteOperations.unsafeWrap(Encryption.computeCryptoKeyHash(conf, keyBytes)));
102116
ByteArrayOutputStream out = new ByteArrayOutputStream();
103-
Encryption.encryptWithSubjectKey(out, new ByteArrayInputStream(keyBytes), subject, conf, cipher,
104-
iv);
117+
if (kek != null) {
118+
Encryption.encryptWithGivenKey(kek, out, new ByteArrayInputStream(keyBytes), cipher, iv);
119+
} else {
120+
Encryption.encryptWithSubjectKey(out, new ByteArrayInputStream(keyBytes), subject, conf,
121+
cipher, iv);
122+
}
105123
builder.setData(UnsafeByteOperations.unsafeWrap(out.toByteArray()));
106124
// Build and return the protobuf message
107125
out.reset();
@@ -118,6 +136,21 @@ public static byte[] wrapKey(Configuration conf, String subject, Key key) throws
118136
* @return the raw key bytes
119137
*/
120138
public static Key unwrapKey(Configuration conf, String subject, byte[] value)
139+
throws IOException, KeyException {
140+
return unwrapKey(conf, subject, value, null);
141+
}
142+
143+
/**
144+
* Unwrap a key by decrypting it with the secret key of the given subject. The configuration must
145+
* be set up correctly for key alias resolution. Only one of the {@code subject} or {@code kek}
146+
* needs to be specified and the other one can be {@code null}.
147+
* @param conf configuration
148+
* @param subject subject key alias
149+
* @param value the encrypted key bytes
150+
* @param kek the key encryption key
151+
* @return the raw key bytes
152+
*/
153+
public static Key unwrapKey(Configuration conf, String subject, byte[] value, Key kek)
121154
throws IOException, KeyException {
122155
EncryptionProtos.WrappedKey wrappedKey =
123156
EncryptionProtos.WrappedKey.parser().parseDelimitedFrom(new ByteArrayInputStream(value));
@@ -126,11 +159,12 @@ public static Key unwrapKey(Configuration conf, String subject, byte[] value)
126159
if (cipher == null) {
127160
throw new RuntimeException("Cipher '" + algorithm + "' not available");
128161
}
129-
return getUnwrapKey(conf, subject, wrappedKey, cipher);
162+
return getUnwrapKey(conf, subject, wrappedKey, cipher, kek);
130163
}
131164

132165
private static Key getUnwrapKey(Configuration conf, String subject,
133-
EncryptionProtos.WrappedKey wrappedKey, Cipher cipher) throws IOException, KeyException {
166+
EncryptionProtos.WrappedKey wrappedKey, Cipher cipher, Key kek)
167+
throws IOException, KeyException {
134168
String configuredHashAlgorithm = Encryption.getConfiguredHashAlgorithm(conf);
135169
String wrappedHashAlgorithm = wrappedKey.getHashAlgorithm().trim();
136170
if (!configuredHashAlgorithm.equalsIgnoreCase(wrappedHashAlgorithm)) {
@@ -143,8 +177,13 @@ private static Key getUnwrapKey(Configuration conf, String subject,
143177
}
144178
ByteArrayOutputStream out = new ByteArrayOutputStream();
145179
byte[] iv = wrappedKey.hasIv() ? wrappedKey.getIv().toByteArray() : null;
146-
Encryption.decryptWithSubjectKey(out, wrappedKey.getData().newInput(), wrappedKey.getLength(),
147-
subject, conf, cipher, iv);
180+
if (kek != null) {
181+
Encryption.decryptWithGivenKey(kek, out, wrappedKey.getData().newInput(),
182+
wrappedKey.getLength(), cipher, iv);
183+
} else {
184+
Encryption.decryptWithSubjectKey(out, wrappedKey.getData().newInput(), wrappedKey.getLength(),
185+
subject, conf, cipher, iv);
186+
}
148187
byte[] keyBytes = out.toByteArray();
149188
if (wrappedKey.hasHash()) {
150189
if (
@@ -176,58 +215,7 @@ public static Key unwrapWALKey(Configuration conf, String subject, byte[] value)
176215
if (cipher == null) {
177216
throw new RuntimeException("Cipher '" + algorithm + "' not available");
178217
}
179-
return getUnwrapKey(conf, subject, wrappedKey, cipher);
180-
}
181-
182-
/**
183-
* Helper to create an encyption context.
184-
* @param conf The current configuration.
185-
* @param family The current column descriptor.
186-
* @return The created encryption context.
187-
* @throws IOException if an encryption key for the column cannot be unwrapped
188-
* @throws IllegalStateException in case of encryption related configuration errors
189-
*/
190-
public static Encryption.Context createEncryptionContext(Configuration conf,
191-
ColumnFamilyDescriptor family) throws IOException {
192-
Encryption.Context cryptoContext = Encryption.Context.NONE;
193-
String cipherName = family.getEncryptionType();
194-
if (cipherName != null) {
195-
if (!Encryption.isEncryptionEnabled(conf)) {
196-
throw new IllegalStateException("Encryption for family '" + family.getNameAsString()
197-
+ "' configured with type '" + cipherName + "' but the encryption feature is disabled");
198-
}
199-
Cipher cipher;
200-
Key key;
201-
byte[] keyBytes = family.getEncryptionKey();
202-
if (keyBytes != null) {
203-
// Family provides specific key material
204-
key = unwrapKey(conf, keyBytes);
205-
// Use the algorithm the key wants
206-
cipher = Encryption.getCipher(conf, key.getAlgorithm());
207-
if (cipher == null) {
208-
throw new IllegalStateException("Cipher '" + key.getAlgorithm() + "' is not available");
209-
}
210-
// Fail if misconfigured
211-
// We use the encryption type specified in the column schema as a sanity check on
212-
// what the wrapped key is telling us
213-
if (!cipher.getName().equalsIgnoreCase(cipherName)) {
214-
throw new IllegalStateException(
215-
"Encryption for family '" + family.getNameAsString() + "' configured with type '"
216-
+ cipherName + "' but key specifies algorithm '" + cipher.getName() + "'");
217-
}
218-
} else {
219-
// Family does not provide key material, create a random key
220-
cipher = Encryption.getCipher(conf, cipherName);
221-
if (cipher == null) {
222-
throw new IllegalStateException("Cipher '" + cipherName + "' is not available");
223-
}
224-
key = cipher.getRandomKey();
225-
}
226-
cryptoContext = Encryption.newContext(conf);
227-
cryptoContext.setCipher(cipher);
228-
cryptoContext.setKey(key);
229-
}
230-
return cryptoContext;
218+
return getUnwrapKey(conf, subject, wrappedKey, cipher, null);
231219
}
232220

233221
/**

hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/Context.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class Context implements Configurable {
3434
private Configuration conf;
3535
private Cipher cipher;
3636
private Key key;
37+
private ManagedKeyData kekData;
38+
private String keyNamespace;
3739
private String keyHash;
3840

3941
Context(Configuration conf) {
@@ -97,4 +99,22 @@ public Context setKey(Key key) {
9799
this.keyHash = new String(Hex.encodeHex(Encryption.computeCryptoKeyHash(conf, encoded)));
98100
return this;
99101
}
102+
103+
public Context setKeyNamespace(String keyNamespace) {
104+
this.keyNamespace = keyNamespace;
105+
return this;
106+
}
107+
108+
public String getKeyNamespace() {
109+
return keyNamespace;
110+
}
111+
112+
public Context setKEKData(ManagedKeyData kekData) {
113+
this.kekData = kekData;
114+
return this;
115+
}
116+
117+
public ManagedKeyData getKEKData() {
118+
return kekData;
119+
}
100120
}

0 commit comments

Comments
 (0)