/* * Copyright 2012-2014 the original author or authors. * * 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 com.mygeopay.wallet.util; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.security.SecureRandom; import java.util.Arrays; import javax.annotation.Nonnull; import org.spongycastle.crypto.BufferedBlockCipher; import org.spongycastle.crypto.CipherParameters; import org.spongycastle.crypto.DataLengthException; import org.spongycastle.crypto.InvalidCipherTextException; 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.common.io.BaseEncoding; /** * 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 * @author Andreas Schildbach */ public class Crypto { private static final BaseEncoding BASE64 = BaseEncoding.base64().withSeparator("\n", 76); private static final Charset UTF_8 = Charset.forName("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. */ private static final String OPENSSL_SALTED_TEXT = "Salted__"; /** * OpenSSL salted prefix bytes - also used as magic number for encrypted key file. */ private static final byte[] OPENSSL_SALTED_BYTES = OPENSSL_SALTED_TEXT.getBytes(UTF_8); /** * Magic text that appears at the beginning of every OpenSSL encrypted file. Used in identifying encrypted key * files. */ private static final String OPENSSL_MAGIC_TEXT = BASE64.encode(Crypto.OPENSSL_SALTED_BYTES).substring(0, Crypto.NUMBER_OF_CHARACTERS_TO_MATCH_IN_OPENSSL_MAGIC_TEXT); private static final int NUMBER_OF_CHARACTERS_TO_MATCH_IN_OPENSSL_MAGIC_TEXT = 10; private static final SecureRandom secureRandom = new SecureRandom(); /** * 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 */ private static CipherParameters getAESPasswordKey(final char[] password, final byte[] salt) { final PBEParametersGenerator generator = new OpenSSLPBEParametersGenerator(); generator.init(PBEParametersGenerator.PKCS5PasswordToBytes(password), salt, NUMBER_OF_ITERATIONS); final ParametersWithIV key = (ParametersWithIV) generator.generateDerivedParameters(KEY_LENGTH, IV_LENGTH); return key; } /** * 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 IOException */ public static String encrypt(@Nonnull final String plainText, @Nonnull final char[] password) throws IOException { final byte[] plainTextAsBytes = plainText.getBytes(UTF_8); return encrypt(plainTextAsBytes, password); } /** * Password based encryption using AES - CBC 256 bits. * * @param plainTextAsBytes * The bytes to encrypt * @param password * The password to use for encryption * @return The encrypted string * @throws IOException */ public static String encrypt(@Nonnull final byte[] plainTextAsBytes, @Nonnull final char[] password) throws IOException { final byte[] encryptedBytes = encryptRaw(plainTextAsBytes, password); // OpenSSL prefixes the salt bytes + encryptedBytes with Salted___ and then base64 encodes it final byte[] encryptedBytesPlusSaltedText = concat(OPENSSL_SALTED_BYTES, encryptedBytes); return BASE64.encode(encryptedBytesPlusSaltedText); } /** * Password based encryption using AES - CBC 256 bits. * * @param plainTextAsBytes * The bytes to encrypt * @param password * The password to use for encryption * @return SALT_LENGTH bytes of salt followed by the encrypted bytes. * @throws IOException */ private static byte[] encryptRaw(final byte[] plainTextAsBytes, final char[] password) throws IOException { try { // Generate salt - each encryption call has a different salt. final byte[] salt = new byte[SALT_LENGTH]; secureRandom.nextBytes(salt); final ParametersWithIV key = (ParametersWithIV) getAESPasswordKey(password, salt); // The following code uses an AES cipher to encrypt the message. final BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); cipher.init(true, key); final byte[] encryptedBytes = new byte[cipher.getOutputSize(plainTextAsBytes.length)]; final int processLen = cipher.processBytes(plainTextAsBytes, 0, plainTextAsBytes.length, encryptedBytes, 0); final int doFinalLen = cipher.doFinal(encryptedBytes, processLen); // The result bytes are the SALT_LENGTH bytes followed by the encrypted bytes. return concat(salt, Arrays.copyOf(encryptedBytes, processLen + doFinalLen)); } catch (final InvalidCipherTextException x) { throw new IOException("Could not encrypt bytes", x); } catch (final DataLengthException x) { throw new IOException("Could not encrypt bytes", x); } } /** * Decrypt text previously encrypted with this class. * * @param textToDecode * The code to decrypt * @param password * password to use for decryption * @return The decrypted text * @throws IOException */ public static String decrypt(@Nonnull final String textToDecode, @Nonnull final char[] password) throws IOException { final byte[] decryptedBytes = decryptBytes(textToDecode, password); return new String(decryptedBytes, UTF_8).trim(); } /** * Decrypt bytes previously encrypted with this class. * * @param textToDecode * The code to decrypt * @param password * password to use for decryption * @return The decrypted bytes * @throws IOException */ public static byte[] decryptBytes(@Nonnull final String textToDecode, @Nonnull final char[] password) throws IOException { final byte[] decodeTextAsBytes = BASE64.decode(textToDecode); if (decodeTextAsBytes.length < OPENSSL_SALTED_BYTES.length) throw new IOException("out of salt"); final byte[] cipherBytes = new byte[decodeTextAsBytes.length - OPENSSL_SALTED_BYTES.length]; System.arraycopy(decodeTextAsBytes, OPENSSL_SALTED_BYTES.length, cipherBytes, 0, decodeTextAsBytes.length - OPENSSL_SALTED_BYTES.length); final byte[] decryptedBytes = decryptRaw(cipherBytes, password); return decryptedBytes; } /** * Decrypt bytes previously encrypted with this class. * * @param bytesToDecode * The bytes to decrypt * @param password * password to use for decryption * @return The decrypted bytes * @throws IOException */ private static byte[] decryptRaw(final byte[] bytesToDecode, final char[] password) throws IOException { try { // separate the salt and bytes to decrypt final byte[] salt = new byte[SALT_LENGTH]; System.arraycopy(bytesToDecode, 0, salt, 0, SALT_LENGTH); final byte[] cipherBytes = new byte[bytesToDecode.length - SALT_LENGTH]; System.arraycopy(bytesToDecode, SALT_LENGTH, cipherBytes, 0, bytesToDecode.length - SALT_LENGTH); final ParametersWithIV key = (ParametersWithIV) getAESPasswordKey(password, salt); // decrypt the message final BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); cipher.init(false, key); final byte[] decryptedBytes = new byte[cipher.getOutputSize(cipherBytes.length)]; final int processLen = cipher.processBytes(cipherBytes, 0, cipherBytes.length, decryptedBytes, 0); final int doFinalLen = cipher.doFinal(decryptedBytes, processLen); return Arrays.copyOf(decryptedBytes, processLen + doFinalLen); } catch (final InvalidCipherTextException x) { throw new IOException("Could not decrypt bytes", x); } catch (final DataLengthException x) { throw new IOException("Could not decrypt bytes", x); } } /** * Concatenate two byte arrays. */ private static byte[] concat(final byte[] arrayA, final byte[] arrayB) { final 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; } public final static FileFilter OPENSSL_FILE_FILTER = new FileFilter() { private final char[] buf = new char[OPENSSL_MAGIC_TEXT.length()]; @Override public boolean accept(final File file) { Reader in = null; try { in = new InputStreamReader(new FileInputStream(file), UTF_8); if (in.read(buf) == -1) return false; final String str = new String(buf); if (!str.toString().equals(OPENSSL_MAGIC_TEXT)) return false; return true; } catch (final IOException x) { return false; } finally { if (in != null) { try { in.close(); } catch (final IOException x2) { } } } } }; }