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

huangapple go评论89阅读模式
英文:

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 {
    // ... (略去了main方法和其他方法的内容)
}
英文:

I referred this and trying to do file decryption with base64 decoding

My requirement is to encode data with base64 during encryption and decode data with base64 during decryption.

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

答案1

得分: 1

正如我在第一条评论中提到的:加密和解密在密码方面使用不同的编码(加密中使用Base64,解密中使用ASCII)。此外,在加密和解密中,前缀也是Base64编码的,因此前缀加盐大于一个块(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

在这里,前缀、盐和密文在字节级别上连接在一起,然后对结果进行Base64编码。为了实现这一点,您在问题中发布的实现可以按照以下方式进行更改:

static void decrypt(String path, String password, String outPath) {
    // 代码...
}

static void decryptfile(String path, String password, String outPath) {
    // 代码...
}

如上所述,decryptfile()方法是与OpenSSL不兼容的,无法解密使用上面的OpenSSL语句(带或不带“-base64”选项)生成的密文。要解密使用OpenSSL生成的带“-base64”选项的密文,请使用decrypt()方法。

请注意,为了满足您的要求,我只翻译了您提供的代码部分。如果您有任何进一步的问题或需要其他帮助,请随时提问。

英文:

As mentioned in my first comment: Encryption and decryption use different encodings for the password (Base64 in encryption, ASCII in decryption).<br>
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.<br>
Both issues can be solved if the same encoding is used for the password in encryption and decryption (e.g. ASCII) and the prefix is ASCII encoded, e.g. for encryption:

...
byte[] pass = password.getBytes(StandardCharsets.US_ASCII);
byte[] SALTED_MAGIC = &quot;Salted__&quot;.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 = &quot;Salted__&quot;.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).<br>
Even a Base64 encoded plaintext does not change this, as the ciphertext itself is not Base64 encoded.<br>
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, i.e. with

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. 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, &quot;\r\n&quot;.getBytes(StandardCharsets.US_ASCII))) { // from Apache Commons Codec
		
		// Read prefix and salt
        byte[] SALTED_MAGIC = &quot;Salted__&quot;.getBytes(StandardCharsets.US_ASCII);
		byte[] prefix = bis.readNBytes(8);
		if (!Arrays.equals(prefix, SALTED_MAGIC)) {
			throw new IllegalArgumentException(&quot;Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value.&quot;);
		}
		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 &lt; 3 &amp;&amp; keyAndIv.length &lt; 48; i++) {
            byte[] hashData = concatenateByteArrays(hash, passAndSalt);
            MessageDigest md = null;
            md = MessageDigest.getInstance(&quot;SHA-1&quot;);   // Use digest from encryption
            hash = md.digest(hashData);
            keyAndIv = concatenateByteArrays(keyAndIv, hash);
        }
        byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
		SecretKeySpec key = new SecretKeySpec(keyValue, &quot;AES&quot;);
		byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);

		// Decrypt
		Cipher cipher = Cipher.getInstance(&quot;AES/CBC/PKCS5Padding&quot;);
		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(&quot;Decrypt is completed&quot;);
		
	} 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. The Base64 decoding can be achieved with the class Base64InputStream of the Apache Commons Codec, as also addressed by Michael Fehr. This is a convenient way to perform Base64 decoding and decryption together. If the data does not need to be Base64 decoded (e.g. if the -base64 option was not used during encryption) the Base64InputStream class can simply be omitted.

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, i.e. the encryption would have to be adapted if necessary (analogous to the decryption posted above).

Edit: The encryptfile() method posted in the question creates a file containing the Base64 encoded prefix, the raw salt and the raw ciphertext. For key derivation the Base64 encoded password is applied. The method is used to encrypt a Base64 encoded plaintext. The following method is the counterpart of encryptfile() and allows the decryption and Base64 decoding of the plaintext:

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(&quot;Salted__&quot;.getBytes());
		byte[] prefix = new byte[SALTED_MAGIC.length];
		fis.readNBytes(prefix, 0, prefix.length);
		if (!Arrays.equals(prefix, SALTED_MAGIC)) {
			throw new IllegalArgumentException(&quot;Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value.&quot;);
		}
		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 &lt; 3 &amp;&amp; keyAndIv.length &lt; 48; i++) {
            byte[] hashData = concatenateByteArrays(hash, passAndSalt);
            MessageDigest md = null;
            md = MessageDigest.getInstance(&quot;SHA-1&quot;);   // Use digest from encryption
            hash = md.digest(hashData);
            keyAndIv = concatenateByteArrays(keyAndIv, hash);
        }
        byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
		SecretKeySpec key = new SecretKeySpec(keyValue, &quot;AES&quot;);
		byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);

		// Decrypt
		Cipher cipher = Cipher.getInstance(&quot;AES/CBC/PKCS5Padding&quot;);
		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(&quot;Decrypt is completed&quot;);
		
	} catch (Exception e) {
		e.printStackTrace();
	}
}

The decryptfile()-method writes the Base64 decoded plaintext to the file in outPath. To get the Base64 encoded plaintext simply remove the Base64InputStream instance from the stream hierarchy.

As already explained in the comment, this method is incompatible to OpenSSL, i.e. it can't decrypt ciphertexts generated with the OpenSSL statement posted above (with or without -base64 option). To decrypt a ciphertext generated with OpenSSL with -base64 option use the decrypt() method.

huangapple
  • 本文由 发表于 2020年10月21日 15:51:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/64459011.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定