AES使用Base64数据编码解密文件Java

问题描述

我引用了this,并尝试通过base64解码进行文件解密

我的要求是在加密过程中使用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轻松地实现块处理。也可以使用Base64InputStreamApache 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()方法。