Skip to content

Commit b8eedac

Browse files
authored
feat: Introduce delayed authentication (#23)
* Introduce delayed authentication: * Rename enableLegacyModes option to enableUnauthenticatedLegacyModes * Adds a new configuration option for delayedAuthenticationMode and provides it to the GetDecryptedObjectPipeline * Refactors ContentDecryptionStrategy and its consumers to always use InputStream for ciphertext. For the existing implementations, the logic to buffer this stream into an array has been encapsulated in the content decryption strategies
1 parent 5131ade commit b8eedac

12 files changed

+207
-61
lines changed

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ public class S3EncryptionClient implements S3Client {
3636

3737
private final S3Client _wrappedClient;
3838
private final CryptographicMaterialsManager _cryptoMaterialsManager;
39-
private final boolean _enableLegacyModes;
39+
private final boolean _enableLegacyUnauthenticatedModes;
40+
private final boolean _enableDelayedAuthenticationMode;
4041

4142
private S3EncryptionClient(Builder builder) {
4243
_wrappedClient = builder._wrappedClient;
4344
_cryptoMaterialsManager = builder._cryptoMaterialsManager;
44-
_enableLegacyModes = builder._enableLegacyModes;
45+
_enableLegacyUnauthenticatedModes = builder._enableLegacyUnauthenticatedModes;
46+
_enableDelayedAuthenticationMode = builder._enableDelayedAuthenticationMode;
4547
}
4648

4749
public static Builder builder() {
@@ -74,7 +76,8 @@ public <T> T getObject(GetObjectRequest getObjectRequest,
7476
GetEncryptedObjectPipeline pipeline = GetEncryptedObjectPipeline.builder()
7577
.s3Client(_wrappedClient)
7678
.cryptoMaterialsManager(_cryptoMaterialsManager)
77-
.enableLegacyModes(_enableLegacyModes)
79+
.enableLegacyUnauthenticatedModes(_enableLegacyUnauthenticatedModes)
80+
.enableDelayedAuthentication(_enableDelayedAuthenticationMode)
7881
.build();
7982

8083
return pipeline.getObject(getObjectRequest, responseTransformer);
@@ -97,7 +100,8 @@ public static class Builder {
97100
private SecretKey _aesKey;
98101
private PartialRsaKeyPair _rsaKeyPair;
99102
private String _kmsKeyId;
100-
private boolean _enableLegacyModes = false;
103+
private boolean _enableLegacyUnauthenticatedModes = false;
104+
private boolean _enableDelayedAuthenticationMode = false;
101105

102106
private Builder() {}
103107

@@ -172,8 +176,13 @@ private boolean onlyOneNonNull(Object... values) {
172176
return haveOneNonNull;
173177
}
174178

175-
public Builder enableLegacyModes(boolean shouldEnableLegacyModes) {
176-
this._enableLegacyModes = shouldEnableLegacyModes;
179+
public Builder enableLegacyUnauthenticatedModes(boolean shouldEnableLegacyUnauthenticatedModes) {
180+
this._enableLegacyUnauthenticatedModes = shouldEnableLegacyUnauthenticatedModes;
181+
return this;
182+
}
183+
184+
public Builder enableDelayedAuthenticationMode(boolean shouldEnableDelayedAuthenticationMode) {
185+
this._enableDelayedAuthenticationMode = shouldEnableDelayedAuthenticationMode;
177186
return this;
178187
}
179188

@@ -186,17 +195,17 @@ public S3EncryptionClient build() {
186195
if (_aesKey != null) {
187196
_keyring = AesKeyring.builder()
188197
.wrappingKey(_aesKey)
189-
.enableLegacyModes(_enableLegacyModes)
198+
.enableLegacyUnauthenticatedModes(_enableLegacyUnauthenticatedModes)
190199
.build();
191200
} else if (_rsaKeyPair != null) {
192201
_keyring = RsaKeyring.builder()
193202
.wrappingKeyPair(_rsaKeyPair)
194-
.enableLegacyModes(_enableLegacyModes)
203+
.enableLegacyUnauthenticatedModes(_enableLegacyUnauthenticatedModes)
195204
.build();
196205
} else if (_kmsKeyId != null) {
197206
_keyring = KmsKeyring.builder()
198207
.wrappingKeyId(_kmsKeyId)
199-
.enableLegacyModes(_enableLegacyModes)
208+
.enableLegacyUnauthenticatedModes(_enableLegacyUnauthenticatedModes)
200209
.build();
201210
}
202211
}

src/main/java/software/amazon/encryption/s3/internal/AesGcmContentStrategy.java renamed to src/main/java/software/amazon/encryption/s3/internal/BufferedAesGcmContentStrategy.java

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package software.amazon.encryption.s3.internal;
22

3+
import java.io.ByteArrayInputStream;
4+
import java.io.IOException;
5+
import java.io.InputStream;
36
import java.security.InvalidAlgorithmParameterException;
47
import java.security.InvalidKeyException;
58
import java.security.NoSuchAlgorithmException;
@@ -11,6 +14,8 @@
1114
import javax.crypto.SecretKey;
1215
import javax.crypto.spec.GCMParameterSpec;
1316
import javax.crypto.spec.SecretKeySpec;
17+
18+
import software.amazon.awssdk.utils.IoUtils;
1419
import software.amazon.encryption.s3.S3EncryptionClientException;
1520
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
1621
import software.amazon.encryption.s3.materials.DecryptionMaterials;
@@ -19,11 +24,15 @@
1924
/**
2025
* This class will encrypt data according to the algorithm suite constants
2126
*/
22-
public class AesGcmContentStrategy implements ContentEncryptionStrategy, ContentDecryptionStrategy {
27+
public class BufferedAesGcmContentStrategy implements ContentEncryptionStrategy, ContentDecryptionStrategy {
28+
29+
// 64MiB ought to be enough for most usecases
30+
private final long BUFFERED_MAX_CONTENT_LENGTH_MiB = 64;
31+
private final long BUFFERED_MAX_CONTENT_LENGTH_BYTES = 1024 * 1024 * BUFFERED_MAX_CONTENT_LENGTH_MiB;
2332

2433
final private SecureRandom _secureRandom;
2534

26-
private AesGcmContentStrategy(Builder builder) {
35+
private BufferedAesGcmContentStrategy(Builder builder) {
2736
this._secureRandom = builder._secureRandom;
2837
}
2938

@@ -60,7 +69,26 @@ public EncryptedContent encryptContent(EncryptionMaterials materials, byte[] con
6069
}
6170

6271
@Override
63-
public byte[] decryptContent(ContentMetadata contentMetadata, DecryptionMaterials materials, byte[] ciphertext) {
72+
public InputStream decryptContent(ContentMetadata contentMetadata, DecryptionMaterials materials,
73+
InputStream ciphertextStream) {
74+
// Check the size of the object. If it exceeds a predefined limit in default mode,
75+
// do not buffer it into memory. Throw an exception and instruct the client to
76+
// reconfigure using Delayed Authentication mode which supports decryption of
77+
// large objects over an InputStream.
78+
if (materials.ciphertextLength() > BUFFERED_MAX_CONTENT_LENGTH_BYTES) {
79+
throw new S3EncryptionClientException(String.format("The object you are attempting to decrypt exceeds the maximum content " +
80+
"length allowed in default mode. Please enable Delayed Authentication mode to decrypt objects larger" +
81+
"than %d", BUFFERED_MAX_CONTENT_LENGTH_MiB));
82+
}
83+
84+
// Buffer the ciphertextStream into a byte array
85+
byte[] ciphertext;
86+
try {
87+
ciphertext = IoUtils.toByteArray(ciphertextStream);
88+
} catch (IOException e) {
89+
throw new S3EncryptionClientException("Unexpected exception while buffering ciphertext input stream!", e);
90+
}
91+
6492
AlgorithmSuite algorithmSuite = contentMetadata.algorithmSuite();
6593
SecretKey contentKey = new SecretKeySpec(materials.plaintextDataKey(), algorithmSuite.dataKeyAlgorithm());
6694
final int tagLength = algorithmSuite.cipherTagLengthBits();
@@ -80,7 +108,7 @@ public byte[] decryptContent(ContentMetadata contentMetadata, DecryptionMaterial
80108
throw new S3EncryptionClientException("Unable to " + algorithmSuite.cipherName() + " content decrypt.", e);
81109
}
82110

83-
return plaintext;
111+
return new ByteArrayInputStream(plaintext);
84112
}
85113

86114
public static class Builder {
@@ -93,8 +121,8 @@ public Builder secureRandom(SecureRandom secureRandom) {
93121
return this;
94122
}
95123

96-
public AesGcmContentStrategy build() {
97-
return new AesGcmContentStrategy(this);
124+
public BufferedAesGcmContentStrategy build() {
125+
return new BufferedAesGcmContentStrategy(this);
98126
}
99127
}
100128
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import software.amazon.encryption.s3.materials.DecryptionMaterials;
44

5+
import java.io.InputStream;
6+
57
@FunctionalInterface
68
public interface ContentDecryptionStrategy {
7-
byte[] decryptContent(ContentMetadata contentMetadata, DecryptionMaterials materials, byte[] ciphertext);
9+
InputStream decryptContent(ContentMetadata contentMetadata, DecryptionMaterials materials, InputStream ciphertext);
810
}

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

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.io.ByteArrayInputStream;
44
import java.io.IOException;
5+
import java.io.InputStream;
56
import java.util.Collections;
67
import java.util.List;
78

@@ -30,33 +31,29 @@ public class GetEncryptedObjectPipeline {
3031

3132
private final S3Client _s3Client;
3233
private final CryptographicMaterialsManager _cryptoMaterialsManager;
33-
private final boolean _enableLegacyModes;
34+
private final boolean _enableLegacyUnauthenticatedModes;
35+
private final boolean _enableDelayedAuthentication;
3436

3537
public static Builder builder() { return new Builder(); }
3638

3739
private GetEncryptedObjectPipeline(Builder builder) {
3840
this._s3Client = builder._s3Client;
3941
this._cryptoMaterialsManager = builder._cryptoMaterialsManager;
40-
this._enableLegacyModes = builder._enableLegacyModes;
42+
this._enableLegacyUnauthenticatedModes = builder._enableLegacyUnauthenticatedModes;
43+
this._enableDelayedAuthentication = builder._enableDelayedAuthentication;
4144
}
4245

4346
public <T> T getObject(GetObjectRequest getObjectRequest,
4447
ResponseTransformer<GetObjectResponse, T> responseTransformer) {
4548
ResponseInputStream<GetObjectResponse> objectStream = _s3Client.getObject(
4649
getObjectRequest);
47-
byte[] ciphertext;
48-
try {
49-
ciphertext = IoUtils.toByteArray(objectStream);
50-
} catch (IOException e) {
51-
throw new RuntimeException(e);
52-
}
5350

5451
GetObjectResponse getObjectResponse = objectStream.response();
5552
ContentMetadata contentMetadata = ContentMetadataStrategy.decode(_s3Client, getObjectRequest, getObjectResponse);
5653

5754
AlgorithmSuite algorithmSuite = contentMetadata.algorithmSuite();
58-
if (!_enableLegacyModes && algorithmSuite.isLegacy()) {
59-
throw new S3EncryptionClientException("Enable legacy modes to use legacy content encryption: " + algorithmSuite.cipherName());
55+
if (!_enableLegacyUnauthenticatedModes && algorithmSuite.isLegacy()) {
56+
throw new S3EncryptionClientException("Enable legacy unauthenticated modes to use legacy content decryption: " + algorithmSuite.cipherName());
6057
}
6158

6259
List<EncryptedDataKey> encryptedDataKeys = Collections.singletonList(contentMetadata.encryptedDataKey());
@@ -66,33 +63,46 @@ public <T> T getObject(GetObjectRequest getObjectRequest,
6663
.algorithmSuite(algorithmSuite)
6764
.encryptedDataKeys(encryptedDataKeys)
6865
.encryptionContext(contentMetadata.encryptedDataKeyContext())
66+
.ciphertextLength(getObjectResponse.contentLength())
6967
.build();
7068

7169
DecryptionMaterials materials = _cryptoMaterialsManager.decryptMaterials(materialsRequest);
7270

73-
ContentDecryptionStrategy contentDecryptionStrategy = null;
74-
switch (algorithmSuite) {
75-
case ALG_AES_256_CBC_IV16_NO_KDF:
76-
contentDecryptionStrategy = AesCbcContentStrategy.builder().build();
77-
break;
78-
case ALG_AES_256_GCM_IV12_TAG16_NO_KDF:
79-
contentDecryptionStrategy = AesGcmContentStrategy.builder().build();
80-
break;
81-
}
82-
byte[] plaintext = contentDecryptionStrategy.decryptContent(contentMetadata, materials, ciphertext);
71+
ContentDecryptionStrategy contentDecryptionStrategy = selectContentDecryptionStrategy(materials);
72+
final InputStream plaintext = contentDecryptionStrategy.decryptContent(contentMetadata, materials, objectStream);
8373

8474
try {
8575
return responseTransformer.transform(getObjectResponse,
86-
AbortableInputStream.create(new ByteArrayInputStream(plaintext)));
76+
AbortableInputStream.create(plaintext));
8777
} catch (Exception e) {
8878
throw new S3EncryptionClientException("Unable to transform response.", e);
8979
}
9080
}
9181

82+
private ContentDecryptionStrategy selectContentDecryptionStrategy(final DecryptionMaterials materials) {
83+
switch (materials.algorithmSuite()) {
84+
case ALG_AES_256_CBC_IV16_NO_KDF:
85+
return AesCbcContentStrategy.builder().build();
86+
case ALG_AES_256_GCM_IV12_TAG16_NO_KDF:
87+
if (_enableDelayedAuthentication) {
88+
// TODO: Implement StreamingAesGcmContentStrategy
89+
throw new UnsupportedOperationException("Delayed Authentication mode using streaming AES-GCM decryption" +
90+
"is currently unsupported.");
91+
} else {
92+
return BufferedAesGcmContentStrategy.builder().build();
93+
}
94+
default:
95+
// This should never happen in practice.
96+
throw new S3EncryptionClientException(String.format("No content strategy available for algorithm suite:" +
97+
" %s", materials.algorithmSuite()));
98+
}
99+
}
100+
92101
public static class Builder {
93102
private S3Client _s3Client;
94103
private CryptographicMaterialsManager _cryptoMaterialsManager;
95-
private boolean _enableLegacyModes;
104+
private boolean _enableLegacyUnauthenticatedModes;
105+
private boolean _enableDelayedAuthentication;
96106

97107
private Builder() {}
98108

@@ -106,8 +116,13 @@ public Builder cryptoMaterialsManager(CryptographicMaterialsManager cryptoMateri
106116
return this;
107117
}
108118

109-
public Builder enableLegacyModes(boolean enableLegacyModes) {
110-
this._enableLegacyModes = enableLegacyModes;
119+
public Builder enableLegacyUnauthenticatedModes(boolean enableLegacyUnauthenticatedModes) {
120+
this._enableLegacyUnauthenticatedModes = enableLegacyUnauthenticatedModes;
121+
return this;
122+
}
123+
124+
public Builder enableDelayedAuthentication(boolean enableDelayedAuthentication) {
125+
this._enableDelayedAuthentication = enableDelayedAuthentication;
111126
return this;
112127
}
113128

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static class Builder {
5353
private CryptographicMaterialsManager _cryptoMaterialsManager;
5454
// Default to AesGcm since it is the only active (non-legacy) content encryption strategy
5555
private ContentEncryptionStrategy _contentEncryptionStrategy =
56-
AesGcmContentStrategy
56+
BufferedAesGcmContentStrategy
5757
.builder()
5858
.build();
5959
private ContentMetadataEncodingStrategy _contentMetadataEncodingStrategy = ContentMetadataStrategy.OBJECT_METADATA;

src/main/java/software/amazon/encryption/s3/legacy/internal/AesCbcContentStrategy.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package software.amazon.encryption.s3.legacy.internal;
22

3+
import java.io.ByteArrayInputStream;
4+
import java.io.IOException;
5+
import java.io.InputStream;
36
import java.security.InvalidAlgorithmParameterException;
47
import java.security.InvalidKeyException;
58
import java.security.NoSuchAlgorithmException;
@@ -10,6 +13,8 @@
1013
import javax.crypto.SecretKey;
1114
import javax.crypto.spec.IvParameterSpec;
1215
import javax.crypto.spec.SecretKeySpec;
16+
17+
import software.amazon.awssdk.utils.IoUtils;
1318
import software.amazon.encryption.s3.S3EncryptionClientException;
1419
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
1520
import software.amazon.encryption.s3.internal.ContentDecryptionStrategy;
@@ -26,7 +31,16 @@ private AesCbcContentStrategy(Builder builder) {}
2631
public static Builder builder() { return new Builder(); }
2732

2833
@Override
29-
public byte[] decryptContent(ContentMetadata contentMetadata, DecryptionMaterials materials, byte[] ciphertext) {
34+
public InputStream decryptContent(ContentMetadata contentMetadata, DecryptionMaterials materials,
35+
InputStream ciphertextStream) {
36+
// TODO: AES-CBC should always use a stream cipher.
37+
byte[] ciphertext;
38+
try {
39+
ciphertext = IoUtils.toByteArray(ciphertextStream);
40+
} catch (IOException e) {
41+
throw new RuntimeException(e);
42+
}
43+
3044
AlgorithmSuite algorithmSuite = contentMetadata.algorithmSuite();
3145
SecretKey contentKey = new SecretKeySpec(materials.plaintextDataKey(), algorithmSuite.dataKeyAlgorithm());
3246
byte[] iv = contentMetadata.contentNonce();
@@ -45,7 +59,7 @@ public byte[] decryptContent(ContentMetadata contentMetadata, DecryptionMaterial
4559
throw new S3EncryptionClientException("Unable to " + algorithmSuite.cipherName() + " content decrypt.", e);
4660
}
4761

48-
return plaintext;
62+
return new ByteArrayInputStream(plaintext);
4963
}
5064

5165
public static class Builder {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ public class DecryptMaterialsRequest {
1212
private final AlgorithmSuite _algorithmSuite;
1313
private final List<EncryptedDataKey> _encryptedDataKeys;
1414
private final Map<String, String> _encryptionContext;
15+
private final long _ciphertextLength;
1516

1617
private DecryptMaterialsRequest(Builder builder) {
1718
this._s3Request = builder._s3Request;
1819
this._algorithmSuite = builder._algorithmSuite;
1920
this._encryptedDataKeys = builder._encryptedDataKeys;
2021
this._encryptionContext = builder._encryptionContext;
22+
this._ciphertextLength = builder._ciphertextLength;
2123
}
2224

2325
static public Builder builder() {
@@ -40,12 +42,17 @@ public Map<String, String> encryptionContext() {
4042
return _encryptionContext;
4143
}
4244

45+
public long ciphertextLength() {
46+
return _ciphertextLength;
47+
}
48+
4349
static public class Builder {
4450

4551
public GetObjectRequest _s3Request = null;
4652
private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;
4753
private Map<String, String> _encryptionContext = Collections.emptyMap();
4854
private List<EncryptedDataKey> _encryptedDataKeys = Collections.emptyList();
55+
private long _ciphertextLength = -1;
4956

5057
private Builder() {
5158
}
@@ -74,6 +81,11 @@ public Builder encryptedDataKeys(List<EncryptedDataKey> encryptedDataKeys) {
7481
return this;
7582
}
7683

84+
public Builder ciphertextLength(long ciphertextLength) {
85+
_ciphertextLength = ciphertextLength;
86+
return this;
87+
}
88+
7789
public DecryptMaterialsRequest build() {
7890
return new DecryptMaterialsRequest(this);
7991
}

0 commit comments

Comments
 (0)