问题描述
我的要求是在加密过程中使用base64编码数据,在解密过程中使用base64解码数据。
但是我面临以下错误:
javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
at java.base/com.sun.crypto.provider.CipherCore.doFinal(UnkNown Source)
at java.base/com.sun.crypto.provider.CipherCore.doFinal(UnkNown Source)
at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(UnkNown Source)
at java.base/javax.crypto.Cipher.doFinal(UnkNown Source)
at aes.DecryptNew.decryptNew(DecryptNew.java:124)
at aes.DecryptNew.main(DecryptNew.java:32)
我也对如何执行块解密感到困惑。 请向我建议此代码中的问题。
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.util.Arrays;
import java.util.Base64;
public class DecryptNew {
public static void main(String[] args) {
String plaintextFilename = "D:\\\\plaintext.txt";
String ciphertextFilename = "D:\\\\plaintext.txt.crypt";
String decryptedtextFilename = "D:\\\\plaintextDecrypted.txt";
String password = "testpass";
writetoFile();
String ciphertext = encryptfile(plaintextFilename,password);
System.out.println("ciphertext: " + ciphertext);
decryptNew(ciphertextFilename,password,decryptedtextFilename);
}
static void writetoFile() {
BufferedWriter writer = null;
try
{
writer = new BufferedWriter( new FileWriter("D:\\\\plaintext.txt"));
byte[] data = Base64.getEncoder().encode("hello\r\nhello".getBytes(StandardCharsets.UTF_8));
writer.write(new String(data));
}
catch ( IOException e)
{
}
finally
{
try
{
if ( writer != null)
writer.close( );
}
catch ( IOException e)
{
}
}
}
public static String encryptfile(String path,String password) {
try {
FileInputStream fis = new FileInputStream(path);
FileOutputStream fos = new FileOutputStream(path.concat(".crypt"));
final byte[] pass = Base64.getEncoder().encode(password.getBytes());
final byte[] salt = (new SecureRandom()).generateSeed(8);
fos.write(Base64.getEncoder().encode("Salted__".getBytes()));
fos.write(salt);
final byte[] passAndSalt = concatenateByteArrays(pass,salt);
byte[] hash = new byte[0];
byte[] keyAndIv = new byte[0];
for (int i = 0; i < 3 && keyAndIv.length < 48; i++) {
final byte[] hashData = concatenateByteArrays(hash,passAndSalt);
final MessageDigest md = MessageDigest.getInstance("SHA-1");
hash = md.digest(hashData);
keyAndIv = concatenateByteArrays(keyAndIv,hash);
}
final byte[] keyvalue = Arrays.copyOfRange(keyAndIv,32);
final byte[] iv = Arrays.copyOfRange(keyAndIv,32,48);
final SecretKeySpec key = new SecretKeySpec(keyvalue,"AES");
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE,key,new IvParameterSpec(iv));
CipherOutputStream cos = new CipherOutputStream(fos,cipher);
int b;
byte[] d = new byte[8];
while ((b = fis.read(d)) != -1) {
cos.write(d,b);
}
cos.flush();
cos.close();
fis.close();
System.out.println("encrypt done " + path);
} catch (IOException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
e.printstacktrace();
}
return path;
}
static void decryptNew(String path,String password,String outPath) {
byte[] SALTED_MAGIC = Base64.getEncoder().encode("Salted__".getBytes());
try{
FileInputStream fis = new FileInputStream(path);
FileOutputStream fos = new FileOutputStream(outPath);
final byte[] pass = password.getBytes(StandardCharsets.US_ASCII);
final byte[] inBytes = Files.readAllBytes(Paths.get(path));
final byte[] shouldBeMagic = Arrays.copyOfRange(inBytes,SALTED_MAGIC.length);
if (!Arrays.equals(shouldBeMagic,SALTED_MAGIC)) {
throw new IllegalArgumentException("Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value.");
}
final byte[] salt = Arrays.copyOfRange(inBytes,SALTED_MAGIC.length,SALTED_MAGIC.length + 8);
final byte[] passAndSalt = concatenateByteArrays(pass,passAndSalt);
MessageDigest md = null;
md = MessageDigest.getInstance("SHA-1");
hash = md.digest(hashData);
keyAndIv = concatenateByteArrays(keyAndIv,32);
final SecretKeySpec key = new SecretKeySpec(keyvalue,"AES");
final byte[] iv = Arrays.copyOfRange(keyAndIv,48);
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE,new IvParameterSpec(iv));
final byte[] clear = cipher.doFinal(inBytes,16,inBytes.length - 16);
String contentDecoded = new String(Base64.getDecoder().decode(clear));
fos.write(contentDecoded.getBytes());
fos.close();
System.out.println("Decrypt is completed");
}catch(Exception e){
e.printstacktrace();
}
}
public static byte[] concatenateByteArrays(byte[] a,byte[] b) {
return ByteBuffer
.allocate(a.length + b.length)
.put(a).put(b)
.array();
}
}
解决方法
正如我的第一条评论所述:加密和解密使用不同的密码编码(加密中使用Base64,解密中使用ASCII)。
此外,前缀在加密和解密中均以Base64编码,因此前缀加盐大于1个块(16字节),因此,在解密过程中对密文进行后续的长度确定会失败,因为假定密文以第二块。
如果密码在加密和解密中使用相同的编码(例如ASCII),并且前缀是ASCII编码(例如,ASCII),则可以解决这两个问题。用于加密:
...
byte[] pass = password.getBytes(StandardCharsets.US_ASCII);
byte[] SALTED_MAGIC = "Salted__".getBytes(StandardCharsets.US_ASCII);
byte[] salt = (new SecureRandom()).generateSeed(8);
fos.write(SALTED_MAGIC);
fos.write(salt);
...
并用于解密:
...
byte[] pass = password.getBytes(StandardCharsets.US_ASCII);
byte[] SALTED_MAGIC = "Salted__".getBytes(StandardCharsets.US_ASCII);
byte[] prefix = fis.readNBytes(8);
byte[] salt = fis.readNBytes(8);
...
但是,当前的加密方法不是 Base64编码(仅前缀是Base64编码的,这适得其反,同上)。
甚至Base64编码的明文也不会更改此设置,因为密文本身不是Base64编码的。
考虑到您的声明我的要求是在加密过程中使用base64编码数据,在解密过程中使用base64解码数据以及使用的OpenSSL格式,我假设您想解密用 -base64 选项,类似于OpenSSL,即使用
openssl enc -aes-256-cbc -base64 -pass pass:testpass -p -in sample.txt -out sample.crypt
此处,前缀,盐和密文在字节级别上连接在一起,然后对结果进行Base64编码。为此,您可以将问题中发布的实现更改如下:
static void decrypt(String path,String password,String outPath) {
try (FileInputStream fis = new FileInputStream(path);
Base64InputStream bis = new Base64InputStream(fis,false,64,"\r\n".getBytes(StandardCharsets.US_ASCII))) { // from Apache Commons Codec
// Read prefix and salt
byte[] SALTED_MAGIC = "Salted__".getBytes(StandardCharsets.US_ASCII);
byte[] prefix = bis.readNBytes(8);
if (!Arrays.equals(prefix,SALTED_MAGIC)) {
throw new IllegalArgumentException("Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value.");
}
byte[] salt = bis.readNBytes(8);
// Derive key and IV
byte[] pass = password.getBytes(StandardCharsets.US_ASCII);
byte[] passAndSalt = concatenateByteArrays(pass,salt);
byte[] hash = new byte[0];
byte[] keyAndIv = new byte[0];
for (int i = 0; i < 3 && keyAndIv.length < 48; i++) {
byte[] hashData = concatenateByteArrays(hash,passAndSalt);
MessageDigest md = null;
md = MessageDigest.getInstance("SHA-1"); // Use digest from encryption
hash = md.digest(hashData);
keyAndIv = concatenateByteArrays(keyAndIv,hash);
}
byte[] keyValue = Arrays.copyOfRange(keyAndIv,32);
SecretKeySpec key = new SecretKeySpec(keyValue,"AES");
byte[] iv = Arrays.copyOfRange(keyAndIv,32,48);
// Decrypt
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE,key,new IvParameterSpec(iv));
try (CipherInputStream cis = new CipherInputStream(bis,cipher);
FileOutputStream fos = new FileOutputStream(outPath)) {
int length;
byte[] buffer = new byte[1024];
while ((length = cis.read(buffer)) != -1) {
fos.write(buffer,length);
}
}
System.out.println("Decrypt is completed");
} catch (Exception e) {
e.printStackTrace();
}
}
正如我在链接question的注释中已经提到的那样,可以使用类CipherInputStream
轻松地实现块处理。也可以使用Base64InputStream
的Apache Commons Codec类来实现Base64解码,这也由Michael Fehr解决。这是一起执行Base64解码和解密的便捷方法。如果不需要对数据进行Base64解码(例如,如果在加密过程中未使用 -base64 选项),则可以简单地省略Base64InputStream
类。
如注释中先前所述,不需要对小文件进行分块处理。仅当文件相对于内存变大时才需要这样做。但是,由于您的加密方法是按块处理数据,因此解密也一样。
请注意,问题中发布的加密与上述OpenSSL语句的结果不兼容,即,必要时必须修改 加密(类似于上面发布的解密)。
编辑:问题中发布的encryptfile()
方法将创建一个文件,其中包含Base64编码的前缀,原始盐和原始密文。对于密钥派生,将应用Base64编码的密码。该方法用于加密Base64编码的纯文本。以下方法是encryptfile()
的对应方法,并允许对纯文本进行解密和Base64解码:
static void decryptfile(String path,String outPath) {
try (FileInputStream fis = new FileInputStream(path)) {
// Read prefix and salt
byte[] SALTED_MAGIC = Base64.getEncoder().encode("Salted__".getBytes());
byte[] prefix = new byte[SALTED_MAGIC.length];
fis.readNBytes(prefix,prefix.length);
if (!Arrays.equals(prefix,SALTED_MAGIC)) {
throw new IllegalArgumentException("Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value.");
}
byte[] salt = new byte[8];
fis.readNBytes(salt,salt.length);
// Derive key and IV
final byte[] pass = Base64.getEncoder().encode(password.getBytes());
byte[] passAndSalt = concatenateByteArrays(pass,new IvParameterSpec(iv));
try (CipherInputStream cis = new CipherInputStream(fis,cipher);
Base64InputStream bis = new Base64InputStream(cis,-1,null); // from Apache Commons Codec
FileOutputStream fos = new FileOutputStream(outPath)) {
int length;
byte[] buffer = new byte[1024];
while ((length = bis.read(buffer)) != -1) {
fos.write(buffer,length);
}
}
System.out.println("Decrypt is completed");
} catch (Exception e) {
e.printStackTrace();
}
}
decryptfile()
方法将Base64解码的纯文本写入outPath
中的文件。要获取Base64编码的纯文本,只需从流层次结构中删除Base64InputStream
实例。
正如评论中已经解释的那样,此方法与OpenSSL不兼容,即,它无法解密通过上面发布的OpenSSL语句生成的密文(带有或不带有 -base64 选项)。要使用 -base64 选项解密由OpenSSL生成的密文,请使用decrypt()
方法。