java - 使用带有 aes-cbc-256 的 openssl 解密文件
问题描述
我已经使用以下命令加密了一个文件
openssl rand 32 > test.key
openssl enc -aes-256-cbc -iter 10000 -pbkdf2 -salt -in test.txt -out test.txt.enc -pass file:test.key
现在我正在尝试使用java对其进行解密。从最近几天开始尝试,但没有成功。
任何人都可以帮忙吗?
我的代码
package test;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.io.IOUtils;
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 = 10000;
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(final int key_len, final int iv_len, final MessageDigest md,
final byte[] salt, final byte[] data, final int count) {
final byte[][] both = new byte[2][];
final byte[] key = new byte[key_len];
int key_ix = 0;
final 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(final String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
final String fileName = "test.txt.enc";
final File f = new File(fileName );
try {
// --- read base 64 encoded file ---
List<String> lines = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader(f))) {
//br returns as stream and convert it into a List
lines = br.lines().collect(Collectors.toList());
} catch (final IOException e) {
e.printStackTrace();
}
final StringBuilder sb = new StringBuilder();
for (final String line : lines) {
sb.append(line.trim());
}
final String random_bin_key = "test.key";
final byte[] passwordKey = IOUtils.toByteArray(new FileInputStream(random_bin_key));
// --- extract salt & encrypted ---
final byte[] headerSaltAndCipherText = sb.toString().getBytes();
// header is "Salted__", ASCII encoded, if salt is being used (the default)
final byte[] salt = Arrays.copyOfRange(
headerSaltAndCipherText, SALT_OFFSET, SALT_OFFSET + SALT_SIZE);
final byte[] encrypted = Arrays.copyOfRange(
headerSaltAndCipherText, CIPHERTEXT_OFFSET, headerSaltAndCipherText.length);
// --- specify cipher and digest for EVP_BytesToKey method ---
final Cipher aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding");
final 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,
passwordKey,
ITERATIONS);
final SecretKeySpec key = new SecretKeySpec(keyAndIV[INDEX_KEY], "AES");
final IvParameterSpec iv = new IvParameterSpec(keyAndIV[INDEX_IV]);
// --- initialize cipher instance and decrypt ---
aesCBC.init(Cipher.DECRYPT_MODE, key, iv);
final byte[] decrypted = aesCBC.doFinal(encrypted);
final String answer = new String(decrypted);
System.out.println(answer);
} catch (final BadPaddingException e) {
System.out.println(e.getMessage());
} catch (final IllegalBlockSizeException e) {
System.out.println(e.getMessage());
} catch (final GeneralSecurityException e) {
System.out.println(e.getMessage());
} catch (final IOException e) {
System.out.println(e.getMessage());
}
}
我得到的错误
Given final block not properly padded. Such issues can arise if a bad key is used during decryption.
我参考了以下链接
试过
` final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
// strip off the salt and iv
final ByteBuffer buffer = ByteBuffer.wrap(encryptedText);
byte[] saltBytes = new byte[16];
buffer.get(saltBytes, 0, saltBytes.length);
saltBytes = Arrays.copyOfRange(saltBytes, 8, 16);
final byte[] ivBytes1 = new byte[cipher.getBlockSize()];
buffer.get(ivBytes1, 0, ivBytes1.length);
final int length = buffer.capacity() - 16 - ivBytes1.length;
// length = length+ 16 -(length%16);
final byte[] encryptedTextBytes = new byte[length];
buffer.get(encryptedTextBytes);
// Deriving the key
final SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
final PBEKeySpec spec = new PBEKeySpec(new String(password).toCharArray(), saltBytes, 10000,
256);
final SecretKey secretKey = factory.generateSecret(spec);
final SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(ivBytes1));
byte[] decryptedTextBytes = null;
try {
decryptedTextBytes = cipher.doFinal(encryptedTextBytes);
} catch (final IllegalBlockSizeException e) {
e.printStackTrace();
} catch (final BadPaddingException e) {
e.printStackTrace();
}
获取 badpadding 异常
尝试PBKDF2WithHmacSHA256
仍然收到错误
解决方案
你有几个问题。最明显的是,您正在尝试从文件中读取 IV,但openssl enc
在其默认的基于密码的模式下,即使使用 PBKDF2,也会从密码和 salt 派生密钥和 IV。但是,Java 中的标准(Sun/Oracle/OpenJDK)和 BouncyCastle 提供程序都实现 PBKDF2 以仅派生一个密钥——它在PBES2中的使用方式。
即使没有这个,您将“密码”生成为随机字节的方法也不起作用。PKCS5标准实际上定义了 PBKDF2 来取密码为
一个任意长度的八位组字符串,其作为文本字符串的解释是未指定的。然而,为了互操作性,建议应用程序遵循一些常见的文本编码规则。ASCII 和 UTF-8 [RFC3629] 是两种可能性。(ASCII 是 UTF-8 的子集。)
许多系统更加重视可互操作编码,特别是 Java(从一开始就被设计为全球性的)定义PBEKeySpec
包含字符——char[]
在 Java 中是 UTF-16——在执行 PBKDF2 时被编码为 UTF-8。相比之下openssl
,一个 C 程序可以追溯到世纪之交之前,当时 C 开始承认美国以外的国家的存在,所以它只知道字节 - 字节可能可以是 ASCII 码,或者其他一些单字节代码,比如 EBCDIC,但可能根本不是字符,当然也不是任何不适合一个字节的奇怪外来字符。32 个随机字节序列成为有效 UTF-8 的概率非常低;对我来说,分析计算的工作量太大,但我对 1 亿个随机值进行了测试,结果只有一个可以与你的方案配合使用。(我打算测试十亿,但厌倦了等待。)
另外,由于密码应该是文本,因此将其作为文本openssl
文件读取并将其视为字符串。这意味着如果任何随机字节是空字节或对应于换行符的字节,则文件中的其余数据将被丢弃并忽略密钥和 IV 派生。对于随机的 32 字节值,这将平均大约 4 次出现,大约 20 次中的 1 次会在文件中发生得足够早,以使结果在密码学上很弱且易破解。-pass file:
这就提出了一点:你为什么要使用基于密码的加密?如果您的“密钥”是来自一个体面的安全 RNG 的 32 字节——也就是说openssl rand
——你不需要加强它,它已经作为一个有效的密钥。openssl enc
您可以使用基于密钥的加密,而不是基于密码的加密,它在 Java 中更高效、更安全、更容易——这是一个巨大的胜利。如果您为每个加密使用一个新的随机密钥,您甚至不必使用真正的 IV,您可以像我在下面所做的那样使用零 IV。但是,如果您要重用/任何密钥,则需要为每次加密使用唯一且不可预测的(通常是随机的)IV,并将其与数据一起传送,也许只需将其放在前面即可。
所以无论如何,这是一个相当简单的 Java 程序,它可以处理任何一种情况:pdbkf2 的 openssl 形式,其“密码”实际上不是密码,也不是 UTF-8,或者更明智的基于密钥的形式(但对于这个具有零 IV 的演示):
//nopackage
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.*;
import java.util.*;
import javax.crypto.*;
import javax.crypto.spec.*;
public class SO61613286 {
static public void main (String[] args) throws Exception /* let JVM give error */{
// arguments: P/K, filename output from openssl enc, filename of text pw or binary key
byte[] file = Files.readAllBytes(Paths.get(args[1]));
byte[] fil2 = Files.readAllBytes(Paths.get(args[2]));
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
if( args[0].startsWith("P") ){
// possibly truncate 'password' in fil2
int n = 0; for( ; n < fil2.length; n++ ) if( fil2[n]==0 || fil2[n]=='\n' ) break;
if( n < fil2.length ) fil2 = Arrays.copyOf(fil2, n);
// extract salt and derive ...
byte[] salt = Arrays.copyOfRange(file, 8, 16);
byte[] derive = PBKDF2 ("HmacSHA256", fil2, salt, 10000, 48);
// ... key is first 32, IV is last 16
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(derive,0,32,"AES"), new IvParameterSpec(derive,32,16));
// remainder of file is ciphertext
System.out.write( cipher.doFinal(file,16,file.length-16) );
}else{
// just use fil2 as key and zeros for IV ...
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(fil2,"AES"), new IvParameterSpec(new byte[16]));
// ... all of file is ciphertext
System.out.write( cipher.doFinal(file,0,file.length) );
// !!!if key will be reused, must use better IVs and transmit with the data!!!
}
}
public static byte[] PBKDF2 (String prf, byte[] pass, byte[] salt, int iter, int len)
throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeyException {
byte[] result = new byte[len];
Mac mac = Mac.getInstance(prf);
mac.init (new SecretKeySpec (pass,prf));
byte[] saltcnt = Arrays.copyOf (salt, salt.length+4);
while( /*remaining*/len>0 ){
for( int o = saltcnt.length; --o>=saltcnt.length-4; ) if( ++saltcnt[o] != 0 ) break;
byte[] u = saltcnt, x = new byte[mac.getMacLength()];
for( int i = 1; i <= iter; i++ ){
u = mac.doFinal (u);
for( int o = 0; o < x.length; o++ ) x[o] ^= u[o];
}
int len2 = Math.min (len, x.length);
System.arraycopy (x,0, result,result.length-len, len2);
len -= len2;
}
return result;
}
public static void testutf8 (){
Random r = new Random();
byte[] t = new byte[32];
for( int i = 0; i < 1000000000; i++ ){
r.nextBytes(t);
if( Arrays.equals(new String (t, StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8), t) )
System.out.println(i+" "+Arrays.toString(t));
if( i % 1000000 == 999999 ) System.out.println (i);
}
}
}
和一个演示:
$ openssl rand 32 >SO61613286.rnd # repeated several times until I got this:
$ xxd SO61613286.rnd # notice the null byte
0000000: ab1a 1384 9238 0900 c947 6b9a c23d 5ee0 .....8...Gk..=^.
0000010: 32f0 6b2f 91ec 2dd9 a69d eb7d e00e 37ff 2.k/..-....}..7.
$
$ echo the name of the cat >SO61613286.in
$
$ openssl aes-256-cbc -in SO61613286.in -out SO61613286.enc1 -pass file:SO61613286.rnd -pbkdf2 -iter 10000
$ java8 -cp . SO61613286 P SO61613286.enc1 SO61613286.rnd
the name of the cat
$
$ openssl aes-256-cbc -in SO61613286.in -out SO61613286.enc2 -K $(xxd -p -c32 SO61613286.rnd) -iv 00
hex string is too short, padding with zero bytes to length
$ # that's a warning and can be ignored, as long as we don't need real IVs (see above)
$ java8 -cp . SO61613286 K SO61613286.enc2 SO61613286.rnd
the name of the cat
$
推荐阅读
- libreoffice - How to start a Paragraph always in a new page?
- performance - 包含 O(1) 操作的 for 循环的复杂性分析的歧义
- azure - 获取子文件夹中的所有文件名 Azure 数据工厂
- c# - 如何在 SQL 和 asp.net 核心中使用存储过程删除不同的字符串日期
- python - 使用 .items() 遍历字典时出现不可散列的类型列表错误
- google-apps-script - 如何将谷歌表格列中的值与数组进行比较并根据列数据发送到特定电子邮件
- r - 在 R 包中为 inst/extdata 文件夹记录外部数据?
- vue.js - 当我想显示食谱时未定义的对象
- typescript - 打字稿“类型不可分配给类型”错误
- python - 即使在使用“http.client”Python 时引发异常,AssertRaises 也会失败