/* * 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.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using Amazon.Extensions.S3.Encryption.Primitives; using Amazon.Extensions.S3.Encryption.Util; using Amazon.KeyManagementService; using Amazon.Runtime; using Amazon.S3.Model; using ThirdParty.Json.LitJson; namespace Amazon.Extensions.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 partial class EncryptionUtils { /// /// Decrypt the envelope key with RSA-OAEP-SHA1 /// /// Encrypted envelope key /// Encryption materials needed to decrypt the encrypted envelope key /// internal static byte[] DecryptNonKmsEnvelopeKeyV2(byte[] encryptedEnvelopeKey, EncryptionMaterialsBase materials) { if (materials.AsymmetricProvider != null) { return DecryptEnvelopeKeyUsingAsymmetricKeyPairV2(materials.AsymmetricProvider, encryptedEnvelopeKey); } if (materials.SymmetricProvider != null) { return DecryptEnvelopeKeyUsingSymmetricKeyV2(materials.SymmetricProvider, encryptedEnvelopeKey); } throw new ArgumentException("Error decrypting non-KMS envelope key. " + "EncryptionMaterials must have the AsymmetricProvider or SymmetricProvider set."); } private static byte[] DecryptEnvelopeKeyUsingAsymmetricKeyPairV2(AsymmetricAlgorithm asymmetricAlgorithm, byte[] encryptedEnvelopeKey) { var rsa = asymmetricAlgorithm as RSA; if (rsa == null) { throw new NotSupportedException("RSA-OAEP-SHA1 is the only supported algorithm with AsymmetricProvider."); } var cipher = RsaUtils.CreateRsaOaepSha1Cipher(false, rsa); var decryptedEnvelopeKey = cipher.DoFinal(encryptedEnvelopeKey); return DecryptedDataKeyFromDecryptedEnvelopeKey(decryptedEnvelopeKey); } private static byte[] DecryptEnvelopeKeyUsingSymmetricKeyV2(SymmetricAlgorithm symmetricAlgorithm, byte[] encryptedEnvelopeKey) { var nonce = encryptedEnvelopeKey.Take(DefaultNonceSize).ToArray(); var encryptedKey = encryptedEnvelopeKey.Skip(nonce.Length).ToArray(); var associatedText = Encoding.UTF8.GetBytes(XAmzAesGcmCekAlgValue); var cipher = AesGcmUtils.CreateCipher(false, symmetricAlgorithm.Key, DefaultTagBitsLength, nonce, associatedText); var envelopeKey = cipher.DoFinal(encryptedKey); return envelopeKey; } /// /// Extract and return data key from the decrypted envelope key /// Format: (1 byte is length of the key) + (envelope key) + (UTF-8 encoding of CEK algorithm) /// /// DecryptedEnvelopeKey that contains the data key /// /// Throws when the CEK algorithm isn't supported for given envelope key private static byte[] DecryptedDataKeyFromDecryptedEnvelopeKey(byte[] decryptedEnvelopeKey) { var keyLength = (int) decryptedEnvelopeKey[0]; var dataKey = decryptedEnvelopeKey.Skip(1).Take(keyLength); var cekAlgorithm = Encoding.UTF8.GetString(decryptedEnvelopeKey.Skip(keyLength + 1).ToArray()); if (!XAmzAesGcmCekAlgValue.Equals(cekAlgorithm)) { throw new InvalidDataException($"Value '{cekAlgorithm}' for CEK algorithm is invalid." + $"{nameof(AmazonS3EncryptionClientV2)} only supports '{XAmzAesGcmCekAlgValue}' as the key CEK algorithm."); } return dataKey.ToArray(); } /// /// 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. /// /// /// Flag indicating if the caching stream should be used or not. /// /// /// Encrypted stream, i.e input stream wrapped into encrypted stream /// internal static Stream EncryptRequestUsingInstructionV2(Stream toBeEncrypted, EncryptionInstructions instructions, bool shouldUseCachingStream) { Stream gcmEncryptStream = (shouldUseCachingStream ? new AesGcmEncryptCachingStream(toBeEncrypted, instructions.EnvelopeKey, instructions.InitializationVector, DefaultTagBitsLength) : new AesGcmEncryptStream(toBeEncrypted, instructions.EnvelopeKey, instructions.InitializationVector, DefaultTagBitsLength) ); return gcmEncryptStream; } /// /// 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 GenerateInstructionsForNonKmsMaterialsV2(EncryptionMaterialsV2 materials) { // Generate the IV and key, and encrypt the key locally. if (materials.AsymmetricProvider != null) { return EncryptEnvelopeKeyUsingAsymmetricKeyPairV2(materials); } if (materials.SymmetricProvider != null) { return EncryptEnvelopeKeyUsingSymmetricKeyV2(materials); } throw new ArgumentException("Error generating encryption instructions. " + "EncryptionMaterials must have the AsymmetricProvider or SymmetricProvider set."); } private static EncryptionInstructions EncryptEnvelopeKeyUsingAsymmetricKeyPairV2(EncryptionMaterialsV2 materials) { var rsa = materials.AsymmetricProvider as RSA; if (rsa == null) { throw new NotSupportedException("RSA is the only supported algorithm with this method."); } switch (materials.AsymmetricProviderType) { case AsymmetricAlgorithmType.RsaOaepSha1: { var aesObject = Aes.Create(); var nonce = aesObject.IV.Take(DefaultNonceSize).ToArray(); var envelopeKeyToEncrypt = EnvelopeKeyForDataKey(aesObject.Key); var cipher = RsaUtils.CreateRsaOaepSha1Cipher(true, rsa); var encryptedEnvelopeKey = cipher.DoFinal(envelopeKeyToEncrypt); var instructions = new EncryptionInstructions(materials.MaterialsDescription, aesObject.Key, encryptedEnvelopeKey, nonce, XAmzWrapAlgRsaOaepSha1, XAmzAesGcmCekAlgValue); return instructions; } default: { throw new NotSupportedException($"{materials.AsymmetricProviderType} isn't supported with AsymmetricProvider"); } } } /// /// Returns encryption instructions to encrypt content with AES/GCM/NoPadding algorithm /// Creates encryption key used for AES/GCM/NoPadding and encrypt it with AES/GCM /// Encrypted key follows nonce(12 bytes) + key cipher text(16 or 32 bytes) + tag(16 bytes) format /// Tag is appended by the AES/GCM cipher with encryption process /// /// /// private static EncryptionInstructions EncryptEnvelopeKeyUsingSymmetricKeyV2(EncryptionMaterialsV2 materials) { var aes = materials.SymmetricProvider as Aes; if (aes == null) { throw new NotSupportedException("AES is the only supported algorithm with this method."); } switch (materials.SymmetricProviderType) { case SymmetricAlgorithmType.AesGcm: { var aesObject = Aes.Create(); var nonce = aesObject.IV.Take(DefaultNonceSize).ToArray(); var associatedText = Encoding.UTF8.GetBytes(XAmzAesGcmCekAlgValue); var cipher = AesGcmUtils.CreateCipher(true, materials.SymmetricProvider.Key, DefaultTagBitsLength, nonce, associatedText); var envelopeKey = cipher.DoFinal(aesObject.Key); var encryptedEnvelopeKey = nonce.Concat(envelopeKey).ToArray(); var instructions = new EncryptionInstructions(materials.MaterialsDescription, aesObject.Key, encryptedEnvelopeKey, nonce, XAmzWrapAlgAesGcmValue, XAmzAesGcmCekAlgValue); return instructions; } default: { throw new NotSupportedException($"{materials.SymmetricProviderType} isn't supported with SymmetricProvider"); } } } /// /// Bundle envelope key with key length and CEK algorithm information /// Format: (1 byte is length of the key) + (envelope key) + (UTF-8 encoding of CEK algorithm) /// /// Data key to be bundled /// private static byte[] EnvelopeKeyForDataKey(byte[] dataKey) { var cekAlgorithm = Encoding.UTF8.GetBytes(XAmzAesGcmCekAlgValue); int length = 1 + dataKey.Length + cekAlgorithm.Length; var envelopeKeyToEncrypt = new byte[length]; envelopeKeyToEncrypt[0] = (byte)dataKey.Length; dataKey.CopyTo(envelopeKeyToEncrypt, 1); cekAlgorithm.CopyTo(envelopeKeyToEncrypt, 1 + dataKey.Length); return envelopeKeyToEncrypt; } /// /// 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 . /// /// Encryption client used for put objects internal static void UpdateMetadataWithEncryptionInstructionsV2(AmazonWebServiceRequest request, EncryptionInstructions instructions, AmazonS3EncryptionClientBase encryptionClient) { var keyBytesToStoreInMetadata = instructions.EncryptedEnvelopeKey; var base64EncodedEnvelopeKey = Convert.ToBase64String(keyBytesToStoreInMetadata); var ivToStoreInMetadata = instructions.InitializationVector; var 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) { metadata.Add(XAmzWrapAlg, instructions.WrapAlgorithm); metadata.Add(XAmzTagLen, DefaultTagBitsLength.ToString()); metadata.Add(XAmzKeyV2, base64EncodedEnvelopeKey); metadata.Add(XAmzCekAlg, instructions.CekAlgorithm); metadata.Add(XAmzIV, base64EncodedIv); metadata.Add(XAmzMatDesc, JsonMapper.ToJson(instructions.MaterialsDescription)); } } internal static PutObjectRequest CreateInstructionFileRequestV2(AmazonWebServiceRequest request, EncryptionInstructions instructions) { var keyBytesToStoreInInstructionFile = instructions.EncryptedEnvelopeKey; var base64EncodedEnvelopeKey = Convert.ToBase64String(keyBytesToStoreInInstructionFile); var ivToStoreInInstructionFile = instructions.InitializationVector; var base64EncodedIv = Convert.ToBase64String(ivToStoreInInstructionFile); var jsonData = new JsonData { [XAmzTagLen] = DefaultTagBitsLength.ToString(), [XAmzKeyV2] = base64EncodedEnvelopeKey, [XAmzCekAlg] = instructions.CekAlgorithm, [XAmzWrapAlg] = instructions.WrapAlgorithm, [XAmzIV] = base64EncodedIv, [XAmzMatDesc] = JsonMapper.ToJson(instructions.MaterialsDescription) }; var contentBody = jsonData.ToJson(); var putObjectRequest = request as PutObjectRequest; if (putObjectRequest != null) { return GetInstructionFileRequest(putObjectRequest.BucketName, putObjectRequest.Key, EncryptionInstructionFileV2Suffix, contentBody); } var completeMultiPartRequest = request as CompleteMultipartUploadRequest; if (completeMultiPartRequest != null) { return GetInstructionFileRequest(completeMultiPartRequest.BucketName, completeMultiPartRequest.Key, EncryptionInstructionFileV2Suffix, contentBody); } return null; } private static PutObjectRequest GetInstructionFileRequest(string bucketName, string key, string suffix, string contentBody) { var instructionFileRequest = new PutObjectRequest() { BucketName = bucketName, Key = $"{key}{suffix}", ContentBody = contentBody }; instructionFileRequest.Metadata.Add(XAmzCryptoInstrFile, ""); return instructionFileRequest; } /// /// 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 EncryptUploadPartRequestUsingInstructionsV2(Stream toBeEncrypted, EncryptionInstructions instructions) { //wrap input stream into AesGcmEncryptCachingStream wrapper Stream aesGcmEncryptStream = new AesGcmEncryptCachingStream(toBeEncrypted, instructions.EnvelopeKey, instructions.InitializationVector, DefaultTagBitsLength); return aesGcmEncryptStream; } /// /// 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 GenerateInstructionsForKMSMaterialsV2(IAmazonKeyManagementService kmsClient, EncryptionMaterialsV2 materials) { if (materials.KMSKeyID == null) { throw new ArgumentNullException(nameof(materials.KMSKeyID), KmsKeyIdNullMessage); } switch (materials.KmsType) { case KmsType.KmsContext: { var nonce = new byte[DefaultNonceSize]; // Generate nonce, and get both the key and the encrypted key from KMS. RandomNumberGenerator.Create().GetBytes(nonce); var result = kmsClient.GenerateDataKey(materials.KMSKeyID, materials.MaterialsDescription, KMSKeySpec); var instructions = new EncryptionInstructions(materials.MaterialsDescription, result.KeyPlaintext, result.KeyCiphertext, nonce, XAmzWrapAlgKmsContextValue, XAmzAesGcmCekAlgValue); return instructions; } default: throw new NotSupportedException($"{materials.KmsType} is not supported for KMS Key Id {materials.KMSKeyID}"); } } #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 GenerateInstructionsForKMSMaterialsV2Async(IAmazonKeyManagementService kmsClient, EncryptionMaterialsV2 materials) { if (materials.KMSKeyID == null) { throw new ArgumentNullException(nameof(materials.KMSKeyID), KmsKeyIdNullMessage); } switch (materials.KmsType) { case KmsType.KmsContext: { var nonce = new byte[DefaultNonceSize]; // Generate nonce, and get both the key and the encrypted key from KMS. RandomNumberGenerator.Create().GetBytes(nonce); var result = await kmsClient.GenerateDataKeyAsync(materials.KMSKeyID, materials.MaterialsDescription, KMSKeySpec).ConfigureAwait(false); var instructions = new EncryptionInstructions(materials.MaterialsDescription, result.KeyPlaintext, result.KeyCiphertext, nonce, XAmzWrapAlgKmsContextValue, XAmzAesGcmCekAlgValue); return instructions; } default: throw new NotSupportedException($"{materials.KmsType} is not supported for KMS Key Id {materials.KMSKeyID}"); } } #endif /// /// Converts x-amz-matdesc JSON string to dictionary /// /// Metadata that contains x-amz-matdesc key /// internal static Dictionary GetMaterialDescriptionFromMetaData(MetadataCollection metadata) { var materialDescriptionJsonString = metadata[XAmzMatDesc]; if (materialDescriptionJsonString == null) { return new Dictionary(); } var materialDescription = JsonMapper.ToObject>(materialDescriptionJsonString); return materialDescription; } internal static GetObjectRequest GetInstructionFileRequestV2(GetObjectResponse response) { var request = new GetObjectRequest { BucketName = response.BucketName, Key = response.Key + EncryptionInstructionFileV2Suffix }; return request; } /// /// Build encryption instructions for UploadPartEncryptionContext /// /// UploadPartEncryptionContext which contains instructions used for encrypting multipart object /// EncryptionMaterials which contains material used for encrypting multipart object /// internal static EncryptionInstructions BuildEncryptionInstructionsForInstructionFileV2(UploadPartEncryptionContext context, EncryptionMaterialsBase encryptionMaterials) { var instructions = new EncryptionInstructions(encryptionMaterials.MaterialsDescription, context.EnvelopeKey, context.EncryptedEnvelopeKey, context.FirstIV, context.WrapAlgorithm, context.CekAlgorithm); return instructions; } /// /// 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 BuildInstructionsUsingInstructionFileV2(GetObjectResponse response, EncryptionMaterialsBase materials) { using (TextReader textReader = new StreamReader(response.ResponseStream)) { var jsonData = JsonMapper.ToObject(textReader); if (jsonData[XAmzKeyV2] != null) { // The envelope contains data in V2 format var encryptedEnvelopeKey = Base64DecodedDataValue(jsonData, XAmzKeyV2); var decryptedEnvelopeKey = DecryptNonKmsEnvelopeKeyV2(encryptedEnvelopeKey, materials); var initializationVector = Base64DecodedDataValue(jsonData, XAmzIV); var materialDescription = JsonMapper.ToObject>((string)jsonData[XAmzMatDesc]); var cekAlgorithm = StringValue(jsonData, XAmzCekAlg); var wrapAlgorithm = StringValue(jsonData, XAmzWrapAlg); var instructions = new EncryptionInstructions(materialDescription, decryptedEnvelopeKey, null, initializationVector, wrapAlgorithm, cekAlgorithm); return instructions; } else if (jsonData[XAmzKey] != null) { // The envelope contains data in V1 format var encryptedEnvelopeKey = Base64DecodedDataValue(jsonData, XAmzKey); var decryptedEnvelopeKey = DecryptNonKMSEnvelopeKey(encryptedEnvelopeKey, materials); var initializationVector = Base64DecodedDataValue(jsonData, XAmzIV); var materialDescription = JsonMapper.ToObject>((string)jsonData[XAmzMatDesc]); var instructions = new EncryptionInstructions(materialDescription, decryptedEnvelopeKey, null, initializationVector); return instructions; } else if (jsonData[EncryptedEnvelopeKey] != null) { // The envelope contains data in older format var encryptedEnvelopeKey = Base64DecodedDataValue(jsonData, EncryptedEnvelopeKey); var decryptedEnvelopeKey = DecryptNonKMSEnvelopeKey(encryptedEnvelopeKey, materials); var initializationVector = Base64DecodedDataValue(jsonData, IV); return new EncryptionInstructions(materials.MaterialsDescription, decryptedEnvelopeKey, initializationVector); } else { throw new ArgumentException("Missing parameters required for decryption"); } } } private static byte[] Base64DecodedDataValue(JsonData jsonData, string key) { var base64EncodedValue = jsonData[key]; if (base64EncodedValue == null) { throw new ArgumentNullException(nameof(key)); } return Convert.FromBase64String((string)base64EncodedValue); } private static string StringValue(JsonData jsonData, string key) { var stringValue = jsonData[key]; if (stringValue == null) { throw new ArgumentNullException(nameof(key)); } return (string)stringValue; } } }