Aes 解密文件 java 用 base64 数据编码

Aes decrypt file java with base64 data encoding

我参考了 并尝试使用 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, 0, 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, 0, 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, 0, 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, 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);
                MessageDigest md = null;
                md = MessageDigest.getInstance("SHA-1");
                hash = md.digest(hashData);
                keyAndIv = concatenateByteArrays(keyAndIv, hash);
            }
            final byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
            final SecretKeySpec key = new SecretKeySpec(keyValue, "AES");
            final byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);
            final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, key, 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)。
另外加解密时prefix都是Base64编码的,所以prefix加上salt大于一个block(16字节),因此后面解密时的密文定长失败,假设密文以第二块。
如果在加密和解密时对密码使用相同的编码(例如 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

在这里,prefix、salt和密文在字节级别上进行连接,然后对结果进行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, 0, 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, 0, length);
            }
        }
        System.out.println("Decrypt is completed");
        
    } catch (Exception e) {
        e.printStackTrace();
    }
}

正如我在链接 , the processing in chunks can easily be implemented with the class CipherInputStream. The Base64 decoding can be achieved with the class Base64InputStream of the Apache Commons Codec 的评论中提到的那样,Michael Fehr 也提到了这一点。这是一种同时进行Base64解码和解密的便捷方式。如果数据不需要进行 Base64 解码(例如,如果在加密期间未使用 -base64 选项),则可以简单地省略 Base64InputStream class。

前面评论说了,小文件不需要分块处理。仅当文件相对于内存变大时才需要这样做。但是,由于您的加密方法以块的形式处理数据,因此解密也是如此。

请注意,问题中发布的加密与上述 OpenSSL 语句的结果不兼容,即必须根据需要调整加密(类似于解密贴在上面)。

编辑:问题中发布的encryptfile()方法创建了一个包含Base64编码前缀、原始盐和原始密文的文件。对于密钥派生,应用 Base64 编码的密码。该方法用于加密 Base64 编码的明文。下面的方法是encryptfile()的对应方法,可以对明文进行解密和Base64解码:

static void decryptfile(String path, String password, 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, 0, 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, 0, salt.length);

        // Derive key and IV
        final byte[] pass = Base64.getEncoder().encode(password.getBytes());
        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, 0, 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(fis, cipher);
             Base64InputStream bis = new Base64InputStream(cis, false, -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, 0, length);
            }
        }
        System.out.println("Decrypt is completed");
        
    } catch (Exception e) {
        e.printStackTrace();
    }
}

decryptfile()-方法将Base64解码明文写入outPath中的文件。要获得 Base64 编码的明文,只需从流层次结构中删除 Base64InputStream 实例。

如评论中所述,此方法与 OpenSSL 不兼容,即它无法解密使用上面发布的 OpenSSL 语句生成的密文(有或没有 -base64 选项).要使用 -base64 选项解密由 OpenSSL 生成的密文,请使用 decrypt() 方法。