Skip to content

Commit b54cb7d

Browse files
authored
HBASE-29822 Add API surface and refactoring for key management feature (HBASE-29368) (#7584)
1 parent d11ee6a commit b54cb7d

80 files changed

Lines changed: 1707 additions & 275 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.Private
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)