Java tutorial
/* * Copyright 2010-2013 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 cn.ctyun.amazonaws.services.s3.internal.crypto; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.Provider; import java.security.SecureRandom; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; import cn.ctyun.amazonaws.AmazonClientException; import cn.ctyun.amazonaws.services.s3.Headers; import cn.ctyun.amazonaws.services.s3.internal.InputSubstream; import cn.ctyun.amazonaws.services.s3.internal.Mimetypes; import cn.ctyun.amazonaws.services.s3.internal.RepeatableCipherInputStream; import cn.ctyun.amazonaws.services.s3.internal.RepeatableFileInputStream; import cn.ctyun.amazonaws.services.s3.model.DeleteObjectRequest; import cn.ctyun.amazonaws.services.s3.model.EncryptionMaterials; import cn.ctyun.amazonaws.services.s3.model.EncryptionMaterialsAccessor; import cn.ctyun.amazonaws.services.s3.model.EncryptionMaterialsProvider; import cn.ctyun.amazonaws.services.s3.model.GetObjectRequest; import cn.ctyun.amazonaws.services.s3.model.InitiateMultipartUploadRequest; import cn.ctyun.amazonaws.services.s3.model.ObjectMetadata; import cn.ctyun.amazonaws.services.s3.model.PutObjectRequest; import cn.ctyun.amazonaws.services.s3.model.S3Object; import cn.ctyun.amazonaws.services.s3.model.S3ObjectInputStream; import cn.ctyun.amazonaws.services.s3.model.StaticEncryptionMaterialsProvider; import cn.ctyun.amazonaws.services.s3.model.UploadPartRequest; import cn.ctyun.amazonaws.util.json.JSONException; import cn.ctyun.amazonaws.util.json.JSONObject; /** * 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. */ public class EncryptionUtils { /** Suffix appended to the end of instruction file names */ private static final String INSTRUCTION_SUFFIX = ".instruction"; /** * Returns an updated request where the metadata contains encryption information and the input stream contains * the encrypted object contents. The specified encryption materials will be used to encrypt and decrypt data. * * @param request * The request whose contents are to be encrypted. * @param materials * The encryption materials to be used to encrypt and decrypt data. * @param cryptoProvider * The crypto provider whose encryption implementation will be used to encrypt data * @return * The updated request where the metadata is set up for encryption and input stream contains * the encrypted contents. * * @deprecated use generateInstruction, encryptRequestUsingInstruction, and updateMetadataWithEncryptionInfo instead */ @Deprecated public static PutObjectRequest encryptRequestUsingMetadata(PutObjectRequest request, EncryptionMaterials materials, Provider cryptoProvider) { // Create instruction EncryptionInstruction instruction = EncryptionUtils.generateInstruction(materials, cryptoProvider); // Encrypt the object data with the instruction PutObjectRequest encryptedObjectRequest = EncryptionUtils.encryptRequestUsingInstruction(request, instruction); // Update the metadata EncryptionUtils.updateMetadataWithEncryptionInstruction(request, instruction); return encryptedObjectRequest; } /** * Returns an updated object where the object content input stream contains the decrypted contents. * * @param object * The object whose contents are to be decrypted. * @param materials * The encryption materials to be used to encrypt and decrypt data. * @param cryptoProvider * The crypto provider whose encryption implementation will be used to decrypt data * @return * The updated object where the object content input stream contains the decrypted contents. * * @deprecated use buildInstructionFromObjectMetadata and decryptObjectUsingInstruction instead. */ @Deprecated public static S3Object decryptObjectUsingMetadata(S3Object object, EncryptionMaterials materials, Provider cryptoProvider) { // Create an instruction object from the object headers EncryptionInstruction instruction = EncryptionUtils.buildInstructionFromObjectMetadata(object, materials, cryptoProvider); // Decrypt the object file with the instruction return EncryptionUtils.decryptObjectUsingInstruction(object, instruction); } /** * Generates an instruction that will be used to encrypt an object. * * @param materials * The encryption materials to be used to encrypt and decrypt data. * @param cryptoProvider * The crypto provider whose encryption implementation will be used to encrypt and decrypt data. * @return * The instruction that will be used to encrypt an object. */ @Deprecated public static EncryptionInstruction generateInstruction(EncryptionMaterials materials, Provider cryptoProvider) { return generateInstruction(new StaticEncryptionMaterialsProvider(materials), cryptoProvider); } public static EncryptionInstruction generateInstruction(EncryptionMaterialsProvider materialsProvider, Provider cryptoProvider) { // Generate a one-time use symmetric key and initialize a cipher to encrypt object data SecretKey envelopeSymmetricKey = generateOneTimeUseSymmetricKey(); CipherFactory cipherFactory = new CipherFactory(envelopeSymmetricKey, Cipher.ENCRYPT_MODE, null, cryptoProvider); // Encrypt the envelope symmetric key EncryptionMaterials materials = materialsProvider.getEncryptionMaterials(); byte[] encryptedEnvelopeSymmetricKey = getEncryptedSymmetricKey(envelopeSymmetricKey, materials, cryptoProvider); // Return a new instruction with the appropriate fields. return new EncryptionInstruction(materials.getMaterialsDescription(), encryptedEnvelopeSymmetricKey, envelopeSymmetricKey, cipherFactory); } /** * Builds an instruction object from the contents of an instruction file. * * @param instructionFile * A non-null instruction file retrieved from S3 that contains encryption information * @param materials * The non-null encryption materials to be used to encrypt and decrypt data. * @param cryptoProvider * The crypto provider whose encryption implementation will be used to encrypt and decrypt data. Null is ok and uses the * preferred provider from Security.getProviders(). * @return * A non-null instruction object containing encryption information */ @Deprecated public static EncryptionInstruction buildInstructionFromInstructionFile(S3Object instructionFile, EncryptionMaterials materials, Provider cryptoProvider) { return buildInstructionFromInstructionFile(instructionFile, new StaticEncryptionMaterialsProvider(materials), cryptoProvider); } /** * Builds an instruction object from the contents of an instruction file. * * @param instructionFile * A non-null instruction file retrieved from S3 that contains encryption information * @param materialsProvider * The non-null encryption materials provider to be used to encrypt and decrypt data. * @param cryptoProvider * The crypto provider whose encryption implementation will be used to encrypt and decrypt data. Null is ok and uses the * preferred provider from Security.getProviders(). * @return * A non-null instruction object containing encryption information */ public static EncryptionInstruction buildInstructionFromInstructionFile(S3Object instructionFile, EncryptionMaterialsProvider materialsProvider, Provider cryptoProvider) { JSONObject instructionJSON = parseJSONInstruction(instructionFile); try { // Get fields from instruction object byte[] encryptedSymmetricKeyBytes = instructionJSON.getString(Headers.CRYPTO_KEY).getBytes(); byte[] initVectorBytes = instructionJSON.getString(Headers.CRYPTO_IV).getBytes(); String materialsDescriptionString = instructionJSON.getString(Headers.MATERIALS_DESCRIPTION); Map<String, String> materialsDescription = convertJSONToMap(materialsDescriptionString); // Decode from Base 64 to standard binary bytes encryptedSymmetricKeyBytes = Base64.decodeBase64(encryptedSymmetricKeyBytes); initVectorBytes = Base64.decodeBase64(initVectorBytes); if (encryptedSymmetricKeyBytes == null || initVectorBytes == null || materialsDescription == null) { // If necessary encryption info was not found in the instruction file, throw an exception. throw new AmazonClientException(String.format( "Necessary encryption info not found in the instruction file '%s' in bucket '%s'", instructionFile.getKey(), instructionFile.getBucketName())); } EncryptionMaterials materials = retrieveOriginalMaterials(materialsDescription, materialsProvider); // If we're unable to retrieve the original encryption materials, we can't decrypt the object, so // throw an exception. if (materials == null) { throw new AmazonClientException(String.format( "Unable to retrieve the encryption materials that originally " + "encrypted object corresponding to instruction file '%s' in bucket '%s'.", instructionFile.getKey(), instructionFile.getBucketName())); } // Decrypt the symmetric key and create the symmetric cipher SecretKey symmetricKey = getDecryptedSymmetricKey(encryptedSymmetricKeyBytes, materials, cryptoProvider); CipherFactory cipherFactory = new CipherFactory(symmetricKey, Cipher.DECRYPT_MODE, initVectorBytes, cryptoProvider); return new EncryptionInstruction(materialsDescription, encryptedSymmetricKeyBytes, symmetricKey, cipherFactory); } catch (JSONException e) { throw new AmazonClientException("Unable to parse retrieved instruction file : " + e.getMessage()); } } /** * Builds an instruction object from the object metadata. * * @param object * A non-null object that contains encryption information in its headers * @param materials * The non-null encryption materials to be used to encrypt and decrypt data. * @param cryptoProvider * The crypto provider whose encryption implementation will be used to encrypt and decrypt data. Null is ok and uses the * preferred provider from Security.getProviders(). * @return * A non-null instruction object containing encryption information * * @throws AmazonClientException * if encryption information is missing in the metadata, or the encryption * materials used to encrypt the object are not available via the materials Accessor */ @Deprecated public static EncryptionInstruction buildInstructionFromObjectMetadata(S3Object object, EncryptionMaterials materials, Provider cryptoProvider) { return buildInstructionFromObjectMetadata(object, new StaticEncryptionMaterialsProvider(materials), cryptoProvider); } /** * Builds an instruction object from the object metadata. * * @param object * A non-null object that contains encryption information in its headers * @param materialsProvider * The non-null encryption materials provider to be used to encrypt and decrypt data. * @param cryptoProvider * The crypto provider whose encryption implementation will be used to encrypt and decrypt data. Null is ok and uses the * preferred provider from Security.getProviders(). * @return * A non-null instruction object containing encryption information * * @throws AmazonClientException * if encryption information is missing in the metadata, or the encryption * materials used to encrypt the object are not available via the materials Accessor */ public static EncryptionInstruction buildInstructionFromObjectMetadata(S3Object object, EncryptionMaterialsProvider materialsProvider, Provider cryptoProvider) { ObjectMetadata metadata = object.getObjectMetadata(); // Get encryption info from metadata. byte[] encryptedSymmetricKeyBytes = getCryptoBytesFromMetadata(Headers.CRYPTO_KEY, metadata); byte[] initVectorBytes = getCryptoBytesFromMetadata(Headers.CRYPTO_IV, metadata); String materialsDescriptionString = getStringFromMetadata(Headers.MATERIALS_DESCRIPTION, metadata); Map<String, String> materialsDescription = convertJSONToMap(materialsDescriptionString); if (encryptedSymmetricKeyBytes == null || initVectorBytes == null || materialsDescription == null) { // If necessary encryption info was not found in the instruction file, throw an exception. throw new AmazonClientException( String.format("Necessary encryption info not found in the headers of file '%s' in bucket '%s'", object.getKey(), object.getBucketName())); } EncryptionMaterials materials = retrieveOriginalMaterials(materialsDescription, materialsProvider); // If we're unable to retrieve the original encryption materials, we can't decrypt the object, so // throw an exception. if (materials == null) { throw new AmazonClientException( String.format( "Unable to retrieve the encryption materials that originally " + "encrypted file '%s' in bucket '%s'.", object.getKey(), object.getBucketName())); } // Decrypt the symmetric key and create the symmetric cipher SecretKey symmetricKey = getDecryptedSymmetricKey(encryptedSymmetricKeyBytes, materials, cryptoProvider); CipherFactory cipherFactory = new CipherFactory(symmetricKey, Cipher.DECRYPT_MODE, initVectorBytes, cryptoProvider); return new EncryptionInstruction(materialsDescription, encryptedSymmetricKeyBytes, symmetricKey, cipherFactory); } /** * Returns an updated request where the input stream contains the encrypted object contents. * The specified instruction will be used to encrypt data. * * @param request * The request whose contents are to be encrypted. * @param instruction * The instruction that will be used to encrypt the object data. * @return * The updated request where the input stream contains the encrypted contents. */ public static PutObjectRequest encryptRequestUsingInstruction(PutObjectRequest request, EncryptionInstruction instruction) { // Create a new metadata object if there is no metadata already. ObjectMetadata metadata = request.getMetadata(); if (metadata == null) { metadata = new ObjectMetadata(); } // Record the original Content MD5, if present, for the unencrypted data if (metadata.getContentMD5() != null) { metadata.addUserMetadata(Headers.UNENCRYPTED_CONTENT_MD5, metadata.getContentMD5()); } // Record the original, unencrypted content-length so it can be accessed later long originalContentLength = getUnencryptedContentLength(request, metadata); if (originalContentLength >= 0) metadata.addUserMetadata(Headers.UNENCRYPTED_CONTENT_LENGTH, Long.toString(originalContentLength)); // Put the calculated length of the encrypted contents in the metadata long cryptoContentLength = calculateCryptoContentLength(instruction.getSymmetricCipher(), request, metadata); if (cryptoContentLength >= 0) metadata.setContentLength(cryptoContentLength); request.setMetadata(metadata); // Create encrypted input stream request.setInputStream(getEncryptedInputStream(request, instruction.getCipherFactory())); // Treat all encryption requests as input stream upload requests, not as file upload requests. request.setFile(null); return request; } /** * Returns an updated object where the object content input stream contains the decrypted contents. * * @param object * The object whose contents are to be decrypted. * @param instruction * 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. */ public static S3Object decryptObjectUsingInstruction(S3Object object, EncryptionInstruction instruction) { S3ObjectInputStream objectContent = object.getObjectContent(); InputStream decryptedInputStream = new RepeatableCipherInputStream(objectContent, instruction.getCipherFactory()); object.setObjectContent(new S3ObjectInputStream(decryptedInputStream, objectContent.getHttpRequest())); return object; } /** * Creates a put request to store the specified instruction object in S3. * * @param request * The put request for the original object to be stored in S3. * @param instruction * The instruction object to be stored in S3. * @return * A put request to store the specified instruction object in S3. */ public static PutObjectRequest createInstructionPutRequest(PutObjectRequest request, EncryptionInstruction instruction) { JSONObject instructionJSON = convertInstructionToJSONObject(instruction); byte[] instructionBytes = instructionJSON.toString().getBytes(); InputStream instructionInputStream = new ByteArrayInputStream(instructionBytes); ObjectMetadata metadata = request.getMetadata(); // Set the content-length of the upload metadata.setContentLength(instructionBytes.length); // Set the crypto instruction file header metadata.addUserMetadata(Headers.CRYPTO_INSTRUCTION_FILE, ""); // Update the instruction request request.setKey(request.getKey() + INSTRUCTION_SUFFIX); request.setMetadata(metadata); request.setInputStream(instructionInputStream); return request; } public static PutObjectRequest createInstructionPutRequest(String bucketName, String key, EncryptionInstruction instruction) { JSONObject instructionJSON = convertInstructionToJSONObject(instruction); byte[] instructionBytes = instructionJSON.toString().getBytes(); InputStream instructionInputStream = new ByteArrayInputStream(instructionBytes); ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentLength(instructionBytes.length); metadata.addUserMetadata(Headers.CRYPTO_INSTRUCTION_FILE, ""); return new PutObjectRequest(bucketName, key + INSTRUCTION_SUFFIX, instructionInputStream, metadata); } /** * Creates a get request to retrieve an instruction file from S3. * * @param request * The get request for the original object to be retrieved from S3. * @return * A get request to retrieve an instruction file from S3. */ public static GetObjectRequest createInstructionGetRequest(GetObjectRequest request) { return new GetObjectRequest(request.getBucketName(), request.getKey() + INSTRUCTION_SUFFIX, request.getVersionId()); } /** * Creates a delete request to delete an instruction file in S3. * * @param request * The delete request for the original object to be deleted from S3. * @return * A delete request to delete an instruction file in S3. */ public static DeleteObjectRequest createInstructionDeleteObjectRequest(DeleteObjectRequest request) { return new DeleteObjectRequest(request.getBucketName(), request.getKey() + INSTRUCTION_SUFFIX); } /** * Returns true if the specified S3Object contains encryption info in its * metadata, false otherwise. * * @param retrievedObject * An S3Object * @return * True if the specified S3Object contains encryption info in its * metadata, false otherwise. */ public static boolean isEncryptionInfoInMetadata(S3Object retrievedObject) { Map<String, String> metadata = retrievedObject.getObjectMetadata().getUserMetadata(); if (metadata == null) { return false; } return (metadata.containsKey(Headers.CRYPTO_IV) && metadata.containsKey(Headers.CRYPTO_KEY) && metadata.containsKey(Headers.MATERIALS_DESCRIPTION)); } /** * Returns true if the specified S3Object is an instruction file containing * encryption info, false otherwise. * * @param instructionFile * An S3Object that may potentially be an instruction file * @return * True if the specified S3Object is an instruction file containing * encryption info, false otherwise. */ public static boolean isEncryptionInfoInInstructionFile(S3Object instructionFile) { if (instructionFile == null) { return false; } Map<String, String> metadata = instructionFile.getObjectMetadata().getUserMetadata(); if (metadata == null) { return false; } return metadata.containsKey(Headers.CRYPTO_INSTRUCTION_FILE); } /** * Adjusts a user specified range to retrieve all of the cipher blocks (each of size 16 bytes) that * contain the specified range. * * For Chained Block Cipher decryption to function properly, we need to retrieve the cipher block that precedes * the range, all of the cipher blocks that contain the range, and the cipher block that follows the range. * * @param range * A two-element array of longs corresponding to the start and finish (inclusive) of a desired * range of bytes. * @return * A two-element array of longs corresponding to the start and finish of the cipher blocks to * be retrieved. If the range is invalid, then return null. */ public static long[] getAdjustedCryptoRange(long[] range) { // If range is invalid, then return null. if (range == null || range[0] > range[1]) { return null; } long[] adjustedCryptoRange = new long[2]; adjustedCryptoRange[0] = getCipherBlockLowerBound(range[0]); adjustedCryptoRange[1] = getCipherBlockUpperBound(range[1]); return adjustedCryptoRange; } /** * 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 object * 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. * @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. */ public static S3Object adjustOutputToDesiredRange(S3Object object, long[] range) { if (range == null || range[0] > range[1]) { // Make no modifications if range is invalid. return object; } else { try { S3ObjectInputStream objectContent = object.getObjectContent(); InputStream adjustedRangeContents = new AdjustedRangeInputStream(objectContent, range[0], range[1]); object.setObjectContent( new S3ObjectInputStream(adjustedRangeContents, objectContent.getHttpRequest())); return object; } catch (IOException e) { throw new AmazonClientException("Error adjusting output to desired byte range: " + e.getMessage()); } } } /** * Generates a one-time use Symmetric Key on-the-fly for use in envelope encryption. */ public static SecretKey generateOneTimeUseSymmetricKey() { KeyGenerator generator; try { generator = KeyGenerator.getInstance(JceEncryptionConstants.SYMMETRIC_KEY_ALGORITHM); generator.init(JceEncryptionConstants.SYMMETRIC_KEY_LENGTH, new SecureRandom()); return generator.generateKey(); } catch (NoSuchAlgorithmException e) { throw new AmazonClientException("Unable to generate envelope symmetric key:" + e.getMessage(), e); } } /** * Creates a symmetric cipher in the specified mode from the given symmetric key and IV. The given * crypto provider will provide the encryption implementation. If the crypto provider is null, then * the default JCE crypto provider will be used. */ public static Cipher createSymmetricCipher(SecretKey symmetricCryptoKey, int encryptMode, Provider cryptoProvider, byte[] initVector) { try { Cipher cipher; if (cryptoProvider != null) { cipher = Cipher.getInstance(JceEncryptionConstants.SYMMETRIC_CIPHER_METHOD, cryptoProvider); } else { cipher = Cipher.getInstance(JceEncryptionConstants.SYMMETRIC_CIPHER_METHOD); } if (initVector != null) { cipher.init(encryptMode, symmetricCryptoKey, new IvParameterSpec(initVector)); } else { cipher.init(encryptMode, symmetricCryptoKey); } return cipher; } catch (Exception e) { throw new AmazonClientException("Unable to build cipher: " + e.getMessage() + "\nMake sure you have the JCE unlimited strength policy files installed and " + "configured for your JVM: http://www.ngs.ac.uk/tools/jcepolicyfiles", e); } } /** * Encrypts a symmetric key using the provided encryption materials and returns * it in raw byte array form. */ public static byte[] getEncryptedSymmetricKey(SecretKey toBeEncrypted, EncryptionMaterials materials, Provider cryptoProvider) { Key keyToDoEncryption; if (materials.getKeyPair() != null) { // Do envelope encryption with public key from key pair keyToDoEncryption = materials.getKeyPair().getPublic(); } else { // Do envelope encryption with symmetric key keyToDoEncryption = materials.getSymmetricKey(); } try { Cipher cipher; byte[] toBeEncryptedBytes = toBeEncrypted.getEncoded(); if (cryptoProvider != null) { cipher = Cipher.getInstance(keyToDoEncryption.getAlgorithm(), cryptoProvider); } else { cipher = Cipher.getInstance(keyToDoEncryption.getAlgorithm()); // Use default JCE Provider } cipher.init(Cipher.ENCRYPT_MODE, keyToDoEncryption); return cipher.doFinal(toBeEncryptedBytes); } catch (Exception e) { throw new AmazonClientException("Unable to encrypt symmetric key: " + e.getMessage(), e); } } /** * Decrypts an encrypted symmetric key using the provided encryption materials and returns * it as a SecretKey object. */ private static SecretKey getDecryptedSymmetricKey(byte[] encryptedSymmetricKeyBytes, EncryptionMaterials materials, Provider cryptoProvider) { Key keyToDoDecryption; if (materials.getKeyPair() != null) { // Do envelope decryption with private key from key pair keyToDoDecryption = materials.getKeyPair().getPrivate(); } else { // Do envelope decryption with symmetric key keyToDoDecryption = materials.getSymmetricKey(); } try { Cipher cipher; if (cryptoProvider != null) { cipher = Cipher.getInstance(keyToDoDecryption.getAlgorithm(), cryptoProvider); } else { cipher = Cipher.getInstance(keyToDoDecryption.getAlgorithm()); } cipher.init(Cipher.DECRYPT_MODE, keyToDoDecryption); byte[] decryptedSymmetricKeyBytes = cipher.doFinal(encryptedSymmetricKeyBytes); return new SecretKeySpec(decryptedSymmetricKeyBytes, JceEncryptionConstants.SYMMETRIC_KEY_ALGORITHM); } catch (Exception e) { throw new AmazonClientException( "Unable to decrypt symmetric key from object metadata : " + e.getMessage(), e); } } private static InputStream getEncryptedInputStream(PutObjectRequest request, CipherFactory cipherFactory) { try { InputStream originalInputStream = request.getInputStream(); if (request.getFile() != null) { originalInputStream = new RepeatableFileInputStream(request.getFile()); } return new RepeatableCipherInputStream(originalInputStream, cipherFactory); } catch (Exception e) { throw new AmazonClientException("Unable to create cipher input stream: " + e.getMessage(), e); } } public static InputStream getEncryptedInputStream(UploadPartRequest request, CipherFactory cipherFactory) { try { InputStream originalInputStream = request.getInputStream(); if (request.getFile() != null) { originalInputStream = new InputSubstream(new RepeatableFileInputStream(request.getFile()), request.getFileOffset(), request.getPartSize(), request.isLastPart()); } originalInputStream = new RepeatableCipherInputStream(originalInputStream, cipherFactory); if (request.isLastPart() == false) { // We want to prevent the final padding from being sent on the stream... originalInputStream = new InputSubstream(originalInputStream, 0, request.getPartSize(), false); } long partSize = request.getPartSize(); int cipherBlockSize = cipherFactory.createCipher().getBlockSize(); return new ByteRangeCapturingInputStream(originalInputStream, partSize - cipherBlockSize, partSize); } catch (Exception e) { throw new AmazonClientException("Unable to create cipher input stream: " + e.getMessage(), e); } } /** * Retrieves the byte[] value of either the crypto key or crypto IV. If these fields are not found in * the metadata, returns null. * * Note: The bytes are transported in Base64-encoding, so they are decoded before they are returned. */ private static byte[] getCryptoBytesFromMetadata(String headerName, ObjectMetadata metadata) throws NullPointerException { Map<String, String> userMetadata = metadata.getUserMetadata(); if (userMetadata == null || !userMetadata.containsKey(headerName)) { return null; } else { byte[] valueBytes = userMetadata.get(headerName).getBytes(); // Convert Base64 bytes to binary data. return Base64.decodeBase64(valueBytes); } } /** * Retrieves the String value of the given header from the metadata. Returns null if the field is not * found in the metadata. */ private static String getStringFromMetadata(String headerName, ObjectMetadata metadata) throws NullPointerException { Map<String, String> userMetadata = metadata.getUserMetadata(); if (userMetadata == null || !userMetadata.containsKey(headerName)) { return null; } else { return userMetadata.get(headerName); } } /** * Converts the JSON encoded materials description to a Map<String, String> */ @SuppressWarnings("unchecked") // Suppresses Iterator<String> type warning private static Map<String, String> convertJSONToMap(String descriptionJSONString) { if (descriptionJSONString == null) { return null; } try { JSONObject descriptionJSON = new JSONObject(descriptionJSONString); Iterator<String> keysIterator = descriptionJSON.keys(); Map<String, String> materialsDescription = new HashMap<String, String>(); while (keysIterator.hasNext()) { String key = keysIterator.next(); materialsDescription.put(key, descriptionJSON.getString(key)); } return materialsDescription; } catch (JSONException e) { throw new AmazonClientException( "Unable to parse encryption materials description from metadata :" + e.getMessage()); } } /** * Update the request's ObjectMetadata with the necessary information for decrypting the object * * @param request * Non-null PUT request encrypted using the given instruction * @param instruction * Non-null instruction used to encrypt the data in this PUT request. */ public static void updateMetadataWithEncryptionInstruction(PutObjectRequest request, EncryptionInstruction instruction) { byte[] keyBytesToStoreInMetadata = instruction.getEncryptedSymmetricKey(); Cipher symmetricCipher = instruction.getSymmetricCipher(); Map<String, String> materialsDescription = instruction.getMaterialsDescription(); ObjectMetadata metadata = request.getMetadata(); if (metadata == null) metadata = new ObjectMetadata(); if (request.getFile() != null) { Mimetypes mimetypes = Mimetypes.getInstance(); metadata.setContentType(mimetypes.getMimetype(request.getFile())); } updateMetadata(metadata, keyBytesToStoreInMetadata, symmetricCipher, materialsDescription); request.setMetadata(metadata); } private static void updateMetadata(ObjectMetadata metadata, byte[] keyBytesToStoreInMetadata, Cipher symmetricCipher, Map<String, String> materialsDescription) { // If we generated a symmetric key to encrypt the data, store it in the object metadata. if (keyBytesToStoreInMetadata != null) { keyBytesToStoreInMetadata = Base64.encodeBase64(keyBytesToStoreInMetadata); metadata.addUserMetadata(Headers.CRYPTO_KEY, new String(keyBytesToStoreInMetadata)); } // Put the cipher initialization vector (IV) into the object metadata byte[] initVectorBytes = symmetricCipher.getIV(); initVectorBytes = Base64.encodeBase64(initVectorBytes); metadata.addUserMetadata(Headers.CRYPTO_IV, new String(initVectorBytes)); // Put the materials description into the object metadata as JSON JSONObject descriptionJSON = new JSONObject(materialsDescription); metadata.addUserMetadata(Headers.MATERIALS_DESCRIPTION, descriptionJSON.toString()); } public static ObjectMetadata updateMetadataWithEncryptionInfo(InitiateMultipartUploadRequest request, byte[] keyBytesToStoreInMetadata, Cipher symmetricCipher, Map<String, String> materialsDescription) { ObjectMetadata metadata = request.getObjectMetadata(); if (metadata == null) metadata = new ObjectMetadata(); updateMetadata(metadata, keyBytesToStoreInMetadata, symmetricCipher, materialsDescription); return metadata; } /** * Retrieve the original materials corresponding to the specified materials description. * Returns null if unable to retrieve the original materials. */ private static EncryptionMaterials retrieveOriginalMaterials(Map<String, String> materialsDescription, EncryptionMaterialsAccessor accessor) { if (accessor == null) return null; return accessor.getEncryptionMaterials(materialsDescription); } /** * Calculates the length of the encrypted file given the original plaintext * file length and the cipher that will be used for encryption. * * @return * The size of the encrypted file in bytes, or -1 if no content length * has been set yet. */ private static long calculateCryptoContentLength(Cipher symmetricCipher, PutObjectRequest request, ObjectMetadata metadata) { long plaintextLength = getUnencryptedContentLength(request, metadata); // If we have a zero length object, return zero as the encrypted size if (plaintextLength == 0) return 0; // If we don't know the unencrypted size, then report -1 if (plaintextLength < 0) return -1; long cipherBlockSize = symmetricCipher.getBlockSize(); long offset = cipherBlockSize - (plaintextLength % cipherBlockSize); return plaintextLength + offset; } public static long calculateCryptoContentLength(Cipher symmetricCipher, UploadPartRequest request) { long plaintextLength; if (request.getFile() != null) { if (request.getPartSize() > 0) plaintextLength = request.getPartSize(); else plaintextLength = request.getFile().length(); } else if (request.getInputStream() != null) { plaintextLength = request.getPartSize(); } else { return -1; } long cipherBlockSize = symmetricCipher.getBlockSize(); long offset = cipherBlockSize - (plaintextLength % cipherBlockSize); return plaintextLength + offset; } /** * Returns the content length of the unencrypted data in a PutObjectRequest, * or -1 if the original content-length isn't known. * * @param request * The request to examine. * @param metadata * The metadata for the request. * * @return The content length of the unencrypted data in the request, or -1 * if it isn't known. */ private static long getUnencryptedContentLength(PutObjectRequest request, ObjectMetadata metadata) { if (request.getFile() != null) { return request.getFile().length(); } else if (request.getInputStream() != null && metadata.getContentLength() > 0) { return metadata.getContentLength(); } return -1; } /** * Returns a JSONObject representation of the instruction object. */ private static JSONObject convertInstructionToJSONObject(EncryptionInstruction instruction) { JSONObject instructionJSON = new JSONObject(); try { JSONObject materialsDescriptionJSON = new JSONObject(instruction.getMaterialsDescription()); byte[] initVector = instruction.getSymmetricCipher().getIV(); initVector = Base64.encodeBase64(initVector); byte[] encryptedKeyBytes = instruction.getEncryptedSymmetricKey(); encryptedKeyBytes = Base64.encodeBase64(encryptedKeyBytes); instructionJSON.put(Headers.MATERIALS_DESCRIPTION, materialsDescriptionJSON.toString()); instructionJSON.put(Headers.CRYPTO_KEY, new String(encryptedKeyBytes)); instructionJSON.put(Headers.CRYPTO_IV, new String(initVector)); } catch (JSONException e) { } // Keys are never null, so JSONException will never be thrown. return instructionJSON; } /** * Parses instruction data retrieved from S3 and returns a JSONObject representing the instruction */ private static JSONObject parseJSONInstruction(S3Object instructionObject) { try { String instructionString = convertStreamToString(instructionObject.getObjectContent()); return new JSONObject(instructionString); } catch (Exception e) { throw new AmazonClientException("Error parsing JSON instruction file: " + e.getMessage()); } } /** * Converts the contents of an input stream to a String */ private static String convertStreamToString(InputStream inputStream) throws IOException { if (inputStream == null) { return ""; } else { StringBuilder stringBuilder = new StringBuilder(); String line; try { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); while ((line = reader.readLine()) != null) { stringBuilder.append(line); } } finally { inputStream.close(); } return stringBuilder.toString(); } } /** * Takes the position of the leftmost desired byte of a user specified range and returns the * position of the start of the previous cipher block, or returns 0 if the leftmost byte is in * the first cipher block. */ private static long getCipherBlockLowerBound(long leftmostBytePosition) { long cipherBlockSize = JceEncryptionConstants.SYMMETRIC_CIPHER_BLOCK_SIZE; long offset = leftmostBytePosition % cipherBlockSize; long lowerBound = leftmostBytePosition - offset - cipherBlockSize; if (lowerBound < 0) { return 0; } else { return lowerBound; } } /** * Takes the position of the rightmost desired byte of a user specified range and returns the * position of the end of the following cipher block. */ private static long getCipherBlockUpperBound(long rightmostBytePosition) { long cipherBlockSize = JceEncryptionConstants.SYMMETRIC_CIPHER_BLOCK_SIZE; long offset = cipherBlockSize - (rightmostBytePosition % cipherBlockSize); return rightmostBytePosition + offset + cipherBlockSize; } }