Skip to content

Commit 4fb7793

Browse files
authored
Merge pull request #4 from smswz/legacy-modes
Add AESWrap legacy keyring and migration examples
2 parents 113b784 + 549e657 commit 4fb7793

File tree

11 files changed

+634
-90
lines changed

11 files changed

+634
-90
lines changed

README.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,141 @@
22

33
This library provides an S3 client that supports client-side encryption.
44

5+
## Migration
6+
7+
This version of the library supports reading encrypted objects from previous versions.
8+
It also supports writing objects with non-legacy algorithms.
9+
The list of legacy modes and operations will be provided below.
10+
11+
### Examples
12+
#### V2 KMS Materials Provider to V3 KMS w/ Context Materials Manager and Keyring
13+
```java
14+
class Example {
15+
public static void main(String[] args) {
16+
// V2
17+
EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_WRAPPING_KEY_ID);
18+
AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
19+
.withEncryptionMaterialsProvider(materialsProvider)
20+
.build();
21+
22+
// V3
23+
Keyring keyring = KmsContextKeyring.builder()
24+
.wrappingKeyId(KMS_WRAPPING_KEY_ID)
25+
.build();
26+
27+
MaterialsManager materialsManager = DefaultMaterialsManager.builder()
28+
.keyring(keyring)
29+
.build();
30+
S3Client v3Client = S3EncryptionClient.builder()
31+
.materialsManager(materialsManager)
32+
.build();
33+
}
34+
}
35+
```
36+
37+
#### V2 AES Key Materials Provider to V3 AES/GCM Materials Manager and Keyring
38+
```java
39+
class Example {
40+
public static void main(String[] args) {
41+
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
42+
keyGen.init(256);
43+
SecretKey aesKey = keyGen.generateKey();
44+
45+
// V2
46+
EncryptionMaterialsProvider materialsProvider = new StaticEncryptionMaterialsProvider(new EncryptionMaterials(aesKey));
47+
AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
48+
.withEncryptionMaterialsProvider(materialsProvider)
49+
.build();
50+
51+
// V3
52+
Keyring keyring = AesGcmKeyring.builder()
53+
.wrappingKey(aesKey)
54+
.build();
55+
56+
MaterialsManager materialsManager = DefaultMaterialsManager.builder()
57+
.keyring(keyring)
58+
.build();
59+
S3Client v3Client = S3EncryptionClient.builder()
60+
.materialsManager(materialsManager)
61+
.build();
62+
}
63+
}
64+
```
65+
66+
#### V2 RSA Key Materials Provider to V3 RSA-OAEP Materials Manager and Keyring
67+
```java
68+
class Example {
69+
public static void main(String[] args) {
70+
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
71+
keyPairGen.initialize(2048);
72+
KeyPair rsaKey = keyPairGen.generateKeyPair();
73+
74+
// V2
75+
EncryptionMaterialsProvider materialsProvider = new StaticEncryptionMaterialsProvider(new EncryptionMaterials(rsaKey));
76+
AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder()
77+
.withEncryptionMaterialsProvider(materialsProvider)
78+
.build();
79+
80+
// V3
81+
Keyring keyring = RsaOaepKeyring.builder()
82+
.wrappingKeyPair(rsaKey)
83+
.build();
84+
85+
MaterialsManager materialsManager = DefaultMaterialsManager.builder()
86+
.keyring(keyring)
87+
.build();
88+
S3Client v3Client = S3EncryptionClient.builder()
89+
.materialsManager(materialsManager)
90+
.build();
91+
}
92+
}
93+
```
94+
95+
#### V1 Key Materials Provider to V3 AES/GCM Materials Manager, Legacy AESWrap Keyring, and Keyring
96+
Since legacy algorithms are supported for decryption only, a non-legacy keyring is required for any writes.
97+
```java
98+
class Example {
99+
public static void main(String[] args) {
100+
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
101+
keyGen.init(256);
102+
SecretKey aesKey = keyGen.generateKey();
103+
104+
// V1
105+
EncryptionMaterialsProvider materialsProvider = new StaticEncryptionMaterialsProvider(new EncryptionMaterials(aesKey));
106+
AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder()
107+
.withEncryptionMaterials(materialsProvider)
108+
.build();
109+
110+
// V3
111+
Keyring keyring = AesGcmKeyring.builder()
112+
.wrappingKey(aesKey)
113+
.build();
114+
115+
Keyring legacyKeyring = AesWrapKeyring.builder()
116+
.wrappingKey(aesKey)
117+
.build();
118+
119+
MaterialsManager materialsManager = LegacyDecryptMaterialsManager.builder()
120+
.keyring(keyring)
121+
.legacyKeyring(legacyKeyring)
122+
.build();
123+
S3Client v3Client = S3EncryptionClient.builder()
124+
.materialsManager(materialsManager)
125+
.build();
126+
}
127+
}
128+
```
129+
130+
### Legacy Algorithms and Modes
131+
#### Content Encryption
132+
* AES/CBC
133+
#### Key Wrap Encryption
134+
* AESWrap
135+
* RSA-OAEP w/MGF-1 and SHA-256
136+
* KMS (without context)
137+
#### Encryption Metadata Storage
138+
* Instruction File
139+
5140
## Security
6141

7142
See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.

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

Lines changed: 51 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@
22

33
import java.io.ByteArrayInputStream;
44
import java.io.IOException;
5-
import java.nio.charset.StandardCharsets;
65
import java.security.InvalidAlgorithmParameterException;
76
import java.security.InvalidKeyException;
87
import java.security.NoSuchAlgorithmException;
9-
import java.security.SecureRandom;
108
import java.util.Base64;
119
import java.util.Collections;
1210
import java.util.HashMap;
1311
import java.util.List;
1412
import java.util.Map;
15-
import java.util.Map.Entry;
1613
import javax.crypto.BadPaddingException;
1714
import javax.crypto.Cipher;
1815
import javax.crypto.IllegalBlockSizeException;
@@ -28,109 +25,55 @@
2825
import software.amazon.awssdk.http.AbortableInputStream;
2926
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
3027
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
31-
import software.amazon.awssdk.protocols.jsoncore.JsonWriter;
32-
import software.amazon.awssdk.protocols.jsoncore.JsonWriter.JsonGenerationException;
3328
import software.amazon.awssdk.services.s3.S3Client;
3429
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
3530
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
36-
import software.amazon.awssdk.services.s3.model.InvalidObjectStateException;
37-
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
3831
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
3932
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
40-
import software.amazon.awssdk.services.s3.model.S3Exception;
4133
import software.amazon.awssdk.utils.IoUtils;
4234
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
43-
import software.amazon.encryption.s3.materials.DecryptionMaterials;
35+
import software.amazon.encryption.s3.internal.MetadataKey;
36+
import software.amazon.encryption.s3.internal.PutEncryptedObjectPipeline;
4437
import software.amazon.encryption.s3.materials.DecryptMaterialsRequest;
45-
import software.amazon.encryption.s3.materials.EncryptionMaterialsRequest;
38+
import software.amazon.encryption.s3.materials.DecryptionMaterials;
4639
import software.amazon.encryption.s3.materials.EncryptedDataKey;
47-
import software.amazon.encryption.s3.materials.EncryptionMaterials;
4840
import software.amazon.encryption.s3.materials.MaterialsManager;
4941

5042
public class S3EncryptionClient implements S3Client {
5143

5244
private final S3Client _wrappedClient;
5345
private final MaterialsManager _materialsManager;
5446

55-
public S3EncryptionClient(S3Client client, MaterialsManager materialsManager) {
56-
_wrappedClient = client;
57-
_materialsManager = materialsManager;
47+
private S3EncryptionClient(Builder builder) {
48+
_wrappedClient = builder._wrappedClient;
49+
_materialsManager = builder._materialsManager;
50+
}
51+
52+
public static Builder builder() {
53+
return new Builder();
5854
}
5955

6056
@Override
6157
public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBody requestBody)
62-
throws AwsServiceException, SdkClientException, S3Exception {
63-
64-
// TODO: This is proof-of-concept code and needs to be refactored
58+
throws AwsServiceException, SdkClientException {
6559

66-
// Get content encryption key
67-
EncryptionMaterials materials = _materialsManager.getEncryptionMaterials(EncryptionMaterialsRequest.builder()
68-
.build());
69-
SecretKey contentKey = materials.dataKey();
70-
// Encrypt content
71-
byte[] iv = new byte[12]; // default GCM IV length
72-
new SecureRandom().nextBytes(iv);
73-
74-
final String contentEncryptionAlgorithm = "AES/GCM/NoPadding";
75-
final Cipher cipher;
76-
try {
77-
cipher = Cipher.getInstance(contentEncryptionAlgorithm);
78-
cipher.init(Cipher.ENCRYPT_MODE, contentKey, new GCMParameterSpec(128, iv));
79-
} catch (NoSuchAlgorithmException
80-
| NoSuchPaddingException
81-
| InvalidAlgorithmParameterException
82-
| InvalidKeyException e) {
83-
throw new RuntimeException(e);
84-
}
85-
86-
byte[] ciphertext;
87-
try {
88-
byte[] input = IoUtils.toByteArray(requestBody.contentStreamProvider().newStream());
89-
ciphertext = cipher.doFinal(input);
90-
} catch (IOException e) {
91-
throw new RuntimeException(e);
92-
} catch (IllegalBlockSizeException e) {
93-
throw new RuntimeException(e);
94-
} catch (BadPaddingException e) {
95-
throw new RuntimeException(e);
96-
}
97-
98-
// Save content metadata into request
99-
Base64.Encoder encoder = Base64.getEncoder();
100-
Map<String,String> metadata = new HashMap<>(putObjectRequest.metadata());
101-
EncryptedDataKey edk = materials.encryptedDataKeys().get(0);
102-
metadata.put("x-amz-key-v2", encoder.encodeToString(edk.ciphertext()));
103-
metadata.put("x-amz-iv", encoder.encodeToString(iv));
104-
metadata.put("x-amz-matdesc", /* TODO: JSON encoded */ "{}");
105-
metadata.put("x-amz-cek-alg", contentEncryptionAlgorithm);
106-
metadata.put("x-amz-tag-len", /* TODO: take from algo suite */ "128");
107-
metadata.put("x-amz-wrap-alg", edk.keyProviderId());
108-
109-
try (JsonWriter jsonWriter = JsonWriter.create()) {
110-
jsonWriter.writeStartObject();
111-
for (Entry<String,String> entry : materials.encryptionContext().entrySet()) {
112-
jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue());
113-
}
114-
jsonWriter.writeEndObject();
115-
116-
String jsonEncryptionContext = new String(jsonWriter.getBytes(), StandardCharsets.UTF_8);
117-
metadata.put("x-amz-matdesc", jsonEncryptionContext);
118-
} catch (JsonGenerationException e) {
119-
throw new RuntimeException(e);
120-
}
121-
122-
putObjectRequest = putObjectRequest.toBuilder().metadata(metadata).build();
60+
PutEncryptedObjectPipeline pipeline = PutEncryptedObjectPipeline.builder()
61+
.s3Client(_wrappedClient)
62+
.materialsManager(_materialsManager)
63+
.build();
12364

124-
return _wrappedClient.putObject(putObjectRequest, RequestBody.fromBytes(ciphertext));
65+
return pipeline.putObject(putObjectRequest, requestBody);
12566
}
12667

12768
@Override
128-
public <T> T getObject(GetObjectRequest getObjectRequest, ResponseTransformer<GetObjectResponse, T> responseTransformer)
129-
throws NoSuchKeyException, InvalidObjectStateException, AwsServiceException, SdkClientException, S3Exception {
69+
public <T> T getObject(GetObjectRequest getObjectRequest,
70+
ResponseTransformer<GetObjectResponse, T> responseTransformer)
71+
throws AwsServiceException, SdkClientException {
13072

13173
// TODO: This is proof-of-concept code and needs to be refactored
13274

133-
ResponseInputStream<GetObjectResponse> objectStream = _wrappedClient.getObject(getObjectRequest);
75+
ResponseInputStream<GetObjectResponse> objectStream = _wrappedClient.getObject(
76+
getObjectRequest);
13477
byte[] output;
13578
try {
13679
output = IoUtils.toByteArray(objectStream);
@@ -143,8 +86,8 @@ public <T> T getObject(GetObjectRequest getObjectRequest, ResponseTransformer<Ge
14386

14487
// Build encrypted data key
14588
Base64.Decoder decoder = Base64.getDecoder();
146-
byte[] edkCiphertext = decoder.decode(metadata.get("x-amz-key-v2"));
147-
String keyProviderId = metadata.get("x-amz-wrap-alg");
89+
byte[] edkCiphertext = decoder.decode(metadata.get(MetadataKey.ENCRYPTED_DATA_KEY));
90+
String keyProviderId = metadata.get(MetadataKey.ENCRYPTED_DATA_KEY_ALGORITHM);
14891
EncryptedDataKey edk = EncryptedDataKey.builder()
14992
.ciphertext(edkCiphertext)
15093
.keyProviderId(keyProviderId)
@@ -153,7 +96,7 @@ public <T> T getObject(GetObjectRequest getObjectRequest, ResponseTransformer<Ge
15396

15497
// Get encryption context
15598
final Map<String, String> encryptionContext = new HashMap<>();
156-
final String jsonEncryptionContext = metadata.get("x-amz-matdesc");
99+
final String jsonEncryptionContext = metadata.get(MetadataKey.ENCRYPTED_DATA_KEY_CONTEXT);
157100
try {
158101
JsonNodeParser parser = JsonNodeParser.create();
159102
JsonNode objectNode = parser.parse(jsonEncryptionContext);
@@ -166,14 +109,15 @@ public <T> T getObject(GetObjectRequest getObjectRequest, ResponseTransformer<Ge
166109
}
167110

168111
// Get decryption materials
169-
final String contentEncryptionAlgorithm = metadata.get("x-amz-cek-alg");
112+
final String contentEncryptionAlgorithm = metadata.get(MetadataKey.CONTENT_CIPHER);
170113
AlgorithmSuite algorithmSuite = null;
171114
if (contentEncryptionAlgorithm.equals("AES/GCM/NoPadding")) {
172-
algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;;
115+
algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;
173116
}
174117

175118
if (algorithmSuite == null) {
176-
throw new RuntimeException("Unknown content encryption algorithm: " + contentEncryptionAlgorithm);
119+
throw new RuntimeException(
120+
"Unknown content encryption algorithm: " + contentEncryptionAlgorithm);
177121
}
178122

179123
DecryptMaterialsRequest request = DecryptMaterialsRequest.builder()
@@ -185,8 +129,8 @@ public <T> T getObject(GetObjectRequest getObjectRequest, ResponseTransformer<Ge
185129

186130
// Get content encryption information
187131
SecretKey contentKey = new SecretKeySpec(materials.plaintextDataKey(), "AES");
188-
final int tagLength = Integer.parseInt(metadata.get("x-amz-tag-len"));
189-
byte[] iv = decoder.decode(metadata.get("x-amz-iv"));
132+
final int tagLength = Integer.parseInt(metadata.get(MetadataKey.CONTENT_CIPHER_TAG_LENGTH));
133+
byte[] iv = decoder.decode(metadata.get(MetadataKey.CONTENT_NONCE));
190134
final Cipher cipher;
191135
byte[] plaintext;
192136
try {
@@ -221,4 +165,25 @@ public String serviceName() {
221165
public void close() {
222166
_wrappedClient.close();
223167
}
168+
169+
public static class Builder {
170+
private S3Client _wrappedClient = S3Client.builder().build();
171+
private MaterialsManager _materialsManager;
172+
173+
private Builder() {}
174+
175+
public Builder wrappedClient(S3Client wrappedClient) {
176+
this._wrappedClient = wrappedClient;
177+
return this;
178+
}
179+
180+
public Builder materialsManager(MaterialsManager materialsManager) {
181+
this._materialsManager = materialsManager;
182+
return this;
183+
}
184+
185+
public S3EncryptionClient build() {
186+
return new S3EncryptionClient(this);
187+
}
188+
}
224189
}

src/main/java/software/amazon/encryption/s3/algorithms/Constants.java renamed to src/main/java/software/amazon/encryption/s3/algorithms/AlgorithmConstants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package software.amazon.encryption.s3.algorithms;
22

3-
class Constants {
3+
class AlgorithmConstants {
44
// Maximum length of the content that can be encrypted in GCM mode.
55
static final long GCM_MAX_CONTENT_LENGTH_BITS = (1L<<39) - 256;
66
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public enum AlgorithmSuite {
99
128,
1010
96,
1111
128,
12-
Constants.GCM_MAX_CONTENT_LENGTH_BITS);
12+
AlgorithmConstants.GCM_MAX_CONTENT_LENGTH_BITS);
1313

1414
private int _id;
1515
private String _dataKeyAlgorithm;
@@ -51,6 +51,10 @@ public String cipherName() {
5151
return _cipherName;
5252
}
5353

54+
public int cipherTagLengthBits() {
55+
return _cipherTagLengthBits;
56+
}
57+
5458
public int nonceLengthBytes() {
5559
return _cipherNonceLengthBits / 8;
5660
}

0 commit comments

Comments
 (0)