Skip to content

Commit a78cb52

Browse files
authored
feat: allow raw keyrings to decrypt with multiple wrapping keys (#485)
* feat: allow raw keyrings to decrypt with multiple wrapping keys * remove exploratory tests in Compatibility tests class * add tests for ReEncrypt
1 parent ab41a57 commit a78cb52

21 files changed

+2224
-75
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import software.amazon.encryption.s3.materials.EncryptionMaterials;
6969
import software.amazon.encryption.s3.materials.Keyring;
7070
import software.amazon.encryption.s3.materials.KmsKeyring;
71+
import software.amazon.encryption.s3.materials.MaterialsDescription;
7172
import software.amazon.encryption.s3.materials.MultipartConfiguration;
7273
import software.amazon.encryption.s3.materials.PartialRsaKeyPair;
7374
import software.amazon.encryption.s3.materials.RawKeyring;
@@ -246,7 +247,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru
246247
//Extract cryptographic parameters from the current instruction file that MUST be preserved during re-encryption
247248
final AlgorithmSuite algorithmSuite = contentMetadata.algorithmSuite();
248249
final EncryptedDataKey originalEncryptedDataKey = contentMetadata.encryptedDataKey();
249-
final Map<String, String> currentKeyringMaterialsDescription = contentMetadata.encryptedDataKeyMatDescOrContext();
250+
final MaterialsDescription currentKeyringMaterialsDescription = contentMetadata.materialsDescription();
250251
final byte[] iv = contentMetadata.contentIv();
251252

252253
//Decrypt the data key using the current keyring
@@ -257,6 +258,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru
257258
DecryptMaterialsRequest.builder()
258259
.algorithmSuite(algorithmSuite)
259260
.encryptedDataKeys(Collections.singletonList(originalEncryptedDataKey))
261+
.materialsDescription(contentMetadata.materialsDescription())
260262
.s3Request(request)
261263
.build()
262264
);
@@ -277,7 +279,7 @@ public ReEncryptInstructionFileResponse reEncryptInstructionFile(ReEncryptInstru
277279
RawKeyring newKeyring = reEncryptInstructionFileRequest.newKeyring();
278280
EncryptionMaterials encryptedMaterials = newKeyring.onEncrypt(encryptionMaterials);
279281

280-
final Map<String, String> newMaterialsDescription = encryptedMaterials.materialsDescription().getMaterialsDescription();
282+
final MaterialsDescription newMaterialsDescription = encryptedMaterials.materialsDescription();
281283
//Validate that the new keyring has different materials description than the old keyring
282284
if (newMaterialsDescription.equals(currentKeyringMaterialsDescription)) {
283285
throw new S3EncryptionClientException("New keyring must have new materials description!");

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

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
66
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
77
import software.amazon.encryption.s3.materials.EncryptedDataKey;
8+
import software.amazon.encryption.s3.materials.MaterialsDescription;
89

910
import java.util.Collections;
1011
import java.util.Map;
@@ -17,11 +18,14 @@ public class ContentMetadata {
1718
private final String _encryptedDataKeyAlgorithm;
1819

1920
/**
20-
* This field stores either encryption context or material description.
21-
* We use a single field to store both in order to maintain backwards
22-
* compatibility with V2, which treated both as the same.
21+
* This field stores the encryption context.
2322
*/
24-
private final Map<String, String> _encryptionContextOrMatDesc;
23+
private final Map<String, String> _encryptionContext;
24+
25+
/**
26+
* This field stores the materials description used for RSA and AES keyrings.
27+
*/
28+
private final MaterialsDescription _materialsDescription;
2529

2630
private final byte[] _contentIv;
2731
private final String _contentCipher;
@@ -33,7 +37,8 @@ private ContentMetadata(Builder builder) {
3337

3438
_encryptedDataKey = builder._encryptedDataKey;
3539
_encryptedDataKeyAlgorithm = builder._encryptedDataKeyAlgorithm;
36-
_encryptionContextOrMatDesc = builder._encryptionContextOrMatDesc;
40+
_encryptionContext = builder._encryptionContext;
41+
_materialsDescription = builder._materialsDescription;
3742

3843
_contentIv = builder._contentIv;
3944
_contentCipher = builder._contentCipher;
@@ -64,8 +69,16 @@ public String encryptedDataKeyAlgorithm() {
6469
*/
6570
@SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "False positive; underlying"
6671
+ " implementation is immutable")
67-
public Map<String, String> encryptedDataKeyMatDescOrContext() {
68-
return _encryptionContextOrMatDesc;
72+
public Map<String, String> encryptionContext() {
73+
return _encryptionContext;
74+
}
75+
76+
/**
77+
* Returns the materials description used for RSA and AES keyrings.
78+
* @return the materials description
79+
*/
80+
public MaterialsDescription materialsDescription() {
81+
return _materialsDescription;
6982
}
7083

7184
public byte[] contentIv() {
@@ -92,7 +105,8 @@ public static class Builder {
92105

93106
private EncryptedDataKey _encryptedDataKey;
94107
private String _encryptedDataKeyAlgorithm;
95-
private Map<String, String> _encryptionContextOrMatDesc;
108+
private Map<String, String> _encryptionContext;
109+
private MaterialsDescription _materialsDescription = MaterialsDescription.builder().build();
96110

97111
private byte[] _contentIv;
98112
private String _contentCipher;
@@ -118,8 +132,15 @@ public Builder encryptedDataKeyAlgorithm(String encryptedDataKeyAlgorithm) {
118132
return this;
119133
}
120134

121-
public Builder encryptionContextOrMatDesc(Map<String, String> encryptionContextOrMatDesc) {
122-
_encryptionContextOrMatDesc = Collections.unmodifiableMap(encryptionContextOrMatDesc);
135+
public Builder encryptionContext(Map<String, String> encryptionContext) {
136+
_encryptionContext = Collections.unmodifiableMap(encryptionContext);
137+
return this;
138+
}
139+
140+
public Builder materialsDescription(MaterialsDescription materialsDescription) {
141+
_materialsDescription = materialsDescription == null
142+
? MaterialsDescription.builder().build()
143+
: materialsDescription;
123144
return this;
124145
}
125146

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import software.amazon.encryption.s3.S3EncryptionClientException;
1414
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;
1515
import software.amazon.encryption.s3.materials.EncryptedDataKey;
16+
import software.amazon.encryption.s3.materials.MaterialsDescription;
1617
import software.amazon.encryption.s3.materials.S3Keyring;
1718

1819
import java.io.ByteArrayOutputStream;
@@ -137,8 +138,8 @@ private ContentMetadata readFromMap(Map<String, String> metadata, GetObjectRespo
137138
.keyProviderInfo(keyProviderInfo.getBytes(StandardCharsets.UTF_8))
138139
.build();
139140

140-
// Get encrypted data key encryption context or materials description (depending on the keyring)
141-
final Map<String, String> encryptionContextOrMatDesc = new HashMap<>();
141+
// Parse the JSON materials description or encryption context
142+
final Map<String, String> matDescMap = new HashMap<>();
142143
// The V2 client treats null value here as empty, do the same to avoid incompatibility
143144
String jsonEncryptionContext = metadata.getOrDefault(MetadataKeyConstants.ENCRYPTED_DATA_KEY_CONTEXT, "{}");
144145
// When the encryption context contains non-US-ASCII characters,
@@ -150,19 +151,37 @@ private ContentMetadata readFromMap(Map<String, String> metadata, GetObjectRespo
150151
JsonNode objectNode = parser.parse(decodedJsonEncryptionContext);
151152

152153
for (Map.Entry<String, JsonNode> entry : objectNode.asObject().entrySet()) {
153-
encryptionContextOrMatDesc.put(entry.getKey(), entry.getValue().asString());
154+
matDescMap.put(entry.getKey(), entry.getValue().asString());
154155
}
155156
} catch (Exception e) {
156157
throw new RuntimeException(e);
157158
}
158159

160+
// By default, assume the context is a materials description unless it's a KMS keyring
161+
Map<String, String> encryptionContext;
162+
MaterialsDescription materialsDescription;
163+
164+
if (keyProviderInfo.contains("kms")) {
165+
// For KMS keyrings, use the map as encryption context
166+
encryptionContext = matDescMap;
167+
materialsDescription = MaterialsDescription.builder().build();
168+
} else {
169+
// For all other keyrings (AES, RSA), use the map as materials description
170+
materialsDescription = MaterialsDescription.builder()
171+
.putAll(matDescMap)
172+
.build();
173+
// Set an empty encryption context
174+
encryptionContext = new HashMap<>();
175+
}
176+
159177
// Get content iv
160178
byte[] iv = DECODER.decode(metadata.get(MetadataKeyConstants.CONTENT_IV));
161179

162180
return ContentMetadata.builder()
163181
.algorithmSuite(algorithmSuite)
164182
.encryptedDataKey(edk)
165-
.encryptionContextOrMatDesc(encryptionContextOrMatDesc)
183+
.encryptionContext(encryptionContext)
184+
.materialsDescription(materialsDescription)
166185
.contentIv(iv)
167186
.contentRange(contentRange)
168187
.build();

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ private DecryptionMaterials prepareMaterialsFromRequest(final GetObjectRequest g
9191
.s3Request(getObjectRequest)
9292
.algorithmSuite(algorithmSuite)
9393
.encryptedDataKeys(encryptedDataKeys)
94-
.encryptionContext(contentMetadata.encryptedDataKeyMatDescOrContext())
94+
.encryptionContext(contentMetadata.encryptionContext())
95+
.materialsDescription(contentMetadata.materialsDescription())
9596
.ciphertextLength(getObjectResponse.contentLength())
9697
.contentRange(getObjectRequest.range())
9798
.build();
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package software.amazon.encryption.s3.materials;
4+
5+
import javax.crypto.SecretKey;
6+
7+
/**
8+
* A concrete implementation of RawKeyMaterial for AES keys.
9+
* This class provides a more convenient way to create key material for AES keyrings
10+
* without having to specify the generic type parameter.
11+
*/
12+
public class AesKeyMaterial extends RawKeyMaterial<SecretKey> {
13+
14+
/**
15+
* Creates a new AesKeyMaterial with the specified materials description and key material.
16+
*
17+
* @param materialsDescription the materials description
18+
* @param keyMaterial the AES key material
19+
*/
20+
public AesKeyMaterial(MaterialsDescription materialsDescription, SecretKey keyMaterial) {
21+
super(materialsDescription, keyMaterial);
22+
}
23+
24+
/**
25+
* @return a new builder instance for AesKeyMaterial
26+
*/
27+
public static Builder aesBuilder() {
28+
return new Builder();
29+
}
30+
31+
/**
32+
* Builder for AesKeyMaterial.
33+
*/
34+
public static class Builder {
35+
private MaterialsDescription _materialsDescription;
36+
private SecretKey _keyMaterial;
37+
38+
/**
39+
* Sets the materials description for this AES key material.
40+
*
41+
* @param materialsDescription the materials description
42+
* @return a reference to this object so that method calls can be chained together.
43+
*/
44+
public Builder materialsDescription(MaterialsDescription materialsDescription) {
45+
this._materialsDescription = materialsDescription;
46+
return this;
47+
}
48+
49+
/**
50+
* Sets the AES key material.
51+
*
52+
* @param keyMaterial the AES key material
53+
* @return a reference to this object so that method calls can be chained together.
54+
*/
55+
public Builder keyMaterial(SecretKey keyMaterial) {
56+
this._keyMaterial = keyMaterial;
57+
return this;
58+
}
59+
60+
/**
61+
* @return the built AesKeyMaterial
62+
*/
63+
public AesKeyMaterial build() {
64+
return new AesKeyMaterial(_materialsDescription, _keyMaterial);
65+
}
66+
}
67+
}

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

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
* This keyring can wrap keys with the active keywrap algorithm and
2121
* unwrap with the active and legacy algorithms for AES keys.
2222
*/
23-
public class AesKeyring extends RawKeyring {
23+
public class AesKeyring extends RawKeyring<SecretKey> {
2424

2525
private static final String KEY_ALGORITHM = "AES";
2626

@@ -41,13 +41,16 @@ public String keyProviderInfo() {
4141
return KEY_PROVIDER_INFO;
4242
}
4343

44-
@Override
45-
public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException {
46-
final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider());
47-
cipher.init(Cipher.DECRYPT_MODE, _wrappingKey);
44+
@Override
45+
public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException {
46+
// Find the appropriate key material to use for decryption
47+
SecretKey keyToUse = findKeyMaterialForDecryption(materials, _wrappingKey);
4848

49-
return cipher.doFinal(encryptedDataKey);
50-
}
49+
final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider());
50+
cipher.init(Cipher.DECRYPT_MODE, keyToUse);
51+
52+
return cipher.doFinal(encryptedDataKey);
53+
}
5154
};
5255

5356
private final DecryptDataKeyStrategy _aesWrapStrategy = new DecryptDataKeyStrategy() {
@@ -65,14 +68,17 @@ public String keyProviderInfo() {
6568
return KEY_PROVIDER_INFO;
6669
}
6770

68-
@Override
69-
public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException {
70-
final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider());
71-
cipher.init(Cipher.UNWRAP_MODE, _wrappingKey);
71+
@Override
72+
public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException {
73+
// Find the appropriate key material to use for decryption
74+
SecretKey keyToUse = findKeyMaterialForDecryption(materials, _wrappingKey);
7275

73-
Key plaintextKey = cipher.unwrap(encryptedDataKey, CIPHER_ALGORITHM, Cipher.SECRET_KEY);
74-
return plaintextKey.getEncoded();
75-
}
76+
final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider());
77+
cipher.init(Cipher.UNWRAP_MODE, keyToUse);
78+
79+
Key plaintextKey = cipher.unwrap(encryptedDataKey, CIPHER_ALGORITHM, Cipher.SECRET_KEY);
80+
return plaintextKey.getEncoded();
81+
}
7682
};
7783

7884
private final DataKeyStrategy _aesGcmStrategy = new DataKeyStrategy() {
@@ -126,22 +132,25 @@ public byte[] encryptDataKey(SecureRandom secureRandom,
126132
return encodedBytes;
127133
}
128134

129-
@Override
130-
public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException {
131-
byte[] iv = new byte[IV_LENGTH_BYTES];
132-
byte[] ciphertext = new byte[encryptedDataKey.length - iv.length];
135+
@Override
136+
public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) throws GeneralSecurityException {
137+
byte[] iv = new byte[IV_LENGTH_BYTES];
138+
byte[] ciphertext = new byte[encryptedDataKey.length - iv.length];
133139

134-
System.arraycopy(encryptedDataKey, 0, iv, 0, iv.length);
135-
System.arraycopy(encryptedDataKey, iv.length, ciphertext, 0, ciphertext.length);
140+
System.arraycopy(encryptedDataKey, 0, iv, 0, iv.length);
141+
System.arraycopy(encryptedDataKey, iv.length, ciphertext, 0, ciphertext.length);
136142

137-
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv);
138-
final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider());
139-
cipher.init(Cipher.DECRYPT_MODE, _wrappingKey, gcmParameterSpec);
143+
// Find the appropriate key material to use for decryption
144+
SecretKey keyToUse = findKeyMaterialForDecryption(materials, _wrappingKey);
140145

141-
final byte[] aADBytes = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName().getBytes(StandardCharsets.UTF_8);
142-
cipher.updateAAD(aADBytes);
143-
return cipher.doFinal(ciphertext);
144-
}
146+
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, iv);
147+
final Cipher cipher = CryptoFactory.createCipher(CIPHER_ALGORITHM, materials.cryptoProvider());
148+
cipher.init(Cipher.DECRYPT_MODE, keyToUse, gcmParameterSpec);
149+
150+
final byte[] aADBytes = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName().getBytes(StandardCharsets.UTF_8);
151+
cipher.updateAAD(aADBytes);
152+
return cipher.doFinal(ciphertext);
153+
}
145154
};
146155

147156
private final Map<String, DecryptDataKeyStrategy> decryptDataKeyStrategies = new HashMap<>();
@@ -175,7 +184,7 @@ protected Map<String, DecryptDataKeyStrategy> decryptDataKeyStrategies() {
175184
return decryptDataKeyStrategies;
176185
}
177186

178-
public static class Builder extends RawKeyring.Builder<AesKeyring, Builder> {
187+
public static class Builder extends RawKeyring.Builder<AesKeyring, Builder, SecretKey> {
179188
private SecretKey _wrappingKey;
180189

181190
private Builder() {

0 commit comments

Comments
 (0)