【问题标题】:Encrypt with Node.js Crypto module and decrypt with Java (in Android app)使用 Node.js Crypto 模块加密并使用 Java 解密(在 Android 应用程序中)
【发布时间】:2011-12-08 22:07:30
【问题描述】:

寻找一种在节点中加密数据(主要是字符串)并在安卓应用程序(java)中解密的方法。

在每一个中都已成功完成(在节点中加密/解密,在 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;
}

【问题讨论】:

  • 请向我们展示代码,至少包含一个示例输入,以及 Node 加密版本和 Java 解密版本。我们需要一些东西在这里继续。

标签: java android node.js cryptography


【解决方案1】:

由于 Java 7 抱怨“AES/ECB/PKCS7Padding”无法使用,因此在尝试 Java SE 时,之前答案中的示例对我不起作用。

然而这有效:

加密:

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;
}

【讨论】:

    【解决方案2】:

    谢谢大家。您的回答和 cmets 为我指明了正确的方向,并且通过更多研究,我设法获得了一个工作原型(粘贴在下面)。 事实证明,节点的加密使用 MD5 对密钥进行哈希处理,而填充显然是使用 PKCS7Padding 完成的(通过反复试验得到了那个)

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

    后端服务准备数据并将其发布给第三方。 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;
    }
    

    【讨论】:

    • 太棒了!像魅力一样工作
    • 大家好,这段代码对我不起作用(java.security.NoSuchAlgorithmException),但 Dima Gutzeit 的回答有帮助!
    • 使用 PKCS5Padding - 它在 Java 中的功能等效,不会抛出您看到的异常。
    • toByte的调用可以替换为new BigInteger(encrypted,16).toByteArray()
    • OP 没有描述他们的数据超出“主要是字符串”,但对于大多数非微小数据,ECB 比 CBC 更容易被攻击者操纵、破坏甚至伪造,以及检测重复这在技术上是安全故障。 @DeveshChanchlani:如果第一个字节(即前两个十六进制字符)为 80 或更高,或者第一个字节为 00 而第二个小于 80,则不会
    【解决方案3】:

    显然,如果您将密码传递给 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”值的?谢谢
    【解决方案4】:

    你需要确保你正在使用

    • 同一个键
    • 相同的算法、操作模式和填充。

    在连接的两侧。

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

    再看,线

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

    确实有一些密钥派生,只是documentation is silent about what it does exactly

    crypto.createCipher(算法,密码)

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

    algorithm 依赖于 OpenSSL,例如 'aes192' 等。在最近的版本中,openssl list-cipher-algorithms 将显示可用的密码算法。 password用于派生key和IV,必须是'binary'编码 字符串(有关详细信息,请参阅Buffers)。

    好的,这至少说明了如何对其进行编码,但没有说明这里做了什么。所以,我们要么使用另一种初始化方法crypto.createCipheriv(直接取key和初始化向量,不做任何修改就使用),或者看源码。

    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 端选择相同的模式)。 (从OpenSSL EVP documentation看来,这里也默认是PKCS5Padding。)

    此外,不要自行实施加密,而应考虑使用 TLS 进行传输加密。 (当然,这只有在双方之间有实时连接时才有效。)

    【讨论】:

    • A.非常感谢您的回答! B. 不知道如何在 Node.js 中计算密钥,也不知道如何像在 Node.js 中那样简单地在 java 中提供密钥。我假设 Node 以某种方式派生密钥,但我不知道如何检查它是如何做到的。算法细节的默认值也是如此。
    • 另外,就 TLS 而言,Node 将数据传输给第三方,Java 客户端从中提取数据。而且我不希望第三方能够读取数据。
    • 我花了一些时间阅读源代码和谷歌搜索文档。我希望这会有所帮助。
    猜你喜欢
    • 2017-06-01
    • 1970-01-01
    • 2016-01-08
    • 1970-01-01
    • 2013-06-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-26
    相关资源
    最近更新 更多