com.amazonaws.encryptionsdk.internal.DecryptionHandler Maven / Gradle / Ivy
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package com.amazonaws.encryptionsdk.internal;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import com.amazonaws.encryptionsdk.CommitmentPolicy;
import com.amazonaws.encryptionsdk.CryptoAlgorithm;
import com.amazonaws.encryptionsdk.CryptoMaterialsManager;
import com.amazonaws.encryptionsdk.DataKey;
import com.amazonaws.encryptionsdk.DefaultCryptoMaterialsManager;
import com.amazonaws.encryptionsdk.MasterKey;
import com.amazonaws.encryptionsdk.MasterKeyProvider;
import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
import com.amazonaws.encryptionsdk.exception.BadCiphertextException;
import com.amazonaws.encryptionsdk.model.CiphertextFooters;
import com.amazonaws.encryptionsdk.model.CiphertextHeaders;
import com.amazonaws.encryptionsdk.model.CiphertextType;
import com.amazonaws.encryptionsdk.model.ContentType;
import com.amazonaws.encryptionsdk.model.DecryptionMaterialsRequest;
import com.amazonaws.encryptionsdk.model.DecryptionMaterials;
/**
* This class implements the CryptoHandler interface by providing methods for
* the decryption of ciphertext produced by the methods in
* {@link EncryptionHandler}.
*
*
* This class reads and parses the values in the ciphertext headers and
* delegates the decryption of the ciphertext to the
* {@link BlockDecryptionHandler} or {@link FrameDecryptionHandler} based on the
* content type parsed in the ciphertext headers.
*/
public class DecryptionHandler> implements MessageCryptoHandler {
private final CryptoMaterialsManager materialsManager_;
private final CommitmentPolicy commitmentPolicy_;
private final CiphertextHeaders ciphertextHeaders_;
private final CiphertextFooters ciphertextFooters_;
private boolean ciphertextHeadersParsed_;
private CryptoHandler contentCryptoHandler_;
private DataKey dataKey_;
private SecretKey decryptionKey_;
private CryptoAlgorithm cryptoAlgo_;
private Signature trailingSig_;
private Map encryptionContext_ = null;
private byte[] unparsedBytes_ = new byte[0];
private boolean complete_ = false;
private long ciphertextSizeBound_ = -1;
private long ciphertextBytesSupplied_ = 0;
// These ctors are private to ensure type safety - we must ensure construction using a CMM results in a
// DecryptionHandler>, not a DecryptionHandler, since the CryptoMaterialsManager is not itself
// genericized.
private DecryptionHandler(final CryptoMaterialsManager materialsManager, final CommitmentPolicy commitmentPolicy) {
Utils.assertNonNull(materialsManager, "materialsManager");
Utils.assertNonNull(commitmentPolicy, "commitmentPolicy");
this.materialsManager_ = materialsManager;
this.commitmentPolicy_ = commitmentPolicy;
ciphertextHeaders_ = new CiphertextHeaders();
ciphertextFooters_ = new CiphertextFooters();
}
private DecryptionHandler(final CryptoMaterialsManager materialsManager, final CiphertextHeaders headers, final CommitmentPolicy commitmentPolicy)
throws AwsCryptoException
{
Utils.assertNonNull(materialsManager, "materialsManager");
Utils.assertNonNull(commitmentPolicy, "commitmentPolicy");
materialsManager_ = materialsManager;
ciphertextHeaders_ = headers;
commitmentPolicy_ = commitmentPolicy;
ciphertextFooters_ = new CiphertextFooters();
readHeaderFields(headers);
updateTrailingSignature(headers);
}
/**
* Create a decryption handler using the provided master key.
*
*
* Note the methods in the provided master key are used in decrypting the
* encrypted data key parsed from the ciphertext headers.
*
* @param customerMasterKeyProvider
* the master key provider to use in picking a master key from
* the key blobs encoded in the provided ciphertext.
* @throws AwsCryptoException
* if the master key is null.
*/
@SuppressWarnings("unchecked")
public static > DecryptionHandler create(
final MasterKeyProvider customerMasterKeyProvider,
final CommitmentPolicy commitmentPolicy
) throws AwsCryptoException {
Utils.assertNonNull(customerMasterKeyProvider, "customerMasterKeyProvider");
return (DecryptionHandler)create(new DefaultCryptoMaterialsManager(customerMasterKeyProvider), commitmentPolicy);
}
/**
* Create a decryption handler using the provided master key and already parsed {@code headers}.
*
*
* Note the methods in the provided master key are used in decrypting the encrypted data key
* parsed from the ciphertext headers.
*
* @param customerMasterKeyProvider
* the master key provider to use in picking a master key from the key blobs encoded
* in the provided ciphertext.
* @param headers
* already parsed headers which will not be passed into
* {@link #processBytes(byte[], int, int, byte[], int)}
* @throws AwsCryptoException
* if the master key is null.
*/
@SuppressWarnings("unchecked")
public static > DecryptionHandler create(
final MasterKeyProvider customerMasterKeyProvider, final CiphertextHeaders headers,
final CommitmentPolicy commitmentPolicy
) throws AwsCryptoException {
Utils.assertNonNull(customerMasterKeyProvider, "customerMasterKeyProvider");
return (DecryptionHandler) create(new DefaultCryptoMaterialsManager(customerMasterKeyProvider), headers, commitmentPolicy);
}
/**
* Create a decryption handler using the provided materials manager.
*
*
* Note the methods in the provided materials manager are used in decrypting the encrypted data key
* parsed from the ciphertext headers.
*
* @param materialsManager
* the materials manager to use in decrypting the data key from the key blobs encoded
* in the provided ciphertext.
* @throws AwsCryptoException
* if the master key is null.
*/
public static DecryptionHandler> create(
final CryptoMaterialsManager materialsManager,
final CommitmentPolicy commitmentPolicy
) throws AwsCryptoException {
return new DecryptionHandler(materialsManager, commitmentPolicy);
}
/**
* Create a decryption handler using the provided materials manager and already parsed {@code headers}.
*
*
* Note the methods in the provided materials manager are used in decrypting the encrypted data key
* parsed from the ciphertext headers.
*
* @param materialsManager
* the materials manager to use in decrypting the data key from the key blobs encoded
* in the provided ciphertext.
* @param headers
* already parsed headers which will not be passed into
* {@link #processBytes(byte[], int, int, byte[], int)}
* @throws AwsCryptoException
* if the master key is null.
*/
public static DecryptionHandler> create(
final CryptoMaterialsManager materialsManager, final CiphertextHeaders headers,
final CommitmentPolicy commitmentPolicy
) throws AwsCryptoException {
return new DecryptionHandler(materialsManager, headers, commitmentPolicy);
}
/**
* Decrypt the ciphertext bytes provided in {@code in} and copy the plaintext bytes to
* {@code out}.
*
*
* This method consumes and parses the ciphertext headers. The decryption of the actual content
* is delegated to {@link BlockDecryptionHandler} or {@link FrameDecryptionHandler} based on the
* content type parsed in the ciphertext header.
*
* @param in
* the input byte array.
* @param off
* the offset into the in array where the data to be decrypted starts.
* @param len
* the number of bytes to be decrypted.
* @param out
* the output buffer the decrypted plaintext bytes go into.
* @param outOff
* the offset into the output byte array the decrypted data starts at.
* @return the number of bytes written to {@code out} and processed.
*
* @throws BadCiphertextException
* if the ciphertext header contains invalid entries or if the header integrity
* check fails.
* @throws AwsCryptoException
* if any of the offset or length arguments are negative or if the total bytes to
* decrypt exceeds the maximum allowed value.
*/
@Override
public ProcessingSummary processBytes(final byte[] in, final int off, final int len, final byte[] out,
final int outOff)
throws BadCiphertextException, AwsCryptoException {
if (len < 0 || off < 0) {
throw new AwsCryptoException(String.format(
"Invalid values for input offset: %d and length: %d", off, len));
}
if (in.length == 0 || len == 0) {
return ProcessingSummary.ZERO;
}
final long totalBytesToParse = unparsedBytes_.length + (long) len;
// check for integer overflow
if (totalBytesToParse > Integer.MAX_VALUE) {
throw new AwsCryptoException(
"Size of the total bytes to parse and decrypt exceeded allowed maximum:" + Integer.MAX_VALUE);
}
checkSizeBound(len);
ciphertextBytesSupplied_ += len;
final byte[] bytesToParse = new byte[(int) totalBytesToParse];
final int leftoverBytes = unparsedBytes_.length;
// If there were previously unparsed bytes, add them as the first
// set of bytes to be parsed in this call.
System.arraycopy(unparsedBytes_, 0, bytesToParse, 0, unparsedBytes_.length);
System.arraycopy(in, off, bytesToParse, unparsedBytes_.length, len);
int totalParsedBytes = 0;
if (ciphertextHeadersParsed_ == false) {
totalParsedBytes += ciphertextHeaders_.deserialize(bytesToParse, 0);
// When ciphertext headers are complete, we have the data
// key and cipher mode to initialize the underlying cipher
if (ciphertextHeaders_.isComplete() == true) {
readHeaderFields(ciphertextHeaders_);
updateTrailingSignature(ciphertextHeaders_);
// reset unparsed bytes as parsing of ciphertext headers is
// complete.
unparsedBytes_ = new byte[0];
} else {
// If there aren't enough bytes to parse ciphertext
// headers, we don't have anymore bytes to continue parsing.
// But first copy the leftover bytes to unparsed bytes.
unparsedBytes_ = Arrays.copyOfRange(bytesToParse, totalParsedBytes, bytesToParse.length);
return new ProcessingSummary(0, len);
}
}
int actualOutLen = 0;
if (!contentCryptoHandler_.isComplete()) {
// if there are bytes to parse further, pass it off to underlying
// content cryptohandler.
if ((bytesToParse.length - totalParsedBytes) > 0) {
final ProcessingSummary contentResult = contentCryptoHandler_.processBytes(bytesToParse,
totalParsedBytes, bytesToParse.length - totalParsedBytes,
out, outOff);
updateTrailingSignature(bytesToParse, totalParsedBytes, contentResult.getBytesProcessed());
actualOutLen = contentResult.getBytesWritten();
totalParsedBytes += contentResult.getBytesProcessed();
}
if (contentCryptoHandler_.isComplete()) {
actualOutLen += contentCryptoHandler_.doFinal(out, outOff + actualOutLen);
}
}
if (contentCryptoHandler_.isComplete() ) {
// If the crypto algorithm contains trailing signature, we will need to verify
// the footer of the message.
if (cryptoAlgo_.getTrailingSignatureLength() > 0) {
totalParsedBytes += ciphertextFooters_.deserialize(bytesToParse, totalParsedBytes);
if (ciphertextFooters_.isComplete() && trailingSig_ != null) {
try {
if (!trailingSig_.verify(ciphertextFooters_.getMAuth())) {
throw new BadCiphertextException("Bad trailing signature");
}
} catch (final SignatureException ex) {
throw new BadCiphertextException("Bad trailing signature", ex);
}
complete_ = true;
}
} else {
complete_ = true;
}
}
return new ProcessingSummary(actualOutLen, totalParsedBytes - leftoverBytes);
}
/**
* Finish processing of the bytes.
*
* @param out
* space for any resulting output data.
* @param outOff
* offset into {@code out} to start copying the data at.
* @return
* number of bytes written into {@code out}.
* @throws BadCiphertextException
* if the bytes do not decrypt correctly.
*/
@Override
public int doFinal(final byte[] out, final int outOff) throws BadCiphertextException {
// check if cryptohandler for content has been created. There are cases
// when it might not have been created such as when doFinal() is called
// before the ciphertext headers are fully received and parsed.
if (contentCryptoHandler_ == null) {
return 0;
} else {
int result = contentCryptoHandler_.doFinal(out, outOff);
if (!ciphertextHeaders_.isComplete() || !contentCryptoHandler_.isComplete()) {
throw new BadCiphertextException("Unable to process entire ciphertext.");
}
return result;
}
}
/**
* Return the size of the output buffer required for a
* processBytes
plus a doFinal
with an input of
* inLen bytes.
*
* @param inLen
* the length of the input.
* @return
* the space required to accommodate a call to processBytes and
* doFinal with input of size {@code inLen} bytes.
*/
@Override
public int estimateOutputSize(final int inLen) {
if (contentCryptoHandler_ != null) {
return contentCryptoHandler_.estimateOutputSize(inLen);
} else {
return (inLen > 0) ? inLen : 0;
}
}
@Override
public int estimatePartialOutputSize(int inLen) {
if (contentCryptoHandler_ != null) {
return contentCryptoHandler_.estimatePartialOutputSize(inLen);
} else {
return (inLen > 0) ? inLen : 0;
}
}
@Override
public int estimateFinalOutputSize() {
if (contentCryptoHandler_ != null) {
return contentCryptoHandler_.estimateFinalOutputSize();
} else {
return 0;
}
}
/**
* Return the encryption context. This value is parsed from the ciphertext.
*
* @return
* the key-value map containing the encryption client.
*/
@Override
public Map getEncryptionContext() {
return encryptionContext_;
}
private void checkSizeBound(long additionalBytes) {
if (ciphertextSizeBound_ != -1 && ciphertextBytesSupplied_ + additionalBytes > ciphertextSizeBound_) {
throw new IllegalStateException("Ciphertext size exceeds size bound");
}
}
@Override
public void setMaxInputLength(long size) {
if (size < 0) {
throw Utils.cannotBeNegative("Max input length");
}
if (ciphertextSizeBound_ != -1 && ciphertextSizeBound_ < size) {
ciphertextSizeBound_ = size;
}
// check that we haven't already exceeded the limit
checkSizeBound(0);
}
/**
* Check integrity of the header bytes by processing the parsed MAC tag in
* the headers through the cipher.
*
* @param ciphertextHeaders
* the ciphertext headers object whose integrity needs to be
* checked.
* @return
* true if the integrity of the header is intact; false otherwise.
*/
private void verifyHeaderIntegrity(final CiphertextHeaders ciphertextHeaders) throws BadCiphertextException {
final CipherHandler cipherHandler = new CipherHandler(decryptionKey_, Cipher.DECRYPT_MODE, cryptoAlgo_);
try {
final byte[] headerTag = ciphertextHeaders.getHeaderTag();
cipherHandler.cipherData(ciphertextHeaders.getHeaderNonce(),
ciphertextHeaders.serializeAuthenticatedFields(),
headerTag, 0, headerTag.length);
} catch (BadCiphertextException e) {
throw new BadCiphertextException("Header integrity check failed.", e);
}
}
/**
* Read the fields in the ciphertext headers to populate the corresponding
* instance variables used during decryption.
*
* @param ciphertextHeaders
* the ciphertext headers object to read.
*/
@SuppressWarnings("unchecked")
private void readHeaderFields(final CiphertextHeaders ciphertextHeaders) {
cryptoAlgo_ = ciphertextHeaders.getCryptoAlgoId();
final CiphertextType ciphertextType = ciphertextHeaders.getType();
if (ciphertextType != CiphertextType.CUSTOMER_AUTHENTICATED_ENCRYPTED_DATA) {
throw new BadCiphertextException("Invalid type in ciphertext.");
}
final byte[] messageId = ciphertextHeaders.getMessageId();
if (!commitmentPolicy_.algorithmAllowedForDecrypt(cryptoAlgo_)) {
throw new AwsCryptoException("Configuration conflict. " +
"Cannot decrypt message with ID " + messageId + " due to CommitmentPolicy " +
commitmentPolicy_ + " requiring only committed messages. Algorithm ID was " +
cryptoAlgo_ + ". See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/troubleshooting-migration.html");
}
encryptionContext_ = ciphertextHeaders.getEncryptionContextMap();
DecryptionMaterialsRequest request = DecryptionMaterialsRequest.newBuilder()
.setAlgorithm(cryptoAlgo_)
.setEncryptionContext(encryptionContext_)
.setEncryptedDataKeys(ciphertextHeaders.getEncryptedKeyBlobs())
.build();
DecryptionMaterials result = materialsManager_.decryptMaterials(request);
//noinspection unchecked
dataKey_ = (DataKey)result.getDataKey();
PublicKey trailingPublicKey = result.getTrailingSignatureKey();
try {
decryptionKey_ = cryptoAlgo_.getEncryptionKeyFromDataKey(dataKey_.getKey(), ciphertextHeaders);
} catch (final InvalidKeyException ex) {
throw new AwsCryptoException(ex);
}
if (cryptoAlgo_.getTrailingSignatureLength() > 0) {
Utils.assertNonNull(trailingPublicKey, "trailing public key");
TrailingSignatureAlgorithm trailingSignatureAlgorithm
= TrailingSignatureAlgorithm.forCryptoAlgorithm(cryptoAlgo_);
try {
trailingSig_ = Signature.getInstance(
trailingSignatureAlgorithm.getHashAndSignAlgorithm()
);
trailingSig_.initVerify(trailingPublicKey);
} catch (GeneralSecurityException e) {
throw new AwsCryptoException(e);
}
} else {
if (trailingPublicKey != null) {
throw new AwsCryptoException("Unexpected trailing signature key in context");
}
trailingSig_ = null;
}
final ContentType contentType = ciphertextHeaders.getContentType();
final short nonceLen = ciphertextHeaders.getNonceLength();
final int frameLen = ciphertextHeaders.getFrameLength();
verifyHeaderIntegrity(ciphertextHeaders);
switch (contentType) {
case FRAME:
contentCryptoHandler_ = new FrameDecryptionHandler(decryptionKey_, (byte) nonceLen, cryptoAlgo_,
messageId, frameLen);
break;
case SINGLEBLOCK:
contentCryptoHandler_ = new BlockDecryptionHandler(decryptionKey_, (byte) nonceLen, cryptoAlgo_,
messageId);
break;
default:
// should never get here because an invalid content type is
// detected when parsing.
break;
}
ciphertextHeadersParsed_ = true;
}
private void updateTrailingSignature(final CiphertextHeaders headers) {
if (trailingSig_ != null) {
final byte[] reserializedHeaders = headers.toByteArray();
updateTrailingSignature(reserializedHeaders, 0, reserializedHeaders.length);
}
}
private void updateTrailingSignature(byte[] input, int offset, int len) {
if (trailingSig_ != null) {
try {
trailingSig_.update(input, offset, len);
} catch (final SignatureException ex) {
throw new AwsCryptoException(ex);
}
}
}
@Override
public CiphertextHeaders getHeaders() {
return ciphertextHeaders_;
}
@Override
public List getMasterKeys() {
return Collections.singletonList(dataKey_.getMasterKey());
}
@Override
public boolean isComplete() {
return complete_;
}
}