Skip to content

Commit 8d3c06a

Browse files
authored
feat: add KMS Discovery Keyring (#324)
* feat: add KmsDiscoveryKeyring
1 parent cfe325e commit 8d3c06a

File tree

4 files changed

+417
-4
lines changed

4 files changed

+417
-4
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package software.amazon.encryption.s3.materials;
2+
3+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4+
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
5+
import software.amazon.awssdk.core.ApiName;
6+
import software.amazon.awssdk.core.SdkBytes;
7+
import software.amazon.awssdk.services.kms.KmsClient;
8+
import software.amazon.awssdk.services.kms.model.DecryptRequest;
9+
import software.amazon.awssdk.services.kms.model.DecryptResponse;
10+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
11+
import software.amazon.encryption.s3.S3EncryptionClient;
12+
import software.amazon.encryption.s3.S3EncryptionClientException;
13+
import software.amazon.encryption.s3.internal.ApiNameVersion;
14+
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
import java.util.Optional;
18+
19+
public class KmsDiscoveryKeyring extends S3Keyring {
20+
private static final ApiName API_NAME = ApiNameVersion.apiNameWithVersion();
21+
private static final String KEY_ID_CONTEXT_KEY = "kms_cmk_id";
22+
23+
private final KmsClient _kmsClient;
24+
25+
private final Map<String, DecryptDataKeyStrategy> decryptDataKeyStrategies = new HashMap<>();
26+
27+
/**
28+
* This keyring will decrypt the object without specifying the KMS key
29+
* in its configuration. This is similar to the Encryption SDK's Discovery Keyring.
30+
* NOTE: There is no Discovery Filter, as kms+context mode used in v2/v3 does not persist the KMS key ID
31+
* to the object metadata, so it is not possible to safely filter on this attribute.
32+
* @param builder
33+
*/
34+
public KmsDiscoveryKeyring(Builder builder) {
35+
super(builder);
36+
37+
_kmsClient = builder._kmsClient;
38+
decryptDataKeyStrategies.put(_kmsDiscoveryStrategy.keyProviderInfo(), _kmsDiscoveryStrategy);
39+
decryptDataKeyStrategies.put(_kmsContextDiscoveryStrategy.keyProviderInfo(), _kmsContextDiscoveryStrategy);
40+
}
41+
42+
/**
43+
* This DecryptDataKeyStrategy decrypts objects encrypted using the legacy kms v1 mode
44+
* using whichever key the object was encrypted with.
45+
*/
46+
private final DecryptDataKeyStrategy _kmsDiscoveryStrategy = new DecryptDataKeyStrategy() {
47+
48+
private static final String KEY_PROVIDER_INFO = "kms";
49+
50+
@Override
51+
public boolean isLegacy() {
52+
return true;
53+
}
54+
55+
@Override
56+
public String keyProviderInfo() {
57+
return KEY_PROVIDER_INFO;
58+
}
59+
60+
@Override
61+
public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) {
62+
DecryptRequest request = DecryptRequest.builder()
63+
.encryptionContext(materials.encryptionContext())
64+
.ciphertextBlob(SdkBytes.fromByteArray(encryptedDataKey))
65+
.overrideConfiguration(builder -> builder.addApiName(API_NAME))
66+
.build();
67+
68+
DecryptResponse response = _kmsClient.decrypt(request);
69+
return response.plaintext().asByteArray();
70+
}
71+
};
72+
73+
/**
74+
* This DecryptDataKeyStrategy decrypts objects encrypted using the kms+context v2 mode
75+
* using whichever key the object was encrypted with.
76+
*/
77+
private final DecryptDataKeyStrategy _kmsContextDiscoveryStrategy = new DecryptDataKeyStrategy() {
78+
79+
private static final String KEY_PROVIDER_INFO = "kms+context";
80+
private static final String ENCRYPTION_CONTEXT_ALGORITHM_KEY = "aws:x-amz-cek-alg";
81+
82+
@Override
83+
public boolean isLegacy() {
84+
return false;
85+
}
86+
87+
@Override
88+
public String keyProviderInfo() {
89+
return KEY_PROVIDER_INFO;
90+
}
91+
92+
@Override
93+
public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) {
94+
Map<String, String> requestEncryptionContext = new HashMap<>();
95+
GetObjectRequest s3Request = materials.s3Request();
96+
if (s3Request.overrideConfiguration().isPresent()) {
97+
AwsRequestOverrideConfiguration overrideConfig = s3Request.overrideConfiguration().get();
98+
Optional<Map<String, String>> optEncryptionContext = overrideConfig
99+
.executionAttributes()
100+
.getOptionalAttribute(S3EncryptionClient.ENCRYPTION_CONTEXT);
101+
if (optEncryptionContext.isPresent()) {
102+
requestEncryptionContext = new HashMap<>(optEncryptionContext.get());
103+
}
104+
}
105+
106+
// We are validating the encryption context to match S3EC V2 behavior
107+
// Refer to KMSMaterialsHandler in the V2 client for details
108+
Map<String, String> materialsEncryptionContextCopy = new HashMap<>(materials.encryptionContext());
109+
materialsEncryptionContextCopy.remove(KEY_ID_CONTEXT_KEY);
110+
materialsEncryptionContextCopy.remove(ENCRYPTION_CONTEXT_ALGORITHM_KEY);
111+
if (!materialsEncryptionContextCopy.equals(requestEncryptionContext)) {
112+
throw new S3EncryptionClientException("Provided encryption context does not match information retrieved from S3");
113+
}
114+
115+
DecryptRequest request = DecryptRequest.builder()
116+
.encryptionContext(materials.encryptionContext())
117+
.ciphertextBlob(SdkBytes.fromByteArray(encryptedDataKey))
118+
.overrideConfiguration(builder -> builder.addApiName(API_NAME))
119+
.build();
120+
121+
DecryptResponse response = _kmsClient.decrypt(request);
122+
return response.plaintext().asByteArray();
123+
}
124+
};
125+
126+
public static Builder builder() {
127+
return new Builder();
128+
}
129+
130+
@Override
131+
protected GenerateDataKeyStrategy generateDataKeyStrategy() {
132+
throw new S3EncryptionClientException("KmsDiscoveryKeyring does not support GenerateDataKey");
133+
}
134+
135+
@Override
136+
protected EncryptDataKeyStrategy encryptDataKeyStrategy() {
137+
throw new S3EncryptionClientException("KmsDiscoveryKeyring does not support EncryptDataKey");
138+
}
139+
140+
@Override
141+
protected Map<String, DecryptDataKeyStrategy> decryptDataKeyStrategies() {
142+
return decryptDataKeyStrategies;
143+
}
144+
145+
public static class Builder extends S3Keyring.Builder<KmsDiscoveryKeyring, Builder> {
146+
private KmsClient _kmsClient;
147+
148+
private Builder() {
149+
super();
150+
}
151+
152+
@Override
153+
protected Builder builder() {
154+
return this;
155+
}
156+
157+
/**
158+
* Note that this does NOT create a defensive clone of KmsClient. Any modifications made to the wrapped
159+
* client will be reflected in this Builder.
160+
*/
161+
@SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Pass mutability into wrapping client")
162+
public Builder kmsClient(KmsClient kmsClient) {
163+
_kmsClient = kmsClient;
164+
return this;
165+
}
166+
167+
public KmsDiscoveryKeyring build() {
168+
if (_kmsClient == null) {
169+
_kmsClient = KmsClient.create();
170+
}
171+
172+
return new KmsDiscoveryKeyring(this);
173+
}
174+
}
175+
}

src/main/java/software/amazon/encryption/s3/materials/KmsKeyring.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedData
178178
}
179179

180180
// We are validating the encryption context to match S3EC V2 behavior
181+
// Refer to KMSMaterialsHandler in the V2 client for details
181182
Map<String, String> materialsEncryptionContextCopy = new HashMap<>(materials.encryptionContext());
182183
materialsEncryptionContextCopy.remove(KEY_ID_CONTEXT_KEY);
183184
materialsEncryptionContextCopy.remove(ENCRYPTION_CONTEXT_ALGORITHM_KEY);

0 commit comments

Comments
 (0)