在 Android 中使用 AES 方法解密文件

Decrypt file using AES method in Android

我在 php 中使用以下代码使用 AES 加密对文件进行了加密。

$ALGORITHM = 'AES-128-CBC';
$IV = '12dasdq3g5b2434b';
$password = '123';
openssl_encrypt($contenuto, $ALGORITHM, $password, 0, $IV);

现在我试图在 Android 中解密它,但我总是遇到 InvalidKeyException: Key length not 128/192/256 bits 错误。这是 android 代码:

String initializationVector = "12dasdq3g5b2434b";
String password = "123";
FileInputStream fis = new FileInputStream(cryptFilepath);
FileOutputStream fos = new FileOutputStream(outputFilePath);
byte[] key = (password).getBytes("UTF-8");
MessageDigest sha = MessageDigest.getInstance("SHA-1");
key = sha.digest(key);
key = Arrays.copyOf(key,16);
SecretKeySpec sks = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, sks, new IvParameterSpec(initializationVector.getBytes()));
CipherInputStream cis = new CipherInputStream(fis, cipher);
int b;
byte[] d = new byte[16];
while((b = cis.read(d)) != -1) {
fos.write(d, 0, b);
}
fos.flush();
fos.close();
cis.close();

任何人都可以建议我该怎么做。任何帮助将不胜感激。

以下完整的工作示例展示了如何处理密码问题和进行 Base64 解码,这些示例只使用字符串而不是文件。请记住@Topaco 所说的 openssl 在 Base64 编码中对输出进行编码,在文件可以与 CipherInputStream 一起用于解密之前需要将其转换为字节格式!

第三点(不是真正的错误)是在 Java/Android 方面你没有为从字符串到字节数组的转换设置字符集 - 只需添加 StandardCharsets.UTF_8 和你没意见。

注意没有正确的异常处理!

这是我的示例PHP-代码:

<?php
// 
$ALGORITHM = 'AES-128-CBC';
$IV = '12dasdq3g5b2434b';
$password = '123';
$plaintext = "my content to encrypt";
echo 'plaintext: ' . $plaintext . PHP_EOL;
$ciphertext = openssl_encrypt($plaintext, $ALGORITHM, $password, 0, $IV);
echo 'ciphertext: ' . $ciphertext . PHP_EOL;
$decryptedtext = openssl_decrypt($ciphertext, $ALGORITHM, $password, 0, $IV);
echo 'decryptedtext: ' . $decryptedtext . PHP_EOL;
?>

PHP 端的输出:

plaintext: my content to encrypt
ciphertext: DElx3eON2WX0MCj2GS8MnD+kn5NOu1i5IOTcrpKegG4=
decryptedtext: my content to encrypt

样本Java-代码:

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class DecryptInJava {
    public static void main(String[] args) throws NoSuchPaddingException, NoSuchAlgorithmException,
            InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        System.out.println("");
        String password = "123";
        String initializationVector = "12dasdq3g5b2434b";
        String ciphertext = "DElx3eON2WX0MCj2GS8MnD+kn5NOu1i5IOTcrpKegG4="; // password 123 in openssl
        // openssl encodes the output in base64 encoding, so first we have to decode it
        byte[] ciphertextByte = Base64.getDecoder().decode(ciphertext);
        // creating a key filled with 16 'x0'
        byte[] key = new byte[16]; // 16 bytes for aes cbc 128
        // copying the password to the key, leaving the x0 at the end
        System.arraycopy(password.getBytes(StandardCharsets.UTF_8), 0, key, 0, password.getBytes(StandardCharsets.UTF_8).length);
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
        IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector.getBytes(StandardCharsets.UTF_8));
        // don't use just AES because that defaults to AES/ECB...
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
        byte[] decryptedtext = cipher.doFinal(ciphertextByte);
        System.out.println("decryptedtext: " + new String(decryptedtext));
    }
}

Java 端的输出:

decryptedtext: my content to encrypt

的帮助下,我写下了成功解密文件的代码。

在您的应用级别 build.gradle 文件中添加以下依赖项:

implementation 'commons-io:commons-io:2.7'
implementation 'commons-codec:commons-codec:1.13'

Java代码:

public static void decrypt(String path, String outPath) throws Exception {
    String password = "123";
    String initializationVector = "12dasdq3g5b2434b";
    byte[] key = new byte[16]; // 16 bytes for aes cbc 128
    System.arraycopy(password.getBytes(StandardCharsets.UTF_8), 0, key, 0, password.getBytes(StandardCharsets.UTF_8).length);
    SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
    IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector.getBytes(StandardCharsets.UTF_8));

    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);

    byte[] input_file;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        input_file = Files.readAllBytes(Paths.get(path));
    } else {
        input_file = org.apache.commons.io.FileUtils.readFileToByteArray(new File(path));
    }

    byte[] decodedBytes;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        decodedBytes = Base64.getDecoder().decode(input_file);
    } else {
        decodedBytes = org.apache.commons.codec.binary.Base64.decodeBase64(input_file);
    }

    byte[] decryptedtext = cipher.doFinal(decodedBytes);
    FileOutputStream fos = new FileOutputStream(outPath);
    fos.write(decryptedtext);
    fos.flush();
    fos.close();
}

希望对您有所帮助。

问题中发布的原始代码使用流来读取、解密和写入相应的文件。这使得处理大于可用内存的文件成为可能。

然而,最初发布的代码缺少Base64解码,这是必需的,因为PHP代码的密文是Base64编码的。

Base64 解码可以使用 Apache Commons Codec 的 Base64InputStream class 轻松实现,它在 FileInputStreamCipherInputStream 之间运行,因此易于集成:

import org.apache.commons.codec.binary.Base64InputStream;

...

public static void decrypt(String ciphertextFilepath, String decryptedFilePath) throws Exception {
    
    String password = "123";
    String initializationVector = "12dasdq3g5b2434b";

    byte[] key = new byte[16];
    byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
    System.arraycopy(passwordBytes, 0, key, 0, passwordBytes.length);
    SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
    IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector.getBytes(StandardCharsets.UTF_8));
    
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
    
    try (FileInputStream fis = new FileInputStream(ciphertextFilepath);
         Base64InputStream b64is = new Base64InputStream(fis);
         CipherInputStream cis = new CipherInputStream(b64is, cipher);
         FileOutputStream fos = new FileOutputStream(decryptedFilePath)) {
            
        int read;
        byte[] buffer = new byte[16]; // 16 bytes for testing, in practice use a suitable size (depending on your RAM size), e.g. 64 Mi
        while((read = cis.read(buffer)) != -1) {
            fos.write(buffer, 0, read);
        }
    }
}

其他修复的错误/优化点是(另见其他答案/评论):

  • CBC模式的使用类似于PHP代码
  • 使用 PHP 代码中的密钥
  • 所用编码的明确规范

编辑: IV 的考虑,请参阅@Michael Fehr 的评论。

通常每次加密都会生成一个新的随机IV。 IV 不是秘密的,通常放在密文之前,结果是 Base64 编码的。接收者可以将两个部分分开,因为 IV 的大小是已知的(对应于块大小)。此构造也可以与 Base64InputStream class 结合使用,其中 IV 必须在 Base64InputStream 实例化和 Cipher instantiation/initialization 之间确定:

...
try (FileInputStream fis = new FileInputStream(ciphertextFilepath);
     Base64InputStream b64is = new Base64InputStream(fis)){
        
    byte[] iv = b64is.readNBytes(16); // 16 bytes for AES
    IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
    
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
            
    try (CipherInputStream cis = new CipherInputStream(b64is, cipher);
         FileOutputStream fos = new FileOutputStream(decryptedFilePath)) {
        ...

如果在加密过程中 IV 和密文是 Base64 编码分别然后连接起来,由分隔符分隔(参见@Michael Fehr 的评论),则 IV 的确定必须在两者之间完成FileInputStreamBase64InputStream 实例化(分隔符也必须被刷新)。

很好的答案,但想补充我关于 Usually a new random IV is generated for each encryption 的 2 美分。将通常更改为需要必须。我不知道有什么理由不每次都使用新的 IV。如果您不这样做并且碰巧使用相同的密钥,则相同的数据每次都会加密为相同的输出。这意味着攻击者可以通过比较加密值来判断解密值是否相同。加密可以保护原始数据免遭窥探,您仍然必须确保他们也无法确定有关原始数据的任何信息。如果您加密了足够多的数据,对每条记录使用相同的密钥和 IV,那么黑客就有机会开始攻击您的系统。