Skip to content

Commit 2b7b54d

Browse files
raphaelazzoliniHexiaoqiao
authored andcommitted
HADOOP-19197. S3A: Support AWS KMS Encryption Context (apache#6874)
The new property fs.s3a.encryption.context allow users to specify the AWS KMS Encryption Context to be used in S3A. The value of the encryption context is a key/value string that will be Base64 encoded and set in the parameter ssekmsEncryptionContext from the S3 client. Contributed by Raphael Azzolini
1 parent 6d09cd1 commit 2b7b54d

File tree

18 files changed

+513
-29
lines changed

18 files changed

+513
-29
lines changed

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeysPublic.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,7 @@ public class CommonConfigurationKeysPublic {
10221022
"fs.s3a.*.server-side-encryption.key",
10231023
"fs.s3a.encryption.algorithm",
10241024
"fs.s3a.encryption.key",
1025+
"fs.s3a.encryption.context",
10251026
"fs.azure\\.account.key.*",
10261027
"credential$",
10271028
"oauth.*secret",

hadoop-common-project/hadoop-common/src/main/resources/core-default.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@
742742
fs.s3a.*.server-side-encryption.key
743743
fs.s3a.encryption.algorithm
744744
fs.s3a.encryption.key
745+
fs.s3a.encryption.context
745746
fs.s3a.secret.key
746747
fs.s3a.*.secret.key
747748
fs.s3a.session.key
@@ -1760,6 +1761,15 @@
17601761
</description>
17611762
</property>
17621763

1764+
<property>
1765+
<name>fs.s3a.encryption.context</name>
1766+
<description>Specific encryption context to use if fs.s3a.encryption.algorithm
1767+
has been set to 'SSE-KMS' or 'DSSE-KMS'. The value of this property is a set
1768+
of non-secret comma-separated key-value pairs of additional contextual
1769+
information about the data that are separated by equal operator (=).
1770+
</description>
1771+
</property>
1772+
17631773
<property>
17641774
<name>fs.s3a.signing-algorithm</name>
17651775
<description>Override the default signing algorithm so legacy

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,16 @@ private Constants() {
736736
public static final String S3_ENCRYPTION_KEY =
737737
"fs.s3a.encryption.key";
738738

739+
/**
740+
* Set S3-SSE encryption context.
741+
* The value of this property is a set of non-secret comma-separated key-value pairs
742+
* of additional contextual information about the data that are separated by equal
743+
* operator (=).
744+
* value:{@value}
745+
*/
746+
public static final String S3_ENCRYPTION_CONTEXT =
747+
"fs.s3a.encryption.context";
748+
739749
/**
740750
* List of custom Signers. The signer class will be loaded, and the signer
741751
* name will be associated with this signer class in the S3 SDK.

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AUtils.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.apache.hadoop.fs.PathFilter;
3838
import org.apache.hadoop.fs.PathIOException;
3939
import org.apache.hadoop.fs.RemoteIterator;
40+
import org.apache.hadoop.fs.s3a.impl.S3AEncryption;
4041
import org.apache.hadoop.util.functional.RemoteIterators;
4142
import org.apache.hadoop.fs.s3a.auth.delegation.EncryptionSecrets;
4243
import org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteException;
@@ -1312,7 +1313,7 @@ static void patchSecurityCredentialProviders(Configuration conf) {
13121313
* @throws IOException on any IO problem
13131314
* @throws IllegalArgumentException bad arguments
13141315
*/
1315-
private static String lookupBucketSecret(
1316+
public static String lookupBucketSecret(
13161317
String bucket,
13171318
Configuration conf,
13181319
String baseKey)
@@ -1458,6 +1459,8 @@ public static EncryptionSecrets buildEncryptionSecrets(String bucket,
14581459
int encryptionKeyLen =
14591460
StringUtils.isBlank(encryptionKey) ? 0 : encryptionKey.length();
14601461
String diagnostics = passwordDiagnostics(encryptionKey, "key");
1462+
String encryptionContext = S3AEncryption.getS3EncryptionContextBase64Encoded(bucket, conf,
1463+
encryptionMethod.requiresSecret());
14611464
switch (encryptionMethod) {
14621465
case SSE_C:
14631466
LOG.debug("Using SSE-C with {}", diagnostics);
@@ -1493,7 +1496,7 @@ public static EncryptionSecrets buildEncryptionSecrets(String bucket,
14931496
LOG.debug("Data is unencrypted");
14941497
break;
14951498
}
1496-
return new EncryptionSecrets(encryptionMethod, encryptionKey);
1499+
return new EncryptionSecrets(encryptionMethod, encryptionKey, encryptionContext);
14971500
}
14981501

14991502
/**
@@ -1686,6 +1689,21 @@ public static Map<String, String> getTrimmedStringCollectionSplitByEquals(
16861689
final Configuration configuration,
16871690
final String name) {
16881691
String valueString = configuration.get(name);
1692+
return getTrimmedStringCollectionSplitByEquals(valueString);
1693+
}
1694+
1695+
/**
1696+
* Get the equal op (=) delimited key-value pairs of the <code>name</code> property as
1697+
* a collection of pair of <code>String</code>s, trimmed of the leading and trailing whitespace
1698+
* after delimiting the <code>name</code> by comma and new line separator.
1699+
* If no such property is specified then empty <code>Map</code> is returned.
1700+
*
1701+
* @param valueString the string containing the key-value pairs.
1702+
* @return property value as a <code>Map</code> of <code>String</code>s, or empty
1703+
* <code>Map</code>.
1704+
*/
1705+
public static Map<String, String> getTrimmedStringCollectionSplitByEquals(
1706+
final String valueString) {
16891707
if (null == valueString) {
16901708
return new HashMap<>();
16911709
}

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/EncryptionSecretOperations.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,20 @@ public static Optional<String> getSSEAwsKMSKey(final EncryptionSecrets secrets)
6161
return Optional.empty();
6262
}
6363
}
64+
65+
/**
66+
* Gets the SSE-KMS context if present, else don't set it in the S3 request.
67+
*
68+
* @param secrets source of the encryption secrets.
69+
* @return an optional AWS KMS encryption context to attach to a request.
70+
*/
71+
public static Optional<String> getSSEAwsKMSEncryptionContext(final EncryptionSecrets secrets) {
72+
if ((secrets.getEncryptionMethod() == S3AEncryptionMethods.SSE_KMS
73+
|| secrets.getEncryptionMethod() == S3AEncryptionMethods.DSSE_KMS)
74+
&& secrets.hasEncryptionContext()) {
75+
return Optional.of(secrets.getEncryptionContext());
76+
} else {
77+
return Optional.empty();
78+
}
79+
}
6480
}

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/EncryptionSecrets.java

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ public class EncryptionSecrets implements Writable, Serializable {
6767
*/
6868
private String encryptionKey = "";
6969

70+
/**
71+
* Encryption context: base64-encoded UTF-8 string.
72+
*/
73+
private String encryptionContext = "";
74+
7075
/**
7176
* This field isn't serialized/marshalled; it is rebuilt from the
7277
* encryptionAlgorithm field.
@@ -84,23 +89,28 @@ public EncryptionSecrets() {
8489
* Create a pair of secrets.
8590
* @param encryptionAlgorithm algorithm enumeration.
8691
* @param encryptionKey key/key reference.
92+
* @param encryptionContext base64-encoded string with the encryption context key-value pairs.
8793
* @throws IOException failure to initialize.
8894
*/
8995
public EncryptionSecrets(final S3AEncryptionMethods encryptionAlgorithm,
90-
final String encryptionKey) throws IOException {
91-
this(encryptionAlgorithm.getMethod(), encryptionKey);
96+
final String encryptionKey,
97+
final String encryptionContext) throws IOException {
98+
this(encryptionAlgorithm.getMethod(), encryptionKey, encryptionContext);
9299
}
93100

94101
/**
95102
* Create a pair of secrets.
96103
* @param encryptionAlgorithm algorithm name
97104
* @param encryptionKey key/key reference.
105+
* @param encryptionContext base64-encoded string with the encryption context key-value pairs.
98106
* @throws IOException failure to initialize.
99107
*/
100108
public EncryptionSecrets(final String encryptionAlgorithm,
101-
final String encryptionKey) throws IOException {
109+
final String encryptionKey,
110+
final String encryptionContext) throws IOException {
102111
this.encryptionAlgorithm = encryptionAlgorithm;
103112
this.encryptionKey = encryptionKey;
113+
this.encryptionContext = encryptionContext;
104114
init();
105115
}
106116

@@ -114,6 +124,7 @@ public void write(final DataOutput out) throws IOException {
114124
new LongWritable(serialVersionUID).write(out);
115125
Text.writeString(out, encryptionAlgorithm);
116126
Text.writeString(out, encryptionKey);
127+
Text.writeString(out, encryptionContext);
117128
}
118129

119130
/**
@@ -132,6 +143,7 @@ public void readFields(final DataInput in) throws IOException {
132143
}
133144
encryptionAlgorithm = Text.readString(in, MAX_SECRET_LENGTH);
134145
encryptionKey = Text.readString(in, MAX_SECRET_LENGTH);
146+
encryptionContext = Text.readString(in);
135147
init();
136148
}
137149

@@ -164,6 +176,10 @@ public String getEncryptionKey() {
164176
return encryptionKey;
165177
}
166178

179+
public String getEncryptionContext() {
180+
return encryptionContext;
181+
}
182+
167183
/**
168184
* Does this instance have encryption options?
169185
* That is: is the algorithm non-null.
@@ -181,6 +197,14 @@ public boolean hasEncryptionKey() {
181197
return StringUtils.isNotEmpty(encryptionKey);
182198
}
183199

200+
/**
201+
* Does this instance have an encryption context?
202+
* @return true if there's an encryption context.
203+
*/
204+
public boolean hasEncryptionContext() {
205+
return StringUtils.isNotEmpty(encryptionContext);
206+
}
207+
184208
@Override
185209
public boolean equals(final Object o) {
186210
if (this == o) {
@@ -191,12 +215,13 @@ public boolean equals(final Object o) {
191215
}
192216
final EncryptionSecrets that = (EncryptionSecrets) o;
193217
return Objects.equals(encryptionAlgorithm, that.encryptionAlgorithm)
194-
&& Objects.equals(encryptionKey, that.encryptionKey);
218+
&& Objects.equals(encryptionKey, that.encryptionKey)
219+
&& Objects.equals(encryptionContext, that.encryptionContext);
195220
}
196221

197222
@Override
198223
public int hashCode() {
199-
return Objects.hash(encryptionAlgorithm, encryptionKey);
224+
return Objects.hash(encryptionAlgorithm, encryptionKey, encryptionContext);
200225
}
201226

202227
/**

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/RequestFactoryImpl.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ protected void copyEncryptionParameters(HeadObjectResponse srcom,
270270
LOG.debug("Propagating SSE-KMS settings from source {}",
271271
sourceKMSId);
272272
copyObjectRequestBuilder.ssekmsKeyId(sourceKMSId);
273+
EncryptionSecretOperations.getSSEAwsKMSEncryptionContext(encryptionSecrets)
274+
.ifPresent(copyObjectRequestBuilder::ssekmsEncryptionContext);
273275
return;
274276
}
275277

@@ -282,11 +284,15 @@ protected void copyEncryptionParameters(HeadObjectResponse srcom,
282284
// Set the KMS key if present, else S3 uses AWS managed key.
283285
EncryptionSecretOperations.getSSEAwsKMSKey(encryptionSecrets)
284286
.ifPresent(copyObjectRequestBuilder::ssekmsKeyId);
287+
EncryptionSecretOperations.getSSEAwsKMSEncryptionContext(encryptionSecrets)
288+
.ifPresent(copyObjectRequestBuilder::ssekmsEncryptionContext);
285289
break;
286290
case DSSE_KMS:
287291
copyObjectRequestBuilder.serverSideEncryption(ServerSideEncryption.AWS_KMS_DSSE);
288292
EncryptionSecretOperations.getSSEAwsKMSKey(encryptionSecrets)
289293
.ifPresent(copyObjectRequestBuilder::ssekmsKeyId);
294+
EncryptionSecretOperations.getSSEAwsKMSEncryptionContext(encryptionSecrets)
295+
.ifPresent(copyObjectRequestBuilder::ssekmsEncryptionContext);
290296
break;
291297
case SSE_C:
292298
EncryptionSecretOperations.getSSECustomerKey(encryptionSecrets)
@@ -371,11 +377,15 @@ private void putEncryptionParameters(PutObjectRequest.Builder putObjectRequestBu
371377
// Set the KMS key if present, else S3 uses AWS managed key.
372378
EncryptionSecretOperations.getSSEAwsKMSKey(encryptionSecrets)
373379
.ifPresent(putObjectRequestBuilder::ssekmsKeyId);
380+
EncryptionSecretOperations.getSSEAwsKMSEncryptionContext(encryptionSecrets)
381+
.ifPresent(putObjectRequestBuilder::ssekmsEncryptionContext);
374382
break;
375383
case DSSE_KMS:
376384
putObjectRequestBuilder.serverSideEncryption(ServerSideEncryption.AWS_KMS_DSSE);
377385
EncryptionSecretOperations.getSSEAwsKMSKey(encryptionSecrets)
378386
.ifPresent(putObjectRequestBuilder::ssekmsKeyId);
387+
EncryptionSecretOperations.getSSEAwsKMSEncryptionContext(encryptionSecrets)
388+
.ifPresent(putObjectRequestBuilder::ssekmsEncryptionContext);
379389
break;
380390
case SSE_C:
381391
EncryptionSecretOperations.getSSECustomerKey(encryptionSecrets)
@@ -447,11 +457,15 @@ private void multipartUploadEncryptionParameters(
447457
// Set the KMS key if present, else S3 uses AWS managed key.
448458
EncryptionSecretOperations.getSSEAwsKMSKey(encryptionSecrets)
449459
.ifPresent(mpuRequestBuilder::ssekmsKeyId);
460+
EncryptionSecretOperations.getSSEAwsKMSEncryptionContext(encryptionSecrets)
461+
.ifPresent(mpuRequestBuilder::ssekmsEncryptionContext);
450462
break;
451463
case DSSE_KMS:
452464
mpuRequestBuilder.serverSideEncryption(ServerSideEncryption.AWS_KMS_DSSE);
453465
EncryptionSecretOperations.getSSEAwsKMSKey(encryptionSecrets)
454466
.ifPresent(mpuRequestBuilder::ssekmsKeyId);
467+
EncryptionSecretOperations.getSSEAwsKMSEncryptionContext(encryptionSecrets)
468+
.ifPresent(mpuRequestBuilder::ssekmsEncryptionContext);
455469
break;
456470
case SSE_C:
457471
EncryptionSecretOperations.getSSECustomerKey(encryptionSecrets)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
19+
package org.apache.hadoop.fs.s3a.impl;
20+
21+
import java.io.IOException;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.Map;
24+
25+
import com.fasterxml.jackson.databind.ObjectMapper;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
29+
import org.apache.commons.codec.binary.Base64;
30+
import org.apache.commons.lang3.StringUtils;
31+
import org.apache.hadoop.conf.Configuration;
32+
import org.apache.hadoop.fs.s3a.S3AUtils;
33+
34+
import static org.apache.hadoop.fs.s3a.Constants.S3_ENCRYPTION_CONTEXT;
35+
36+
/**
37+
* Utility methods for S3A encryption properties.
38+
*/
39+
public final class S3AEncryption {
40+
41+
private static final Logger LOG = LoggerFactory.getLogger(S3AEncryption.class);
42+
43+
private S3AEncryption() {
44+
}
45+
46+
/**
47+
* Get any SSE context from a configuration/credential provider.
48+
* @param bucket bucket to query for
49+
* @param conf configuration to examine
50+
* @return the encryption context value or ""
51+
* @throws IOException if reading a JCEKS file raised an IOE
52+
* @throws IllegalArgumentException bad arguments.
53+
*/
54+
public static String getS3EncryptionContext(String bucket, Configuration conf)
55+
throws IOException {
56+
// look up the per-bucket value of the encryption context
57+
String encryptionContext = S3AUtils.lookupBucketSecret(bucket, conf, S3_ENCRYPTION_CONTEXT);
58+
if (encryptionContext == null) {
59+
// look up the global value of the encryption context
60+
encryptionContext = S3AUtils.lookupPassword(null, conf, S3_ENCRYPTION_CONTEXT);
61+
}
62+
if (encryptionContext == null) {
63+
// no encryption context, return ""
64+
return "";
65+
}
66+
return encryptionContext;
67+
}
68+
69+
/**
70+
* Get any SSE context from a configuration/credential provider.
71+
* This includes converting the values to a base64-encoded UTF-8 string
72+
* holding JSON with the encryption context key-value pairs
73+
* @param bucket bucket to query for
74+
* @param conf configuration to examine
75+
* @param propagateExceptions should IO exceptions be rethrown?
76+
* @return the Base64 encryption context or ""
77+
* @throws IllegalArgumentException bad arguments.
78+
* @throws IOException if propagateExceptions==true and reading a JCEKS file raised an IOE
79+
*/
80+
public static String getS3EncryptionContextBase64Encoded(
81+
String bucket,
82+
Configuration conf,
83+
boolean propagateExceptions) throws IOException {
84+
try {
85+
final String encryptionContextValue = getS3EncryptionContext(bucket, conf);
86+
if (StringUtils.isBlank(encryptionContextValue)) {
87+
return "";
88+
}
89+
final Map<String, String> encryptionContextMap = S3AUtils
90+
.getTrimmedStringCollectionSplitByEquals(encryptionContextValue);
91+
if (encryptionContextMap.isEmpty()) {
92+
return "";
93+
}
94+
final String encryptionContextJson = new ObjectMapper().writeValueAsString(
95+
encryptionContextMap);
96+
return Base64.encodeBase64String(encryptionContextJson.getBytes(StandardCharsets.UTF_8));
97+
} catch (IOException e) {
98+
if (propagateExceptions) {
99+
throw e;
100+
}
101+
LOG.warn("Cannot retrieve {} for bucket {}",
102+
S3_ENCRYPTION_CONTEXT, bucket, e);
103+
return "";
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)