Skip to content

Commit d0bcd38

Browse files
authored
feat: implement AES-GCM streaming (#45)
Adds AES-GCM streaming implementation for putObject/encrypt and getObject/decrypt (in delayed auth mode).
1 parent e576378 commit d0bcd38

19 files changed

+342
-141
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ public S3EncryptionClient build() {
288288
if (_cryptoMaterialsManager == null) {
289289
_cryptoMaterialsManager = DefaultCryptoMaterialsManager.builder()
290290
.keyring(_keyring)
291-
.cryptoPovider(_cryptoProvider)
291+
.cryptoProvider(_cryptoProvider)
292292
.build();
293293
}
294294

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package software.amazon.encryption.s3;
2+
3+
public class S3EncryptionClientSecurityException extends S3EncryptionClientException {
4+
5+
public S3EncryptionClientSecurityException(String message) {
6+
super(message);
7+
}
8+
9+
public S3EncryptionClientSecurityException(String message, Throwable cause) {
10+
super(message, cause);
11+
}
12+
}

src/main/java/software/amazon/encryption/s3/algorithms/AlgorithmSuite.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ public int cipherTagLengthBits() {
8484
return _cipherTagLengthBits;
8585
}
8686

87+
public int cipherTagLengthBytes() {
88+
return _cipherTagLengthBits / 8;
89+
}
90+
8791
public int nonceLengthBytes() {
8892
return _cipherNonceLengthBits / 8;
8993
}
@@ -95,4 +99,8 @@ public int cipherBlockSizeBytes() {
9599
public long cipherMaxContentLengthBits() {
96100
return _cipherMaxContentLengthBits;
97101
}
102+
103+
public long cipherMaxContentLengthBytes() {
104+
return _cipherMaxContentLengthBits / 8;
105+
}
98106
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package software.amazon.encryption.s3.internal;
2+
3+
import software.amazon.encryption.s3.S3EncryptionClientSecurityException;
4+
5+
import javax.crypto.Cipher;
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.security.GeneralSecurityException;
9+
10+
public class AuthenticatedCipherInputStream extends CipherInputStream {
11+
12+
public AuthenticatedCipherInputStream(InputStream inputStream, Cipher cipher) {
13+
super(inputStream, cipher);
14+
}
15+
16+
/**
17+
* Authenticated ciphers call doFinal upon the last read,
18+
* there is no need to do so upon close.
19+
* @throws IOException from the wrapped InputStream
20+
*/
21+
@Override
22+
public void close() throws IOException {
23+
in.close();
24+
currentPosition = maxPosition = 0;
25+
abortIfNeeded();
26+
}
27+
28+
@Override
29+
protected int endOfFileReached() {
30+
eofReached = true;
31+
try {
32+
outputBuffer = cipher.doFinal();
33+
if (outputBuffer == null) {
34+
return -1;
35+
}
36+
currentPosition = 0;
37+
return maxPosition = outputBuffer.length;
38+
} catch (GeneralSecurityException exception) {
39+
// In an authenticated scheme, this indicates a security
40+
// exception
41+
throw new S3EncryptionClientSecurityException(exception.getMessage(), exception);
42+
}
43+
}
44+
}

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

Lines changed: 3 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package software.amazon.encryption.s3.internal;
22

3-
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
43
import software.amazon.awssdk.utils.IoUtils;
54
import software.amazon.encryption.s3.S3EncryptionClientException;
65
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
76
import software.amazon.encryption.s3.materials.DecryptionMaterials;
8-
import software.amazon.encryption.s3.materials.EncryptionMaterials;
97

108
import javax.crypto.Cipher;
119
import javax.crypto.SecretKey;
@@ -15,52 +13,24 @@
1513
import java.io.IOException;
1614
import java.io.InputStream;
1715
import java.security.GeneralSecurityException;
18-
import java.security.SecureRandom;
1916

2017
/**
21-
* This class will encrypt data according to the algorithm suite constants
18+
* This class will decrypt AES-GCM encrypted data by buffering the ciphertext
19+
* stream into memory. This prevents release of unauthenticated plaintext.
2220
*/
23-
public class BufferedAesGcmContentStrategy implements ContentEncryptionStrategy, ContentDecryptionStrategy {
21+
public class BufferedAesGcmContentStrategy implements ContentDecryptionStrategy {
2422

2523
// 64MiB ought to be enough for most usecases
2624
private static final long BUFFERED_MAX_CONTENT_LENGTH_MiB = 64;
2725
private static final long BUFFERED_MAX_CONTENT_LENGTH_BYTES = 1024 * 1024 * BUFFERED_MAX_CONTENT_LENGTH_MiB;
2826

29-
final private SecureRandom _secureRandom;
30-
3127
private BufferedAesGcmContentStrategy(Builder builder) {
32-
this._secureRandom = builder._secureRandom;
3328
}
3429

3530
public static Builder builder() {
3631
return new Builder();
3732
}
3833

39-
@Override
40-
public EncryptedContent encryptContent(EncryptionMaterials materials, byte[] content) {
41-
final AlgorithmSuite algorithmSuite = materials.algorithmSuite();
42-
43-
final byte[] nonce = new byte[algorithmSuite.nonceLengthBytes()];
44-
_secureRandom.nextBytes(nonce);
45-
46-
final String cipherName = algorithmSuite.cipherName();
47-
try {
48-
final Cipher cipher = CryptoFactory.createCipher(algorithmSuite.cipherName(), materials.cryptoProvider());
49-
50-
cipher.init(Cipher.ENCRYPT_MODE,
51-
materials.dataKey(),
52-
new GCMParameterSpec(algorithmSuite.cipherTagLengthBits(), nonce));
53-
54-
EncryptedContent result = new EncryptedContent();
55-
result.nonce = nonce;
56-
result.ciphertext = cipher.doFinal(content);
57-
58-
return result;
59-
} catch (GeneralSecurityException e) {
60-
throw new S3EncryptionClientException("Unable to " + cipherName + " content encrypt.", e);
61-
}
62-
}
63-
6434
@Override
6535
public InputStream decryptContent(ContentMetadata contentMetadata, DecryptionMaterials materials,
6636
InputStream ciphertextStream) {
@@ -100,24 +70,10 @@ public InputStream decryptContent(ContentMetadata contentMetadata, DecryptionMat
10070
}
10171

10272
public static class Builder {
103-
private SecureRandom _secureRandom;
10473

10574
private Builder() {
10675
}
10776

108-
/**
109-
* Note that this does NOT create a defensive copy of the SecureRandom object. Any modifications to the
110-
* object will be reflected in this Builder.
111-
*/
112-
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
113-
public Builder secureRandom(SecureRandom secureRandom) {
114-
if (secureRandom == null) {
115-
throw new S3EncryptionClientException("SecureRandom provided to BufferedAesGcmContentStrategy cannot be null");
116-
}
117-
_secureRandom = secureRandom;
118-
return this;
119-
}
120-
12177
public BufferedAesGcmContentStrategy build() {
12278
return new BufferedAesGcmContentStrategy(this);
12379
}

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

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
public class CipherInputStream extends SdkFilterInputStream {
1515
private static final int MAX_RETRY_COUNT = 1000;
1616
private static final int DEFAULT_IN_BUFFER_SIZE = 512;
17-
private final Cipher cipher;
17+
protected final Cipher cipher;
1818

19-
private boolean eofReached;
20-
private byte[] inputBuffer;
21-
private byte[] outputBuffer;
22-
private int currentPosition;
23-
private int maxPosition;
19+
protected boolean eofReached;
20+
protected byte[] inputBuffer;
21+
protected byte[] outputBuffer;
22+
protected int currentPosition;
23+
protected int maxPosition;
2424

2525
public CipherInputStream(InputStream inputStream, Cipher cipher) {
2626
super(inputStream);
@@ -121,6 +121,8 @@ public void close() throws IOException {
121121
} catch (BadPaddingException | IllegalBlockSizeException ex) {
122122
// Swallow the exception
123123
}
124+
currentPosition = maxPosition = 0;
125+
abortIfNeeded();
124126
}
125127

126128
@Override
@@ -145,29 +147,34 @@ public void reset() throws IOException {
145147
* stream.
146148
* @throws IOException if there is an IO exception from the underlying input stream
147149
*/
148-
private int nextChunk() throws IOException {
150+
protected int nextChunk() throws IOException {
149151
abortIfNeeded();
150152
if (eofReached) {
151153
return -1;
152154
}
153155
outputBuffer = null;
154156
int length = in.read(inputBuffer);
155157
if (length == -1) {
156-
eofReached = true;
157-
try {
158-
outputBuffer = cipher.doFinal();
159-
if (outputBuffer == null) {
160-
return -1;
161-
}
162-
currentPosition = 0;
163-
return maxPosition = outputBuffer.length;
164-
} catch (IllegalBlockSizeException | BadPaddingException ignore) {
165-
// Swallow exceptions
166-
}
167-
return -1;
158+
return endOfFileReached();
168159
}
169160
outputBuffer = cipher.update(inputBuffer, 0, length);
170161
currentPosition = 0;
171162
return maxPosition = (outputBuffer == null ? 0 : outputBuffer.length);
172163
}
164+
165+
protected int endOfFileReached() {
166+
eofReached = true;
167+
try {
168+
outputBuffer = cipher.doFinal();
169+
if (outputBuffer == null) {
170+
return -1;
171+
}
172+
currentPosition = 0;
173+
return maxPosition = outputBuffer.length;
174+
} catch (IllegalBlockSizeException | BadPaddingException ignore) {
175+
// Swallow exceptions
176+
}
177+
return -1;
178+
179+
}
173180
}

src/main/java/software/amazon/encryption/s3/internal/ContentEncryptionStrategy.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.EncryptionMaterials;
44

5+
import java.io.InputStream;
6+
57
@FunctionalInterface
68
public interface ContentEncryptionStrategy {
7-
EncryptedContent encryptContent(EncryptionMaterials materials, byte[] content);
9+
EncryptedContent encryptContent(EncryptionMaterials materials, InputStream content);
810
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public PutObjectRequest encodeMetadata(EncryptionMaterials materials,
5757
Map<String, String> metadata = new HashMap<>(request.metadata());
5858
EncryptedDataKey edk = materials.encryptedDataKeys().get(0);
5959
metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2, ENCODER.encodeToString(edk.encryptedDatakey()));
60-
metadata.put(MetadataKeyConstants.CONTENT_NONCE, ENCODER.encodeToString(encryptedContent.nonce));
60+
metadata.put(MetadataKeyConstants.CONTENT_NONCE, ENCODER.encodeToString(encryptedContent.getNonce()));
6161
metadata.put(MetadataKeyConstants.CONTENT_CIPHER, materials.algorithmSuite().cipherName());
6262
metadata.put(MetadataKeyConstants.CONTENT_CIPHER_TAG_LENGTH, Integer.toString(materials.algorithmSuite().cipherTagLengthBits()));
6363
metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM, new String(edk.keyProviderInfo(), StandardCharsets.UTF_8));
Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
11
package software.amazon.encryption.s3.internal;
22

3+
import java.io.InputStream;
4+
35
public class EncryptedContent {
46

5-
public byte[] ciphertext;
6-
public byte[] nonce;
7+
private InputStream _ciphertext;
8+
private long _ciphertextLength;
9+
private byte[] _nonce;
10+
public EncryptedContent(final byte[] nonce, final InputStream ciphertext, final long ciphertextLength) {
11+
_nonce = nonce;
12+
_ciphertext = ciphertext;
13+
_ciphertextLength = ciphertextLength;
14+
}
15+
16+
public byte[] getNonce() {
17+
return _nonce;
18+
}
19+
20+
public InputStream getCiphertext() {
21+
return _ciphertext;
22+
}
23+
24+
public long getCiphertextLength() {
25+
return _ciphertextLength;
26+
}
27+
728
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,7 @@ private ContentDecryptionStrategy selectContentDecryptionStrategy(final Decrypti
9494
return UnauthenticatedContentStrategy.builder().build();
9595
case ALG_AES_256_GCM_IV12_TAG16_NO_KDF:
9696
if (_enableDelayedAuthentication) {
97-
// TODO: Implement StreamingAesGcmContentStrategy
98-
throw new UnsupportedOperationException("Delayed Authentication mode using streaming AES-GCM decryption" +
99-
"is currently unsupported.");
97+
return StreamingAesGcmContentStrategy.builder().build();
10098
} else {
10199
return BufferedAesGcmContentStrategy.builder().build();
102100
}

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

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22

33
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
44

5-
import java.io.IOException;
65
import java.security.SecureRandom;
76

87
import software.amazon.awssdk.core.sync.RequestBody;
98
import software.amazon.awssdk.services.s3.S3Client;
109
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
1110
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
12-
import software.amazon.awssdk.utils.IoUtils;
13-
import software.amazon.encryption.s3.S3EncryptionClientException;
1411
import software.amazon.encryption.s3.materials.EncryptionMaterials;
1512
import software.amazon.encryption.s3.materials.EncryptionMaterialsRequest;
1613
import software.amazon.encryption.s3.materials.CryptographicMaterialsManager;
@@ -19,7 +16,6 @@ public class PutEncryptedObjectPipeline {
1916

2017
final private S3Client _s3Client;
2118
final private CryptographicMaterialsManager _cryptoMaterialsManager;
22-
final private SecureRandom _secureRandom;
2319
final private ContentEncryptionStrategy _contentEncryptionStrategy;
2420
final private ContentMetadataEncodingStrategy _contentMetadataEncodingStrategy;
2521

@@ -28,29 +24,22 @@ public class PutEncryptedObjectPipeline {
2824
private PutEncryptedObjectPipeline(Builder builder) {
2925
this._s3Client = builder._s3Client;
3026
this._cryptoMaterialsManager = builder._cryptoMaterialsManager;
31-
this._secureRandom = builder._secureRandom;
3227
this._contentEncryptionStrategy = builder._contentEncryptionStrategy;
3328
this._contentMetadataEncodingStrategy = builder._contentMetadataEncodingStrategy;
3429
}
3530

3631
public PutObjectResponse putObject(PutObjectRequest request, RequestBody requestBody) {
3732
EncryptionMaterialsRequest.Builder requestBuilder = EncryptionMaterialsRequest.builder()
38-
.s3Request(request);
33+
.s3Request(request)
34+
.plaintextLength(requestBody.optionalContentLength().orElse(-1L));
3935

4036
EncryptionMaterials materials = _cryptoMaterialsManager.getEncryptionMaterials(requestBuilder.build());
4137

42-
byte[] input;
43-
try {
44-
// TODO: this needs to be a stream and not a byte[]
45-
input = IoUtils.toByteArray(requestBody.contentStreamProvider().newStream());
46-
} catch (IOException e) {
47-
throw new S3EncryptionClientException("Cannot read input.", e);
48-
}
49-
EncryptedContent encryptedContent = _contentEncryptionStrategy.encryptContent(materials, input);
38+
EncryptedContent encryptedContent = _contentEncryptionStrategy.encryptContent(materials, requestBody.contentStreamProvider().newStream());
5039

5140
request = _contentMetadataEncodingStrategy.encodeMetadata(materials, encryptedContent, request);
5241

53-
return _s3Client.putObject(request, RequestBody.fromBytes(encryptedContent.ciphertext));
42+
return _s3Client.putObject(request, RequestBody.fromInputStream(encryptedContent.getCiphertext(), encryptedContent.getCiphertextLength()));
5443
}
5544

5645
public static class Builder {
@@ -86,10 +75,10 @@ public Builder secureRandom(SecureRandom secureRandom) {
8675
public PutEncryptedObjectPipeline build() {
8776
// Default to AesGcm since it is the only active (non-legacy) content encryption strategy
8877
if (_contentEncryptionStrategy == null) {
89-
_contentEncryptionStrategy = BufferedAesGcmContentStrategy
90-
.builder()
91-
.secureRandom(_secureRandom)
92-
.build();
78+
_contentEncryptionStrategy = StreamingAesGcmContentStrategy
79+
.builder()
80+
.secureRandom(_secureRandom)
81+
.build();
9382
}
9483
return new PutEncryptedObjectPipeline(this);
9584
}

0 commit comments

Comments
 (0)