Java和Javascript之间的加密和解密不起作用



>EDIT 1

在解密文件方法中,解密部分不会输出任何内容。

let decrypted = CryptoJS.AES.decrypt(e.target.result, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});


编辑 2评论部分中给出的链接部分解决了问题。它确实可以跨平台加密和解密,但由于带有SHA256哈希的PBKDF2,它相当慢。我找不到只使用 AES 部分而不使用 PKBDF2 部分的方法。


原文

我在Java和Javascript版本中使用相同的密钥和IV。我无法解密 Javascript 中已在 Java 中加密的文件,也无法解密已在 Javascript 中加密的 Java 文件。我需要这两个相互兼容,但我无法弄清楚如何我正在尝试解密以前在 Java 中加密的 Javascript 文件。我已经成功地实现了两者之间的解密和加密文本,但是当我想解密一个用 Java 加密的文件时,它就是不起作用。

在 Java 中加密/解密文件:

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Test {
private SecretKey secretKey;
private IvParameterSpec ivParameterSpec;
private String key = "ThisIsMyGreatKey";
private byte[] ivKey = "ABCDEFGHabcdefgh".getBytes();
public static void main(String[] args) {
try {
new Test().run();
} catch (Exception e) {
e.printStackTrace();
}
}
private void run() {
ivParameterSpec = new IvParameterSpec(ivKey);
secretKey = new SecretKeySpec(key.getBytes(), "AES");
encryptOrDecryptFile(Cipher.ENCRYPT_MODE,
new File("src/cactus.jpg"), new File("src/cactus-encrypted.jpg"));
}
private void encryptOrDecryptFile(int mode, File inputFile, File outputFile) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(mode, secretKey, ivParameterSpec);
// Read input
byte[] input = new byte[(int) inputFile.length()];
FileInputStream inputStream = new FileInputStream(inputFile);
inputStream.read(input);
// Encrypt and write to output
byte[] output = cipher.doFinal(input);
FileOutputStream outputStream = new FileOutputStream(outputFile);
outputStream.write(output);
inputStream.close();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

在Javascript中加密/解密

<input type="file" id="file-input" onchange="handleFile(this)">
<button onclick="useEncryptionForFile()" id="encrypt-file">Encrypt File</button>
<button onclick="useDecryptionForFile()" id="decrypt-file">Decrypt File</button>
<textarea id="output"></textarea>
<img id="example">
<script>
let key = "ThisIsMyGreatKey";
let iv = "ABCDEFGHabcdefgh";
let useEncryption, useDecryption;
let input = document.getElementById("file-input");
let output = document.getElementById("output");
let example = document.getElementById("example");
function handleFile(element) {
if (element.files && element.files[0]) {
let file = element.files[0];
if (useDecryption) {
decryptFile(file);
} else {
encryptFile(file);
}
}
}
function encryptFile(file) {
let reader = new FileReader();
reader.onload = function (e) {
let encrypted = CryptoJS.AES.encrypt(e.target.result, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
output.textContent = encrypted;
let a = document.createElement("a");
a.setAttribute('href', 'data:application/octet-stream,' + encrypted);
a.setAttribute('download', file.name + '.encrypted');
a.click();
};
reader.readAsDataURL(file);
}
function decryptFile(file) {
let reader = new FileReader();
reader.onload = function (e) {
let decrypted = CryptoJS.AES.decrypt(e.target.result, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
// Decrypted is emtpy    
output.textContent = decrypted;
// Desperate try to get something working
example.src = "data:image/png;base64," + btoa(decrypted);
let a = document.createElement("a");
a.setAttribute('href', decrypted);
a.setAttribute('download', file.name.replace('encrypted', 'decrypted'));
a.click();
};
reader.readAsText(file);
}
function useEncryptionForFile() {
document.getElementById("encrypt-file").style.backgroundColor = "#757575";
document.getElementById("decrypt-file").style.backgroundColor = "#FFFFFF";
useEncryption = true;
useDecryption = false;
}
function useDecryptionForFile() {
document.getElementById("encrypt-file").style.backgroundColor = "#FFFFFF";
document.getElementById("decrypt-file").style.backgroundColor = "#757575";
useDecryption = true;
useEncryption = false;
}
</script>    

我还做了一个小提琴,以防你想要更多:),Java源代码可以在这里下载。

在Java源代码中,我使用了一个仙人掌.jpg作为文件,但任何文件都可以:)使用。仙人掌可以在这里找到。

如何解密已在 Java 中加密的文件?我尝试将 blob 内容转换为字符串,将数据检索为 ArrayBuffer 并将其转换为字符串,将其作为文本接收并将其传递给解密方法,但似乎没有任何效果。

我在Javascript中使用的库是CryptoJS,在Java中使用的是标准的Crypto库。

我发现了其他类似的(1,2)问题。但是,我认为它们差异太大,因为这些问题的答案与这个问题无关,而是一个小错误。

如果我忘记了任何数据,请告诉我。

问题是您将解密结果解释为 UTF8 字符串。 这不是它的工作原理。 文件只是任意字节,它们不一定构成 UTF8 字符串。 如果您只是不尝试将其解释为 UTF8,则解密结果是正确的。

首先,尝试将简单的加密文本从java发送到javascript,反之亦然,并测试代码是否正常工作。

如果代码适用于简单文本,即,您可以从 Java 发送加密字符串并在 JavaScript 中成功解密,反之亦然,那么您可以做的是 Base64 对加密的字节/文件进行编码,然后传输文本,然后在另一端解码和解密它。

如果代码不适用于简单文本,那么您可以尝试在 javascript 和 java 中独立加密简单文本,并检查结果是否相同。如果不是,java和javascript之间的加密/解密逻辑存在一些不匹配。

编辑:

正如您提到的代码适用于字符串,我在下面展示了一个示例,使用 java 中的 apache 通用编解码器库将文件转换为 Base64 字符串。

private static String encodeFileToBase64Binary(String fileName) throws IOException {
File file = new File(fileName);
byte[] encoded = Base64.encodeBase64(FileUtils.readFileToByteArray(file));
return new String(encoded, StandardCharsets.US_ASCII);
}

现在你加密这个字符串并将其发送到javascript。在javascript中,首先解密字符串,然后将其转换为文件对象。

例如。

function base64toFile(encodedstring,filename,mimeType){
return new File([encodedstring.arrayBuffer()],filename, {type:mimeType});
}   
//Usage example:
base64toFile('aGVsbG8gd29ybGQ=', 'hello.txt', 'text/plain');

注释部分中给出的链接部分解决了该问题。它确实可以跨平台加密和解密,但由于带有SHA256哈希的PBKDF2,它相当慢。我找不到只使用 AES 部分而不使用 PKBDF2 部分的方法。

PBKDF2 的目的是将用户选择的密码(通常是可变长度的文本字符串,很少有超过几十位的有效熵*)转换为 AES 密钥(它必须是正好 128、192 或 256 位的二进制字符串,具体取决于所使用的 AES 变体,如果您希望密码尽可能强,则应有效地具有完全熵**)。

要做到这一点,它需要很慢;使猜测具有30位熵的密码与猜测具有128位熵的AES密钥一样困难的唯一方法是使将密码转换为密钥的过程花费与2128 - 30= 298AES加密一样多的时间。 当然,这实际上是不可能的,所以在实践中,人们倾向于只应用几千(&大约 210)到几十亿(&大约 230)次 PBKDF2 迭代,并希望这已经足够了。 如果您的迭代次数接近范围的上限,如果用户足够聪明并且有足够的动力选择一个相当好的密码(例如,随机的 Diceware 密码短语而不是abc123pa$$w0rd),并且如果您的对手不是 NSA,那么可能是这样。

无论如何,所有这一切的重点是,如果你想使用基于密码的加密,那么你几乎必须使用PBKDF2(或类似的东西)。 但是,这并不一定意味着每次加密或解密某些内容时都必须运行慢速密钥派生函数。 事实上,如果您知道需要使用相同的密码加密或解密多个文件,那么从密码中派生一次 AES 密钥,然后将 AES 密钥存储在内存中,只要您需要它,效率要高得多。 (安全地执行此操作也不是一件容易的事,但是如果您使用其内置密钥对象来存储密钥,许多加密库至少可以很好地处理它,并且在任何情况下,它都不太可能是应用程序安全性中最薄弱的环节。

当然,另一种选择是根本不使用密码,而只是生成一个(伪)随机AES密钥(使用加密安全的随机位字符串生成器)并存储在需要访问它的所有设备上。 同样,当然,这里的困难部分是安全地存储密钥。

特别是,如果您使用浏览器内 JavaScript 在客户端进行加密,那么只需将密钥嵌入 JS 代码(或页面上的其他任何位置)就会将其暴露给任何有权访问浏览器开发控制台的人(并且还可能导致密钥被留在浏览器的磁盘缓存中)。 当然,您应该使用HTTPS,否则任何人(例如窃听客户端的公共WiFi连接)也可以获取密钥的副本。


附言。您可能会注意到,我实际上没有包含任何代码来显示如何使用上面的PBKDF2进行纯AES加密。 我没有这样做有两个原因:

如果你对你正在使用的加密
  1. 原语不够了解,比如说,将密钥派生与加密分开,那么你真的不应该编写加密代码(除了玩具练习之外的任何东西)。

    这听起来可能很苛刻,但这是现实——与编程的许多其他子领域不同,你可以只拿一段你不完全理解的代码并调整它直到它工作,使用加密(以及一般的安全相关代码)你需要知道你在做什么,并且第一次就做对了。

    对于其他类型的代码,大多数时候似乎都有效的东西通常就足够了,你可以在出现任何剩余的错误时修复它们。 使用加密,有缺陷的代码很容易完全不安全,同时看起来它运行良好,并且您不会发现它已损坏,直到有人破解它并窃取您努力保密的所有数据。 或者可能只是在那之后很久就已经发生了。

  2. 无论如何,您链接到的代码示例集合(<免责声明>我没有详细审查)已经包含一组采用二进制 AES 密钥且不使用PBKDF2 的方法。 具体来说,这些是encrypt()decrypt()方法(与使用PBKDF2的encryptString()decryptString()相反)。

    请注意,调用encrypt()和/或decrypt()时,您需要以实现所需的格式提供 AES 密钥。 通常,这可能取决于代码使用的底层加密库。 例如,Java 实现似乎期望数组byte[]128/8 = 16 元素。 (如果它也可以直接接受一个SecretKeySpec对象,那就太好了,但它没有。 JS WebCrypto实现似乎想要一个16字节的Uint8Array。 这对于节点.js实现来说显然也很好,尽管它也可以接受Buffer

*)也就是说,

一个体面的密码破解程序,基于对常见的人类密码选择方法和习惯的深入了解,很少需要超过几十亿(&约230)或万亿(&约240)尝试来猜测大多数人为选择的密码。实际上,根据用户的懒惰或缺乏经验,即使只是测试几千个最常见的密码也可能非常有效。

**) 也就是说,猜测密钥不应该比猜测完全随机选择的相同长度的位串容易。

最新更新