/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
using System;
using System.IO;
using System.Security.Cryptography;
using Amazon.S3.Model;
using Amazon.Runtime.Internal.Util;
using Amazon.Runtime;
using ThirdParty.Json.LitJson;
using System.Collections.Generic;
using System.Globalization;
using Amazon.S3.Util;
using Amazon.Runtime.SharedInterfaces;
namespace Amazon.S3.Encryption
{
///
/// The EncryptionUtils class encrypts and decrypts data stored in S3.
/// It can be used to prepare requests for encryption before they are stored in S3
/// and to decrypt objects that are retrieved from S3.
///
internal static class EncryptionUtils
{
// v1-specific constants
private const string XAmzKey = "x-amz-key";
// v2-specific constants
public const string KMSCmkIDKey = "kms_cmk_id";
public const string KMSKeySpec = "AES_256";
public const string XAmzKeyV2 = "x-amz-key-v2";
private const string XAmzWrapAlg = "x-amz-wrap-alg";
private const string XAmzCEKAlg = "x-amz-cek-alg";
// shared constants
public const string XAmzMatDesc = "x-amz-matdesc";
private const string XAmzIV = "x-amz-iv";
private const string XAmzUnencryptedContentLength = "x-amz-unencrypted-content-length";
private const string XAmzCryptoInstrFile = "x-amz-crypto-instr-file";
private const int IVLength = 16;
// v2-specific values
// These values are hard coded here because the
// .NET client only supports a subset of the features of the Java client.
private const string XAmzWrapAlgValue = "kms";
private const string XAmzCEKAlgValue = "AES/CBC/PKCS5Padding";
private const string ModeMessage = "Although this mode is supported by other AWS SDKs, the .NET SDK does not support it at this time.";
private static byte[] EncryptEnvelopeKeyUsingAsymmetricKeyPair(AsymmetricAlgorithm asymmetricAlgorithm, byte[] envelopeKey)
{
#if NETSTANDARD
RSA rsaCrypto = asymmetricAlgorithm as RSA;
if (rsaCrypto == null)
{
throw new NotSupportedException("RSA is the only supported algorithm with this method.");
}
return rsaCrypto.Encrypt(envelopeKey, RSAEncryptionPadding.Pkcs1);
#else
RSACryptoServiceProvider rsaCrypto = asymmetricAlgorithm as RSACryptoServiceProvider;
return rsaCrypto.Encrypt(envelopeKey, false);
#endif
}
private static byte[] EncryptEnvelopeKeyUsingSymmetricKey(SymmetricAlgorithm symmetricAlgorithm, byte[] envelopeKey)
{
symmetricAlgorithm.Mode = CipherMode.ECB;
using (ICryptoTransform encryptor = symmetricAlgorithm.CreateEncryptor())
{
return (encryptor.TransformFinalBlock(envelopeKey, 0, envelopeKey.Length));
}
}
///
/// Decrypts an encrypted Envelope key using the provided encryption materials
/// and returns it in raw byte array form.
///
/// Encrypted envelope key
/// Encryption materials needed to decrypt the encrypted envlelope key
///
internal static byte[] DecryptNonKMSEnvelopeKey(byte[] encryptedEnvelopeKey, EncryptionMaterials materials)
{
if (materials.AsymmetricProvider != null)
return DecryptEnvelopeKeyUsingAsymmetricKeyPair(materials.AsymmetricProvider, encryptedEnvelopeKey);
else if (materials.SymmetricProvider != null)
return DecryptEnvelopeKeyUsingSymmetricKey(materials.SymmetricProvider, encryptedEnvelopeKey);
else
throw new ArgumentException("Error decrypting non-KMS envelope key. " +
"EncryptionMaterials must have the AsymmetricProvider or SymmetricProvider set.");
}
private static byte[] DecryptEnvelopeKeyUsingAsymmetricKeyPair(AsymmetricAlgorithm asymmetricAlgorithm, byte[] encryptedEnvelopeKey)
{
#if NETSTANDARD
RSA rsaCrypto = asymmetricAlgorithm as RSA;
if (rsaCrypto == null)
{
throw new NotSupportedException("RSA is the only supported algorithm with this method.");
}
return rsaCrypto.Decrypt(encryptedEnvelopeKey, RSAEncryptionPadding.Pkcs1);
#else
RSACryptoServiceProvider rsaCrypto = asymmetricAlgorithm as RSACryptoServiceProvider;
return rsaCrypto.Decrypt(encryptedEnvelopeKey, false);
#endif
}
private static byte[] DecryptEnvelopeKeyUsingSymmetricKey(SymmetricAlgorithm symmetricAlgorithm, byte[] encryptedEnvelopeKey)
{
symmetricAlgorithm.Mode = CipherMode.ECB;
using (ICryptoTransform decryptor = symmetricAlgorithm.CreateDecryptor())
{
return (decryptor.TransformFinalBlock(encryptedEnvelopeKey, 0, encryptedEnvelopeKey.Length));
}
}
#region StreamEncryption
///
/// Returns an updated stream where the stream contains the encrypted object contents.
/// The specified instruction will be used to encrypt data.
///
///
/// The stream whose contents are to be encrypted.
///
///
/// The instruction that will be used to encrypt the object data.
///
///
/// Encrypted stream, i.e input stream wrapped into encrypted stream
///
internal static Stream EncryptRequestUsingInstruction(Stream toBeEncrypted, EncryptionInstructions instructions)
{
//wrap input stream into AESEncryptionPutObjectStream wrapper
AESEncryptionPutObjectStream aesEStream;
aesEStream = new AESEncryptionPutObjectStream(toBeEncrypted, instructions.EnvelopeKey, instructions.InitializationVector);
return aesEStream;
}
///
/// Returns an updated input stream where the input stream contains the encrypted object contents.
/// The specified instruction will be used to encrypt data.
///
///
/// The stream whose contents are to be encrypted.
///
///
/// The instruction that will be used to encrypt the object data.
///
///
/// Encrypted stream, i.e input stream wrapped into encrypted stream
///
internal static Stream EncryptUploadPartRequestUsingInstructions(Stream toBeEncrypted, EncryptionInstructions instructions)
{
//wrap input stream into AESEncryptionStreamForUploadPart wrapper
AESEncryptionUploadPartStream aesEStream;
aesEStream = new AESEncryptionUploadPartStream(toBeEncrypted, instructions.EnvelopeKey, instructions.InitializationVector);
return aesEStream;
}
#endregion
#region StreamDecrption
///
/// Updates object where the object
/// input stream contains the decrypted contents.
///
///
/// The getObject response whose contents are to be decrypted.
///
///
/// The instruction that will be used to encrypt the object data.
///
internal static void DecryptObjectUsingInstructions(GetObjectResponse response, EncryptionInstructions instructions)
{
response.ResponseStream = DecryptStream(response.ResponseStream, instructions);
}
//wrap encrypted stream into AESDecriptionStream wrapper
internal static Stream DecryptStream(Stream encryptedStream, EncryptionInstructions encryptionInstructions)
{
AESDecryptionStream aesDecryptStream;
aesDecryptStream = new AESDecryptionStream(encryptedStream, encryptionInstructions.EnvelopeKey, encryptionInstructions.InitializationVector);
return aesDecryptStream;
}
#endregion
#region InstructionGeneration
///
/// Generates an instruction that will be used to encrypt an object
/// using materials with the KMSKeyID set.
///
///
/// Used to call KMS to generate a data key.
///
///
/// The encryption materials to be used to encrypt and decrypt data.
///
///
/// The instruction that will be used to encrypt an object.
///
internal static EncryptionInstructions GenerateInstructionsForKMSMaterials(ICoreAmazonKMS kmsClient, EncryptionMaterials materials)
{
if (materials.KMSKeyID != null)
{
var iv = new byte[IVLength];
// Generate IV, and get both the key and the encrypted key from KMS.
RandomNumberGenerator.Create().GetBytes(iv);
var result = kmsClient.GenerateDataKey(materials.KMSKeyID, materials.MaterialsDescription, KMSKeySpec);
return new EncryptionInstructions(materials.MaterialsDescription, result.KeyPlaintext, result.KeyCiphertext, iv);
}
else
throw new ArgumentException("Error generating encryption instructions. EncryptionMaterials must have the KMSKeyID set.");
}
#if AWS_ASYNC_API
///
/// Generates an instruction that will be used to encrypt an object
/// using materials with the KMSKeyID set.
///
///
/// Used to call KMS to generate a data key.
///
///
/// The encryption materials to be used to encrypt and decrypt data.
///
///
/// The instruction that will be used to encrypt an object.
///
internal static async System.Threading.Tasks.Task GenerateInstructionsForKMSMaterialsAsync(
ICoreAmazonKMS kmsClient, EncryptionMaterials materials)
{
if (materials.KMSKeyID != null)
{
var iv = new byte[IVLength];
// Generate IV, and get both the key and the encrypted key from KMS.
RandomNumberGenerator.Create().GetBytes(iv);
var result = await kmsClient.GenerateDataKeyAsync(materials.KMSKeyID, materials.MaterialsDescription, KMSKeySpec).ConfigureAwait(false);
return new EncryptionInstructions(materials.MaterialsDescription, result.KeyPlaintext, result.KeyCiphertext, iv);
}
else
throw new ArgumentException("Error generating encryption instructions. EncryptionMaterials must have the KMSKeyID set.");
}
#endif
///
/// Generates an instruction that will be used to encrypt an object
/// using materials with the AsymmetricProvider or SymmetricProvider set.
///
///
/// The encryption materials to be used to encrypt and decrypt data.
///
///
/// The instruction that will be used to encrypt an object.
///
internal static EncryptionInstructions GenerateInstructionsForNonKMSMaterials(EncryptionMaterials materials)
{
byte[] encryptedEnvelopeKey = null;
// Generate the IV and key, and encrypt the key locally.
Aes aesObject = Aes.Create();
if (materials.AsymmetricProvider != null)
encryptedEnvelopeKey = EncryptEnvelopeKeyUsingAsymmetricKeyPair(materials.AsymmetricProvider, aesObject.Key);
else if (materials.SymmetricProvider != null)
encryptedEnvelopeKey = EncryptEnvelopeKeyUsingSymmetricKey(materials.SymmetricProvider, aesObject.Key);
else
throw new ArgumentException("Error generating encryption instructions. " +
"EncryptionMaterials must have the AsymmetricProvider or SymmetricProvider set.");
return new EncryptionInstructions(materials.MaterialsDescription, aesObject.Key, encryptedEnvelopeKey, aesObject.IV);
}
internal static GetObjectRequest GetInstructionFileRequest(GetObjectResponse response)
{
GetObjectRequest request = new GetObjectRequest
{
BucketName = response.BucketName,
Key = response.Key + S3Constants.EncryptionInstructionfileSuffix
};
return request;
}
internal static void EnsureSupportedAlgorithms(MetadataCollection metadata)
{
if (metadata[XAmzKeyV2] != null)
{
var xAmzWrapAlgMetadataValue = metadata[XAmzWrapAlg];
if (!XAmzWrapAlgValue.Equals(xAmzWrapAlgMetadataValue))
throw new InvalidDataException(string.Format(CultureInfo.InvariantCulture,
"Value '{0}' for metadata key '{1}' is invalid. {2} only supports '{3}' as the key wrap algorithm. {4}",
xAmzWrapAlgMetadataValue, XAmzWrapAlg, typeof(AmazonS3EncryptionClient).Name, XAmzWrapAlgValue, ModeMessage));
var xAmzCEKAlgMetadataValue = metadata[XAmzCEKAlg];
if (!XAmzCEKAlgValue.Equals(xAmzCEKAlgMetadataValue))
throw new InvalidDataException(string.Format(CultureInfo.InvariantCulture,
"Value '{0}' for metadata key '{1}' is invalid. {2} only supports '{3}' as the content encryption algorithm. {4}",
xAmzCEKAlgMetadataValue, XAmzCEKAlg, typeof(AmazonS3EncryptionClient).Name, XAmzCEKAlgValue, ModeMessage));
}
}
///
/// Builds an instruction object from the object metadata.
///
///
/// A non-null object response that contains encryption information in its metadata.
///
///
/// The non-null encryption materials to be used to encrypt and decrypt Envelope key.
///
///
/// The decrypted envelope key to be use if KMS key wrapping is being used. Or null if non-KMS key wrapping is being used.
///
///
///
internal static EncryptionInstructions BuildInstructionsFromObjectMetadata(
GetObjectResponse response, EncryptionMaterials materials, byte[] decryptedEnvelopeKeyKMS)
{
MetadataCollection metadata = response.Metadata;
if (metadata[XAmzKeyV2] != null)
{
EnsureSupportedAlgorithms(metadata);
string base64EncodedEncryptedEnvelopeKey = metadata[XAmzKeyV2];
byte[] encryptedEnvelopeKey = Convert.FromBase64String(base64EncodedEncryptedEnvelopeKey);
string base64EncodedIV = metadata[XAmzIV];
byte[] IV = Convert.FromBase64String(base64EncodedIV);
return new EncryptionInstructions(materials.MaterialsDescription, decryptedEnvelopeKeyKMS, encryptedEnvelopeKey, IV);
}
else
{
string base64EncodedEncryptedEnvelopeKey = metadata[XAmzKey];
byte[] encryptedEnvelopeKey = Convert.FromBase64String(base64EncodedEncryptedEnvelopeKey);
byte[] decryptedEnvelopeKey = DecryptNonKMSEnvelopeKey(encryptedEnvelopeKey, materials);
string base64EncodedIV = metadata[XAmzIV];
byte[] IV = Convert.FromBase64String(base64EncodedIV);
return new EncryptionInstructions(materials.MaterialsDescription, decryptedEnvelopeKey, encryptedEnvelopeKey, IV);
}
}
///
/// Builds an instruction object from the instruction file.
///
/// Instruction file GetObject response
///
/// The non-null encryption materials to be used to encrypt and decrypt Envelope key.
///
///
/// A non-null instruction object containing encryption information.
///
internal static EncryptionInstructions BuildInstructionsUsingInstructionFile(GetObjectResponse response, EncryptionMaterials materials)
{
using (TextReader textReader = new StreamReader(response.ResponseStream))
{
JsonData jsonData = JsonMapper.ToObject(textReader);
var base64EncodedEncryptedEnvelopeKey = jsonData["EncryptedEnvelopeKey"];
byte[] encryptedEnvelopeKey = Convert.FromBase64String((string)base64EncodedEncryptedEnvelopeKey);
byte[] decryptedEnvelopeKey = DecryptNonKMSEnvelopeKey(encryptedEnvelopeKey, materials);
var base64EncodedIV = jsonData["IV"];
byte[] IV = Convert.FromBase64String((string)base64EncodedIV);
return new EncryptionInstructions(materials.MaterialsDescription, decryptedEnvelopeKey, IV);
}
}
#endregion
#region UpdateMetadata
///
/// Update the request's ObjectMetadata with the necessary information for decrypting the object.
///
///
/// AmazonWebServiceRequest encrypted using the given instruction
///
///
/// Non-null instruction used to encrypt the data in this AmazonWebServiceRequest .
///
///
/// If true use V2 metadata format, otherwise use V1.
///
internal static void UpdateMetadataWithEncryptionInstructions(
AmazonWebServiceRequest request, EncryptionInstructions instructions,
bool useV2Metadata)
{
byte[] keyBytesToStoreInMetadata = instructions.EncryptedEnvelopeKey;
string base64EncodedEnvelopeKey = Convert.ToBase64String(keyBytesToStoreInMetadata);
byte[] IVToStoreInMetadata = instructions.InitializationVector;
string base64EncodedIV = Convert.ToBase64String(IVToStoreInMetadata);
MetadataCollection metadata = null;
var putObjectRequest = request as PutObjectRequest;
if (putObjectRequest != null)
metadata = putObjectRequest.Metadata;
var initiateMultipartrequest = request as InitiateMultipartUploadRequest;
if (initiateMultipartrequest != null)
metadata = initiateMultipartrequest.Metadata;
if (metadata != null)
{
if (useV2Metadata)
{
metadata.Add(XAmzKeyV2, base64EncodedEnvelopeKey);
metadata.Add(XAmzWrapAlg, XAmzWrapAlgValue);
metadata.Add(XAmzCEKAlg, XAmzCEKAlgValue);
}
else
{
metadata.Add(XAmzKey, base64EncodedEnvelopeKey);
}
metadata.Add(XAmzIV, base64EncodedIV);
metadata.Add(XAmzMatDesc, JsonMapper.ToJson(instructions.MaterialsDescription));
}
}
internal static PutObjectRequest CreateInstructionFileRequest(AmazonWebServiceRequest request, EncryptionInstructions instructions)
{
byte[] keyBytesToStoreInInstructionFile = instructions.EncryptedEnvelopeKey;
string base64EncodedEnvelopeKey = Convert.ToBase64String(keyBytesToStoreInInstructionFile);
byte[] IVToStoreInInstructionFile = instructions.InitializationVector;
string base64EncodedIV = Convert.ToBase64String(IVToStoreInInstructionFile);
JsonData jsonData = new JsonData();
jsonData["EncryptedEnvelopeKey"] = base64EncodedEnvelopeKey;
jsonData["IV"] = base64EncodedIV;
string credentials = jsonData.ToJson();
var putObjectRequest = request as PutObjectRequest;
if (putObjectRequest != null)
{
PutObjectRequest requestforInstructionFile = new PutObjectRequest()
{
BucketName = putObjectRequest.BucketName,
Key = putObjectRequest.Key + S3Constants.EncryptionInstructionfileSuffix,
ContentBody = credentials
};
requestforInstructionFile.Metadata.Add(XAmzCryptoInstrFile, "");
return requestforInstructionFile;
}
var completeMultiPartRequest = request as CompleteMultipartUploadRequest;
if (completeMultiPartRequest != null)
{
PutObjectRequest requestforInstructionFile = new PutObjectRequest()
{
BucketName = completeMultiPartRequest.BucketName,
Key = completeMultiPartRequest.Key + S3Constants.EncryptionInstructionfileSuffix,
ContentBody = credentials
};
requestforInstructionFile.Metadata.Add(XAmzCryptoInstrFile, "");
return requestforInstructionFile;
}
else
return null;
}
///
/// Adds UnEncrypted content length to object metadata
///
///
internal static void AddUnencryptedContentLengthToMetadata(PutObjectRequest request)
{
long originalLength = request.InputStream.Length;
request.Metadata.Add(XAmzUnencryptedContentLength, originalLength.ToString(CultureInfo.InvariantCulture));
}
///
/// checks if encryption credentials are in object metadata
///
/// Response of the object
///
internal static bool IsEncryptionInfoInMetadata(GetObjectResponse response)
{
MetadataCollection metadata = response.Metadata;
return ((metadata[XAmzKey] != null || metadata[XAmzKeyV2] != null) && metadata[XAmzIV] != null);
}
#endregion
}
}