我需要使用以下命令在JAVA中解密在UNIX中加密的文件:
openssl aes-256-cbc -a -salt -in password.txt -out password.txt.enc mypass mypass
我必须像在UNIX中一样在Java中解密
openssl aes-256-cbc -d -a -in password.txt.enc -out password.txt.new mypass
有人可以给我一个Java代码来执行此操作吗?
OpenSSL通常使用自己的基于密码的密钥派生方法,该方法在中指定EVP_BytesToKey,请参见下面的代码。此外,它会在多行中隐式地将密文编码为base 64,以便在邮件正文中发送密文。
因此,结果是伪代码:
salt = random(8) keyAndIV = BytesToKey(password, salt, 48) key = keyAndIV[0..31] iv = keyAndIV[32..47] ct = AES-256-CBC-encrypt(key, iv, plaintext) res = base64MimeEncode("Salted__" | salt | ct))
因此解密为:
(salt, ct) = base64MimeDecode(res) key = keyAndIV[0..31] iv = keyAndIV[32..47] pt = AES-256-CBC-decrypt(key, iv, plaintext)
可以像这样在Java中实现:
import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.util.Arrays; import java.util.List; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.bouncycastle.util.encoders.Base64; public class OpenSSLDecryptor { private static final Charset ASCII = Charset.forName("ASCII"); private static final int INDEX_KEY = 0; private static final int INDEX_IV = 1; private static final int ITERATIONS = 1; private static final int ARG_INDEX_FILENAME = 0; private static final int ARG_INDEX_PASSWORD = 1; private static final int SALT_OFFSET = 8; private static final int SALT_SIZE = 8; private static final int CIPHERTEXT_OFFSET = SALT_OFFSET + SALT_SIZE; private static final int KEY_SIZE_BITS = 256; /** * Thanks go to Ola Bini for releasing this source on his blog. * The source was obtained from <a href="http://olabini.com/blog/tag/evp_bytestokey/">here</a> . */ public static byte[][] EVP_BytesToKey(int key_len, int iv_len, MessageDigest md, byte[] salt, byte[] data, int count) { byte[][] both = new byte[2][]; byte[] key = new byte[key_len]; int key_ix = 0; byte[] iv = new byte[iv_len]; int iv_ix = 0; both[0] = key; both[1] = iv; byte[] md_buf = null; int nkey = key_len; int niv = iv_len; int i = 0; if (data == null) { return both; } int addmd = 0; for (;;) { md.reset(); if (addmd++ > 0) { md.update(md_buf); } md.update(data); if (null != salt) { md.update(salt, 0, 8); } md_buf = md.digest(); for (i = 1; i < count; i++) { md.reset(); md.update(md_buf); md_buf = md.digest(); } i = 0; if (nkey > 0) { for (;;) { if (nkey == 0) break; if (i == md_buf.length) break; key[key_ix++] = md_buf[i]; nkey--; i++; } } if (niv > 0 && i != md_buf.length) { for (;;) { if (niv == 0) break; if (i == md_buf.length) break; iv[iv_ix++] = md_buf[i]; niv--; i++; } } if (nkey == 0 && niv == 0) { break; } } for (i = 0; i < md_buf.length; i++) { md_buf[i] = 0; } return both; } public static void main(String[] args) { try { // --- read base 64 encoded file --- File f = new File(args[ARG_INDEX_FILENAME]); List<String> lines = Files.readAllLines(f.toPath(), ASCII); StringBuilder sb = new StringBuilder(); for (String line : lines) { sb.append(line.trim()); } String dataBase64 = sb.toString(); byte[] headerSaltAndCipherText = Base64.decode(dataBase64); // --- extract salt & encrypted --- // header is "Salted__", ASCII encoded, if salt is being used (the default) byte[] salt = Arrays.copyOfRange( headerSaltAndCipherText, SALT_OFFSET, SALT_OFFSET + SALT_SIZE); byte[] encrypted = Arrays.copyOfRange( headerSaltAndCipherText, CIPHERTEXT_OFFSET, headerSaltAndCipherText.length); // --- specify cipher and digest for EVP_BytesToKey method --- Cipher aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding"); MessageDigest md5 = MessageDigest.getInstance("MD5"); // --- create key and IV --- // the IV is useless, OpenSSL might as well have use zero's final byte[][] keyAndIV = EVP_BytesToKey( KEY_SIZE_BITS / Byte.SIZE, aesCBC.getBlockSize(), md5, salt, args[ARG_INDEX_PASSWORD].getBytes(ASCII), ITERATIONS); SecretKeySpec key = new SecretKeySpec(keyAndIV[INDEX_KEY], "AES"); IvParameterSpec iv = new IvParameterSpec(keyAndIV[INDEX_IV]); // --- initialize cipher instance and decrypt --- aesCBC.init(Cipher.DECRYPT_MODE, key, iv); byte[] decrypted = aesCBC.doFinal(encrypted); String answer = new String(decrypted, ASCII); System.out.println(answer); } catch (BadPaddingException e) { // AKA "something went wrong" throw new IllegalStateException( "Bad password, algorithm, mode or padding;" + " no salt, wrong number of iterations or corrupted ciphertext."); } catch (IllegalBlockSizeException e) { throw new IllegalStateException( "Bad algorithm, mode or corrupted (resized) ciphertext."); } catch (GeneralSecurityException e) { throw new IllegalStateException(e); } catch (IOException e) { throw new IllegalStateException(e); } } }
请注意,该代码将ASCII指定为字符集。对于你的应用程序/终端/操作系统,所使用的字符集可能有所不同。
通常,你应该强制OpenSSL使用NIST批准的PBKDF2算法,因为使用OpenSSL密钥派生方法(迭代计数为1)是不安全的。这可能会迫使你使用与OpenSSL不同的解决方案。请注意,基于密码的加密从本质上讲是不安全的-密码比随机生成的对称密钥安全性低得多。
OpenSSL 1.1.0c更改了某些内部组件中使用的摘要算法。以前使用的是MD5,而1.1.0切换到SHA256。小心的变化不影响你在这两个EVP_BytesToKey和命令状openssl enc。
EVP_BytesToKey
openssl enc
最好在命令行界面中显式指定摘要(例如,-md md5为了向后兼容或sha-256向前兼容),并确保Java代码使用相同的摘要算法("MD5"或"SHA-256"包括破折号)。另请参阅此答案中的信息。
"MD5"
"SHA-256"