Skip to content

Commit 45b4c1b

Browse files
author
Bryan Donlan
committed
Add Builder.clone() and KMSMKP.withGrantTokens()
1 parent f9834f3 commit 45b4c1b

File tree

4 files changed

+213
-69
lines changed

4 files changed

+213
-69
lines changed

src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProvider.java

Lines changed: 70 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313

1414
package com.amazonaws.encryptionsdk.kms;
1515

16+
import static java.util.Arrays.asList;
17+
import static java.util.Collections.emptyList;
1618
import static java.util.Collections.singletonList;
1719

1820
import java.nio.charset.StandardCharsets;
1921
import java.util.ArrayList;
20-
import java.util.Arrays;
2122
import java.util.Collection;
2223
import java.util.Collections;
2324
import java.util.List;
@@ -39,7 +40,6 @@
3940
import com.amazonaws.encryptionsdk.exception.NoSuchMasterKeyException;
4041
import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException;
4142
import com.amazonaws.handlers.RequestHandler2;
42-
import com.amazonaws.regions.DefaultAwsRegionProviderChain;
4343
import com.amazonaws.regions.Region;
4444
import com.amazonaws.regions.Regions;
4545
import com.amazonaws.services.kms.AWSKMS;
@@ -70,12 +70,27 @@ public interface RegionalClientSupplier {
7070
AWSKMS getClient(String regionName);
7171
}
7272

73-
public static class Builder implements Cloneable {
73+
public static final class Builder implements Cloneable {
7474
private String defaultRegion_ = null;
7575
private RegionalClientSupplier regionalClientSupplier_ = null;
7676
private AWSKMSClientBuilder templateBuilder_ = null;
7777
private List<String> keyIds_ = new ArrayList<>();
78-
private List<String> grantTokens_ = new ArrayList<>();
78+
79+
public Builder clone() {
80+
try {
81+
Builder cloned = (Builder)super.clone();
82+
83+
if (templateBuilder_ != null) {
84+
cloned.templateBuilder_ = cloneClientBuilder(templateBuilder_);
85+
}
86+
87+
cloned.keyIds_ = new ArrayList<>(keyIds_);
88+
89+
return cloned;
90+
} catch (CloneNotSupportedException e) {
91+
throw new Error("Impossible: CloneNotSupportedException", e);
92+
}
93+
}
7994

8095
/**
8196
* Adds key ID(s) to the list of keys to use on encryption.
@@ -84,7 +99,7 @@ public static class Builder implements Cloneable {
8499
* @return
85100
*/
86101
public Builder withKeysForEncryption(String... keyIds) {
87-
keyIds_.addAll(Arrays.asList(keyIds));
102+
keyIds_.addAll(asList(keyIds));
88103
return this;
89104
}
90105

@@ -99,17 +114,6 @@ public Builder withKeysForEncryption(List<String> keyIds) {
99114
return this;
100115
}
101116

102-
/**
103-
* Adds grant tokens to the provider under construction.
104-
*
105-
* @param tokens
106-
* @return
107-
*/
108-
public Builder withGrantTokens(List<String> tokens) {
109-
this.grantTokens_.addAll(tokens);
110-
return this;
111-
}
112-
113117
/**
114118
* Sets the default region. This region will be used when specifying key IDs for encryption or in
115119
* {@link KmsMasterKeyProvider#getMasterKey(String)} that are not full ARNs, but are instead bare key IDs or
@@ -230,9 +234,19 @@ private AWSKMSClientBuilder cloneClientBuilder(final AWSKMSClientBuilder builder
230234
* @return
231235
*/
232236
public KmsMasterKeyProvider build() {
237+
// If we don't have a default region, we need to check that all key IDs will be usable
238+
if (defaultRegion_ == null) {
239+
for (String keyId : keyIds_) {
240+
if (parseRegionfromKeyArn(keyId) == null) {
241+
throw new AwsCryptoException("Can't use non-ARN key identifiers or aliases when " +
242+
"no default region is set");
243+
}
244+
}
245+
}
246+
233247
RegionalClientSupplier supplier = clientFactory();
234248

235-
return new KmsMasterKeyProvider(supplier, defaultRegion_, keyIds_, grantTokens_, false);
249+
return new KmsMasterKeyProvider(supplier, defaultRegion_, keyIds_, emptyList(), false);
236250
}
237251

238252
private RegionalClientSupplier clientFactory() {
@@ -283,17 +297,12 @@ private KmsMasterKeyProvider(
283297
this.defaultRegion_ = defaultRegion;
284298
this.keyIds_ = Collections.unmodifiableList(new ArrayList<>(keyIds));
285299

286-
if (grantTokens != null) {
287-
this.grantTokens_ = Collections.unmodifiableList(new ArrayList<>(grantTokens));
288-
} else {
289-
// Legacy support for the mutating API
290-
this.grantTokens_ = new ArrayList<>();
291-
}
300+
this.grantTokens_ = grantTokens;
292301
}
293302

294303
// Helper ctor for legacy ctors
295304
private KmsMasterKeyProvider(RegionalClientSupplier supplier, String defaultRegion, List<String> keyIds) {
296-
this(supplier, defaultRegion, keyIds, null, true);
305+
this(supplier, defaultRegion, keyIds, new ArrayList<>(), true);
297306
}
298307

299308
private static RegionalClientSupplier defaultProvider() {
@@ -309,7 +318,7 @@ private static RegionalClientSupplier defaultProvider() {
309318
*/
310319
@Deprecated
311320
public KmsMasterKeyProvider() {
312-
this(defaultProvider(), Regions.DEFAULT_REGION.getName(), Collections.emptyList());
321+
this(defaultProvider(), Regions.DEFAULT_REGION.getName(), emptyList());
313322
}
314323

315324

@@ -439,7 +448,7 @@ public KmsMasterKey getMasterKey(final String provider, final String keyId) thro
439448
throw new UnsupportedProviderException();
440449
}
441450

442-
String regionName = identifyKeyRegion(keyId);
451+
String regionName = parseRegionfromKeyArn(keyId);
443452
AWSKMS kms = regionalClientSupplier_.getClient(regionName);
444453
if (kms == null) {
445454
throw new AwsCryptoException("Can't use keys from region " + regionName);
@@ -456,7 +465,7 @@ public KmsMasterKey getMasterKey(final String provider, final String keyId) thro
456465
@Override
457466
public List<KmsMasterKey> getMasterKeysForEncryption(final MasterKeyRequest request) {
458467
if (keyIds_ == null) {
459-
return Collections.emptyList();
468+
return emptyList();
460469
}
461470
List<KmsMasterKey> result = new ArrayList<>(keyIds_.size());
462471
for (String id : keyIds_) {
@@ -485,7 +494,7 @@ public DataKey<KmsMasterKey> decryptDataKey(final CryptoAlgorithm algorithm,
485494
}
486495

487496
/**
488-
* @deprecated This method is inherently not thread safe. Use {@link Builder#setGrantTokens(List)} instead.
497+
* @deprecated This method is inherently not thread safe. Use {@link KmsMasterKey#setGrantTokens(List)} instead.
489498
* {@link KmsMasterKeyProvider}s constructed using the builder will throw an exception on attempts to modify the
490499
* list of grant tokens.
491500
*/
@@ -496,9 +505,7 @@ public void setGrantTokens(final List<String> grantTokens) {
496505
this.grantTokens_.clear();
497506
this.grantTokens_.addAll(grantTokens);
498507
} catch (UnsupportedOperationException e) {
499-
throw new IllegalStateException(
500-
"Changing grant tokens are not supported when constructing using the new builder API. Set them " +
501-
"at construction time instead.");
508+
throw grantTokenError();
502509
}
503510
}
504511

@@ -508,22 +515,48 @@ public List<String> getGrantTokens() {
508515
}
509516

510517
/**
511-
* @deprecated This method is inherently not thread safe. Use {@link Builder#setGrantTokens(List)} instead.
512-
* {@link KmsMasterKeyProvider}s constructed using the builder will throw an exception on attempts to modify the
513-
* list of grant tokens.
518+
* @deprecated This method is inherently not thread safe. Use {@link #withGrantTokens(List)} or
519+
* {@link KmsMasterKey#setGrantTokens(List)} instead. {@link KmsMasterKeyProvider}s constructed using the builder
520+
* will throw an exception on attempts to modify the list of grant tokens.
514521
*/
515522
@Deprecated
516523
@Override
517524
public void addGrantToken(final String grantToken) {
518525
try {
519526
grantTokens_.add(grantToken);
520527
} catch (UnsupportedOperationException e) {
521-
throw new IllegalStateException(
522-
"Changing grant tokens are not supported when constructing using the new builder API. Set them " +
523-
"at construction time instead.");
528+
throw grantTokenError();
524529
}
525530
}
526531

532+
private RuntimeException grantTokenError() {
533+
return new IllegalStateException("This master key provider is immutable. Use withGrantTokens instead.");
534+
}
535+
536+
/**
537+
* Returns a new {@link KmsMasterKeyProvider} that is configured identically to this one, except with the given list
538+
* of grant tokens. The grant token list in the returned provider is immutable (but can be further overridden by
539+
* invoking withGrantTokens again).
540+
* @param grantTokens
541+
* @return
542+
*/
543+
public KmsMasterKeyProvider withGrantTokens(List<String> grantTokens) {
544+
grantTokens = Collections.unmodifiableList(new ArrayList<>(grantTokens));
545+
546+
return new KmsMasterKeyProvider(regionalClientSupplier_, defaultRegion_, keyIds_, grantTokens, false);
547+
}
548+
549+
/**
550+
* Returns a new {@link KmsMasterKeyProvider} that is configured identically to this one, except with the given list
551+
* of grant tokens. The grant token list in the returned provider is immutable (but can be further overridden by
552+
* invoking withGrantTokens again).
553+
* @param grantTokens
554+
* @return
555+
*/
556+
public KmsMasterKeyProvider withGrantTokens(String... grantTokens) {
557+
return withGrantTokens(asList(grantTokens));
558+
}
559+
527560
private static Region getStartingRegion(final String keyArn) {
528561
final String region = parseRegionfromKeyArn(keyArn);
529562
if (region != null) {
@@ -537,20 +570,6 @@ private static Region getStartingRegion(final String keyArn) {
537570
return Region.getRegion(Regions.DEFAULT_REGION);
538571
}
539572

540-
private String identifyKeyRegion(final String keyArn) {
541-
String region = parseRegionfromKeyArn(keyArn);
542-
543-
if (region != null) {
544-
return region;
545-
}
546-
547-
if (defaultRegion_ == null) {
548-
throw new AwsCryptoException("Can't use non-ARN key identifiers or aliases when no default region is set");
549-
}
550-
551-
return defaultRegion_;
552-
}
553-
554573
private static String parseRegionfromKeyArn(final String keyArn) {
555574
final String[] parts = keyArn.split(":", 5);
556575

src/test/java/com/amazonaws/services/kms/KMSProviderBuilderIntegrationTests.java

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package com.amazonaws.services.kms;
22

33
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertTrue;
45
import static org.junit.Assert.fail;
56
import static org.mockito.ArgumentMatchers.any;
67
import static org.mockito.Mockito.atLeastOnce;
8+
import static org.mockito.Mockito.never;
9+
import static org.mockito.Mockito.reset;
710
import static org.mockito.Mockito.spy;
811
import static org.mockito.Mockito.verify;
912

10-
import java.lang.reflect.Field;
1113
import java.util.Arrays;
1214

13-
import org.junit.Assume;
1415
import org.junit.Test;
1516

1617
import com.amazonaws.AbortedException;
@@ -20,23 +21,20 @@
2021
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
2122
import com.amazonaws.client.builder.AwsClientBuilder;
2223
import com.amazonaws.encryptionsdk.AwsCrypto;
24+
import com.amazonaws.encryptionsdk.CryptoResult;
25+
import com.amazonaws.encryptionsdk.MasterKeyProvider;
2326
import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException;
2427
import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider;
2528
import com.amazonaws.handlers.RequestHandler2;
2629
import com.amazonaws.http.exception.HttpRequestTimeoutException;
27-
import com.amazonaws.regions.DefaultAwsRegionProviderChain;
2830

2931
public class KMSProviderBuilderIntegrationTests {
30-
public String[] keyIds = new String[] {
31-
"arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f",
32-
"arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2"
33-
};
3432

3533
@Test
3634
public void whenConstructedWithoutArguments_canUseMultipleRegions() throws Exception {
3735
KmsMasterKeyProvider mkp = KmsMasterKeyProvider.builder().build();
3836

39-
for (String key : keyIds) {
37+
for (String key : KMSTestFixtures.TEST_KEY_IDS) {
4038
byte[] ciphertext =
4139
new AwsCrypto().encryptData(
4240
KmsMasterKeyProvider.builder()
@@ -53,7 +51,7 @@ public void whenConstructedWithoutArguments_canUseMultipleRegions() throws Excep
5351
public void whenLegacyConstructorsUsed_multiRegionDecryptIsNotSupported() throws Exception {
5452
KmsMasterKeyProvider mkp = new KmsMasterKeyProvider();
5553

56-
for (String key : keyIds) {
54+
for (String key : KMSTestFixtures.TEST_KEY_IDS) {
5755
byte[] ciphertext =
5856
new AwsCrypto().encryptData(
5957
KmsMasterKeyProvider.builder()
@@ -75,7 +73,7 @@ public void whenHandlerConfigured_handlerIsInvoked() throws Exception {
7573
AWSKMSClientBuilder.standard()
7674
.withRequestHandlers(handler)
7775
)
78-
.withKeysForEncryption(keyIds[0])
76+
.withKeysForEncryption(KMSTestFixtures.TEST_KEY_IDS[0])
7977
.build();
8078

8179
new AwsCrypto().encryptData(mkp, new byte[1]);
@@ -95,7 +93,7 @@ public void whenShortTimeoutSet_timesOut() throws Exception {
9593
.withRequestTimeout(1)
9694
)
9795
)
98-
.withKeysForEncryption(Arrays.asList(keyIds))
96+
.withKeysForEncryption(Arrays.asList(KMSTestFixtures.TEST_KEY_IDS))
9997
.build();
10098

10199
try {
@@ -118,7 +116,7 @@ public void whenCustomCredentialsSet_theyAreUsed() throws Exception {
118116

119117
KmsMasterKeyProvider mkp = KmsMasterKeyProvider.builder()
120118
.withCredentials(customProvider)
121-
.withKeysForEncryption(keyIds[0])
119+
.withKeysForEncryption(KMSTestFixtures.TEST_KEY_IDS[0])
122120
.build();
123121

124122
new AwsCrypto().encryptData(mkp, new byte[1]);
@@ -129,14 +127,63 @@ public void whenCustomCredentialsSet_theyAreUsed() throws Exception {
129127

130128
mkp = KmsMasterKeyProvider.builder()
131129
.withCredentials(customCredentials)
132-
.withKeysForEncryption(keyIds[0])
130+
.withKeysForEncryption(KMSTestFixtures.TEST_KEY_IDS[0])
133131
.build();
134132

135133
new AwsCrypto().encryptData(mkp, new byte[1]);
136134

137135
verify(customCredentials, atLeastOnce()).getAWSSecretKey();
138136
}
139137

138+
@Test
139+
public void whenBuilderCloned_credentialsAndConfigurationAreRetained() throws Exception {
140+
AWSCredentialsProvider customProvider1 = spy(new DefaultAWSCredentialsProviderChain());
141+
AWSCredentialsProvider customProvider2 = spy(new DefaultAWSCredentialsProviderChain());
142+
143+
KmsMasterKeyProvider.Builder builder = KmsMasterKeyProvider.builder()
144+
.withCredentials(customProvider1)
145+
.withKeysForEncryption(KMSTestFixtures.TEST_KEY_IDS[0]);
146+
147+
KmsMasterKeyProvider.Builder builder2 = builder.clone();
148+
149+
// This will mutate the first builder to add the new key and change the creds, but leave the clone unchanged.
150+
MasterKeyProvider<?> mkp2 = builder.withKeysForEncryption(KMSTestFixtures.TEST_KEY_IDS[1]).withCredentials(customProvider2).build();
151+
MasterKeyProvider<?> mkp1 = builder2.build();
152+
153+
CryptoResult<byte[], ?> result = new AwsCrypto().encryptData(mkp1, new byte[0]);
154+
155+
assertEquals(KMSTestFixtures.TEST_KEY_IDS[0], result.getMasterKeyIds().get(0));
156+
assertEquals(1, result.getMasterKeyIds().size());
157+
verify(customProvider1, atLeastOnce()).getCredentials();
158+
verify(customProvider2, never()).getCredentials();
159+
160+
reset(customProvider1, customProvider2);
161+
162+
result = new AwsCrypto().encryptData(mkp2, new byte[0]);
163+
164+
assertTrue(result.getMasterKeyIds().contains(KMSTestFixtures.TEST_KEY_IDS[0]));
165+
assertTrue(result.getMasterKeyIds().contains(KMSTestFixtures.TEST_KEY_IDS[1]));
166+
assertEquals(2, result.getMasterKeyIds().size());
167+
verify(customProvider1, never()).getCredentials();
168+
verify(customProvider2, atLeastOnce()).getCredentials();
169+
}
170+
171+
@Test
172+
public void whenBuilderCloned_clientBuilderCustomizationIsRetained() throws Exception {
173+
RequestHandler2 handler = spy(new RequestHandler2() {});
174+
175+
KmsMasterKeyProvider mkp = KmsMasterKeyProvider.builder()
176+
.withClientBuilder(
177+
AWSKMSClientBuilder.standard().withRequestHandlers(handler)
178+
)
179+
.withKeysForEncryption(KMSTestFixtures.TEST_KEY_IDS[0])
180+
.clone().build();
181+
182+
new AwsCrypto().encryptData(mkp, new byte[0]);
183+
184+
verify(handler, atLeastOnce()).beforeRequest(any());
185+
}
186+
140187
@Test(expected = IllegalArgumentException.class)
141188
public void whenBogusEndpointIsSet_constructionFails() throws Exception {
142189
KmsMasterKeyProvider.builder()
@@ -155,3 +202,4 @@ public void whenDefaultRegionSet_itIsUsedForBareKeyIds() throws Exception {
155202
// TODO: Need to set up a role to assume as bare key IDs are relative to the caller account
156203
}
157204
}
205+

0 commit comments

Comments
 (0)