Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,25 @@
import aws.cryptography.esdk.testserver.server.registry.EsdkClient;
import aws.cryptography.esdk.testserver.server.registry.RealEsdkClient;
import com.amazonaws.encryptionsdk.CommitmentPolicy;
import com.amazonaws.encryptionsdk.MasterKeyProvider;
import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager;
import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCache;
import com.amazonaws.encryptionsdk.jce.JceMasterKey;
import com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider;
import com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider;
import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.crypto.spec.SecretKeySpec;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.kms.KmsClient;
Expand Down Expand Up @@ -103,8 +117,15 @@
* that case the factory fetches it once via {@code kms:GetPublicKey} at
* construction (a network call the design explicitly permits at
* {@code CreateClient} time); supplying {@code publicKey} in the config keeps
* construction fully offline. The Caching CMM remains unwired and causes a
* construction failure (GenericServerError) if requested (Requirement 3.6).
* construction fully offline.
*
* <p>The Caching CMM is the ESDK's {@code CachingCryptoMaterialsManager}: the
* Material Providers Library has no caching CMM, and the ESDK's caching CMM wraps
* master key providers rather than MPL keyrings. So a top-level Caching config is
* built over master key providers equivalent to its wrapped Default CMM's keyring
* (Raw AES and Raw RSA as {@code JceMasterKey}, the AWS KMS keyrings as the KMS
* master key providers, and Multi as a multi-provider); any other keyring, or a
* Caching CMM nested under another CMM, is a construction failure.
*/
public final class EsdkClientFactory {

Expand All @@ -127,6 +148,10 @@ public EsdkClient create(ESDKClientConfig config) {
Integer maxEdk = config.getMaxEncryptedDataKeys() == null
? null
: Math.toIntExact(config.getMaxEncryptedDataKeys());
CachingCmmConfig caching = config.getCmm().getCaching();
if (caching != null) {
return new RealEsdkClient(commitmentPolicy, maxEdk, buildCachingCmm(caching));
}
ICryptographicMaterialsManager cmm = buildCmm(config.getCmm());
return new RealEsdkClient(commitmentPolicy, maxEdk, cmm);
}
Expand Down Expand Up @@ -154,12 +179,154 @@ private ICryptographicMaterialsManager buildCmm(CryptographicMaterialsManager cm
}
if (caching != null) {
throw new UnsupportedOperationException(
"Caching CMM is not wired in this pass of the ESDK TestServer");
"a Caching CMM is supported only as the client's top-level CMM");
}
throw new IllegalArgumentException(
"CryptographicMaterialsManager had no variant member set");
}

/** Entries the Caching CMM's local cache holds. */
private static final int CACHE_CAPACITY = 100;

private static CachingCryptoMaterialsManager buildCachingCmm(CachingCmmConfig config) {
DefaultCmmConfig underlying = config.getUnderlyingCMM().getDefault();
if (underlying == null) {
throw new UnsupportedOperationException(
"the Caching CMM must wrap a Default CMM: the ESDK's caching CMM wraps master key providers");
}
CachingCryptoMaterialsManager.Builder builder = CachingCryptoMaterialsManager.newBuilder()
.withMasterKeyProvider(toMasterKeyProvider(underlying.getKeyring()))
.withCache(new LocalCryptoMaterialsCache(CACHE_CAPACITY))
.withMaxAge(config.getCacheLimitTtlSeconds(), TimeUnit.SECONDS);
if (config.getPartitionId() != null) {
builder.withPartitionId(config.getPartitionId());
}
if (config.getLimitMessages() != null) {
builder.withMessageUseLimit(config.getLimitMessages());
}
if (config.getLimitBytes() != null) {
builder.withByteUseLimit(config.getLimitBytes());
}
return builder.build();
}

/**
* The master key provider equivalent to {@code keyring}: it writes and reads
* the same encrypted data keys as the MPL keyring of the same configuration.
*/
private static MasterKeyProvider<?> toMasterKeyProvider(Keyring keyring) {
if (keyring.getRawAes() != null) {
RawAesKeyringConfig rawAes = keyring.getRawAes();
return JceMasterKey.getInstance(
new SecretKeySpec(bytes(rawAes.getWrappingKey()), "AES"),
rawAes.getKeyNamespace(), rawAes.getKeyName(), "AES/GCM/NoPadding");
}
if (keyring.getRawRsa() != null) {
RawRsaKeyringConfig rawRsa = keyring.getRawRsa();
return JceMasterKey.getInstance(
rawRsa.getPublicKey() == null ? null : rsaPublicKey(rawRsa.getPublicKey()),
rawRsa.getPrivateKey() == null ? null : rsaPrivateKey(rawRsa.getPrivateKey()),
rawRsa.getKeyNamespace(), rawRsa.getKeyName(),
rsaTransformation(rawRsa.getPaddingScheme().getValue()));
}
if (keyring.getAwsKms() != null) {
requireNoGrantTokens(keyring.getAwsKms().hasGrantTokens()
&& !keyring.getAwsKms().getGrantTokens().isEmpty());
return KmsMasterKeyProvider.builder().buildStrict(keyring.getAwsKms().getKmsKeyId());
}
if (keyring.getAwsKmsMrk() != null) {
requireNoGrantTokens(keyring.getAwsKmsMrk().hasGrantTokens()
&& !keyring.getAwsKmsMrk().getGrantTokens().isEmpty());
return AwsKmsMrkAwareMasterKeyProvider.builder()
.buildStrict(keyring.getAwsKmsMrk().getKmsKeyId());
}
if (keyring.getAwsKmsMultiKeyring() != null) {
AwsKmsMultiKeyringConfig multi = keyring.getAwsKmsMultiKeyring();
requireNoGrantTokens(multi.hasGrantTokens() && !multi.getGrantTokens().isEmpty());
return KmsMasterKeyProvider.builder().buildStrict(kmsKeyIds(multi.getGenerator(),
multi.hasKmsKeyIds() ? multi.getKmsKeyIds() : List.of()));
}
if (keyring.getAwsKmsMrkMultiKeyring() != null) {
AwsKmsMrkMultiKeyringConfig multi = keyring.getAwsKmsMrkMultiKeyring();
requireNoGrantTokens(multi.hasGrantTokens() && !multi.getGrantTokens().isEmpty());
return AwsKmsMrkAwareMasterKeyProvider.builder().buildStrict(kmsKeyIds(multi.getGenerator(),
multi.hasKmsKeyIds() ? multi.getKmsKeyIds() : List.of()));
}
if (keyring.getMulti() != null) {
MultiKeyringConfig multi = keyring.getMulti();
List<MasterKeyProvider<?>> providers = new ArrayList<>();
if (multi.getGenerator() != null) {
providers.add(toMasterKeyProvider(multi.getGenerator()));
}
for (Keyring child : multi.getChildKeyrings()) {
providers.add(toMasterKeyProvider(child));
}
return MultipleProviderFactory.buildMultiProvider(providers);
}
throw new UnsupportedOperationException(
"this keyring is not supported under the Caching CMM: the ESDK's caching CMM wraps"
+ " master key providers, and only the Raw AES, Raw RSA, AWS KMS, AWS KMS MRK, AWS KMS"
+ " (MRK) multi, and Multi keyrings have master key provider equivalents");
}

private static List<String> kmsKeyIds(String generator, List<String> keyIds) {
List<String> ids = new ArrayList<>();
if (generator != null) {
ids.add(generator);
}
ids.addAll(keyIds);
return ids;
}

private static void requireNoGrantTokens(boolean present) {
if (present) {
throw new UnsupportedOperationException(
"grant tokens are not supported under the Caching CMM");
}
}

/** The {@code JceMasterKey} RSA transformation for a modeled padding scheme. */
private static String rsaTransformation(String paddingScheme) {
return switch (paddingScheme) {
case "PKCS1" -> "RSA/ECB/PKCS1Padding";
case "OAEP_SHA1_MGF1" -> "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
case "OAEP_SHA256_MGF1" -> "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
case "OAEP_SHA384_MGF1" -> "RSA/ECB/OAEPWithSHA-384AndMGF1Padding";
case "OAEP_SHA512_MGF1" -> "RSA/ECB/OAEPWithSHA-512AndMGF1Padding";
default -> throw new IllegalArgumentException("Unknown RSA padding scheme: " + paddingScheme);
};
}

private static PublicKey rsaPublicKey(ByteBuffer pem) {
try {
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(pemBody(pem)));
} catch (Exception e) {
throw new IllegalArgumentException("invalid PEM RSA public key", e);
}
}

private static PrivateKey rsaPrivateKey(ByteBuffer pem) {
try {
return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(pemBody(pem)));
} catch (Exception e) {
throw new IllegalArgumentException("invalid PEM RSA private key", e);
}
}

/** The DER bytes inside a PEM block. */
private static byte[] pemBody(ByteBuffer pem) {
String text = new String(bytes(pem), StandardCharsets.US_ASCII)
.replaceAll("-----[A-Z ]+-----", "")
.replaceAll("\\s", "");
return Base64.getDecoder().decode(text);
}

private static byte[] bytes(ByteBuffer buffer) {
byte[] copy = new byte[buffer.remaining()];
buffer.duplicate().get(copy);
return copy;
}

private IKeyring buildKeyring(Keyring keyring) {
RawAesKeyringConfig rawAes = keyring.getRawAes();
RawRsaKeyringConfig rawRsa = keyring.getRawRsa();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.amazonaws.encryptionsdk.CommitmentPolicy;
import com.amazonaws.encryptionsdk.CryptoAlgorithm;
import com.amazonaws.encryptionsdk.CryptoInputStream;
import com.amazonaws.encryptionsdk.CryptoMaterialsManager;
import com.amazonaws.encryptionsdk.CryptoResult;
import java.io.InputStream;
import java.io.OutputStream;
Expand All @@ -16,7 +17,9 @@
* A configured {@link EsdkClient} backed by the REAL AWS Encryption SDK for Java.
* It pairs an {@link AwsCrypto} configuration (commitment policy, optional max
* encrypted data keys) with a real cryptographic materials manager built from the
* modeled config by {@code EsdkClientFactory}.
* modeled config by {@code EsdkClientFactory}: a Material Providers Library CMM, or,
* for the Caching CMM, the ESDK's own {@link CryptoMaterialsManager}
* ({@code CachingCryptoMaterialsManager}), which wraps master key providers.
*
* <p>Every call reaches the real ESDK. Any exception the ESDK throws is caught and
* rethrown as an {@link EsdkClientException} so the operation wrapper forwards it
Expand All @@ -28,23 +31,38 @@ public final class RealEsdkClient implements EsdkClient {

private final CommitmentPolicy commitmentPolicy;
private final Integer maxEncryptedDataKeys;
/** Exactly one of {@code cmm} and {@code legacyCmm} is set. */
private final ICryptographicMaterialsManager cmm;
private final CryptoMaterialsManager legacyCmm;

public RealEsdkClient(CommitmentPolicy commitmentPolicy,
Integer maxEncryptedDataKeys,
ICryptographicMaterialsManager cmm) {
this(commitmentPolicy, maxEncryptedDataKeys, cmm, null);
}

public RealEsdkClient(CommitmentPolicy commitmentPolicy,
Integer maxEncryptedDataKeys,
CryptoMaterialsManager legacyCmm) {
this(commitmentPolicy, maxEncryptedDataKeys, null, legacyCmm);
}

private RealEsdkClient(CommitmentPolicy commitmentPolicy, Integer maxEncryptedDataKeys,
ICryptographicMaterialsManager cmm, CryptoMaterialsManager legacyCmm) {
this.commitmentPolicy = commitmentPolicy;
this.maxEncryptedDataKeys = maxEncryptedDataKeys;
this.cmm = cmm;
this.legacyCmm = legacyCmm;
}

@Override
public byte[] encrypt(byte[] plaintext, Map<String, String> encryptionContext,
String algorithmSuiteId, Long frameLength) throws EsdkClientException {
try {
AwsCrypto crypto = buildCrypto(algorithmSuiteId, frameLength);
CryptoResult<byte[], ?> result =
crypto.encryptData(cmm, plaintext, nonNull(encryptionContext));
CryptoResult<byte[], ?> result = legacyCmm != null
? crypto.encryptData(legacyCmm, plaintext, nonNull(encryptionContext))
: crypto.encryptData(cmm, plaintext, nonNull(encryptionContext));
return result.getResult();
} catch (Exception esdkFailure) {
throw new EsdkClientException(esdkFailure);
Expand All @@ -57,6 +75,11 @@ public byte[] decrypt(byte[] ciphertext, Map<String, String> encryptionContext)
try {
AwsCrypto crypto = buildCrypto(null, null);
Map<String, String> ec = nonNull(encryptionContext);
if (legacyCmm != null) {
CryptoResult<byte[], ?> result = crypto.decryptData(legacyCmm, ciphertext);
requireReproducedContext(ec, result.getEncryptionContext());
return result.getResult();
}
CryptoResult<byte[], ?> result = ec.isEmpty()
? crypto.decryptData(cmm, ciphertext)
: crypto.decryptData(cmm, ciphertext, ec);
Expand All @@ -79,8 +102,9 @@ public void encryptStream(InputStream plaintext, OutputStream ciphertext,
// stream round trip (Requirement 4.9); the read-side form finalizes correctly
// for all inputs including empty. Both drive the real ESDK streaming encrypt
// API (Requirement 4.5).
try (CryptoInputStream<?> encrypting =
crypto.createEncryptingStream(cmm, plaintext, nonNull(encryptionContext))) {
try (CryptoInputStream<?> encrypting = legacyCmm != null
? crypto.createEncryptingStream(legacyCmm, plaintext, nonNull(encryptionContext))
: crypto.createEncryptingStream(cmm, plaintext, nonNull(encryptionContext))) {
encrypting.transferTo(ciphertext);
}
} catch (Exception esdkFailure) {
Expand All @@ -98,9 +122,15 @@ public void decryptStream(InputStream ciphertext, OutputStream plaintext,
// Required-Encryption-Context CMM (which drops the required keys from the
// message header) can reconstruct them — mirroring the blob decrypt path.
// Both drive the real ESDK streaming decrypt API (Requirement 4.6).
try (CryptoInputStream<?> decrypting = ec.isEmpty()
? crypto.createDecryptingStream(cmm, ciphertext)
: crypto.createDecryptingStream(cmm, ciphertext, ec)) {
try (CryptoInputStream<?> decrypting = legacyCmm != null
? crypto.createDecryptingStream(legacyCmm, ciphertext)
: ec.isEmpty()
? crypto.createDecryptingStream(cmm, ciphertext)
: crypto.createDecryptingStream(cmm, ciphertext, ec)) {
if (legacyCmm != null) {
// Reads only the header, so no plaintext is released on a mismatch.
requireReproducedContext(ec, decrypting.getCryptoResult().getEncryptionContext());
}
decrypting.transferTo(plaintext);
}
} catch (Exception esdkFailure) {
Expand Down Expand Up @@ -134,6 +164,21 @@ private AwsCrypto buildCrypto(String algorithmSuiteId, Long frameLength) {
return builder.build();
}

/**
* The ESDK's own CMMs take no reproduced encryption context on decrypt, so the
* caller checks it against the message's, as the ESDK for Java examples do:
* every reproduced pair must be present with the same value.
*/
private static void requireReproducedContext(Map<String, String> reproduced,
Map<String, String> message) {
for (Map.Entry<String, String> entry : reproduced.entrySet()) {
if (!entry.getValue().equals(message.get(entry.getKey()))) {
throw new IllegalArgumentException(
"the reproduced encryption context does not match the message's encryption context");
}
}
}

private static Map<String, String> nonNull(Map<String, String> encryptionContext) {
return encryptionContext == null ? Collections.emptyMap() : encryptionContext;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import aws.cryptography.esdk.testserver.server.model.Keyring;
import aws.cryptography.esdk.testserver.server.model.MultiKeyringConfig;
import aws.cryptography.esdk.testserver.server.model.RawAesKeyringConfig;
import aws.cryptography.esdk.testserver.server.model.RequiredEncryptionContextCmmConfig;
import aws.cryptography.esdk.testserver.server.registry.ClientRegistry;
import java.nio.ByteBuffer;
import java.util.List;
Expand All @@ -31,8 +32,9 @@
* unchanged, using jqwik. Runs a minimum of 100 generated iterations. Uses the
* real {@link ConfigValidator} and {@link EsdkClientFactory}; the generated
* configs are valid per the exactly-one-variant rule but cause the real ESDK
* client construction to fail deterministically and offline (an unwired
* Caching CMM, an AWS KMS RSA keyring missing its encryption algorithm, or a
* client construction to fail deterministically and offline (a Caching CMM
* wrapping a non-Default CMM, which the ESDK's caching CMM cannot wrap, an AWS
* KMS RSA keyring missing its encryption algorithm, or a
* Raw AES keyring whose wrapping key length does not match its wrapping
* algorithm), nested to varying depth.
*/
Expand Down Expand Up @@ -76,7 +78,12 @@ private static ESDKClientConfig constructionFailingConfig(
CryptographicMaterialsManager cmm = useCachingCmm
? CryptographicMaterialsManager.builder()
.caching(CachingCmmConfig.builder()
.underlyingCMM(validDefaultCmm())
.underlyingCMM(CryptographicMaterialsManager.builder()
.requiredEncryptionContext(RequiredEncryptionContextCmmConfig.builder()
.underlyingCMM(validDefaultCmm())
.requiredEncryptionContextKeys(List.of("purpose"))
.build())
.build())
.cacheLimitTtlSeconds(60)
.build())
.build()
Expand Down
Loading