All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager Maven / Gradle / Ivy

package com.amazonaws.encryptionsdk.caching;

import com.amazonaws.encryptionsdk.CryptoAlgorithm;
import com.amazonaws.encryptionsdk.CryptoMaterialsManager;
import com.amazonaws.encryptionsdk.DefaultCryptoMaterialsManager;
import com.amazonaws.encryptionsdk.MasterKeyProvider;
import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
import com.amazonaws.encryptionsdk.internal.EncryptionContextSerializer;
import com.amazonaws.encryptionsdk.internal.Utils;
import com.amazonaws.encryptionsdk.model.DecryptionMaterials;
import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest;
import com.amazonaws.encryptionsdk.model.EncryptionMaterials;
import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest;
import com.amazonaws.encryptionsdk.model.KeyBlob;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * The CachingCryptoMaterialsManager wraps another {@link CryptoMaterialsManager}, and caches its
 * results. This helps reduce the number of calls made to the underlying {@link
 * CryptoMaterialsManager} and/or {@link MasterKeyProvider}, which may help reduce cost and/or
 * improve performance.
 *
 * 

The CachingCryptoMaterialsManager helps enforce a number of usage limits on encrypt. * Specifically, it limits the number of individual messages encrypted with a particular data key, * and the number of plaintext bytes encrypted with the same data key. It also allows you to * configure a maximum time-to-live for cache entries. * *

Note that when performing streaming encryption operations, unless you set the stream size * before writing any data using {@link * com.amazonaws.encryptionsdk.CryptoOutputStream#setMaxInputLength(long)} or {@link * com.amazonaws.encryptionsdk.CryptoInputStream#setMaxInputLength(long)}, the size of the message * will not be known, and to avoid exceeding byte use limits, caching will not be performed. * *

By default, two different {@link CachingCryptoMaterialsManager}s will not share cached * entries, even when using the same {@link CryptoMaterialsCache}. However, it's possible to make * different {@link CachingCryptoMaterialsManager}s share the same cached entries by assigning a * partition ID to them; all {@link CachingCryptoMaterialsManager}s with the same partition ID will * share the same cached entries. * *

Assigning partition IDs manually requires great care; if the backing {@link * CryptoMaterialsManager}s are not equivalent, having entries cross over between them can result in * problems such as encrypting messages to the wrong key, or accidentally bypassing access controls. * For this reason we recommend not supplying a partition ID unless required for your use case. */ public class CachingCryptoMaterialsManager implements CryptoMaterialsManager { private static final String CACHE_ID_HASH_ALGORITHM = "SHA-512"; private static final long MAX_MESSAGE_USE_LIMIT = 1L << 32; private static final long MAX_BYTE_USE_LIMIT = Long.MAX_VALUE; // 2^63 - 1 private final CryptoMaterialsManager backingCMM; private final CryptoMaterialsCache cache; private final byte[] partitionIdHash; private final String partitionId; private final long maxAgeMs; private final long messageUseLimit; private final long byteUseLimit; private final CryptoMaterialsCache.CacheHint hint = new CryptoMaterialsCache.CacheHint() { @Override public long getMaxAgeMillis() { return maxAgeMs; } }; public static class Builder { private CryptoMaterialsManager backingCMM; private CryptoMaterialsCache cache; private String partitionId = null; private long maxAge = 0; private long messageUseLimit = MAX_MESSAGE_USE_LIMIT; private long byteUseLimit = Long.MAX_VALUE; private Builder() {} /** * Sets the {@link CryptoMaterialsManager} that should be queried when the {@link * CachingCryptoMaterialsManager} (CCMM) incurs a cache miss. * *

You can set either a MasterKeyProvider or a CryptoMaterialsManager to back the CCMM - the * last value set will be used. * * @param backingCMM The CryptoMaterialsManager to invoke on cache misses * @return this builder */ public Builder withBackingMaterialsManager(CryptoMaterialsManager backingCMM) { this.backingCMM = backingCMM; return this; } /** * Sets the {@link MasterKeyProvider} that should be queried when the {@link * CachingCryptoMaterialsManager} (CCMM) incurs a cache miss. * *

You can set either a MasterKeyProvider or a CryptoMaterialsManager to back the CCMM - the * last value set will be used. * *

This method is equivalent to calling {@link * #withBackingMaterialsManager(CryptoMaterialsManager)} passing a {@link * DefaultCryptoMaterialsManager} constructed using your {@link MasterKeyProvider}. * * @param mkp The MasterKeyProvider to invoke on cache misses * @return this builder */ public Builder withMasterKeyProvider(MasterKeyProvider mkp) { return withBackingMaterialsManager(new DefaultCryptoMaterialsManager(mkp)); } /** * Sets the cache to which this {@link CryptoMaterialsManager} will be bound. * * @param cache The cache to associate with the CMM * @return this builder */ public Builder withCache(CryptoMaterialsCache cache) { this.cache = cache; return this; } /** * Sets the partition ID for this CMM. This is an optional operation. * *

By default, two CMMs will never use each other's cache entries. This helps ensure that * CMMs with different delegates won't incorrectly use each other's encrypt and decrypt results. * However, in certain special circumstances it can be useful to share entries between different * CMMs - for example, if the backing CMM is constructed based on some parameters that depend on * the operation, you may wish for delegates constructed with the same parameters to share the * same partition. * *

To accomplish this, set the same partition ID and backing cache on both CMMs; entries * cached from one of these CMMs can then be used by the other. This should only be done with * careful consideration and verification that the CMM delegates are equivalent for your * application's purposes. * *

By default, the partition ID is set to a random UUID to avoid any collisions. * * @param partitionId The partition ID * @return this builder */ public Builder withPartitionId(String partitionId) { this.partitionId = partitionId; return this; } /** * Sets the maximum lifetime for entries in the cache, for both encrypt and decrypt operations. * When the specified amount of time passes after initial creation of the entry, the entry will * be considered unusable, and the next operation will incur a cache miss. * * @param maxAge The amount of time entries are allowed to live. Must be positive. * @param units The units maxAge is expressed in * @return this builder */ public Builder withMaxAge(long maxAge, TimeUnit units) { if (maxAge <= 0) { throw new IllegalArgumentException("Max age must be positive"); } this.maxAge = units.toMillis(maxAge); return this; } /** * Sets the maximum number of individual messages that can be encrypted under the same a cached * data key. This does not affect decrypt operations. * *

Specifying this limit is optional; by default, the limit is set to 2^32. This is also the * maximum accepted value; if you specify a higher limit, an {@link IllegalArgumentException} * will be thrown. * * @param messageUseLimit The maximum number of messages that can be encrypted by the same data * key. Must be positive. * @return this builder */ public Builder withMessageUseLimit(long messageUseLimit) { if (messageUseLimit <= 0) { throw new IllegalArgumentException("Message use limit must be positive"); } if (messageUseLimit > MAX_MESSAGE_USE_LIMIT) { throw new IllegalArgumentException( "Message use limit exceeds limit of " + MAX_MESSAGE_USE_LIMIT); } // We limit the number of messages encrypted under the same data key primarily to stay far // away from any // chance of message ID collisions (and therefore collisions of the key+IV used for the actual // message // encryption). this.messageUseLimit = messageUseLimit; return this; } /** * Sets the maximum number of plaintext bytes that can be encrypted under the same a cached data * key. This does not affect decrypt operations. * *

Specifying this limit is optional; by default, the limit is set to 2^63 - 1. * *

While this limit can be set to zero, in this case keys can only be cached if they are used * for zero-length messages. * * @param byteUseLimit The maximum number of bytes that can be encrypted by the same data key. * Must be non-negative. * @return this builder */ public Builder withByteUseLimit(long byteUseLimit) { if (byteUseLimit < 0) { throw new IllegalArgumentException("Byte use limit must be non-negative"); } // Currently, since the byte use limit is Long.MAX_VALUE, this can't be reached, but is // included for // consistency. //noinspection ConstantConditions if (byteUseLimit > MAX_BYTE_USE_LIMIT) { throw new IllegalArgumentException( "Byte use limit exceeds maximum of " + MAX_BYTE_USE_LIMIT); } this.byteUseLimit = byteUseLimit; return this; } public CachingCryptoMaterialsManager build() { if (backingCMM == null) { throw new IllegalArgumentException("Backing CMM must be set"); } if (cache == null) { throw new IllegalArgumentException("Cache must be set"); } if (maxAge <= 0) { throw new IllegalArgumentException("Max age must be set"); } return new CachingCryptoMaterialsManager(this); } } public static Builder newBuilder() { return new Builder(); } private CachingCryptoMaterialsManager(Builder builder) { this.backingCMM = builder.backingCMM; this.cache = builder.cache; this.partitionId = builder.partitionId != null ? builder.partitionId : UUID.randomUUID().toString(); this.maxAgeMs = builder.maxAge; this.messageUseLimit = builder.messageUseLimit; this.byteUseLimit = builder.byteUseLimit; try { this.partitionIdHash = MessageDigest.getInstance(CACHE_ID_HASH_ALGORITHM) .digest(partitionId.getBytes(StandardCharsets.UTF_8)); } catch (GeneralSecurityException e) { throw new AwsCryptoException(e); } } @Override public EncryptionMaterials getMaterialsForEncrypt(EncryptionMaterialsRequest request) { // We cannot correctly enforce size limits if the request has no known plaintext size, so bypass // the cache in // this case. if (request.getPlaintextSize() == -1) { return backingCMM.getMaterialsForEncrypt(request); } // Strip off information on the plaintext length & contents - we do this because we will be // (potentially) // reusing the result from the backing CMM across multiple requests, and as such it would be // misleading to pass on // the first such request's information to the backing CMM. EncryptionMaterialsRequest upstreamRequest = request.toBuilder().setPlaintext(null).setPlaintextSize(-1).build(); byte[] cacheId = getCacheIdentifier(upstreamRequest); CryptoMaterialsCache.UsageStats increment = initialIncrementForRequest(request); // If our plaintext size is such that even a brand new entry would reach or exceed cache limits, // there's no // point in accessing the cache - in fact, doing so would poison a cache entry that could // potentially be still // used for a smaller request. So we'll bypass the cache and just call the backing CMM directly // in this case. if (increment.getBytesEncrypted() >= byteUseLimit) { return backingCMM.getMaterialsForEncrypt(request); } CryptoMaterialsCache.EncryptCacheEntry entry = cache.getEntryForEncrypt(cacheId, increment); if (entry != null && !isEntryExpired(entry.getEntryCreationTime()) && !hasExceededLimits(entry.getUsageStats())) { return entry.getResult(); } else if (entry != null) { // entry has potentially expired, so hint to the cache that it should be removed, in case the // cache stores // multiple entries or something entry.invalidate(); } // Cache miss. EncryptionMaterials result = backingCMM.getMaterialsForEncrypt(request); if (result.getAlgorithm().isSafeToCache()) { cache.putEntryForEncrypt(cacheId, result, hint, initialIncrementForRequest(request)); } return result; } private boolean hasExceededLimits(final CryptoMaterialsCache.UsageStats stats) { return stats.getBytesEncrypted() > byteUseLimit || stats.getMessagesEncrypted() > messageUseLimit; } private boolean isEntryExpired(final long entryCreationTime) { return System.currentTimeMillis() - entryCreationTime > maxAgeMs; } private CryptoMaterialsCache.UsageStats initialIncrementForRequest( EncryptionMaterialsRequest request) { return new CryptoMaterialsCache.UsageStats(request.getPlaintextSize(), 1); } @Override public DecryptionMaterials decryptMaterials(DecryptionMaterialsRequest request) { byte[] cacheId = getCacheIdentifier(request); CryptoMaterialsCache.DecryptCacheEntry entry = cache.getEntryForDecrypt(cacheId); if (entry != null && !isEntryExpired(entry.getEntryCreationTime())) { return entry.getResult(); } DecryptionMaterials result = backingCMM.decryptMaterials(request); cache.putEntryForDecrypt(cacheId, result, hint); return result; } private byte[] getCacheIdentifier(EncryptionMaterialsRequest req) { try { MessageDigest digest = MessageDigest.getInstance(CACHE_ID_HASH_ALGORITHM); digest.update(partitionIdHash); CryptoAlgorithm algorithm = req.getRequestedAlgorithm(); digest.update((byte) (algorithm != null ? 1 : 0)); if (algorithm != null) { updateDigestWithAlgorithm(digest, algorithm); } digest.update( MessageDigest.getInstance(CACHE_ID_HASH_ALGORITHM) .digest(EncryptionContextSerializer.serialize(req.getContext()))); return digest.digest(); } catch (GeneralSecurityException e) { throw new AwsCryptoException(e); } } private byte[] getCacheIdentifier(DecryptionMaterialsRequest req) { try { MessageDigest digest = MessageDigest.getInstance(CACHE_ID_HASH_ALGORITHM); byte[] hashOfContext = digest.digest(EncryptionContextSerializer.serialize(req.getEncryptionContext())); ArrayList keyBlobHashes = new ArrayList<>(req.getEncryptedDataKeys().size()); for (KeyBlob blob : req.getEncryptedDataKeys()) { keyBlobHashes.add(digest.digest(blob.toByteArray())); } keyBlobHashes.sort(new Utils.ComparingByteArrays()); // Now starting the digest of the actual cache identifier digest.update(partitionIdHash); updateDigestWithAlgorithm(digest, req.getAlgorithm()); keyBlobHashes.forEach(digest::update); // This all-zero sentinel field indicates the end of the key blob hashes. digest.update(new byte[digest.getDigestLength()]); digest.update(hashOfContext); return digest.digest(); } catch (GeneralSecurityException e) { throw new AwsCryptoException(e); } } // Common helper to add the algorithm identifier (in proper big endian order) for both encrypt and // decrypt paths. private void updateDigestWithAlgorithm(MessageDigest digest, CryptoAlgorithm algorithm) { short algId = algorithm.getValue(); digest.update(new byte[] {(byte) (algId >> 8), (byte) (algId)}); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy