安卓密码无法解密加密数据的前16个字节/字符

6

我正在开发一个文件加密/解密应用程序,使用简单的 .txt 文件进行测试。当我在应用程序中选择文件并选择加密时,整个文件数据都会被加密。但是,当我进行解密时,只有部分文件数据被解密。由于某种原因,前16个字节/字符没有被解密。

test_file.txt 内容:"This sentence is used to check file encryption/decryption results."

加密结果:"¾mÁSTÐÿT:Y­„"O¤]ÞPÕµß~ëqrÈb×ßq²¨†ldµJ,O|56\e^-’@þûÝû"

解密结果:"£ÿÒÜÑàh]VÄþ„- used to check file encryption/decryption results."

在日志中没有任何错误。

我做错了什么?

文件加密方法:

public void encryptFile(String password, String filePath) {
    byte[] encryptedFileData = null;
    byte[] fileData = null;

    try {
        fileData = readFile(filePath);//method provided below

        // 64 bit salt for testing only
        byte[] salt = "goodsalt".getBytes("UTF-8");
        SecretKey key = generateKey(password.toCharArray(), salt);//method provided below

        byte[] keyData = key.getEncoded();
        SecretKeySpec sKeySpec = new SecretKeySpec(keyData, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, sKeySpec);

        encryptedFileData = cipher.doFinal(fileData);

        saveData(encryptedFileData, filePath);//method provided below
    }
    catch (Exception e) {
        e.printStackTrace();
    }
}

读取文件内容的方法:

public byte[] readFile(String filePath) {
    byte[] fileData;
    File file = new File(filePath);
    int size = (int) file.length();
    fileData = new byte[size];

    try {
        BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file));
        inputStream.read(fileData);
        inputStream.close();
    } 
    catch (FileNotFoundException e) {
        e.printStackTrace();
    }
    catch (IOException e) {
        e.printStackTrace();
    }

    return fileData;
}

生成密钥的方法:

private SecretKey generateKey(char[] password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
    // Number of PBKDF2 hardening rounds to use. Larger values increase computation time. You
    // should select a value that causes computation to take >100ms.
    final int iterations = 1000;

    // Generate a 256-bit key
    final int outputKeyLength = 256;

    SecretKeyFactory secretKeyFactory;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // Use compatibility key factory -- only uses lower 8-bits of passphrase chars
        secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1And8bit");
    }
    else {
        // Traditional key factory. Will use lower 8-bits of passphrase chars on
        // older Android versions (API level 18 and lower) and all available bits
        // on KitKat and newer (API level 19 and higher).
        secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    }

    KeySpec keySpec = new PBEKeySpec(password, salt, iterations, outputKeyLength);

    return secretKeyFactory.generateSecret(keySpec);
}

将加密/解密数据保存到文件的方法:

private void saveData(byte[] newFileData, String filePath) {
    File file = new File(filePath);

    try {
        BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file));

        outputStream.write(newFileData);
        outputStream.flush();
        outputStream.close();
    }
    catch (IOException e) {
        e.printStackTrace();
    }
}

解密文件的方法:

public void decryptFile(String password, String filePath) {
    byte[] decryptedFileData = null;
    byte[] fileData = null;

    try {
        fileData = readFile(filePath);

        byte[] salt = "goodsalt".getBytes("UTF-8");//generateSalt();
        SecretKey key = generateKey(password.toCharArray(), salt);

        byte[] keyData = key.getEncoded();
        SecretKeySpec sKeySpec = new SecretKeySpec(keyData, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, sKeySpec);

        decryptedFileData = cipher.doFinal(fileData);

        saveData(decryptedFileData, filePath);
    }
    catch (Exception e) {
        e.printStackTrace();
    }
}

这行代码加密了文件:
//simple password for testing only
encryptor.encryptFile("password", "storage/emulated/0/Download/test_file.txt");

这行代码用于解密文件:

encryptor.decryptFile("password", "storage/emulated/0/Download/test_file.txt");

编辑:感谢DarkSquirrel42和Oncaphillis。你们真棒!

将这行代码添加到加密和解密函数中即可解决我的问题。

//note: the initialization vector (IV) must be 16 bytes in this case
//so, if a user password is being used to create it, measures must
//be taken to ensure proper IV length; random iv is best and should be
//stored, possibly alongside the encrypted data
IvParameterSpec ivSpec = new IvParameterSpec(password.getBytes("UTF-8"));

然后,
cipher.init(Cipher.XXXXXXX_MODE, sKeySpec, ivSpec);

不要使用可预测的IV,而是生成一个随机的IV并将其添加到密文前面。 - Artjom B.
2个回答

9
您的问题与密码模式有关... CBC,或称为密码块链接模式。
通常,CBC 很简单... 取出前一个加密块输出的内容,并将其异或到当前输入上,然后再进行加密。
对于第一个块,显然存在问题... 没有前一个块... 因此我们引入了一种叫做 IV(初始化向量)的东西... 一个随机字节块长度...
现在... 正如您所想象的那样,当您想要解密时,您需要相同的 IV...
由于您没有保存它,AES 实现将每次给您一个随机 IV...
因此,您没有所有信息来解密块 1... 即 AES 的前 16 个字节...
处理 CBC 模式数据时,将使用的 IV 简单地添加到您的密文输出中始终是一个好选择... IV 应该是随机的... 它不是保密的...

3

正如@ÐarkSquirrel42指出的那样,CBC的加密/解密例程似乎将前16个字节解释为初始化向量。这对我很有用:

        // got to be random
        byte[] iv = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
        IvParameterSpec ivspec = new IvParameterSpec(iv);
        cipher.init(Cipher.XXXXX_MODE, sKeySpec,ivspec);

在使用AES-CBC时,不应该使用固定或可预测的初始向量(IV),尤其是当密钥不变时。 - Artjom B.
@ArtjomB。这就是为什么我注释了//必须是随机的 - Oncaphillis
2
但它必须每次都是随机的,而不仅仅是一次。注释只是注释,代码应该反映您的意图。 - Artjom B.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接