5

我正在尝试使用 Java 中的密码将一个文件的内容加密到另一个文件中。该文件被读取到一个字节数组,加密到另一个字节数组,然后写入新文件。不幸的是,当我尝试反转加密时,输出文件被解密为垃圾。

我强烈怀疑这个问题与每次使用相同的密码时生成相同的密钥有关。我编写了一种测试方法,只要生成密钥就将密钥转储到文件中。密钥以直接和编码形式记录。前者每次都是相同的,但后者总是由于某种原因而不同。

老实说,我对加密方法知之甚少,尤其是在 Java 中。我只需要适度安全的数据,并且加密不必承受来自任何拥有大量时间和技能的人的攻击。提前感谢任何对此有建议的人。

编辑:Esailija 非常友好地指出我总是用 ENCRYPT_MODE 设置密码。我使用布尔参数更正了问题,但现在出现以下异常:

javax.crypto.IllegalBlockSizeException:使用填充密码解密时,输入长度必须是 8 的倍数

在我看来,这听起来像是没有正确使用密码。我的印象是“PBEWithMD5AndDES”会将它散列成一个 16 字节的代码,这肯定是 8 的倍数。我想知道为什么密钥生成并被很好地用于加密模式,但是在尝试时它会抱怨在完全相同的条件下解密。

import java.various.stuff;

/**Utility class to encrypt and decrypt files**/
public class FileEncryptor {
    //Arbitrarily selected 8-byte salt sequence:
    private static final byte[] salt = {
        (byte) 0x43, (byte) 0x76, (byte) 0x95, (byte) 0xc7,
        (byte) 0x5b, (byte) 0xd7, (byte) 0x45, (byte) 0x17 
    };

    private static Cipher makeCipher(String pass, Boolean decryptMode) throws GeneralSecurityException{

        //Use a KeyFactory to derive the corresponding key from the passphrase:
        PBEKeySpec keySpec = new PBEKeySpec(pass.toCharArray());
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
        SecretKey key = keyFactory.generateSecret(keySpec);

        //Create parameters from the salt and an arbitrary number of iterations:
        PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, 42);

        /*Dump the key to a file for testing: */
        FileEncryptor.keyToFile(key);

        //Set up the cipher:
        Cipher cipher = Cipher.getInstance("PBEWithMD5AndDES");

        //Set the cipher mode to decryption or encryption:
        if(decryptMode){
            cipher.init(Cipher.ENCRYPT_MODE, key, pbeParamSpec);
        } else {
            cipher.init(Cipher.DECRYPT_MODE, key, pbeParamSpec);
        }

        return cipher;
    }


    /**Encrypts one file to a second file using a key derived from a passphrase:**/
    public static void encryptFile(String fileName, String pass)
                                throws IOException, GeneralSecurityException{
        byte[] decData;
        byte[] encData;
        File inFile = new File(fileName);

        //Generate the cipher using pass:
        Cipher cipher = FileEncryptor.makeCipher(pass, false);

        //Read in the file:
        FileInputStream inStream = new FileInputStream(inFile);
        decData = new byte[(int)inFile.length()];
        inStream.read(decData);
        inStream.close();

        //Encrypt the file data:
        encData = cipher.doFinal(decData);


        //Write the encrypted data to a new file:
        FileOutputStream outStream = new FileOutputStream(new File(fileName + ".encrypted"));
        outStream.write(encData);
        outStream.close();
    }


    /**Decrypts one file to a second file using a key derived from a passphrase:**/
    public static void decryptFile(String fileName, String pass)
                            throws GeneralSecurityException, IOException{
        byte[] encData;
        byte[] decData;
        File inFile = new File(fileName);

        //Generate the cipher using pass:
        Cipher cipher = FileEncryptor.makeCipher(pass, true);

        //Read in the file:
        FileInputStream inStream = new FileInputStream(inFile);
        encData = new byte[(int)inFile.length()];
        inStream.read(encData);
        inStream.close();

        //Decrypt the file data:
        decData = cipher.doFinal(encData);

        //Write the decrypted data to a new file:
        FileOutputStream target = new FileOutputStream(new File(fileName + ".decrypted.txt"));
        target.write(decData);
        target.close();
    }

    /**Record the key to a text file for testing:**/
    private static void keyToFile(SecretKey key){
        try {
            File keyFile = new File("C:\\keyfile.txt");
            FileWriter keyStream = new FileWriter(keyFile);
            String encodedKey = "\n" + "Encoded version of key:  " + key.getEncoded().toString();
            keyStream.write(key.toString());
            keyStream.write(encodedKey);
            keyStream.close();
        } catch (IOException e) {
            System.err.println("Failure writing key to file");
            e.printStackTrace();
        }

    }

}
4

2 回答 2

8

您正在使用Cipher.ENCRYPT_MODE两者,解密和加密。您应该Cipher.DECRYPT_MODE用于解密文件。

这已被修复,但您的布尔值是错误的。加密应该为真,解密应该为假。我强烈建议不要使用false/true作为函数参数,并始终使用 enum like Cipher.ENCRYPT... 继续

然后你正在加密到 .encrypted 文件,但试图解密原始的纯文本文件。

然后,您没有将填充应用于加密。我很惊讶这实际上必须手动完成,但这里解释了填充。填充方案 PKCS5 似乎在这里被隐式使用。

这是完整的工作代码,将加密文件写入test.txt.encrypted,将解密文件写入test.txt.decrypted.txt. 注释中解释了在加密中添加填充并在解密中删除它。

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;

public class FileEncryptor {

    public static void main( String[] args ) {

        try {
            encryptFile( "C:\\test.txt", "password" );
            decryptFile( "C:\\test.txt", "password" );
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (GeneralSecurityException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    //Arbitrarily selected 8-byte salt sequence:
    private static final byte[] salt = {
        (byte) 0x43, (byte) 0x76, (byte) 0x95, (byte) 0xc7,
        (byte) 0x5b, (byte) 0xd7, (byte) 0x45, (byte) 0x17 
    };

    private static Cipher makeCipher(String pass, Boolean decryptMode) throws GeneralSecurityException{

        //Use a KeyFactory to derive the corresponding key from the passphrase:
        PBEKeySpec keySpec = new PBEKeySpec(pass.toCharArray());
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
        SecretKey key = keyFactory.generateSecret(keySpec);

        //Create parameters from the salt and an arbitrary number of iterations:
        PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, 42);

        /*Dump the key to a file for testing: */
        FileEncryptor.keyToFile(key);

        //Set up the cipher:
        Cipher cipher = Cipher.getInstance("PBEWithMD5AndDES");

        //Set the cipher mode to decryption or encryption:
        if(decryptMode){
            cipher.init(Cipher.ENCRYPT_MODE, key, pbeParamSpec);
        } else {
            cipher.init(Cipher.DECRYPT_MODE, key, pbeParamSpec);
        }

        return cipher;
    }


    /**Encrypts one file to a second file using a key derived from a passphrase:**/
    public static void encryptFile(String fileName, String pass)
                                throws IOException, GeneralSecurityException{
        byte[] decData;
        byte[] encData;
        File inFile = new File(fileName);
        //Generate the cipher using pass:
        Cipher cipher = FileEncryptor.makeCipher(pass, true);

        //Read in the file:
        FileInputStream inStream = new FileInputStream(inFile);

        int blockSize = 8;
        //Figure out how many bytes are padded
        int paddedCount = blockSize - ((int)inFile.length()  % blockSize );

        //Figure out full size including padding
        int padded = (int)inFile.length() + paddedCount;

        decData = new byte[padded];


        inStream.read(decData);

        inStream.close();

        //Write out padding bytes as per PKCS5 algorithm
        for( int i = (int)inFile.length(); i < padded; ++i ) {
            decData[i] = (byte)paddedCount;
        }

        //Encrypt the file data:
        encData = cipher.doFinal(decData);


        //Write the encrypted data to a new file:
        FileOutputStream outStream = new FileOutputStream(new File(fileName + ".encrypted"));
        outStream.write(encData);
        outStream.close();
    }


    /**Decrypts one file to a second file using a key derived from a passphrase:**/
    public static void decryptFile(String fileName, String pass)
                            throws GeneralSecurityException, IOException{
        byte[] encData;
        byte[] decData;
        File inFile = new File(fileName+ ".encrypted");

        //Generate the cipher using pass:
        Cipher cipher = FileEncryptor.makeCipher(pass, false);

        //Read in the file:
        FileInputStream inStream = new FileInputStream(inFile );
        encData = new byte[(int)inFile.length()];
        inStream.read(encData);
        inStream.close();
        //Decrypt the file data:
        decData = cipher.doFinal(encData);

        //Figure out how much padding to remove

        int padCount = (int)decData[decData.length - 1];

        //Naive check, will fail if plaintext file actually contained
        //this at the end
        //For robust check, check that padCount bytes at the end have same value
        if( padCount >= 1 && padCount <= 8 ) {
            decData = Arrays.copyOfRange( decData , 0, decData.length - padCount);
        }

        //Write the decrypted data to a new file:



        FileOutputStream target = new FileOutputStream(new File(fileName + ".decrypted.txt"));
        target.write(decData);
        target.close();
    }

    /**Record the key to a text file for testing:**/
    private static void keyToFile(SecretKey key){
        try {
            File keyFile = new File("C:\\keyfile.txt");
            FileWriter keyStream = new FileWriter(keyFile);
            String encodedKey = "\n" + "Encoded version of key:  " + key.getEncoded().toString();
            keyStream.write(key.toString());
            keyStream.write(encodedKey);
            keyStream.close();
        } catch (IOException e) {
            System.err.println("Failure writing key to file");
            e.printStackTrace();
        }

    }
}
于 2012-12-02T20:41:52.357 回答
5

鉴于 Java 中的一些新功能,这些是对 @Esailija 答案的一些改进。

通过使用 CipherInputStream 和 CipherOutputStream 类,代码的长度和复杂度大大降低。

我还使用 char[] 而不是 String 作为密码。

您可以使用 System.console().readPassword("input password: ") 将密码作为 char[] 获取,这样它就永远不是字符串。

public static void encryptFile(String inFileName, String outFileName, char[] pass) throws IOException, GeneralSecurityException {
    Cipher cipher = PasswordProtectFile.makeCipher(pass, true);
    try (CipherOutputStream cipherOutputStream = new CipherOutputStream(new FileOutputStream(outFileName), cipher);
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(inFileName))) {
        int i;
        while ((i = bis.read()) != -1) {
            cipherOutputStream.write(i);
        }
    }
}

public static void decryptFile(String inFileName, String outFileName, char[] pass) throws GeneralSecurityException, IOException {
    Cipher cipher = PasswordProtectFile.makeCipher(pass, false);
    try (CipherInputStream cipherInputStream = new CipherInputStream(new FileInputStream(inFileName), cipher);
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outFileName))) {
        int i;
        while ((i = cipherInputStream.read()) != -1) {
            bos.write(i);
        }
    }
}

private static Cipher makeCipher(char[] pass, Boolean decryptMode) throws GeneralSecurityException {

    // Use a KeyFactory to derive the corresponding key from the passphrase:
    PBEKeySpec keySpec = new PBEKeySpec(pass);
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
    SecretKey key = keyFactory.generateSecret(keySpec);

    // Create parameters from the salt and an arbitrary number of iterations:
    PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, 43);

    // Set up the cipher:
    Cipher cipher = Cipher.getInstance("PBEWithMD5AndDES");

    // Set the cipher mode to decryption or encryption:
    if (decryptMode) {
        cipher.init(Cipher.ENCRYPT_MODE, key, pbeParamSpec);
    } else {
        cipher.init(Cipher.DECRYPT_MODE, key, pbeParamSpec);
    }

    return cipher;
}
于 2016-08-29T15:24:38.057 回答