/* * Copyright 2013-2016 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. */ package com.amazonaws.services.s3.internal.crypto; import static com.amazonaws.services.s3.AmazonS3EncryptionClient.USER_AGENT; import static com.amazonaws.services.s3.internal.crypto.EncryptionUtils.createInstructionGetRequest; import static com.amazonaws.services.s3.internal.crypto.EncryptionUtils.getAdjustedCryptoRange; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.services.s3.internal.InputSubstream; import com.amazonaws.services.s3.internal.RepeatableFileInputStream; import com.amazonaws.services.s3.internal.S3Direct; import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest; import com.amazonaws.services.s3.model.CompleteMultipartUploadResult; import com.amazonaws.services.s3.model.CopyPartRequest; import com.amazonaws.services.s3.model.CopyPartResult; import com.amazonaws.services.s3.model.CryptoConfiguration; import com.amazonaws.services.s3.model.CryptoStorageMode; import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; import com.amazonaws.services.s3.model.GetObjectRequest; import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest; import com.amazonaws.services.s3.model.InitiateMultipartUploadResult; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.PutObjectResult; import com.amazonaws.services.s3.model.S3Object; import com.amazonaws.services.s3.model.S3ObjectInputStream; import com.amazonaws.services.s3.model.UploadPartRequest; import com.amazonaws.services.s3.model.UploadPartResult; import com.amazonaws.util.json.JsonUtils; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Map; /** * Authenticated encryption (AE) cryptographic module for the S3 encryption * client. */ class S3CryptoModuleAE extends S3CryptoModuleBase<MultipartUploadCryptoContext> { static { // Enable bouncy castle if available CryptoRuntime.enableBouncyCastle(); } private static final boolean IS_MULTI_PART = true; S3CryptoModuleAE(S3Direct s3, AWSCredentialsProvider credentialsProvider, EncryptionMaterialsProvider encryptionMaterialsProvider, ClientConfiguration clientConfig, CryptoConfiguration cryptoConfig) { super(s3, credentialsProvider, encryptionMaterialsProvider, clientConfig, cryptoConfig, new S3CryptoScheme(ContentCryptoScheme.AES_GCM)); } /** * Used for testing purposes only. */ S3CryptoModuleAE(S3Direct s3, EncryptionMaterialsProvider encryptionMaterialsProvider, CryptoConfiguration cryptoConfig) { this(s3, new DefaultAWSCredentialsProviderChain(), encryptionMaterialsProvider, new ClientConfiguration(), cryptoConfig); } /** * Returns true if a strict encryption mode is in use in the current crypto * module; false otherwise. */ protected boolean isStrict() { return false; } /** * @throws SecurityException if the crypto scheme used in the given content * crypto material is not allowed in this crypto module. */ protected void securityCheck(ContentCryptoMaterial cekMaterial, S3ObjectWrapper retrieved) { // default is no-op. Sublcass may override. } @Override public PutObjectResult putObjectSecurely(PutObjectRequest putObjectRequest) throws AmazonClientException, AmazonServiceException { appendUserAgent(putObjectRequest, USER_AGENT); if (this.cryptoConfig.getStorageMode() == CryptoStorageMode.InstructionFile) { return putObjectUsingInstructionFile(putObjectRequest); } else { return putObjectUsingMetadata(putObjectRequest); } } private PutObjectResult putObjectUsingMetadata(PutObjectRequest req) throws AmazonClientException, AmazonServiceException { ContentCryptoMaterial cekMaterial = createContentCryptoMaterial(req); // Wraps the object data with a cipher input stream PutObjectRequest wrappedReq = wrapWithCipher(req, cekMaterial); // Update the metadata req.setMetadata(updateMetadataWithContentCryptoMaterial( req.getMetadata(), req.getFile(), cekMaterial)); // Put the encrypted object into S3 return s3.putObject(wrappedReq); } @Override public S3Object getObjectSecurely(GetObjectRequest req) throws AmazonClientException, AmazonServiceException { appendUserAgent(req, USER_AGENT); // Adjust the crypto range to retrieve all of the cipher blocks needed // to contain the user's desired // range of bytes. long[] desiredRange = req.getRange(); if (isStrict() && desiredRange != null) throw new SecurityException("Range get is not allowed in strict crypto mode"); long[] adjustedCryptoRange = getAdjustedCryptoRange(desiredRange); if (adjustedCryptoRange != null) req.setRange(adjustedCryptoRange[0], adjustedCryptoRange[1]); // Get the object from S3 S3Object retrieved = s3.getObject(req); // If the caller has specified constraints, it's possible that // super.getObject(...) // would return null, so we simply return null as well. if (retrieved == null) return null; try { return decipher(req, desiredRange, adjustedCryptoRange, retrieved); } catch (AmazonClientException ace) { // If we're unable to set up the decryption, make sure we close the // HTTP connection try { retrieved.getObjectContent().close(); } catch (Exception e) { log.debug("Safely ignoring", e); } throw ace; } } private S3Object decipher(GetObjectRequest req, long[] desiredRange, long[] cryptoRange, S3Object retrieved) { S3ObjectWrapper wrapped = new S3ObjectWrapper(retrieved); // Check if encryption info is in object metadata if (wrapped.hasEncryptionInfo()) return decipherWithMetadata(desiredRange, cryptoRange, wrapped); // Check if encrypted info is in an instruction file S3ObjectWrapper instructionFile = fetchInstructionFile(req); if (instructionFile != null) { try { if (instructionFile.isInstructionFile()) { return decipherWithInstructionFile(desiredRange, cryptoRange, wrapped, instructionFile); } } finally { try { instructionFile.getObjectContent().close(); } catch (Exception ignore) { } } } if (isStrict()) { try { wrapped.close(); } catch (IOException ignore) { } throw new SecurityException("S3 object with bucket name: " + retrieved.getBucketName() + ", key: " + retrieved.getKey() + " is not encrypted"); } // The object was not encrypted to begin with. Return the object // without decrypting it. log.warn(String.format( "Unable to detect encryption information for object '%s' in bucket '%s'. " + "Returning object without decryption.", retrieved.getKey(), retrieved.getBucketName())); // Adjust the output to the desired range of bytes. S3ObjectWrapper adjusted = adjustToDesiredRange(wrapped, desiredRange, null); return adjusted.getS3Object(); } private S3Object decipherWithInstructionFile(long[] desiredRange, long[] cryptoRange, S3ObjectWrapper retrieved, S3ObjectWrapper instructionFile) { String json = instructionFile.toJsonString(); Map<String, String> instruction = JsonUtils.jsonToMap(json); ContentCryptoMaterial cekMaterial = ContentCryptoMaterial.fromInstructionFile( instruction, kekMaterialsProvider, cryptoConfig.getCryptoProvider(), cryptoRange // range is sometimes necessary to compute // the adjusted IV ); securityCheck(cekMaterial, retrieved); S3ObjectWrapper decrypted = decrypt(retrieved, cekMaterial, cryptoRange); // Adjust the output to the desired range of bytes. S3ObjectWrapper adjusted = adjustToDesiredRange( decrypted, desiredRange, instruction); return adjusted.getS3Object(); } private S3Object decipherWithMetadata(long[] desiredRange, long[] cryptoRange, S3ObjectWrapper retrieved) { ContentCryptoMaterial cekMaterial = ContentCryptoMaterial .fromObjectMetadata(retrieved.getObjectMetadata(), kekMaterialsProvider, cryptoConfig.getCryptoProvider(), // range is sometimes necessary to compute the adjusted // IV cryptoRange ); securityCheck(cekMaterial, retrieved); S3ObjectWrapper decrypted = decrypt(retrieved, cekMaterial, cryptoRange); // Adjust the output to the desired range of bytes. S3ObjectWrapper adjusted = adjustToDesiredRange( decrypted, desiredRange, null); return adjusted.getS3Object(); } /** * Adjusts the retrieved S3Object so that the object contents contain only * the range of bytes desired by the user. Since encrypted contents can only * be retrieved in CIPHER_BLOCK_SIZE (16 bytes) chunks, the S3Object * potentially contains more bytes than desired, so this method adjusts the * contents range. * * @param s3object The S3Object retrieved from S3 that could possibly * contain more bytes than desired by the user. * @param range A two-element array of longs corresponding to the start and * finish (inclusive) of a desired range of bytes. * @param instruction Instruction file in JSON or null if no instruction * file is involved * @return The S3Object with adjusted object contents containing only the * range desired by the user. If the range specified is invalid, * then the S3Object is returned without any modifications. */ protected final S3ObjectWrapper adjustToDesiredRange(S3ObjectWrapper s3object, long[] range, Map<String, String> instruction) { if (range == null) return s3object; // Figure out the original encryption scheme used, which can be // different from the crypto scheme used for decryption. ContentCryptoScheme encryptionScheme = s3object.encryptionSchemeOf(instruction); // range get on data encrypted using AES_GCM final long instanceLen = s3object.getObjectMetadata().getInstanceLength(); final long maxOffset = instanceLen - encryptionScheme.getTagLengthInBits() / 8 - 1; if (range[1] > maxOffset) { range[1] = maxOffset; if (range[0] > range[1]) { // Return empty content try { // First let's close the existing input stream to // avoid resource leakage s3object.getObjectContent().close(); } catch (IOException ignore) { log.trace("", ignore); } s3object.setObjectContent(new ByteArrayInputStream(new byte[0])); return s3object; } } if (range[0] > range[1]) { // Make no modifications if range is invalid. return s3object; } try { S3ObjectInputStream objectContent = s3object.getObjectContent(); InputStream adjustedRangeContents = new AdjustedRangeInputStream(objectContent, range[0], range[1]); s3object.setObjectContent(new S3ObjectInputStream(adjustedRangeContents, objectContent .getHttpRequest())); return s3object; } catch (IOException e) { throw new AmazonClientException("Error adjusting output to desired byte range: " + e.getMessage()); } } @Override public ObjectMetadata getObjectSecurely(GetObjectRequest getObjectRequest, File destinationFile) throws AmazonClientException, AmazonServiceException { assertParameterNotNull(destinationFile, "The destination file parameter must be specified when downloading an object directly to a file"); S3Object s3Object = getObjectSecurely(getObjectRequest); // getObject can return null if constraints were specified but not met if (s3Object == null) return null; OutputStream outputStream = null; try { outputStream = new BufferedOutputStream(new FileOutputStream(destinationFile)); byte[] buffer = new byte[1024 * 10]; int bytesRead; while ((bytesRead = s3Object.getObjectContent().read(buffer)) > -1) { outputStream.write(buffer, 0, bytesRead); } } catch (IOException e) { throw new AmazonClientException( "Unable to store object contents to disk: " + e.getMessage(), e); } finally { try { outputStream.close(); } catch (Exception e) { log.debug(e.getMessage()); } try { s3Object.getObjectContent().close(); } catch (Exception e) { log.debug(e.getMessage()); } } /* * Unlike the standard Amazon S3 Client, the Amazon S3 Encryption Client * does not do an MD5 check here because the contents stored in S3 and * the contents we just retrieved are different. In S3, the stored * contents are encrypted, and locally, the retrieved contents are * decrypted. */ return s3Object.getObjectMetadata(); } @Override public CompleteMultipartUploadResult completeMultipartUploadSecurely( CompleteMultipartUploadRequest req) throws AmazonClientException, AmazonServiceException { appendUserAgent(req, USER_AGENT); String uploadId = req.getUploadId(); MultipartUploadCryptoContext uploadContext = multipartUploadContexts.get(uploadId); if (uploadContext.hasFinalPartBeenSeen() == false) { throw new AmazonClientException( "Unable to complete an encrypted multipart upload without being told which part was the last. " + "Without knowing which part was the last, the encrypted data in Amazon S3 is incomplete and corrupt."); } CompleteMultipartUploadResult result = s3.completeMultipartUpload(req); // In InstructionFile mode, we want to write the instruction file only // after the whole upload has completed correctly. if (cryptoConfig.getStorageMode() == CryptoStorageMode.InstructionFile) { // Put the instruction file into S3 s3.putObject(createInstructionPutRequest( uploadContext.getBucketName(), uploadContext.getKey(), uploadContext.getContentCryptoMaterial())); } multipartUploadContexts.remove(uploadId); return result; } @Override public InitiateMultipartUploadResult initiateMultipartUploadSecurely( InitiateMultipartUploadRequest req) throws AmazonClientException, AmazonServiceException { appendUserAgent(req, USER_AGENT); // Generate a one-time use symmetric key and initialize a cipher to // encrypt object data ContentCryptoMaterial cekMaterial = createContentCryptoMaterial(req); if (cryptoConfig.getStorageMode() == CryptoStorageMode.ObjectMetadata) { ObjectMetadata metadata = req.getObjectMetadata(); if (metadata == null) metadata = new ObjectMetadata(); // Store encryption info in metadata req.setObjectMetadata(updateMetadataWithContentCryptoMaterial( metadata, null, cekMaterial)); } InitiateMultipartUploadResult result = s3.initiateMultipartUpload(req); MultipartUploadCryptoContext uploadContext = new MultipartUploadCryptoContext( req.getBucketName(), req.getKey(), cekMaterial); multipartUploadContexts.put(result.getUploadId(), uploadContext); return result; } /** * {@inheritDoc} * <p> * <b>NOTE:</b> Because the encryption process requires context from * previous blocks, parts uploaded with the AmazonS3EncryptionClient (as * opposed to the normal AmazonS3Client) must be uploaded serially, and in * order. Otherwise, the previous encryption context isn't available to use * when encrypting the current part. */ @Override public UploadPartResult uploadPartSecurely(UploadPartRequest req) throws AmazonClientException, AmazonServiceException { appendUserAgent(req, USER_AGENT); final int blockSize = contentCryptoScheme.getBlockSizeInBytes(); final boolean isLastPart = req.isLastPart(); final String uploadId = req.getUploadId(); final long partSize = req.getPartSize(); final boolean partSizeMultipleOfCipherBlockSize = 0 == (partSize % blockSize); if (!isLastPart && !partSizeMultipleOfCipherBlockSize) { throw new AmazonClientException( "Invalid part size: part sizes for encrypted multipart uploads must be multiples " + "of the cipher block size (" + blockSize + ") with the exception of the last part."); } // Generate the envelope symmetric key and initialize a cipher to // encrypt the object's data MultipartUploadCryptoContext uploadContext = multipartUploadContexts.get(uploadId); if (uploadContext == null) { throw new AmazonClientException( "No client-side information available on upload ID " + uploadId); } CipherLite cipherLite = uploadContext.getCipherLite(); req.setInputStream(newMultipartS3CipherInputStream(req, cipherLite)); // Treat all encryption requests as input stream upload requests, not as // file upload requests. req.setFile(null); req.setFileOffset(0); // The last part of the multipart upload will contain an extra 16-byte // mac if (req.isLastPart()) { // We only change the size of the last part req.setPartSize(partSize + (contentCryptoScheme.getTagLengthInBits() / 8)); if (uploadContext.hasFinalPartBeenSeen()) { throw new AmazonClientException( "This part was specified as the last part in a multipart upload, but a previous part was already marked as the last part. " + "Only the last part of the upload should be marked as the last part."); } uploadContext.setHasFinalPartBeenSeen(true); } UploadPartResult result = s3.uploadPart(req); return result; } protected final CipherLiteInputStream newMultipartS3CipherInputStream( UploadPartRequest req, CipherLite cipherLite) { try { InputStream is = req.getInputStream(); if (req.getFile() != null) { is = new InputSubstream( new RepeatableFileInputStream( req.getFile()), req.getFileOffset(), req.getPartSize(), req.isLastPart()); } return new CipherLiteInputStream(is, cipherLite, DEFAULT_BUFFER_SIZE, IS_MULTI_PART, req.isLastPart()); } catch (Exception e) { throw new AmazonClientException( "Unable to create cipher input stream: " + e.getMessage(), e); } } @Override public CopyPartResult copyPartSecurely(CopyPartRequest copyPartRequest) { String uploadId = copyPartRequest.getUploadId(); MultipartUploadCryptoContext uploadContext = multipartUploadContexts.get(uploadId); if (!uploadContext.hasFinalPartBeenSeen()) { uploadContext.setHasFinalPartBeenSeen(true); } return s3.copyPart(copyPartRequest); } /* * Private helper methods */ /** * Puts an encrypted object into S3, and puts an instruction file into S3. * Encryption info is stored in the instruction file. * * @param putObjectRequest The request object containing all the parameters * to upload a new object to Amazon S3. * @return A {@link PutObjectResult} object containing the information * returned by Amazon S3 for the new, created object. * @throws AmazonClientException If any errors are encountered on the client * while making the request or handling the response. * @throws AmazonServiceException If any errors occurred in Amazon S3 while * processing the request. */ private PutObjectResult putObjectUsingInstructionFile(PutObjectRequest putObjectRequest) throws AmazonClientException, AmazonServiceException { PutObjectRequest putInstFileRequest = putObjectRequest.clone(); // Create instruction ContentCryptoMaterial cekMaterial = createContentCryptoMaterial(putObjectRequest); // Wraps the object data with a cipher input stream; note the metadata // is mutated as a side effect. PutObjectRequest req = wrapWithCipher(putObjectRequest, cekMaterial); // Put the encrypted object into S3 PutObjectResult result = s3.putObject(req); // Put the instruction file into S3 s3.putObject(upateInstructionPutRequest(putInstFileRequest, cekMaterial)); // Return the result of the encrypted object PUT. return result; } /** * Returns an updated object where the object content input stream contains * the decrypted contents. * * @param wrapper The object whose contents are to be decrypted. * @param cekMaterial The instruction that will be used to decrypt the * object data. * @return The updated object where the object content input stream contains * the decrypted contents. */ private S3ObjectWrapper decrypt(S3ObjectWrapper wrapper, ContentCryptoMaterial cekMaterial, long[] range) { S3ObjectInputStream objectContent = wrapper.getObjectContent(); wrapper.setObjectContent(new S3ObjectInputStream( new CipherLiteInputStream(objectContent, cekMaterial .getCipherLite(), DEFAULT_BUFFER_SIZE), objectContent .getHttpRequest())); return wrapper; } /** * Retrieves an instruction file from S3. If no instruction file is found, * returns null. * * @param getObjectRequest A GET request for an object in S3. The parameters * from this request will be used to retrieve the corresponding * instruction file. * @return An instruction file, or null if no instruction file was found. */ private S3ObjectWrapper fetchInstructionFile(GetObjectRequest getObjectRequest) { try { S3Object o = s3.getObject(createInstructionGetRequest(getObjectRequest)); return o == null ? null : new S3ObjectWrapper(o); } catch (AmazonServiceException e) { // If no instruction file is found, log a debug message, and return // null. log.debug("Unable to retrieve instruction file : " + e.getMessage()); return null; } } /** * Asserts that the specified parameter value is not null and if it is, * throws an IllegalArgumentException with the specified error message. * * @param parameterValue The parameter value being checked. * @param errorMessage The error message to include in the * IllegalArgumentException if the specified parameter is null. */ private void assertParameterNotNull(Object parameterValue, String errorMessage) { if (parameterValue == null) throw new IllegalArgumentException(errorMessage); } @Override protected final long ciphertextLength(long originalContentLength) { // Add 16 bytes for the 128-bit tag length using AES/GCM return originalContentLength + contentCryptoScheme.getTagLengthInBits() / 8; } }