Skip to content

Commit 65331fb

Browse files
authored
feat: Implement Ranged-Get (#31)
- Implement Ranged-get for AES/CBC/PKCS5Padding - Implement Ranged-get for AES/CTR/NoPadding - Add test cases for ranged-gets
1 parent 30cf9b1 commit 65331fb

File tree

13 files changed

+717
-68
lines changed

13 files changed

+717
-68
lines changed
Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
package software.amazon.encryption.s3.algorithms;
22

33
class AlgorithmConstants {
4-
// Maximum length of the content that can be encrypted in GCM mode.
5-
static final long GCM_MAX_CONTENT_LENGTH_BITS = (1L<<39) - 256;
4+
/**
5+
* The maximum number of 16-byte blocks that can be encrypted with a
6+
* GCM cipher. Note the maximum bit-length of the plaintext is (2^39 - 256),
7+
* which translates to a maximum byte-length of (2^36 - 32), which in turn
8+
* translates to a maximum block-length of (2^32 - 2).
9+
* <p>
10+
* Reference: <a href="http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf">
11+
* NIST Special Publication 800-38D.</a>.
12+
*/
13+
static final long GCM_MAX_CONTENT_LENGTH_BITS = (1L << 39) - 256;
614

7-
// Maximum length of the content that can be encrypted in CBC mode.
8-
static final long CBC_MAX_CONTENT_LENGTH_BYTES = (1L<<55);
15+
/**
16+
* The Maximum length of the content that can be encrypted in CBC mode.
17+
*/
18+
static final long CBC_MAX_CONTENT_LENGTH_BYTES = (1L << 55);
19+
20+
/**
21+
* The maximum number of bytes that can be securely encrypted per a single key using AES/CTR.
22+
*/
23+
static final long CTR_MAX_CONTENT_LENGTH_BYTES = -1;
924
}

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

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

3-
43
public enum AlgorithmSuite {
54
ALG_AES_256_GCM_IV12_TAG16_NO_KDF(0x0078,
65
false,
@@ -11,6 +10,15 @@ public enum AlgorithmSuite {
1110
96,
1211
128,
1312
AlgorithmConstants.GCM_MAX_CONTENT_LENGTH_BITS),
13+
ALG_AES_256_CTR_IV16_TAG16_NO_KDF(0x0074,
14+
true,
15+
"AES",
16+
256,
17+
"AES/CTR/NoPadding",
18+
128,
19+
128,
20+
128,
21+
AlgorithmConstants.CTR_MAX_CONTENT_LENGTH_BYTES),
1422
ALG_AES_256_CBC_IV16_NO_KDF(0x0070,
1523
true,
1624
"AES",
@@ -32,14 +40,14 @@ public enum AlgorithmSuite {
3240
private long _cipherMaxContentLengthBits;
3341

3442
AlgorithmSuite(int id,
35-
boolean isLegacy,
36-
String dataKeyAlgorithm,
37-
int dataKeyLengthBits,
38-
String cipherName,
39-
int cipherBlockSizeBits,
40-
int cipherNonceLengthBits,
41-
int cipherTagLengthBits,
42-
long cipherMaxContentLengthBits
43+
boolean isLegacy,
44+
String dataKeyAlgorithm,
45+
int dataKeyLengthBits,
46+
String cipherName,
47+
int cipherBlockSizeBits,
48+
int cipherNonceLengthBits,
49+
int cipherTagLengthBits,
50+
long cipherMaxContentLengthBits
4351
) {
4452
this._id = id;
4553
this._isLegacy = isLegacy;
@@ -79,4 +87,12 @@ public int cipherTagLengthBits() {
7987
public int nonceLengthBytes() {
8088
return _cipherNonceLengthBits / 8;
8189
}
90+
91+
public int cipherBlockSizeBytes() {
92+
return _cipherBlockSizeBits / 8;
93+
}
94+
95+
public long cipherMaxContentLengthBits() {
96+
return _cipherMaxContentLengthBits;
97+
}
8298
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ private BufferedAesGcmContentStrategy(Builder builder) {
3939
this._secureRandom = builder._secureRandom;
4040
}
4141

42-
public static Builder builder() { return new Builder(); }
42+
public static Builder builder() {
43+
return new Builder();
44+
}
4345

4446
@Override
4547
public EncryptedContent encryptContent(EncryptionMaterials materials, byte[] content) {
@@ -117,7 +119,8 @@ public InputStream decryptContent(ContentMetadata contentMetadata, DecryptionMat
117119
public static class Builder {
118120
private SecureRandom _secureRandom = new SecureRandom();
119121

120-
private Builder() {}
122+
private Builder() {
123+
}
121124

122125
/**
123126
* Note that this does NOT create a defensive copy of the SecureRandom object. Any modifications to the

src/main/java/software/amazon/encryption/s3/internal/CbcCipherInputStream.java renamed to src/main/java/software/amazon/encryption/s3/internal/CipherInputStream.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
import java.io.InputStream;
1010

1111
/**
12-
* A cipher stream for decrypting CBC encrypted data. There is nothing particularly
13-
* specific to CBC, but other algorithms may require additional considerations.
12+
* A cipher stream for encrypting or decrypting data using an unauthenticated block cipher.
1413
*/
15-
public class CbcCipherInputStream extends SdkFilterInputStream {
14+
public class CipherInputStream extends SdkFilterInputStream {
1615
private static final int MAX_RETRY_COUNT = 1000;
1716
private static final int DEFAULT_IN_BUFFER_SIZE = 512;
1817
private final Cipher cipher;
@@ -23,7 +22,7 @@ public class CbcCipherInputStream extends SdkFilterInputStream {
2322
private int currentPosition;
2423
private int maxPosition;
2524

26-
public CbcCipherInputStream(InputStream inputStream, Cipher cipher) {
25+
public CipherInputStream(InputStream inputStream, Cipher cipher) {
2726
super(inputStream);
2827
this.cipher = cipher;
2928
this.inputBuffer = new byte[DEFAULT_IN_BUFFER_SIZE];
@@ -143,9 +142,8 @@ public void reset() throws IOException {
143142
* Reads and process the next chunk of data into memory.
144143
*
145144
* @return the length of the data chunk read and processed, or -1 if end of
146-
* stream.
147-
* @throws IOException
148-
* if there is an IO exception from the underlying input stream
145+
* stream.
146+
* @throws IOException if there is an IO exception from the underlying input stream
149147
*/
150148
private int nextChunk() throws IOException {
151149
abortIfNeeded();

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class ContentMetadata {
1919
private final byte[] _contentNonce;
2020
private final String _contentCipher;
2121
private final String _contentCipherTagLength;
22+
private final String _contentRange;
2223

2324
private ContentMetadata(Builder builder) {
2425
_algorithmSuite = builder._algorithmSuite;
@@ -30,6 +31,7 @@ private ContentMetadata(Builder builder) {
3031
_contentNonce = builder._contentNonce;
3132
_contentCipher = builder._contentCipher;
3233
_contentCipherTagLength = builder._contentCipherTagLength;
34+
_contentRange = builder._contentRange;
3335
}
3436

3537
public static Builder builder() {
@@ -73,6 +75,10 @@ public String contentCipherTagLength() {
7375
return _contentCipherTagLength;
7476
}
7577

78+
public String contentRange() {
79+
return _contentRange;
80+
}
81+
7682
public static class Builder {
7783
private AlgorithmSuite _algorithmSuite;
7884

@@ -83,8 +89,9 @@ public static class Builder {
8389
private byte[] _contentNonce;
8490
private String _contentCipher;
8591
private String _contentCipherTagLength;
92+
public String _contentRange;
8693

87-
private Builder () {
94+
private Builder() {
8895

8996
}
9097

@@ -113,8 +120,14 @@ public Builder contentNonce(byte[] contentNonce) {
113120
return this;
114121
}
115122

123+
public Builder contentRange(String contentRange) {
124+
_contentRange = contentRange;
125+
return this;
126+
}
116127

117-
public ContentMetadata build() { return new ContentMetadata(this); }
128+
public ContentMetadata build() {
129+
return new ContentMetadata(this);
130+
}
118131
}
119132

120133
}

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

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

3-
import java.nio.charset.StandardCharsets;
4-
import java.util.Base64;
5-
import java.util.HashMap;
6-
import java.util.Map;
7-
import java.util.Map.Entry;
83
import software.amazon.awssdk.core.ResponseInputStream;
94
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
105
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
@@ -20,6 +15,12 @@
2015
import software.amazon.encryption.s3.materials.EncryptionMaterials;
2116
import software.amazon.encryption.s3.materials.S3Keyring;
2217

18+
import java.nio.charset.StandardCharsets;
19+
import java.util.Base64;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
import java.util.Map.Entry;
23+
2324
public abstract class ContentMetadataStrategy implements ContentMetadataEncodingStrategy, ContentMetadataDecodingStrategy {
2425

2526
private static final Base64.Encoder ENCODER = Base64.getEncoder();
@@ -44,17 +45,16 @@ public ContentMetadata decodeMetadata(S3Client client, GetObjectRequest getObjec
4445
for (Map.Entry<String, JsonNode> entry : objectNode.asObject().entrySet()) {
4546
metadata.put(entry.getKey(), entry.getValue().asString());
4647
}
47-
48-
return ContentMetadataStrategy.readFromMap(metadata);
48+
return ContentMetadataStrategy.readFromMap(metadata, response);
4949
}
5050
};
5151

5252
public static final ContentMetadataStrategy OBJECT_METADATA = new ContentMetadataStrategy() {
5353

5454
@Override
5555
public PutObjectRequest encodeMetadata(EncryptionMaterials materials,
56-
EncryptedContent encryptedContent, PutObjectRequest request) {
57-
Map<String,String> metadata = new HashMap<>(request.metadata());
56+
EncryptedContent encryptedContent, PutObjectRequest request) {
57+
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()));
6060
metadata.put(MetadataKeyConstants.CONTENT_NONCE, ENCODER.encodeToString(encryptedContent.nonce));
@@ -64,7 +64,7 @@ public PutObjectRequest encodeMetadata(EncryptionMaterials materials,
6464

6565
try (JsonWriter jsonWriter = JsonWriter.create()) {
6666
jsonWriter.writeStartObject();
67-
for (Entry<String,String> entry : materials.encryptionContext().entrySet()) {
67+
for (Entry<String, String> entry : materials.encryptionContext().entrySet()) {
6868
jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue());
6969
}
7070
jsonWriter.writeEndObject();
@@ -80,19 +80,20 @@ public PutObjectRequest encodeMetadata(EncryptionMaterials materials,
8080

8181
@Override
8282
public ContentMetadata decodeMetadata(S3Client client, GetObjectRequest request, GetObjectResponse response) {
83-
return ContentMetadataStrategy.readFromMap(response.metadata());
83+
return ContentMetadataStrategy.readFromMap(response.metadata(), response);
8484
}
8585
};
8686

87-
private static ContentMetadata readFromMap(Map<String, String> metadata) {
87+
private static ContentMetadata readFromMap(Map<String, String> metadata, GetObjectResponse response) {
8888
// Get algorithm suite
8989
final String contentEncryptionAlgorithm = metadata.get(MetadataKeyConstants.CONTENT_CIPHER);
9090
AlgorithmSuite algorithmSuite;
91+
String contentRange = response.contentRange();
9192
if (contentEncryptionAlgorithm == null
9293
|| contentEncryptionAlgorithm.equals(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF.cipherName())) {
9394
algorithmSuite = AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF;
9495
} else if (contentEncryptionAlgorithm.equals(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName())) {
95-
algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;
96+
algorithmSuite = (contentRange == null ) ? AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF : AlgorithmSuite.ALG_AES_256_CTR_IV16_TAG16_NO_KDF;
9697
} else {
9798
throw new S3EncryptionClientException(
9899
"Unknown content encryption algorithm: " + contentEncryptionAlgorithm);
@@ -115,6 +116,7 @@ private static ContentMetadata readFromMap(Map<String, String> metadata) {
115116

116117
break;
117118
case ALG_AES_256_GCM_IV12_TAG16_NO_KDF:
119+
case ALG_AES_256_CTR_IV16_TAG16_NO_KDF:
118120
// Check tag length
119121
final int tagLength = Integer.parseInt(metadata.get(MetadataKeyConstants.CONTENT_CIPHER_TAG_LENGTH));
120122
if (tagLength != algorithmSuite.cipherTagLengthBits()) {
@@ -162,15 +164,16 @@ private static ContentMetadata readFromMap(Map<String, String> metadata) {
162164
.encryptedDataKey(edk)
163165
.encryptedDataKeyContext(encryptionContext)
164166
.contentNonce(nonce)
167+
.contentRange(contentRange)
165168
.build();
166169
}
167170

168171
public static ContentMetadata decode(S3Client client, GetObjectRequest request, GetObjectResponse response) {
169172
Map<String, String> metadata = response.metadata();
170173
ContentMetadataDecodingStrategy strategy;
171174
if (metadata != null
172-
&& metadata.containsKey(MetadataKeyConstants.CONTENT_NONCE)
173-
&& (metadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1)
175+
&& metadata.containsKey(MetadataKeyConstants.CONTENT_NONCE)
176+
&& (metadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V1)
174177
|| metadata.containsKey(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2))) {
175178
strategy = OBJECT_METADATA;
176179
} else {

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

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,25 @@
22

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

5-
import java.io.ByteArrayInputStream;
6-
import java.io.IOException;
7-
import java.io.InputStream;
8-
import java.util.Collections;
9-
import java.util.List;
10-
115
import software.amazon.awssdk.core.ResponseInputStream;
126
import software.amazon.awssdk.core.sync.ResponseTransformer;
137
import software.amazon.awssdk.http.AbortableInputStream;
148
import software.amazon.awssdk.services.s3.S3Client;
159
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
1610
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
17-
import software.amazon.awssdk.utils.IoUtils;
1811
import software.amazon.encryption.s3.S3EncryptionClientException;
1912
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
20-
import software.amazon.encryption.s3.legacy.internal.AesCbcContentStrategy;
13+
import software.amazon.encryption.s3.legacy.internal.RangedGetUtils;
14+
import software.amazon.encryption.s3.legacy.internal.UnauthenticatedContentStrategy;
2115
import software.amazon.encryption.s3.materials.CryptographicMaterialsManager;
2216
import software.amazon.encryption.s3.materials.DecryptMaterialsRequest;
2317
import software.amazon.encryption.s3.materials.DecryptionMaterials;
2418
import software.amazon.encryption.s3.materials.EncryptedDataKey;
2519

20+
import java.io.InputStream;
21+
import java.util.Collections;
22+
import java.util.List;
23+
2624
/**
2725
* This class will determine the necessary mechanisms to decrypt objects returned from S3.
2826
* Due to supporting various legacy modes, this is not a predefined pipeline like
@@ -36,7 +34,9 @@ public class GetEncryptedObjectPipeline {
3634
private final boolean _enableLegacyUnauthenticatedModes;
3735
private final boolean _enableDelayedAuthentication;
3836

39-
public static Builder builder() { return new Builder(); }
37+
public static Builder builder() {
38+
return new Builder();
39+
}
4040

4141
private GetEncryptedObjectPipeline(Builder builder) {
4242
this._s3Client = builder._s3Client;
@@ -46,9 +46,15 @@ private GetEncryptedObjectPipeline(Builder builder) {
4646
}
4747

4848
public <T> T getObject(GetObjectRequest getObjectRequest,
49-
ResponseTransformer<GetObjectResponse, T> responseTransformer) {
50-
ResponseInputStream<GetObjectResponse> objectStream = _s3Client.getObject(
51-
getObjectRequest);
49+
ResponseTransformer<GetObjectResponse, T> responseTransformer) {
50+
ResponseInputStream<GetObjectResponse> objectStream;
51+
if (!_enableLegacyUnauthenticatedModes && getObjectRequest.range() != null) {
52+
throw new S3EncryptionClientException("Enable legacy unauthenticated modes to use Ranged Get.");
53+
}
54+
objectStream = _s3Client.getObject(getObjectRequest
55+
.toBuilder()
56+
.range(RangedGetUtils.getCryptoRangeAsString(getObjectRequest.range()))
57+
.build());
5258

5359
GetObjectResponse getObjectResponse = objectStream.response();
5460
ContentMetadata contentMetadata = ContentMetadataStrategy.decode(_s3Client, getObjectRequest, getObjectResponse);
@@ -84,7 +90,8 @@ public <T> T getObject(GetObjectRequest getObjectRequest,
8490
private ContentDecryptionStrategy selectContentDecryptionStrategy(final DecryptionMaterials materials) {
8591
switch (materials.algorithmSuite()) {
8692
case ALG_AES_256_CBC_IV16_NO_KDF:
87-
return AesCbcContentStrategy.builder().build();
93+
case ALG_AES_256_CTR_IV16_TAG16_NO_KDF:
94+
return UnauthenticatedContentStrategy.builder().build();
8895
case ALG_AES_256_GCM_IV12_TAG16_NO_KDF:
8996
if (_enableDelayedAuthentication) {
9097
// TODO: Implement StreamingAesGcmContentStrategy
@@ -106,7 +113,8 @@ public static class Builder {
106113
private boolean _enableLegacyUnauthenticatedModes;
107114
private boolean _enableDelayedAuthentication;
108115

109-
private Builder() {}
116+
private Builder() {
117+
}
110118

111119
/**
112120
* Note that this does NOT create a defensive clone of S3Client. Any modifications made to the wrapped

0 commit comments

Comments
 (0)