Skip to content

Commit 39c1137

Browse files
author
Sreesh Maheshwar
committed
Encryption for REST catalog
1 parent 836a9c0 commit 39c1137

File tree

6 files changed

+391
-8
lines changed

6 files changed

+391
-8
lines changed

core/src/main/java/org/apache/iceberg/encryption/EncryptionUtil.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public static EncryptionManager createEncryptionManager(
8787
return createEncryptionManager(List.of(), tableProperties, kmsClient);
8888
}
8989

90-
static EncryptionManager createEncryptionManager(
90+
public static EncryptionManager createEncryptionManager(
9191
List<EncryptedKey> keys, Map<String, String> tableProperties, KeyManagementClient kmsClient) {
9292
Preconditions.checkArgument(kmsClient != null, "Invalid KMS client: null");
9393
String tableKeyId = tableProperties.get(TableProperties.ENCRYPTION_TABLE_KEY);
@@ -108,7 +108,7 @@ static EncryptionManager createEncryptionManager(
108108
"Invalid data key length: %s (must be 16, 24, or 32)",
109109
dataKeyLength);
110110

111-
return new StandardEncryptionManager(tableKeyId, dataKeyLength, kmsClient);
111+
return new StandardEncryptionManager(keys, tableKeyId, dataKeyLength, kmsClient);
112112
}
113113

114114
public static EncryptedOutputFile plainAsEncryptedOutput(OutputFile encryptingOutputFile) {
@@ -156,4 +156,11 @@ static ByteBuffer encryptManifestListKeyMetadata(
156156
encryptor.encrypt(keyMetadataBytes, keyId.getBytes(StandardCharsets.UTF_8));
157157
return ByteBuffer.wrap(encryptedKeyMetadata);
158158
}
159+
160+
public static Map<String, EncryptedKey> encryptionKeys(EncryptionManager em) {
161+
Preconditions.checkState(
162+
em instanceof StandardEncryptionManager,
163+
"Encryption keys are only available for StandardEncryptionManager");
164+
return ((StandardEncryptionManager) em).encryptionKeys();
165+
}
159166
}

core/src/main/java/org/apache/iceberg/encryption/KeyManagementClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import java.util.Map;
2525

2626
/** A minimum client interface to connect to a key management service (KMS). */
27-
interface KeyManagementClient extends Serializable, Closeable {
27+
public interface KeyManagementClient extends Serializable, Closeable {
2828

2929
/**
3030
* Wrap a secret key, using a wrapping/master key which is stored in KMS and referenced by an ID.

core/src/main/java/org/apache/iceberg/encryption/StandardEncryptionManager.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.nio.ByteBuffer;
2424
import java.security.SecureRandom;
2525
import java.util.Base64;
26+
import java.util.List;
2627
import java.util.Map;
2728
import java.util.concurrent.TimeUnit;
2829
import org.apache.iceberg.TableProperties;
@@ -46,9 +47,16 @@ private class TransientEncryptionState {
4647
private final Map<String, EncryptedKey> encryptionKeys;
4748
private final LoadingCache<String, ByteBuffer> unwrappedKeyCache;
4849

49-
private TransientEncryptionState(KeyManagementClient kmsClient) {
50+
private TransientEncryptionState(List<EncryptedKey> keys, KeyManagementClient kmsClient) {
5051
this.kmsClient = kmsClient;
5152
this.encryptionKeys = Maps.newLinkedHashMap();
53+
54+
for (EncryptedKey key : keys) {
55+
Preconditions.checkArgument(
56+
key.keyId() != null, "Key id cannot be null"); // Required by spec.
57+
encryptionKeys.put(key.keyId(), key);
58+
}
59+
5260
this.unwrappedKeyCache =
5361
Caffeine.newBuilder()
5462
.expireAfterWrite(1, TimeUnit.HOURS)
@@ -64,20 +72,33 @@ private TransientEncryptionState(KeyManagementClient kmsClient) {
6472
private transient volatile SecureRandom lazyRNG = null;
6573

6674
/**
75+
* @deprecated will be removed in 1.11.0; use {@link #StandardEncryptionManager(List, String, int,
76+
* KeyManagementClient)} instead.
77+
*/
78+
public StandardEncryptionManager(
79+
String tableKeyId, int dataKeyLength, KeyManagementClient kmsClient) {
80+
this(List.of(), tableKeyId, dataKeyLength, kmsClient);
81+
}
82+
83+
/**
84+
* @param keys a list of existing {@link EncryptedKey}s for this {@link EncryptionManager} to use
6785
* @param tableKeyId table encryption key id
6886
* @param dataKeyLength length of data encryption key (16/24/32 bytes)
6987
* @param kmsClient Client of KMS used to wrap/unwrap keys in envelope encryption
7088
*/
7189
public StandardEncryptionManager(
72-
String tableKeyId, int dataKeyLength, KeyManagementClient kmsClient) {
90+
List<EncryptedKey> keys,
91+
String tableKeyId,
92+
int dataKeyLength,
93+
KeyManagementClient kmsClient) {
7394
Preconditions.checkNotNull(tableKeyId, "Invalid encryption key ID: null");
7495
Preconditions.checkArgument(
7596
dataKeyLength == 16 || dataKeyLength == 24 || dataKeyLength == 32,
7697
"Invalid data key length: %s (must be 16, 24, or 32)",
7798
dataKeyLength);
7899
Preconditions.checkNotNull(kmsClient, "Invalid KMS client: null");
79100
this.tableKeyId = tableKeyId;
80-
this.transientState = new TransientEncryptionState(kmsClient);
101+
this.transientState = new TransientEncryptionState(keys, kmsClient);
81102
this.dataKeyLength = dataKeyLength;
82103
}
83104

@@ -199,6 +220,14 @@ public String addManifestListKeyMetadata(NativeEncryptionKeyMetadata keyMetadata
199220
return manifestListKeyID;
200221
}
201222

223+
public Map<String, EncryptedKey> encryptionKeys() {
224+
if (transientState == null) {
225+
throw new IllegalStateException("Cannot return encryption keys after serialization");
226+
}
227+
228+
return transientState.encryptionKeys;
229+
}
230+
202231
private String generateKeyId() {
203232
byte[] idBytes = new byte[16];
204233
workerRNG().nextBytes(idBytes);

core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
import org.apache.iceberg.catalog.Namespace;
4848
import org.apache.iceberg.catalog.TableCommit;
4949
import org.apache.iceberg.catalog.TableIdentifier;
50+
import org.apache.iceberg.encryption.EncryptionUtil;
51+
import org.apache.iceberg.encryption.KeyManagementClient;
5052
import org.apache.iceberg.exceptions.AlreadyExistsException;
5153
import org.apache.iceberg.exceptions.NoSuchNamespaceException;
5254
import org.apache.iceberg.exceptions.NoSuchTableException;
@@ -158,6 +160,7 @@ public class RESTSessionCatalog extends BaseViewSessionCatalog
158160
private Integer pageSize = null;
159161
private CloseableGroup closeables = null;
160162
private Set<Endpoint> endpoints;
163+
private KeyManagementClient kmsClient = null;
161164

162165
enum SnapshotMode {
163166
ALL,
@@ -251,6 +254,9 @@ public void initialize(String name, Map<String, String> unresolved) {
251254

252255
this.reportingViaRestEnabled =
253256
PropertyUtil.propertyAsBoolean(mergedProps, REST_METRICS_REPORTING_ENABLED, true);
257+
if (mergedProps.containsKey(CatalogProperties.ENCRYPTION_KMS_IMPL)) {
258+
this.kmsClient = EncryptionUtil.createKmsClient(mergedProps);
259+
}
254260
super.initialize(name, mergedProps);
255261
}
256262

@@ -446,6 +452,7 @@ public Table loadTable(SessionContext context, TableIdentifier identifier) {
446452
paths.table(finalIdentifier),
447453
Map::of,
448454
tableFileIO(context, tableConf, response.credentials()),
455+
kmsClient,
449456
tableMetadata,
450457
endpoints);
451458

@@ -525,6 +532,7 @@ public Table registerTable(
525532
paths.table(ident),
526533
Map::of,
527534
tableFileIO(context, tableConf, response.credentials()),
535+
kmsClient,
528536
response.tableMetadata(),
529537
endpoints);
530538

@@ -784,6 +792,7 @@ public Table create() {
784792
paths.table(ident),
785793
Map::of,
786794
tableFileIO(context, tableConf, response.credentials()),
795+
kmsClient,
787796
response.tableMetadata(),
788797
endpoints);
789798

@@ -811,6 +820,7 @@ public Transaction createTransaction() {
811820
paths.table(ident),
812821
Map::of,
813822
tableFileIO(context, tableConf, response.credentials()),
823+
kmsClient,
814824
RESTTableOperations.UpdateType.CREATE,
815825
createChanges(meta),
816826
meta,
@@ -874,6 +884,7 @@ public Transaction replaceTransaction() {
874884
paths.table(ident),
875885
Map::of,
876886
tableFileIO(context, tableConf, response.credentials()),
887+
kmsClient,
877888
RESTTableOperations.UpdateType.REPLACE,
878889
changes.build(),
879890
base,

core/src/main/java/org/apache/iceberg/rest/RESTTableOperations.java

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,19 @@
3131
import org.apache.iceberg.TableProperties;
3232
import org.apache.iceberg.UpdateRequirement;
3333
import org.apache.iceberg.UpdateRequirements;
34+
import org.apache.iceberg.encryption.EncryptedKey;
35+
import org.apache.iceberg.encryption.EncryptingFileIO;
3436
import org.apache.iceberg.encryption.EncryptionManager;
37+
import org.apache.iceberg.encryption.EncryptionUtil;
38+
import org.apache.iceberg.encryption.KeyManagementClient;
39+
import org.apache.iceberg.encryption.PlaintextEncryptionManager;
40+
import org.apache.iceberg.encryption.StandardEncryptionManager;
3541
import org.apache.iceberg.io.FileIO;
3642
import org.apache.iceberg.io.LocationProvider;
3743
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
3844
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
3945
import org.apache.iceberg.relocated.com.google.common.collect.Lists;
46+
import org.apache.iceberg.relocated.com.google.common.collect.Maps;
4047
import org.apache.iceberg.rest.requests.UpdateTableRequest;
4148
import org.apache.iceberg.rest.responses.ErrorResponse;
4249
import org.apache.iceberg.rest.responses.LoadTableResponse;
@@ -55,27 +62,45 @@ enum UpdateType {
5562
private final String path;
5663
private final Supplier<Map<String, String>> headers;
5764
private final FileIO io;
65+
private final KeyManagementClient kmsClient;
5866
private final List<MetadataUpdate> createChanges;
5967
private final TableMetadata replaceBase;
6068
private final Set<Endpoint> endpoints;
6169
private UpdateType updateType;
6270
private TableMetadata current;
6371

72+
private EncryptionManager encryptionManager;
73+
private EncryptingFileIO encryptingFileIO;
74+
private String encryptionKeyId;
75+
private int encryptionDekLength;
76+
private List<EncryptedKey> encryptedKeysFromMetadata;
77+
6478
RESTTableOperations(
6579
RESTClient client,
6680
String path,
6781
Supplier<Map<String, String>> headers,
6882
FileIO io,
83+
KeyManagementClient kmsClient,
6984
TableMetadata current,
7085
Set<Endpoint> endpoints) {
71-
this(client, path, headers, io, UpdateType.SIMPLE, Lists.newArrayList(), current, endpoints);
86+
this(
87+
client,
88+
path,
89+
headers,
90+
io,
91+
kmsClient,
92+
UpdateType.SIMPLE,
93+
Lists.newArrayList(),
94+
current,
95+
endpoints);
7296
}
7397

7498
RESTTableOperations(
7599
RESTClient client,
76100
String path,
77101
Supplier<Map<String, String>> headers,
78102
FileIO io,
103+
KeyManagementClient kmsClient,
79104
UpdateType updateType,
80105
List<MetadataUpdate> createChanges,
81106
TableMetadata current,
@@ -84,6 +109,7 @@ enum UpdateType {
84109
this.path = path;
85110
this.headers = headers;
86111
this.io = io;
112+
this.kmsClient = kmsClient;
87113
this.updateType = updateType;
88114
this.createChanges = createChanges;
89115
this.replaceBase = current;
@@ -93,6 +119,10 @@ enum UpdateType {
93119
this.current = current;
94120
}
95121
this.endpoints = endpoints;
122+
123+
// N.B. We don't use this.current because for tables-to-be-created, because it would be null,
124+
// and ee still want encrypted properties in this case for its TableOperations.
125+
encryptionPropsFromMetadata(current);
96126
}
97127

98128
@Override
@@ -113,6 +143,17 @@ public void commit(TableMetadata base, TableMetadata metadata) {
113143
Consumer<ErrorResponse> errorHandler;
114144
List<UpdateRequirement> requirements;
115145
List<MetadataUpdate> updates;
146+
147+
if (encryption() instanceof StandardEncryptionManager) {
148+
TableMetadata.Builder builder = TableMetadata.buildFrom(metadata);
149+
for (Map.Entry<String, EncryptedKey> entry :
150+
EncryptionUtil.encryptionKeys(encryption()).entrySet()) {
151+
builder.addEncryptionKey(entry.getValue());
152+
}
153+
metadata = builder.build();
154+
// TODO(smaheshwar): Think about requirements.
155+
}
156+
116157
switch (updateType) {
117158
case CREATE:
118159
Preconditions.checkState(
@@ -166,7 +207,67 @@ public void commit(TableMetadata base, TableMetadata metadata) {
166207

167208
@Override
168209
public FileIO io() {
169-
return io;
210+
if (encryptionKeyId == null) {
211+
return io;
212+
}
213+
214+
if (encryptingFileIO == null) {
215+
encryptingFileIO = EncryptingFileIO.combine(io, encryption());
216+
}
217+
218+
return encryptingFileIO;
219+
}
220+
221+
@Override
222+
public EncryptionManager encryption() {
223+
if (encryptionManager != null) {
224+
return encryptionManager;
225+
}
226+
227+
if (encryptionKeyId != null) {
228+
if (kmsClient == null) {
229+
throw new RuntimeException(
230+
"Cant create encryption manager, because key management client is not set");
231+
}
232+
233+
Map<String, String> tableProperties = Maps.newHashMap();
234+
tableProperties.put(TableProperties.ENCRYPTION_TABLE_KEY, encryptionKeyId);
235+
tableProperties.put(
236+
TableProperties.ENCRYPTION_DEK_LENGTH, String.valueOf(encryptionDekLength));
237+
encryptionManager =
238+
EncryptionUtil.createEncryptionManager(
239+
encryptedKeysFromMetadata, tableProperties, kmsClient);
240+
} else {
241+
return PlaintextEncryptionManager.instance();
242+
}
243+
244+
return encryptionManager;
245+
}
246+
247+
private void encryptionPropsFromMetadata(TableMetadata metadata) {
248+
// TODO(smaheshwar): Check generally for changed encryption-related properties!
249+
if (metadata == null || metadata.properties() == null) {
250+
return;
251+
}
252+
253+
encryptedKeysFromMetadata = metadata.encryptionKeys();
254+
255+
Map<String, String> tableProperties = metadata.properties();
256+
if (encryptionKeyId == null) {
257+
encryptionKeyId = tableProperties.get(TableProperties.ENCRYPTION_TABLE_KEY);
258+
}
259+
260+
if (encryptionKeyId != null && encryptionDekLength <= 0) {
261+
String dekLength = tableProperties.get(TableProperties.ENCRYPTION_DEK_LENGTH);
262+
encryptionDekLength =
263+
(dekLength == null)
264+
? TableProperties.ENCRYPTION_DEK_LENGTH_DEFAULT
265+
: Integer.parseInt(dekLength);
266+
}
267+
268+
// Force re-creation of encryptingFileIO and encryptionManager
269+
encryptingFileIO = null;
270+
encryptionManager = null;
170271
}
171272

172273
private TableMetadata updateCurrentMetadata(LoadTableResponse response) {
@@ -175,6 +276,7 @@ private TableMetadata updateCurrentMetadata(LoadTableResponse response) {
175276
// safely ignored. there is no requirement to update config on refresh or commit.
176277
if (current == null
177278
|| !Objects.equals(current.metadataFileLocation(), response.metadataLocation())) {
279+
encryptionPropsFromMetadata(response.tableMetadata());
178280
this.current = response.tableMetadata();
179281
}
180282

0 commit comments

Comments
 (0)