【问题标题】:Aes decrypt file java with base64 data encodingaes 使用 base64 数据编码解密文件 java
【发布时间】:2021-02-04 02:22:44
【问题描述】:

我参考了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, 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 vs ASCII)。两者必须相同。加密和解密都使用了Base64编码前缀Salted__,所以前缀加盐大于一个块(16字节),这与解密时密文的长度确定不一致。一个解决方案是例如一个 ASCII 编码的前缀。
  • 我不清楚您真正想要实现什么。加密仅对前缀执行 Base64 编码。 Base64 编码的明文并不意味着加密本身执行 Base64 编码,而是仅对 Base64 编码的明文进行加密。不应该像 OpenSSL 使用 -base64 选项一样对所有内容进行 Base64 编码(即前缀、盐和密文)吗?
  • 我这边的两件事 - 使用块加密最好使用 CipherInput-/Outputstream 完成。仅当您使用大文件(超过几兆字节)以避免遇到“内存不足错误”时,才需要这样做。其次,您应该考虑使用 Apache 公共编解码器库中的 Base64Input-/Outputstream(将其放在 FileInput/FileOutputStream 之间)。

标签: java encryption openssl base64 sha1


【解决方案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) {

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

正如我在链接的question 的cmets 中已经提到的,可以使用CipherInputStream 类轻松实现块处理。 Base64 解码可以通过Apache Commons Codec 的类Base64InputStream 来实现,Michael Fehr 也提到了这一点。这是一种同时执行 Base64 解码和解密的便捷方式。如果不需要对数据进行 Base64 解码(例如,如果在加密期间未使用 -base64 选项),则可以简单地省略 Base64InputStream 类。

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

请注意,问题中发布的加密与上述 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() 方法。

【讨论】:

  • 感谢您的回答! byte[] salt = bis.readNBytes(8);接受 3 个参数。当我使用 apache codec1.15 jar 时。请建议如何使用这 3 个参数方法。
  • @Anees U:Apache 编解码器的 Base64InputStream 采用 1 或 2 或 4 个参数,但从不采用 3。请参阅文档 commons.apache.org/proper/commons-codec/apidocs/org/apache/…。在 Topaco 的回答中给出 1 个参数,它只是它需要的 InputStream。
  • @Anees U:ReadNBytes() 继承自 java.io.InputStream,因此您使用的 Java 版本决定了哪些重载可用。 ReadNBytes() 从版本 11 开始存在一个参数,从版本 9 开始存在三个参数。后者可以按如下方式使用(例如盐):byte[] salt = new byte[8]; bis.readNBytes(salt, 0, salt.length); 和前缀类似。
  • @MichaelFehr,谢谢,如果我这样使用它会进入异常块:byte[] prefix = new byte[8]; bis.readNBytes(prefix, 0, prefix.length); if (!Arrays.equals(prefix, SALTED_MAGIC)) { throw new IllegalArgumentException("输入的初始字节与 OpenSSL SALTED_MAGIC 盐值不匹配。"); }
  • @AneesU - 你是如何创建要解密的密文的?
猜你喜欢
  • 2021-02-22
  • 1970-01-01
  • 2011-04-26
  • 1970-01-01
  • 1970-01-01
  • 2019-06-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多