简体   繁体   English

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

[英]Aes decrypt file java with base64 data encoding

I referred this and trying to do file decryption with base64 decoding我提到了这个并尝试使用 base64 解码进行文件解密

My requirement is to encode data with base64 during encryption and decode data with base64 during decryption.我的要求是在加密期间用 base64 编码数据,在解密期间用 base64 解码数据。

But im facing below error:但我面临以下错误:

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)

Also im confused on how to perform decryption in chunks.我也对如何分块执行解密感到困惑。 Please suggest me issues in this code.请在此代码中向我建议问题。

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();
    }
}

As mentioned in my first comment: Encryption and decryption use different encodings for the password (Base64 in encryption, ASCII in decryption).正如我在第一条评论中提到的:加密和解密对密码使用不同的编码(加密中的 Base64,解密中的 ASCII)。
In addition the prefix is Base64 encoded in both encryption and decryption, so prefix plus salt are larger than one block (16 bytes), and therefore the subsequent length determination of the ciphertext during decryption fails, as it is assumed that the ciphertext starts with the 2nd block.另外加解密时前缀都是Base64编码的,所以前缀加salt大于一个块(16个字节),因此后续解密时密文的长度判断失败,假设密文以第二块。
Both issues can be solved if the same encoding is used for the password in encryption and decryption (eg ASCII) and the prefix is ASCII encoded, eg for encryption:如果加密和解密中的密码使用相同的编码(例如 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);
...

and for decryption:和解密:

...
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);
...

However the current encryption method does not Base64 encode (only the prefix is Base64 encoded which is rather counterproductive, s. above).然而,当前的加密方法没有Base64 编码(只有前缀是 Base64 编码的,这会适得其反,见上文)。
Even a Base64 encoded plaintext does not change this, as the ciphertext itself is not Base64 encoded.即使是 Base64 编码的明文也不会改变这一点,因为密文本身不是 Base64 编码的。
Considering your statement My requirement is to encode data with base64 during encryption and decode data with base64 during decryption and the OpenSSL format used, I assume that you want to decrypt a ciphertext that has been encrypted with the -base64 option, analogous to OpenSSL, ie with考虑到您的声明我的要求是在加密期间使用 base64 编码数据并在解密期间使用 base64 解码数据以及使用的 OpenSSL 格式,我假设您要解密已使用-base64选项加​​密的密文,类似于 OpenSSL,即和

openssl enc -aes-256-cbc -base64 -pass pass:testpass -p -in sample.txt -out sample.crypt

Here, prefix, salt and ciphertext are concatenated on byte level and the result is then Base64 encoded.在这里,前缀、盐和密文在字节级别连接,然后结果被 Base64 编码。 To achieve this, your implementation posted in the question could be changed as follows:为此,您在问题中发布的实现可以更改如下:

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();
    }
}

As I already mentioned in the comments to the linked question , the processing in chunks can easily be implemented with the class CipherInputStream .正如我在对链接问题的评论中已经提到的那样,可以使用类CipherInputStream轻松实现块中的处理。 The Base64 decoding can be achieved with the class Base64InputStream of the Apache Commons Codec , as also addressed by Michael Fehr. Base64 解码可以通过Apache Commons Codec 的Base64InputStream类来实现,Michael Fehr 也提到了这一点。 This is a convenient way to perform Base64 decoding and decryption together.这是一种同时执行 Base64 解码和解密的便捷方式。 If the data does not need to be Base64 decoded (eg if the -base64 option was not used during encryption) the Base64InputStream class can simply be omitted.如果不需要对数据进行 Base64 解码(例如,如果在加密期间未使用-base64选项), Base64InputStream可以简单地省略Base64InputStream类。

As previously stated in the comments, small files do not need to be processed in chunks.如前所述,小文件不需要分块处理。 This is only necessary when the files become large relative to the memory.仅当文件相对于内存变大时才需要这样做。 However, since your encryption method processes the data in chunks, it is consistent that the decryption does the same.但是,由于您的加密方法以块的形式处理数据,因此解密的过程是一致的。

Note that the encryption posted in the question is not compatible with the result of the above OpenSSL statement, ie the encryption would have to be adapted if necessary (analogous to the decryption posted above).请注意,问题中发布的加密与上述 OpenSSL 语句的结果不兼容,即,如有必要,必须调整加密(类似于上面发布的解密)。

Edit : The encryptfile() method posted in the question creates a file containing the Base64 encoded prefix, the raw salt and the raw ciphertext.编辑:问题中发布的encryptfile()方法创建一个包含 Base64 编码前缀、原始盐和原始密文的文件。 For key derivation the Base64 encoded password is applied.对于密钥派生,将应用 Base64 编码的密码。 The method is used to encrypt a Base64 encoded plaintext.该方法用于加密 Base64 编码的明文。 The following method is the counterpart of encryptfile() and allows the decryption and Base64 decoding of the plaintext:以下方法是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();
    }
}

The decryptfile() -method writes the Base64 decoded plaintext to the file in outPath . decryptfile() - 方法将 Base64 解码的明文写入outPath的文件。 To get the Base64 encoded plaintext simply remove the Base64InputStream instance from the stream hierarchy.要获得 Base64 编码的明文,只需从流层次结构中删除Base64InputStream实例。

As already explained in the comment, this method is incompatible to OpenSSL, ie it can't decrypt ciphertexts generated with the OpenSSL statement posted above (with or without -base64 option).正如评论中已经解释的那样,此方法与 OpenSSL 不兼容,即它无法解密使用上面发布的 OpenSSL 语句(有或没有-base64选项)生成的密文。 To decrypt a ciphertext generated with OpenSSL with -base64 option use the decrypt() method.要使用带有-base64选项的 OpenSSL 解密生成的密文,使用decrypt()方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM