Skip to content

Commit 8807d79

Browse files
fix: Update DecryptionMaterials code to support legacy custom CMMs (#2037)
1 parent 19975b9 commit 8807d79

File tree

8 files changed

+565
-2
lines changed

8 files changed

+565
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package com.amazonaws.crypto.examples.v2;
5+
6+
import com.amazonaws.encryptionsdk.AwsCrypto;
7+
import com.amazonaws.encryptionsdk.CommitmentPolicy;
8+
import com.amazonaws.encryptionsdk.CryptoMaterialsManager;
9+
import com.amazonaws.encryptionsdk.CryptoResult;
10+
import com.amazonaws.encryptionsdk.DefaultCryptoMaterialsManager;
11+
import com.amazonaws.encryptionsdk.MasterKeyProvider;
12+
import com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider;
13+
import com.amazonaws.encryptionsdk.model.DecryptionMaterials;
14+
import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest;
15+
import com.amazonaws.encryptionsdk.model.EncryptionMaterials;
16+
import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest;
17+
18+
import java.nio.charset.StandardCharsets;
19+
import java.util.Arrays;
20+
import java.util.Collections;
21+
import java.util.Map;
22+
23+
/**
24+
* <p>
25+
* Creates a custom implementation of the CryptoMaterialsManager interface,
26+
* then uses that implementation to encrypt and decrypt a file using an AWS KMS CMK.
27+
*
28+
* <p>
29+
* Arguments:
30+
* <ol>
31+
* <li>Key ARN: For help finding the Amazon Resource Name (ARN) of your AWS KMS customer master
32+
* key (CMK), see 'Viewing Keys' at http://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html
33+
* </ol>
34+
*/
35+
public class CustomCMMExample {
36+
37+
private static final byte[] EXAMPLE_DATA = "Hello World".getBytes(StandardCharsets.UTF_8);
38+
39+
public static void main(final String[] args) {
40+
final String keyArn = args[0];
41+
42+
CryptoMaterialsManager cmm = new SigningSuiteOnlyCMM(
43+
KmsMasterKeyProvider.builder().buildStrict(keyArn)
44+
);
45+
46+
encryptAndDecryptWithCMM(cmm);
47+
}
48+
49+
static void encryptAndDecryptWithCMM(final CryptoMaterialsManager cmm) {
50+
// 1. Instantiate the SDK
51+
// This builds the AwsCrypto client with the RequireEncryptRequireDecrypt commitment policy,
52+
// which enforces that this client only encrypts using committing algorithm suites and enforces
53+
// that this client will only decrypt encrypted messages that were created with a committing algorithm suite.
54+
// This is the default commitment policy if you build the client with `AwsCrypto.builder().build()`
55+
// or `AwsCrypto.standard()`.
56+
final AwsCrypto crypto = AwsCrypto.builder()
57+
.withCommitmentPolicy(CommitmentPolicy.RequireEncryptRequireDecrypt)
58+
.build();
59+
60+
// 2. Create an encryption context
61+
// Most encrypted data should have an associated encryption context
62+
// to protect integrity. This sample uses placeholder values.
63+
// For more information see:
64+
// blogs.aws.amazon.com/security/post/Tx2LZ6WBJJANTNW/How-to-Protect-the-Integrity-of-Your-Encrypted-Data-by-Using-AWS-Key-Management
65+
final Map<String, String> encryptionContext = Collections.singletonMap("ExampleContextKey", "ExampleContextValue");
66+
67+
// 3. Encrypt the data with the provided CMM
68+
final CryptoResult<byte[], ?> encryptResult = crypto.encryptData(cmm, EXAMPLE_DATA, encryptionContext);
69+
final byte[] ciphertext = encryptResult.getResult();
70+
71+
// 4. Decrypt the data
72+
final CryptoResult<byte[], ?> decryptResult = crypto.decryptData(cmm, ciphertext);
73+
74+
// 5. Verify that the encryption context in the result contains the
75+
// encryption context supplied to the encryptData method. Because the
76+
// SDK can add values to the encryption context, don't require that
77+
// the entire context matches.
78+
if (!encryptionContext.entrySet().stream()
79+
.allMatch(e -> e.getValue().equals(decryptResult.getEncryptionContext().get(e.getKey())))) {
80+
throw new IllegalStateException("Wrong Encryption Context!");
81+
}
82+
83+
// 6. Verify that the decrypted plaintext matches the original plaintext
84+
assert Arrays.equals(decryptResult.getResult(), EXAMPLE_DATA);
85+
}
86+
87+
// Custom CMM implementation.
88+
// This CMM only allows encryption/decryption using signing algorithms.
89+
// It wraps an underlying CMM implementation and checks its materials
90+
// to ensure that it is only using signed encryption algorithms.
91+
public static class SigningSuiteOnlyCMM implements CryptoMaterialsManager {
92+
93+
// The underlying CMM.
94+
private CryptoMaterialsManager underlyingCMM;
95+
96+
// If only a MasterKeyProvider is constructed, the underlying CMM is the default CMM.
97+
public SigningSuiteOnlyCMM(MasterKeyProvider<?> mkp) {
98+
this.underlyingCMM = new DefaultCryptoMaterialsManager(mkp);
99+
}
100+
101+
// This CMM can wrap any other CMM implementation.
102+
public SigningSuiteOnlyCMM(CryptoMaterialsManager underlyingCMM) {
103+
this.underlyingCMM = underlyingCMM;
104+
}
105+
106+
@Override
107+
public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest request) {
108+
EncryptionMaterials materials = underlyingCMM.getMaterialsForEncrypt(request);
109+
if (materials.getAlgorithm().getTrailingSignatureAlgo() == null) {
110+
throw new IllegalArgumentException("Algorithm provided to SigningSuiteOnlyCMM is not a supported signing algorithm: " + materials.getAlgorithm());
111+
}
112+
return materials;
113+
}
114+
115+
@Override
116+
public DecryptionMaterials decryptMaterials(DecryptionMaterialsRequest request) {
117+
if (request.getAlgorithm().getTrailingSignatureAlgo() == null) {
118+
throw new IllegalArgumentException("Algorithm provided to SigningSuiteOnlyCMM is not a supported signing algorithm: " + request.getAlgorithm());
119+
}
120+
return underlyingCMM.decryptMaterials(request);
121+
}
122+
}
123+
124+
}

src/main/java/com/amazonaws/encryptionsdk/CMMHandler.java

+32-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package com.amazonaws.encryptionsdk;
55

66
import com.amazonaws.encryptionsdk.internal.Utils;
7+
import com.amazonaws.encryptionsdk.model.DecryptionMaterials;
78
import com.amazonaws.encryptionsdk.model.DecryptionMaterialsHandler;
89
import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest;
910
import com.amazonaws.encryptionsdk.model.EncryptionMaterialsHandler;
@@ -63,7 +64,37 @@ private GetEncryptionMaterialsInput getEncryptionMaterialsRequestInput(
6364
public DecryptionMaterialsHandler decryptMaterials(
6465
DecryptionMaterialsRequest request, CommitmentPolicy commitmentPolicy) {
6566
if (cmm != null && mplCMM == null) {
66-
return new DecryptionMaterialsHandler(cmm.decryptMaterials(request));
67+
// This is an implementation of the legacy native CryptoMaterialsManager interface from
68+
// ESDK-Java.
69+
DecryptionMaterials materials = cmm.decryptMaterials(request);
70+
if (materials.getEncryptionContext().isEmpty() && !request.getEncryptionContext().isEmpty()) {
71+
// If the request specified an encryption context,
72+
// and we are using the legacy native CMM,
73+
// add the encryptionContext to the materials.
74+
//
75+
// ESDK-Java 3.0 changed internals of decrypt behavior,
76+
// This code makes earlier CMM implementations compatible with post-3.0 behavior.
77+
//
78+
// Version 3.0 assumes that CMMs' implementations of decryptMaterials
79+
// will set an encryptionContext attribute on returned DecryptionMaterials.
80+
// The DefaultCryptoMaterialsManager's behavior was changed in 3.0.
81+
// It now sets the encryptionContext attribute with the value from the ciphertext's headers.
82+
//
83+
// But custom CMMs' behavior was not updated.
84+
// However, there is no custom CMM before version 3.0 that could set an encryptionContext
85+
// attribute.
86+
// The encryptionContext attribute was only introduced to decryptMaterials objects
87+
// in ESDK 3.0, so no CMM could have configured this attribute before 3.0.
88+
// As a result, the ESDK assumes that any native CMM
89+
// that does not add encryptionContext to its decryptMaterials
90+
// SHOULD add encryptionContext to its decryptMaterials,
91+
//
92+
// If a custom CMM implementation conflicts with this assumption.
93+
// that CMM implementation MUST move to the MPL.
94+
materials =
95+
materials.toBuilder().setEncryptionContext(request.getEncryptionContext()).build();
96+
}
97+
return new DecryptionMaterialsHandler(materials);
6798
} else {
6899
DecryptMaterialsInput input = getDecryptMaterialsInput(request, commitmentPolicy);
69100
DecryptMaterialsOutput output = mplCMM.DecryptMaterials(input);

src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.amazonaws.encryptionsdk.DataKey;
44
import java.security.PublicKey;
5+
import java.util.Collections;
56
import java.util.Map;
67

78
public final class DecryptionMaterials {
@@ -12,7 +13,11 @@ public final class DecryptionMaterials {
1213
private DecryptionMaterials(Builder b) {
1314
dataKey = b.getDataKey();
1415
trailingSignatureKey = b.getTrailingSignatureKey();
15-
encryptionContext = b.getEncryptionContext();
16+
if (b.getEncryptionContext() != null) {
17+
encryptionContext = b.getEncryptionContext();
18+
} else {
19+
encryptionContext = Collections.emptyMap();
20+
}
1621
}
1722

1823
public DataKey<?> getDataKey() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package com.amazonaws.crypto.examples.v2;
5+
6+
import com.amazonaws.encryptionsdk.CryptoMaterialsManager;
7+
import com.amazonaws.encryptionsdk.kms.KMSTestFixtures;
8+
import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider;
9+
import org.junit.Test;
10+
11+
public class CustomCMMExampleTest {
12+
13+
@Test
14+
public void testCustomCMMExample() {
15+
CryptoMaterialsManager cmm =
16+
new CustomCMMExample.SigningSuiteOnlyCMM(
17+
KmsMasterKeyProvider.builder().buildStrict(KMSTestFixtures.US_WEST_2_KEY_ID));
18+
CustomCMMExample.encryptAndDecryptWithCMM(cmm);
19+
}
20+
21+
@Test
22+
public void testV2Cmm() {
23+
V2DefaultCryptoMaterialsManager cmm =
24+
new V2DefaultCryptoMaterialsManager(
25+
KmsMasterKeyProvider.builder().buildStrict(KMSTestFixtures.US_WEST_2_KEY_ID));
26+
CustomCMMExample.encryptAndDecryptWithCMM(cmm);
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package com.amazonaws.crypto.examples.v2;
2+
3+
import static com.amazonaws.encryptionsdk.internal.Utils.assertNonNull;
4+
5+
import com.amazonaws.encryptionsdk.CommitmentPolicy;
6+
import com.amazonaws.encryptionsdk.CryptoAlgorithm;
7+
import com.amazonaws.encryptionsdk.CryptoMaterialsManager;
8+
import com.amazonaws.encryptionsdk.DataKey;
9+
import com.amazonaws.encryptionsdk.MasterKey;
10+
import com.amazonaws.encryptionsdk.MasterKeyProvider;
11+
import com.amazonaws.encryptionsdk.MasterKeyRequest;
12+
import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
13+
import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException;
14+
import com.amazonaws.encryptionsdk.internal.Constants;
15+
import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm;
16+
import com.amazonaws.encryptionsdk.model.DecryptionMaterials;
17+
import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest;
18+
import com.amazonaws.encryptionsdk.model.EncryptionMaterials;
19+
import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest;
20+
import com.amazonaws.encryptionsdk.model.KeyBlob;
21+
import java.security.GeneralSecurityException;
22+
import java.security.KeyPair;
23+
import java.security.PublicKey;
24+
import java.util.ArrayList;
25+
import java.util.HashMap;
26+
import java.util.List;
27+
import java.util.Map;
28+
29+
/*
30+
This is a copy-paste of the DefaultCryptoMaterialsManager implementation
31+
from the final commit of the V2 ESDK: 1870a082358d59e32c60d74116d6f43c0efa466b
32+
ESDK V3 implicitly changed the contract between CMMs and the ESDK.
33+
After V3, DecryptMaterials has an `encryptionContext` attribute,
34+
and CMMs are expected to set this attribute.
35+
The V3 commit modified this DefaultCMM's `decryptMaterials` implementation
36+
to set encryptionContext on returned DecryptionMaterials objects.
37+
However, there are custom implementations of the legacy native CMM
38+
that do not set encryptionContext.
39+
This CMM is used to explicitly assert that the V2 implementation of
40+
the DefaultCMM is compatible with V3 logic,
41+
which implicitly asserts that custom implementations of V2-compatible CMMs
42+
are also compatible with V3 logic.
43+
*/
44+
public class V2DefaultCryptoMaterialsManager implements CryptoMaterialsManager {
45+
private final MasterKeyProvider<?> mkp;
46+
47+
private final CryptoAlgorithm DEFAULT_CRYPTO_ALGORITHM =
48+
CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384;
49+
50+
/** @param mkp The master key provider to delegate to */
51+
public V2DefaultCryptoMaterialsManager(MasterKeyProvider<?> mkp) {
52+
assertNonNull(mkp, "mkp");
53+
this.mkp = mkp;
54+
}
55+
56+
@Override
57+
public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest request) {
58+
Map<String, String> context = request.getContext();
59+
60+
CryptoAlgorithm algo = request.getRequestedAlgorithm();
61+
CommitmentPolicy commitmentPolicy = request.getCommitmentPolicy();
62+
// Set default according to commitment policy
63+
if (algo == null && commitmentPolicy == CommitmentPolicy.ForbidEncryptAllowDecrypt) {
64+
algo = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384;
65+
} else if (algo == null) {
66+
algo = CryptoAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384;
67+
}
68+
69+
KeyPair trailingKeys = null;
70+
if (algo.getTrailingSignatureLength() > 0) {
71+
try {
72+
trailingKeys = generateTrailingSigKeyPair(algo);
73+
if (context.containsKey(Constants.EC_PUBLIC_KEY_FIELD)) {
74+
throw new IllegalArgumentException(
75+
"EncryptionContext contains reserved field " + Constants.EC_PUBLIC_KEY_FIELD);
76+
}
77+
// make mutable
78+
context = new HashMap<>(context);
79+
context.put(Constants.EC_PUBLIC_KEY_FIELD, serializeTrailingKeyForEc(algo, trailingKeys));
80+
} catch (final GeneralSecurityException ex) {
81+
throw new AwsCryptoException(ex);
82+
}
83+
}
84+
85+
final MasterKeyRequest.Builder mkRequestBuilder = MasterKeyRequest.newBuilder();
86+
mkRequestBuilder.setEncryptionContext(context);
87+
88+
mkRequestBuilder.setStreaming(request.getPlaintextSize() == -1);
89+
if (request.getPlaintext() != null) {
90+
mkRequestBuilder.setPlaintext(request.getPlaintext());
91+
} else {
92+
mkRequestBuilder.setSize(request.getPlaintextSize());
93+
}
94+
95+
@SuppressWarnings("unchecked")
96+
final List<MasterKey> mks =
97+
(List<MasterKey>)
98+
assertNonNull(mkp, "provider").getMasterKeysForEncryption(mkRequestBuilder.build());
99+
100+
if (mks.isEmpty()) {
101+
throw new IllegalArgumentException("No master keys provided");
102+
}
103+
104+
DataKey<?> dataKey = mks.get(0).generateDataKey(algo, context);
105+
106+
List<KeyBlob> keyBlobs = new ArrayList<>(mks.size());
107+
keyBlobs.add(new KeyBlob(dataKey));
108+
109+
for (int i = 1; i < mks.size(); i++) {
110+
//noinspection unchecked
111+
keyBlobs.add(new KeyBlob(mks.get(i).encryptDataKey(algo, context, dataKey)));
112+
}
113+
114+
//noinspection unchecked
115+
return EncryptionMaterials.newBuilder()
116+
.setAlgorithm(algo)
117+
.setCleartextDataKey(dataKey.getKey())
118+
.setEncryptedDataKeys(keyBlobs)
119+
.setEncryptionContext(context)
120+
.setTrailingSignatureKey(trailingKeys == null ? null : trailingKeys.getPrivate())
121+
.setMasterKeys(mks)
122+
.build();
123+
}
124+
125+
@Override
126+
public DecryptionMaterials decryptMaterials(DecryptionMaterialsRequest request) {
127+
DataKey<?> dataKey =
128+
mkp.decryptDataKey(
129+
request.getAlgorithm(), request.getEncryptedDataKeys(), request.getEncryptionContext());
130+
131+
if (dataKey == null) {
132+
throw new CannotUnwrapDataKeyException("Could not decrypt any data keys");
133+
}
134+
135+
PublicKey pubKey = null;
136+
if (request.getAlgorithm().getTrailingSignatureLength() > 0) {
137+
try {
138+
String serializedPubKey = request.getEncryptionContext().get(Constants.EC_PUBLIC_KEY_FIELD);
139+
140+
if (serializedPubKey == null) {
141+
throw new AwsCryptoException("Missing trailing signature public key");
142+
}
143+
144+
pubKey = deserializeTrailingKeyFromEc(request.getAlgorithm(), serializedPubKey);
145+
} catch (final IllegalStateException ex) {
146+
throw new AwsCryptoException(ex);
147+
}
148+
} else if (request.getEncryptionContext().containsKey(Constants.EC_PUBLIC_KEY_FIELD)) {
149+
throw new AwsCryptoException("Trailing signature public key found for non-signed algorithm");
150+
}
151+
152+
return DecryptionMaterials.newBuilder()
153+
.setDataKey(dataKey)
154+
.setTrailingSignatureKey(pubKey)
155+
.build();
156+
}
157+
158+
private PublicKey deserializeTrailingKeyFromEc(CryptoAlgorithm algo, String pubKey) {
159+
return TrailingSignatureAlgorithm.forCryptoAlgorithm(algo).deserializePublicKey(pubKey);
160+
}
161+
162+
private static String serializeTrailingKeyForEc(CryptoAlgorithm algo, KeyPair trailingKeys) {
163+
return TrailingSignatureAlgorithm.forCryptoAlgorithm(algo)
164+
.serializePublicKey(trailingKeys.getPublic());
165+
}
166+
167+
private static KeyPair generateTrailingSigKeyPair(CryptoAlgorithm algo)
168+
throws GeneralSecurityException {
169+
return TrailingSignatureAlgorithm.forCryptoAlgorithm(algo).generateKey();
170+
}
171+
}

0 commit comments

Comments
 (0)