Skip to content

Commit 90cab2d

Browse files
feat: Configurable SecureRandom (#40)
1 parent 2438664 commit 90cab2d

File tree

6 files changed

+162
-16
lines changed

6 files changed

+162
-16
lines changed

src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
44

55
import java.security.KeyPair;
6+
import java.security.SecureRandom;
67
import java.util.Map;
78
import java.util.function.Consumer;
89
import javax.crypto.SecretKey;
@@ -22,7 +23,6 @@
2223
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
2324
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
2425
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
25-
import software.amazon.awssdk.services.s3.model.S3Exception;
2626
import software.amazon.encryption.s3.internal.GetEncryptedObjectPipeline;
2727
import software.amazon.encryption.s3.internal.PutEncryptedObjectPipeline;
2828
import software.amazon.encryption.s3.materials.AesKeyring;
@@ -44,12 +44,14 @@ public class S3EncryptionClient implements S3Client {
4444

4545
private final S3Client _wrappedClient;
4646
private final CryptographicMaterialsManager _cryptoMaterialsManager;
47+
private final SecureRandom _secureRandom;
4748
private final boolean _enableLegacyUnauthenticatedModes;
4849
private final boolean _enableDelayedAuthenticationMode;
4950

5051
private S3EncryptionClient(Builder builder) {
5152
_wrappedClient = builder._wrappedClient;
5253
_cryptoMaterialsManager = builder._cryptoMaterialsManager;
54+
_secureRandom = builder._secureRandom;
5355
_enableLegacyUnauthenticatedModes = builder._enableLegacyUnauthenticatedModes;
5456
_enableDelayedAuthenticationMode = builder._enableDelayedAuthenticationMode;
5557
}
@@ -71,6 +73,7 @@ public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBod
7173
PutEncryptedObjectPipeline pipeline = PutEncryptedObjectPipeline.builder()
7274
.s3Client(_wrappedClient)
7375
.cryptoMaterialsManager(_cryptoMaterialsManager)
76+
.secureRandom(_secureRandom)
7477
.build();
7578

7679
return pipeline.putObject(putObjectRequest, requestBody);
@@ -122,6 +125,7 @@ public static class Builder {
122125
private String _kmsKeyId;
123126
private boolean _enableLegacyUnauthenticatedModes = false;
124127
private boolean _enableDelayedAuthenticationMode = false;
128+
private SecureRandom _secureRandom = new SecureRandom();
125129

126130
private Builder() {}
127131

@@ -215,6 +219,14 @@ public Builder enableDelayedAuthenticationMode(boolean shouldEnableDelayedAuthen
215219
return this;
216220
}
217221

222+
public Builder secureRandom(SecureRandom secureRandom) {
223+
if (secureRandom == null) {
224+
throw new S3EncryptionClientException("SecureRandom provided to S3EncryptionClient cannot be null");
225+
}
226+
_secureRandom = secureRandom;
227+
return this;
228+
}
229+
218230
public S3EncryptionClient build() {
219231
if (!onlyOneNonNull(_cryptoMaterialsManager, _keyring, _aesKey, _rsaKeyPair, _kmsKeyId)) {
220232
throw new S3EncryptionClientException("Exactly one must be set of: crypto materials manager, keyring, AES key, RSA key pair, KMS key id");
@@ -225,16 +237,19 @@ public S3EncryptionClient build() {
225237
_keyring = AesKeyring.builder()
226238
.wrappingKey(_aesKey)
227239
.enableLegacyUnauthenticatedModes(_enableLegacyUnauthenticatedModes)
240+
.secureRandom(_secureRandom)
228241
.build();
229242
} else if (_rsaKeyPair != null) {
230243
_keyring = RsaKeyring.builder()
231244
.wrappingKeyPair(_rsaKeyPair)
232245
.enableLegacyUnauthenticatedModes(_enableLegacyUnauthenticatedModes)
246+
.secureRandom(_secureRandom)
233247
.build();
234248
} else if (_kmsKeyId != null) {
235249
_keyring = KmsKeyring.builder()
236250
.wrappingKeyId(_kmsKeyId)
237251
.enableLegacyUnauthenticatedModes(_enableLegacyUnauthenticatedModes)
252+
.secureRandom(_secureRandom)
238253
.build();
239254
}
240255
}

src/main/java/software/amazon/encryption/s3/internal/BufferedAesGcmContentStrategy.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public InputStream decryptContent(ContentMetadata contentMetadata, DecryptionMat
117117
}
118118

119119
public static class Builder {
120-
private SecureRandom _secureRandom = new SecureRandom();
120+
private SecureRandom _secureRandom;
121121

122122
private Builder() {
123123
}
@@ -128,6 +128,9 @@ private Builder() {
128128
*/
129129
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
130130
public Builder secureRandom(SecureRandom secureRandom) {
131+
if (secureRandom == null) {
132+
throw new S3EncryptionClientException("SecureRandom provided to BufferedAesGcmContentStrategy cannot be null");
133+
}
131134
_secureRandom = secureRandom;
132135
return this;
133136
}

src/main/java/software/amazon/encryption/s3/internal/PutEncryptedObjectPipeline.java

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
44

55
import java.io.IOException;
6+
import java.security.SecureRandom;
67

78
import software.amazon.awssdk.core.sync.RequestBody;
89
import software.amazon.awssdk.services.s3.S3Client;
@@ -18,6 +19,7 @@ public class PutEncryptedObjectPipeline {
1819

1920
final private S3Client _s3Client;
2021
final private CryptographicMaterialsManager _cryptoMaterialsManager;
22+
final private SecureRandom _secureRandom;
2123
final private ContentEncryptionStrategy _contentEncryptionStrategy;
2224
final private ContentMetadataEncodingStrategy _contentMetadataEncodingStrategy;
2325

@@ -26,6 +28,7 @@ public class PutEncryptedObjectPipeline {
2628
private PutEncryptedObjectPipeline(Builder builder) {
2729
this._s3Client = builder._s3Client;
2830
this._cryptoMaterialsManager = builder._cryptoMaterialsManager;
31+
this._secureRandom = builder._secureRandom;
2932
this._contentEncryptionStrategy = builder._contentEncryptionStrategy;
3033
this._contentMetadataEncodingStrategy = builder._contentMetadataEncodingStrategy;
3134
}
@@ -53,13 +56,11 @@ public PutObjectResponse putObject(PutObjectRequest request, RequestBody request
5356
public static class Builder {
5457
private S3Client _s3Client;
5558
private CryptographicMaterialsManager _cryptoMaterialsManager;
56-
// Default to AesGcm since it is the only active (non-legacy) content encryption strategy
57-
private ContentEncryptionStrategy _contentEncryptionStrategy =
58-
BufferedAesGcmContentStrategy
59-
.builder()
60-
.build();
59+
private SecureRandom _secureRandom;
60+
private ContentEncryptionStrategy _contentEncryptionStrategy;
6161
private ContentMetadataEncodingStrategy _contentMetadataEncodingStrategy = ContentMetadataStrategy.OBJECT_METADATA;
6262

63+
6364
private Builder() {}
6465

6566
/**
@@ -77,17 +78,19 @@ public Builder cryptoMaterialsManager(CryptographicMaterialsManager cryptoMateri
7778
return this;
7879
}
7980

80-
public Builder contentEncryptionStrategy(ContentEncryptionStrategy strategy) {
81-
this._contentEncryptionStrategy = strategy;
82-
return this;
83-
}
84-
85-
public Builder metadataEncodingStrategy(ContentMetadataEncodingStrategy strategy) {
86-
this._contentMetadataEncodingStrategy = strategy;
81+
public Builder secureRandom(SecureRandom secureRandom) {
82+
this._secureRandom = secureRandom;
8783
return this;
8884
}
8985

9086
public PutEncryptedObjectPipeline build() {
87+
// Default to AesGcm since it is the only active (non-legacy) content encryption strategy
88+
if (_contentEncryptionStrategy == null) {
89+
_contentEncryptionStrategy = BufferedAesGcmContentStrategy
90+
.builder()
91+
.secureRandom(_secureRandom)
92+
.build();
93+
}
9194
return new PutEncryptedObjectPipeline(this);
9295
}
9396
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public DecryptionMaterials onDecrypt(final DecryptionMaterials materials, List<E
104104

105105
abstract public static class Builder<KeyringT extends S3Keyring, BuilderT extends Builder<KeyringT, BuilderT>> {
106106
private boolean _enableLegacyUnauthenticatedModes = false;
107-
private SecureRandom _secureRandom = new SecureRandom();
107+
private SecureRandom _secureRandom;
108108
private DataKeyGenerator _dataKeyGenerator = new DefaultDataKeyGenerator();
109109

110110

@@ -124,7 +124,7 @@ public BuilderT enableLegacyUnauthenticatedModes(boolean shouldEnableLegacyUnaut
124124
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
125125
public BuilderT secureRandom(final SecureRandom secureRandom) {
126126
if (secureRandom == null) {
127-
throw new S3EncryptionClientException("SecureRandom cannot be null!");
127+
throw new S3EncryptionClientException("SecureRandom provided to S3Keyring cannot be null");
128128
}
129129
_secureRandom = secureRandom;
130130
return builder();

src/test/java/software/amazon/encryption/s3/S3EncryptionClientTest.java

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package software.amazon.encryption.s3;
22

3+
import java.security.SecureRandom;
4+
35
import org.junit.jupiter.api.BeforeAll;
46
import org.junit.jupiter.api.Test;
7+
58
import software.amazon.awssdk.core.ResponseBytes;
69
import software.amazon.awssdk.core.sync.RequestBody;
710
import software.amazon.awssdk.services.s3.S3Client;
811
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
912
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
13+
import software.amazon.encryption.s3.materials.AesKeyring;
1014
import software.amazon.encryption.s3.materials.CryptographicMaterialsManager;
1115
import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager;
1216
import software.amazon.encryption.s3.materials.KmsKeyring;
@@ -23,6 +27,13 @@
2327
import java.util.Map;
2428

2529
import static org.junit.jupiter.api.Assertions.*;
30+
import static org.mockito.ArgumentMatchers.any;
31+
import static org.mockito.Mockito.mock;
32+
import static org.mockito.Mockito.never;
33+
import static org.mockito.Mockito.times;
34+
import static org.mockito.Mockito.verify;
35+
import static org.mockito.Mockito.withSettings;
36+
2637
import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalEncryptionContext;
2738
import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.BUCKET;
2839
import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.KMS_KEY_ALIAS;
@@ -249,6 +260,104 @@ public void s3EncryptionClientWithWrappedS3EncryptionClientFails() {
249260
.build());
250261
}
251262

263+
@Test
264+
public void s3EncryptionClientWithNullSecureRandomFails() {
265+
assertThrows(S3EncryptionClientException.class, () -> S3EncryptionClient.builder()
266+
.aesKey(AES_KEY)
267+
.secureRandom(null)
268+
.build());
269+
}
270+
271+
@Test
272+
public void s3EncryptionClientFromKMSKeyDoesNotUseUnprovidedSecureRandom() {
273+
SecureRandom mockSecureRandom = mock(SecureRandom.class, withSettings().withoutAnnotations());
274+
275+
final String objectKey = "no-secure-random-object-kms";
276+
277+
S3Client v3Client = S3EncryptionClient.builder()
278+
.kmsKeyId(KMS_KEY_ID)
279+
.build();
280+
281+
simpleV3RoundTrip(v3Client, objectKey);
282+
283+
verify(mockSecureRandom, never()).nextBytes(any());
284+
}
285+
286+
@Test
287+
public void s3EncryptionClientFromKMSKeyIdWithSecureRandomUsesObjectOnceForRoundTripCall() {
288+
SecureRandom mockSecureRandom = mock(SecureRandom.class, withSettings().withoutAnnotations());
289+
290+
final String objectKey = "secure-random-object-kms";
291+
292+
S3Client v3Client = S3EncryptionClient.builder()
293+
.kmsKeyId(KMS_KEY_ID)
294+
.secureRandom(mockSecureRandom)
295+
.build();
296+
297+
simpleV3RoundTrip(v3Client, objectKey);
298+
299+
// Should only be called from encryption content strategy.
300+
// KMS keyring does not use SecureRandom for encryptDataKey.
301+
verify(mockSecureRandom, times(1)).nextBytes(any());
302+
}
303+
304+
@Test
305+
public void s3EncryptionClientFromAESKeyWithSecureRandomUsesObjectTwiceForRoundTripCall() {
306+
SecureRandom mockSecureRandom = mock(SecureRandom.class, withSettings().withoutAnnotations());
307+
308+
final String objectKey = "secure-random-object-aes";
309+
310+
S3Client v3Client = S3EncryptionClient.builder()
311+
.aesKey(AES_KEY)
312+
.secureRandom(mockSecureRandom)
313+
.build();
314+
315+
simpleV3RoundTrip(v3Client, objectKey);
316+
317+
// Should be called once from encryption content strategy and again from AES encryptDataKey.
318+
verify(mockSecureRandom, times(2)).nextBytes(any());
319+
}
320+
321+
@Test
322+
public void s3EncryptionClientFromRSAKeyWithSecureRandomUsesObjectTwiceForRoundTripCall() {
323+
SecureRandom mockSecureRandom = mock(SecureRandom.class, withSettings().withoutAnnotations());
324+
325+
final String objectKey = "secure-random-object-rsa";
326+
327+
S3Client v3Client = S3EncryptionClient.builder()
328+
.rsaKeyPair(RSA_KEY_PAIR)
329+
.secureRandom(mockSecureRandom)
330+
.build();
331+
332+
simpleV3RoundTrip(v3Client, objectKey);
333+
334+
// Should be called once from encryption content strategy and again from RSA encryptDataKey.
335+
verify(mockSecureRandom, times(2)).nextBytes(any());
336+
}
337+
338+
@Test
339+
public void s3EncryptionClientFromAESKeyringUsesDifferentSecureRandomThanKeyring() {
340+
SecureRandom mockSecureRandomKeyring = mock(SecureRandom.class, withSettings().withoutAnnotations());
341+
SecureRandom mockSecureRandomClient = mock(SecureRandom.class, withSettings().withoutAnnotations());
342+
343+
AesKeyring keyring = AesKeyring.builder()
344+
.wrappingKey(AES_KEY)
345+
.secureRandom(mockSecureRandomKeyring)
346+
.build();
347+
348+
final String objectKey = "secure-random-object-aes-different-keyring";
349+
350+
S3Client v3Client = S3EncryptionClient.builder()
351+
.keyring(keyring)
352+
.secureRandom(mockSecureRandomClient)
353+
.build();
354+
355+
simpleV3RoundTrip(v3Client, objectKey);
356+
357+
verify(mockSecureRandomKeyring, times(1)).nextBytes(any());
358+
verify(mockSecureRandomClient, times(1)).nextBytes(any());
359+
}
360+
252361
/**
253362
* A simple, reusable round-trip (encryption + decryption) using a given
254363
* S3Client. Useful for testing client configuration.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package software.amazon.encryption.s3.internal;
2+
3+
import static org.junit.jupiter.api.Assertions.assertThrows;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import software.amazon.encryption.s3.S3EncryptionClientException;
8+
9+
public class BufferedAesGcmContentStrategyTest {
10+
11+
@Test
12+
public void buildBufferedAesGcmContentStrategyWithNullSecureRandomFails() {
13+
assertThrows(S3EncryptionClientException.class, () -> BufferedAesGcmContentStrategy.builder().secureRandom(null));
14+
}
15+
16+
}

0 commit comments

Comments
 (0)