Java tutorial
/** * Copyright 2012 multibit.org * * Licensed under the MIT license (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://opensource.org/licenses/mit-license.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License 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 org.multibit.crypto; import java.io.UnsupportedEncodingException; import java.nio.CharBuffer; import java.security.SecureRandom; import java.util.Arrays; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.crypto.BufferedBlockCipher; import org.spongycastle.crypto.CipherParameters; import org.spongycastle.crypto.PBEParametersGenerator; import org.spongycastle.crypto.engines.AESFastEngine; import org.spongycastle.crypto.generators.OpenSSLPBEParametersGenerator; import org.spongycastle.crypto.modes.CBCBlockCipher; import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.spongycastle.crypto.params.ParametersWithIV; import com.google.uro.core.Utils; import com.google.uro.crypto.KeyCrypterException; import com.google.common.base.Preconditions; /** * This class encrypts and decrypts a string in a manner that is compatible with * OpenSSL. * * If you encrypt a string with this class you can decrypt it with the OpenSSL * command: openssl enc -d -aes-256-cbc -a -in cipher.txt -out plain.txt -pass * pass:aTestPassword * * where: cipher.txt = file containing the cipher text plain.txt - where you * want the plaintext to be saved * * substitute your password for "aTestPassword" or remove the "-pass" parameter * to be prompted. * * @author jim * */ public class KeyCrypterOpenSSL { private Logger log = LoggerFactory.getLogger(KeyCrypterOpenSSL.class); /** * The string encoding to use when converting strings to bytes */ public static final String STRING_ENCODING = "UTF-8"; /** * number of times the password & salt are hashed during key creation. */ private static final int NUMBER_OF_ITERATIONS = 1024; /** * Key length. */ private static final int KEY_LENGTH = 256; /** * Initialization vector length. */ private static final int IV_LENGTH = 128; /** * The length of the salt. */ private static final int SALT_LENGTH = 8; /** * OpenSSL salted prefix text. */ public static final String OPENSSL_SALTED_TEXT = "Salted__"; /** * OpenSSL salted prefix bytes - also used as magic number for encrypted * key file. */ public byte[] openSSLSaltedBytes; /** * Magic text that appears at the beginning of every OpenSSL encrypted file. * Used in identifying encrypted key files. */ private String openSSLMagicText = null; public static final int NUMBER_OF_CHARACTERS_TO_MATCH_IN_OPENSSL_MAGIC_TEXT = 10; private static SecureRandom secureRandom = new SecureRandom(); public KeyCrypterOpenSSL() { try { openSSLSaltedBytes = OPENSSL_SALTED_TEXT.getBytes(STRING_ENCODING); openSSLMagicText = Base64 .encodeBase64String( KeyCrypterOpenSSL.OPENSSL_SALTED_TEXT.getBytes(KeyCrypterOpenSSL.STRING_ENCODING)) .substring(0, KeyCrypterOpenSSL.NUMBER_OF_CHARACTERS_TO_MATCH_IN_OPENSSL_MAGIC_TEXT); } catch (UnsupportedEncodingException e) { log.error("Could not construct EncrypterDecrypter", e.getMessage()); } } /** * Get password and generate key and iv. * * @param password * The password to use in key generation * @param salt * The salt to use in key generation * @return The CipherParameters containing the created key * @throws Exception */ private CipherParameters getAESPasswordKey(CharSequence password, byte[] salt) throws KeyCrypterException { try { PBEParametersGenerator generator = new OpenSSLPBEParametersGenerator(); generator.init(PBEParametersGenerator.PKCS5PasswordToBytes(convertToCharArray(password)), salt, NUMBER_OF_ITERATIONS); ParametersWithIV key = (ParametersWithIV) generator.generateDerivedParameters(KEY_LENGTH, IV_LENGTH); return key; } catch (Exception e) { throw new KeyCrypterException("Could not generate key from password of length " + password.length() + " and salt '" + Utils.bytesToHexString(salt), e); } } /** * Password based encryption using AES - CBC 256 bits. * * @param plainText * The text to encrypt * @param password * The password to use for encryption * @return The encrypted string * @throws EncrypterDecrypterException */ public String encrypt(String plainText, CharSequence password) throws KeyCrypterException { try { byte[] plainTextAsBytes; if (plainText == null) { plainTextAsBytes = new byte[0]; } else { plainTextAsBytes = plainText.getBytes(STRING_ENCODING); } byte[] encryptedBytes = encrypt(plainTextAsBytes, password); // OpenSSL prefixes the salt bytes + encryptedBytes with Salted___ and then base64 encodes it byte[] encryptedBytesPlusSaltedText = concat(openSSLSaltedBytes, encryptedBytes); return Base64.encodeBase64String(encryptedBytesPlusSaltedText); } catch (Exception e) { throw new KeyCrypterException("Could not encrypt string '" + plainText + "'", e); } } /** * Password based encryption using AES - CBC 256 bits. * * @param plainBytes * The bytes to encrypt * @param password * The password to use for encryption * @return SALT_LENGTH bytes of salt followed by the encrypted bytes. * @throws EncrypterDecrypterException */ public byte[] encrypt(byte[] plainTextAsBytes, CharSequence password) throws KeyCrypterException { try { // Generate salt - each encryption call has a different salt. byte[] salt = new byte[SALT_LENGTH]; secureRandom.nextBytes(salt); ParametersWithIV key = (ParametersWithIV) getAESPasswordKey(password, salt); // The following code uses an AES cipher to encrypt the message. BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); cipher.init(true, key); byte[] encryptedBytes = new byte[cipher.getOutputSize(plainTextAsBytes.length)]; int length = cipher.processBytes(plainTextAsBytes, 0, plainTextAsBytes.length, encryptedBytes, 0); cipher.doFinal(encryptedBytes, length); // The result bytes are the SALT_LENGTH bytes followed by the encrypted bytes. return concat(salt, encryptedBytes); } catch (Exception e) { throw new KeyCrypterException( "Could not encrypt bytes '" + Utils.bytesToHexString(plainTextAsBytes) + "'", e); } } /** * Decrypt text previously encrypted with this class. * * @param textToDecode * The code to decrypt * @param passwordbThe * password to use for decryption * @return The decrypted text * @throws EncrypterDecrypterException */ public String decrypt(String textToDecode, CharSequence password) throws KeyCrypterException { try { final byte[] decodeTextAsBytes = Base64.decodeBase64(textToDecode.getBytes(STRING_ENCODING)); // Strip off the bytes due to the OPENSSL_SALTED_TEXT prefix text. int saltPrefixTextLength = openSSLSaltedBytes.length; byte[] cipherBytes = new byte[decodeTextAsBytes.length - saltPrefixTextLength]; System.arraycopy(decodeTextAsBytes, saltPrefixTextLength, cipherBytes, 0, decodeTextAsBytes.length - saltPrefixTextLength); byte[] decryptedBytes = decrypt(cipherBytes, password); return new String(decryptedBytes, STRING_ENCODING).trim(); } catch (Exception e) { throw new KeyCrypterException("Could not decrypt input string", e); } } /** * Decrypt bytes previously encrypted with this class. * * @param bytesToDecode * The bytes to decrypt * @param passwordbThe * password to use for decryption * @return The decrypted bytes * @throws EncrypterDecrypterException */ public byte[] decrypt(byte[] bytesToDecode, CharSequence password) throws KeyCrypterException { try { // separate the salt and bytes to decrypt byte[] salt = new byte[SALT_LENGTH]; System.arraycopy(bytesToDecode, 0, salt, 0, SALT_LENGTH); byte[] cipherBytes = new byte[bytesToDecode.length - SALT_LENGTH]; System.arraycopy(bytesToDecode, SALT_LENGTH, cipherBytes, 0, bytesToDecode.length - SALT_LENGTH); ParametersWithIV key = (ParametersWithIV) getAESPasswordKey(password, salt); // decrypt the message BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); cipher.init(false, key); byte[] decryptedBytes = new byte[cipher.getOutputSize(cipherBytes.length)]; int length = cipher.processBytes(cipherBytes, 0, cipherBytes.length, decryptedBytes, 0); cipher.doFinal(decryptedBytes, length); return decryptedBytes; } catch (Exception e) { throw new KeyCrypterException("Could not decrypt input string", e); } } /** * Concatenate two byte arrays. */ private byte[] concat(byte[] arrayA, byte[] arrayB) { byte[] result = new byte[arrayA.length + arrayB.length]; System.arraycopy(arrayA, 0, result, 0, arrayA.length); System.arraycopy(arrayB, 0, result, arrayA.length, arrayB.length); return result; } /** * Convert a CharSequence (which are UTF16) into a char array. * * Note: a String.getBytes() is not used to avoid creating a String of the password in the JVM. */ private char[] convertToCharArray(CharSequence charSequence) { if (charSequence == null) { return null; } char[] charArray = new char[charSequence.length()]; for (int i = 0; i < charSequence.length(); i++) { charArray[i] = charSequence.charAt(i); } return charArray; } /** * Get the OpenSSL OPENSSL_SALTED_TEXT prefix text as bytes. * * @return The openSSL salted prefix bytes */ public byte[] getOpenSSLSaltedBytes() { return openSSLSaltedBytes; } /** * Get the magic text that starts every OpenSSL encrypted String. * * @return The magic text that starts every OpenSSL encrypted String */ public String getOpenSSLMagicText() { return openSSLMagicText; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + Arrays.hashCode(openSSLSaltedBytes); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof KeyCrypterOpenSSL)) return false; return true; } }