Skip to content

Commit 530b0ae

Browse files
author
Sreesh Maheshwar
committed
Encryption for REST catalog
1 parent 836a9c0 commit 530b0ae

File tree

6 files changed

+396
-11
lines changed

6 files changed

+396
-11
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: 33 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,34 @@ 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+
@Deprecated
79+
public StandardEncryptionManager(
80+
String tableKeyId, int dataKeyLength, KeyManagementClient kmsClient) {
81+
this(List.of(), tableKeyId, dataKeyLength, kmsClient);
82+
}
83+
84+
/**
85+
* @param keys a list of existing {@link EncryptedKey}s for this {@link EncryptionManager} to use
6786
* @param tableKeyId table encryption key id
6887
* @param dataKeyLength length of data encryption key (16/24/32 bytes)
6988
* @param kmsClient Client of KMS used to wrap/unwrap keys in envelope encryption
7089
*/
7190
public StandardEncryptionManager(
72-
String tableKeyId, int dataKeyLength, KeyManagementClient kmsClient) {
91+
List<EncryptedKey> keys,
92+
String tableKeyId,
93+
int dataKeyLength,
94+
KeyManagementClient kmsClient) {
7395
Preconditions.checkNotNull(tableKeyId, "Invalid encryption key ID: null");
7496
Preconditions.checkArgument(
7597
dataKeyLength == 16 || dataKeyLength == 24 || dataKeyLength == 32,
7698
"Invalid data key length: %s (must be 16, 24, or 32)",
7799
dataKeyLength);
78100
Preconditions.checkNotNull(kmsClient, "Invalid KMS client: null");
79101
this.tableKeyId = tableKeyId;
80-
this.transientState = new TransientEncryptionState(kmsClient);
102+
this.transientState = new TransientEncryptionState(keys, kmsClient);
81103
this.dataKeyLength = dataKeyLength;
82104
}
83105

@@ -199,6 +221,14 @@ public String addManifestListKeyMetadata(NativeEncryptionKeyMetadata keyMetadata
199221
return manifestListKeyID;
200222
}
201223

224+
public Map<String, EncryptedKey> encryptionKeys() {
225+
if (transientState == null) {
226+
throw new IllegalStateException("Cannot return encryption keys after serialization");
227+
}
228+
229+
return transientState.encryptionKeys;
230+
}
231+
202232
private String generateKeyId() {
203233
byte[] idBytes = new byte[16];
204234
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: 108 additions & 5 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,14 +143,26 @@ public void commit(TableMetadata base, TableMetadata metadata) {
113143
Consumer<ErrorResponse> errorHandler;
114144
List<UpdateRequirement> requirements;
115145
List<MetadataUpdate> updates;
146+
147+
TableMetadata metadataToCommit = metadata;
148+
if (encryption() instanceof StandardEncryptionManager) {
149+
TableMetadata.Builder builder = TableMetadata.buildFrom(metadata);
150+
for (Map.Entry<String, EncryptedKey> entry :
151+
EncryptionUtil.encryptionKeys(encryption()).entrySet()) {
152+
builder.addEncryptionKey(entry.getValue());
153+
}
154+
metadataToCommit = builder.build();
155+
// TODO(smaheshwar): Think about requirements.
156+
}
157+
116158
switch (updateType) {
117159
case CREATE:
118160
Preconditions.checkState(
119161
base == null, "Invalid base metadata for create transaction, expected null: %s", base);
120162
updates =
121163
ImmutableList.<MetadataUpdate>builder()
122164
.addAll(createChanges)
123-
.addAll(metadata.changes())
165+
.addAll(metadataToCommit.changes())
124166
.build();
125167
requirements = UpdateRequirements.forCreateTable(updates);
126168
errorHandler = ErrorHandlers.tableErrorHandler(); // throws NoSuchTableException
@@ -131,7 +173,7 @@ public void commit(TableMetadata base, TableMetadata metadata) {
131173
updates =
132174
ImmutableList.<MetadataUpdate>builder()
133175
.addAll(createChanges)
134-
.addAll(metadata.changes())
176+
.addAll(metadataToCommit.changes())
135177
.build();
136178
// use the original replace base metadata because the transaction will refresh
137179
requirements = UpdateRequirements.forReplaceTable(replaceBase, updates);
@@ -140,7 +182,7 @@ public void commit(TableMetadata base, TableMetadata metadata) {
140182

141183
case SIMPLE:
142184
Preconditions.checkState(base != null, "Invalid base metadata: null");
143-
updates = metadata.changes();
185+
updates = metadataToCommit.changes();
144186
requirements = UpdateRequirements.forUpdateTable(base, updates);
145187
errorHandler = ErrorHandlers.tableCommitHandler();
146188
break;
@@ -166,7 +208,67 @@ public void commit(TableMetadata base, TableMetadata metadata) {
166208

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

172274
private TableMetadata updateCurrentMetadata(LoadTableResponse response) {
@@ -175,6 +277,7 @@ private TableMetadata updateCurrentMetadata(LoadTableResponse response) {
175277
// safely ignored. there is no requirement to update config on refresh or commit.
176278
if (current == null
177279
|| !Objects.equals(current.metadataFileLocation(), response.metadataLocation())) {
280+
encryptionPropsFromMetadata(response.tableMetadata());
178281
this.current = response.tableMetadata();
179282
}
180283

0 commit comments

Comments
 (0)