使用Node.js的Crypto模块进行加密,然后在Java中(Android应用程序中)进行解密。

15

寻找一种在Node.js中加密数据(主要是字符串)并在Android应用程序(Java)中进行解密的方法。

我已经成功地在每个平台上进行了加密/解密(在Node.js和Java中都进行了加密/解密),但似乎无法在它们之间起作用。

可能我没有以相同的方式进行加密/解密,但是每种语言中的每个库都对相同的事物有不同的名称...

感谢任何帮助。

以下是一些代码: Node.js

var crypto = require('crypto')
var cipher = crypto.createCipher('aes-128-cbc','somepass')
var text = "uncle had a little farm"
var crypted = cipher.update(text,'utf8','hex')
crypted += cipher.final('hex')
//now crypted contains the hex representation of the ciphertext

并且Java

private static String decrypt(byte[] raw, byte[] encrypted) throws Exception {
    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, skeySpec );
    byte[] decrypted = cipher.doFinal(encrypted);
    return new String(decrypted);
}

原始密钥是这样创建的

private static byte[] getRawKey(String seed) throws Exception {
    KeyGenerator kgen = KeyGenerator.getInstance("AES");
    SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
    byte[] seedBytes = seed.getBytes()
    sr.setSeed(seedBytes);
    kgen.init(128, sr); // 192 and 256 bits may not be available
    SecretKey skey = kgen.generateKey();
    byte[] raw = skey.getEncoded();
    return raw;
}

加密的十六进制字符串会被转换成字节,方法如下

public static byte[] toByte(String hexString) {
    int len = hexString.length()/2;
    byte[] result = new byte[len];
    for (int i = 0; i < len; i++)
        result[i] = Integer.valueOf(hexString.substring(2*i, 2*i+2), 16).byteValue();
    return result;
}

1
请展示代码,至少包含一个样例输入,以及Node加密版本和Java解密版本。我们需要一些线索。 - Matt Ball
4个回答

12

感谢大家,你们的答案和评论指引了我正确的方向,在更多的研究后,我成功地得到了一个可工作的原型(如下所示)。 结果发现 Node 的加密使用 MD5 来哈希密钥,并且填充显然是使用 PKCS7Padding(通过试错获得)

至于为什么要这样做: 我有一个由三部分组成的应用程序: A. 后端服务 B. 第三方数据存储 C. 作为客户端的 Android 应用程序。

后端服务准备数据并将其发布到第三方。 Android 应用程序获取和/或更新数据存储中的数据,服务可能会对其进行操作。

加密的需求是保持数据私密性,即使对于第三方提供商也是如此。

至于密钥管理 - 我想我可以让服务器每隔一段预配置的时间创建一个新密钥,用旧密钥加密并将其发布到数据存储中供客户端解密并开始使用,但这对我的需求来说有点过头了。

我也可以创建一对密钥,并使用它来定期传输新的对称密钥,但这甚至更过头了(更不用说工作量了)

总之,这就是代码: 在 Node.js 上加密

var crypto = require('crypto')
var cipher = crypto.createCipher('aes-128-ecb','somepassword')
var text = "the big brown fox jumped over the fence"
var crypted = cipher.update(text,'utf-8','hex')
crypted += cipher.final('hex')
//now crypted contains the hex representation of the ciphertext

Java解密:

public static String decrypt(String seed, String encrypted) throws Exception {
  byte[] keyb = seed.getBytes("UTF-8");
  MessageDigest md = MessageDigest.getInstance("MD5");
  byte[] thedigest = md.digest(keyb);
  SecretKeySpec skey = new SecretKeySpec(thedigest, "AES/ECB/PKCS7Padding");
  Cipher dcipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
  dcipher.init(Cipher.DECRYPT_MODE, skey);

  byte[] clearbyte = dcipher.doFinal(toByte(encrypted));
  return new String(clearbyte);
}

public static byte[] toByte(String hexString) {
  int len = hexString.length()/2;
  byte[] result = new byte[len];
  for (int i = 0; i < len; i++)
    result[i] = Integer.valueOf(hexString.substring(2*i, 2*i+2), 16).byteValue();
  return result;
}

1
太棒了!运行得非常顺畅。 - Entreco
嘿,大家好,这段代码对我来说无法工作(java.security.NoSuchAlgorithmException),但Dima Gutzeit的答案很有帮助! - Kanglai
1
使用PKCS5Padding - 它在Java中的功能等同,并且不会抛出您看到的异常。 - Ben
调用 toByte 可以被替换为 new BigInteger(encrypted, 16).toByteArray() - Devesh Chanchlani
OP并未对其数据进行详细描述,仅表示其主要为字符串。然而,对于大多数中等规模以上的数据,ECB模式比CBC更容易被攻击者操纵、损坏或伪造,并且可以检测到重复数据,这在技术上是一种安全隐患。@DeveshChanchlani:如果首字节(即前两个十六进制字符)是80或更高,则不会存在此问题;如果第一个字节是00且第二个字节小于80,则也不会存在此问题。 - dave_thompson_085

8
显然,如果你向crypto.createCipher()传递一个口令短语,它将使用OpenSSL的EVP_BytesToKey()来派生密钥。您可以传递原始字节缓冲区并使用相同的缓冲区初始化Java的SecretKey,或在Java代码中模拟EVP_BytesToKey()。使用$ man EVP_BytesToKey了解更多详细信息,但基本上它使用MD5多次哈希口令短语并连接盐。

至于使用原始密钥,像这样的东西应该让您使用原始密钥:

var c = crypto.createCipheriv("aes-128-ecb", new Buffer("00010203050607080a0b0c0d0f101112", "hex").toString("binary"), "");

请注意,由于您正在使用CBC,因此需要为加密和解密使用相同的IV(您可能需要将其附加到消息中等)。

强制警告:自己实现加密协议很少是一个好主意。即使您使其正常工作,您是否会为所有消息使用相同的密钥?多长时间?如果您决定旋转密钥,如何管理此过程。等等。


似乎32位长度的十六进制值不能作为Buffer的参数。你是如何生成这个“00010203050607080a0b0c0d0f101112”值的?谢谢。 - Naz

6

之前答案中的示例在我尝试使用Java SE时无法工作,因为Java 7会报错说无法使用"AES/ECB/PKCS7Padding"。

然而,以下方法可以解决问题:

加密:

var crypto = require('crypto')
var cipher = crypto.createCipher('aes-128-ecb','somepassword')
var text = "the big brown fox jumped over the fence"
var crypted = cipher.update(text,'utf-8','hex')
crypted += cipher.final('hex')
//now crypted contains the hex representation of the ciphertext

解密:

private static String decrypt(String seed, String encrypted) throws Exception {
    byte[] keyb = seed.getBytes("UTF-8");
    MessageDigest md = MessageDigest.getInstance("MD5");
    byte[] thedigest = md.digest(keyb);
    SecretKeySpec skey = new SecretKeySpec(thedigest, "AES");
    Cipher dcipher = Cipher.getInstance("AES");
    dcipher.init(Cipher.DECRYPT_MODE, skey);

    byte[] clearbyte = dcipher.doFinal(toByte(encrypted));
    return new String(clearbyte);
}

private static byte[] toByte(String hexString) {
    int len = hexString.length()/2;
    byte[] result = new byte[len];
    for (int i = 0; i < len; i++) {
        result[i] = Integer.valueOf(hexString.substring(2*i, 2*i+2), 16).byteValue();
    }
    return result;
}

5

你需要确保在连接的两端都使用:

  • 相同的密钥
  • 相同的算法、操作模式和填充方式。

对于密钥,在Java端,你正在使用相当多的工作从字符串中派生密钥 - 在node.js端没有这样的事情。在此处使用标准的密钥派生算法(并且在两端使用相同的算法)。

再次查看,该行是:

var cipher = crypto.createCipher('aes-128-cbc','somepass')

确实进行了一些关键派生,但文档对其具体操作保持沉默:

crypto.createCipher(algorithm, password)

创建并返回一个加密器对象,使用给定的算法和密码。

algorithm 依赖于 OpenSSL,例如 'aes192'。在最新版本中,openssl list-cipher-algorithms 将显示可用的加密算法。password 用于派生密钥和 IV,必须是 'binary' 编码字符串(有关更多信息,请参见 缓冲区)。

好的,这至少说明了如何编码,但没有说明这里做了什么。因此,我们可以使用其他初始化方法 crypto.createCipheriv(直接使用密钥和初始化向量,并在不进行任何修改的情况下使用它们),或查看源代码。

createCipher 会在 node_crypto.cc 中以某种方式调用 C++ 函数 CipherInit。这本质上使用 EVP_BytesToKey 函数从提供的字符串中派生密钥(使用 MD5、空盐和计数 1),然后执行与 CipherInitiv 相同的操作(由 createCipheriv 调用,并直接使用 IV 和密钥)。

由于 AES 使用 128 位的密钥和初始化向量,而 MD5 的输出为 128 位,因此实际上意味着:

key = MD5(password)
iv = MD5(key + password)

(其中+表示连接,而不是加法)。如果需要,您可以使用MessageDigest类在Java中重新实现此密钥派生。

更好的想法是使用一些慢速密钥派生算法,特别是如果您的密码是人类可以记忆的。然后,在node.js端使用pbkdf2函数生成此密钥,并在Java端使用PBEKeySpec和SecretKeyFactory(算法为PBKDF2WithHmacSHA1)。 (选择一个迭代计数,仅使您的客户在最常见的设备上不会抱怨速度慢。)

对于您的密码算法,在Java端,您正在说“使用AES算法以默认模式和默认填充模式”。不要这样做,因为它可能会因提供程序而异。

相反,明确指示操作模式(在您的情况下为CBC)和填充模式的指示。一个例子可能是:

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

请查看node.js文档,了解如何在那里指示填充模式(或者默认值是哪个,在Java侧选择相同的填充模式)。 (从{{link1:OpenSSL EVP文档}}中可以看出,在此处默认值也是PKCS5Padding。)
另外,不要自己实现加密,考虑使用TLS进行传输加密。(当然,这仅适用于两端之间有实时连接的情况。)

A. 非常感谢您的回答! B. 不知道如何在Node.js中计算密钥,也不知道如何像在Node.js中那样简单地提供Java中的密钥。我假设Node以某种方式派生密钥,但我不知道如何检查它是如何做到的。算法细节的默认值也是如此。 - Shh
另外,就TLS而言,Node将数据传输给第三方,然后Java客户端从中提取数据。我不希望第三方能够读取数据。 - Shh
我花了一些时间阅读源代码和搜索文档。希望这能有所帮助。 - Paŭlo Ebermann

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