使用 Node.js Crypto 模块加密并使用 Java 解密(在 Android 应用程序中)
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/7787773/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Encrypt with Node.js Crypto module and decrypt with Java (in Android app)
提问by Shh
Looking for a way to encrypt data (mainly strings) in node and decrypt in an android app (java).
寻找一种在节点中加密数据(主要是字符串)并在 android 应用程序 (java) 中解密的方法。
Have successfully done so in each one (encrypt/decrypt in node, and encrypt/decrypt in java) but can't seem to get it to work between them.
在每一个中都成功地做到了这一点(在节点中加密/解密,在 Java 中加密/解密),但似乎无法让它在它们之间工作。
Possibly I'm not encrypting/decrypting in the same way, but each library in each language has different names for same things...
可能我没有以相同的方式加密/解密,但是每种语言的每个库对于相同的东西都有不同的名称......
Any help appreciated.
任何帮助表示赞赏。
here's some code: Node.js
这是一些代码: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
and 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);
}
the raw key is created like this
原始密钥是这样创建的
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;
}
while the encrypted hex string is converted to bytes like this
而加密的十六进制字符串被转换为这样的字节
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;
}
采纳答案by Nikolay Elenkov
Apparently if you pass a passphrase to crypto.createCipher()
it uses OpenSSL's EVP_BytesToKey()
to derive the key. You can either pass a raw byte buffer and use the same to initialize Java's SecretKey
, or emulate EVP_BytesToKey()
in your Java code. Use $ man EVP_BytesToKey
for more details, but essentially it hashes the passphrase multiple times with MD5 and concatenates a salt.
显然,如果您将密码短语传递给crypto.createCipher()
它,则使用 OpenSSLEVP_BytesToKey()
来派生密钥。您可以传递原始字节缓冲区并使用它来初始化 Java 的SecretKey
,或者EVP_BytesToKey()
在您的 Java 代码中进行模拟。使用$ man EVP_BytesToKey
更多的细节,但它本质上的密码多次使用MD5哈希并连接盐。
As for using a raw key, something like this should let you use a raw key:
至于使用原始密钥,这样的事情应该让您使用原始密钥:
var c = crypto.createCipheriv("aes-128-ecb", new Buffer("00010203050607080a0b0c0d0f101112", "hex").toString("binary"), "");
var c = crypto.createCipheriv("aes-128-ecb", new Buffer("00010203050607080a0b0c0d0f101112", "hex").toString("binary"), "");
Note that since you are using CBC, you need to use the same IV for encryption and decryption (you might want to append it to your message, etc.)
请注意,由于您使用的是 CBC,您需要使用相同的 IV 进行加密和解密(您可能希望将其附加到您的消息等中)
Mandatory warning: implementing a crypto protocol yourself is rarely a good idea. Even if you get this to work, are you going to use the same key for all messages? For how long? If you decide to rotate the key, how to you manage this. Etc, .etc.
强制性警告:自己实施加密协议很少是一个好主意。即使你让这个工作,你会为所有消息使用相同的密钥吗?多长时间?如果您决定轮换密钥,您将如何管理它。等等等等。
回答by Shh
Thanks to all of you. your answers and comments pointed me in the right direction, and with some more research I managed to get a working prototype (pasted below). It turns out that node's crypto uses MD5 to hash the key, and padding is apparently (got that one with trial and error) done using PKCS7Padding
感谢大家。你的回答和评论为我指明了正确的方向,通过更多的研究,我设法得到了一个工作原型(粘贴在下面)。事实证明,节点的加密使用 MD5 来散列密钥,并且填充显然是使用 PKCS7Padding 完成的(通过反复试验得到了那个)
As for the reasons to do it at all in the first place: I have an application comprised of three parts: A. a backend service B. a third party data store C. an android app as a client.
至于首先要这样做的原因:我有一个由三部分组成的应用程序:A. 后端服务 B. 第三方数据存储 C. 作为客户端的 android 应用程序。
The backend service prepares the data and posts it to the third party. The android app gets and/or updates data in the data store, which the service may act upon.
后端服务准备数据并将其发布给第三方。android 应用程序获取和/或更新数据存储中的数据,服务可能会根据这些数据采取行动。
The need for encryption, is keeping the data private, even from the third party provider.
加密的需要是保持数据的私密性,即使是来自第三方提供商。
As for key management - i guess i can have the server create a new key every preconfigured period of time, encrypt it with the old key and post it to the data store for the client to decrypt and start using, but it's kind of overkill for my needs.
至于密钥管理 - 我想我可以让服务器在每个预配置的时间段创建一个新密钥,使用旧密钥对其进行加密并将其发布到数据存储以供客户端解密并开始使用,但这对于我的需要。
I can also create a key pair and use that to transfer the new symmetric key every once in a while, but that's even more overkill (not to mention work)
我还可以创建一个密钥对并使用它每隔一段时间传输一次新的对称密钥,但这更加矫枉过正(更不用说工作了)
Anywho, this is the code: Encrypt on Node.js
任何人,这是代码:在 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
Decrypt on Java:
在 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;
}
回答by Dima Gutzeit
The example from previous answers did not work for me when trying on Java SE since Java 7 complains that "AES/ECB/PKCS7Padding" can not be used.
尝试使用 Java SE 时,先前答案中的示例对我不起作用,因为 Java 7 抱怨无法使用“AES/ECB/PKCS7Padding”。
This however worked:
然而,这有效:
to encrypt:
加密:
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
to decrypt:
解密:
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;
}
回答by Pa?lo Ebermann
You need to make sure you are using
你需要确保你正在使用
- the same key
- the same algorithm, mode of operation and padding.
- 同样的钥匙
- 相同的算法、操作模式和填充。
on both sides of the connection.
在连接的两侧。
For the key, on the Java side you are using quite some work to derive a key from a string - no such thing is done on the node.js side. Use a standard key derivation algorithm here (and the same one on both sides).
对于key,在 Java 端,您使用了相当多的工作来从字符串派生出一个键 - 在 node.js 端没有做这样的事情。在这里使用标准的密钥派生算法(并且双方都使用相同的算法)。
Looking again, the line
再看线
var cipher = crypto.createCipher('aes-128-cbc','somepass')
does indeed some key derivation, just the documentation is silent about what it does exactly:
确实有一些关键的推导,只是文档没有说明它究竟做了什么:
crypto.createCipher(algorithm, password)
Creates and returns a cipher object, with the given algorithm and password.
algorithm
is dependent on OpenSSL, examples are'aes192'
, etc. On recent releases,openssl list-cipher-algorithms
will display the available cipher algorithms.password
is used to derive key and IV, which must be'binary'
encoded string (See the Buffersfor more information).
crypto.createCipher(算法,密码)
使用给定的算法和密码创建并返回一个密码对象。
algorithm
依赖于 OpenSSL,示例是'aes192'
等。在最近的版本中,openssl list-cipher-algorithms
将显示可用的密码算法。password
用于派生密钥和 IV,它们必须是'binary'
编码字符串(有关更多信息,请参阅缓冲区)。
Okay, this at least says how to encode it, but not what is done here. So, we either can use the other initialization method crypto.createCipheriv
(which takes key and initialization vector directly, and uses them without any modification), or look at the source.
好的,这至少说明了如何对其进行编码,但没有说明这里做了什么。所以,我们要么使用其他的初始化方法crypto.createCipheriv
(直接取key和初始化向量,不做任何修改直接使用),要么看源码。
createCipher
will somehow invoke the C++ function CipherInitin node_crypto.cc. This uses in essence the EVP_BytesToKey
function to derive the key from the provided string (with MD5, empty salt and count 1), and then do the same as CipherInitiv
(which is called by createCipheriv
, and uses IV and key directly.)
createCipher
将以某种方式调用node_crypto.cc中的 C++ 函数CipherInit。这本质上使用EVP_BytesToKey
函数从提供的字符串中导出密钥(使用 MD5,空盐和计数 1),然后执行相同的操作CipherInitiv
(由 调用createCipheriv
,并直接使用 IV 和密钥。)
As AES uses 128 bits of key and initialization vector and MD5 has 128 bits of output, this in effect means
由于 AES 使用 128 位密钥和初始化向量,而 MD5 有 128 位输出,这实际上意味着
key = MD5(password)
iv = MD5(key + password)
(where + denotes concatenation, not addition). You can re-implement this key-derivation in Java using the MessageDigest class, if needed.
(其中 + 表示串联,而不是加法)。如果需要,您可以使用 MessageDigest 类在 Java 中重新实现此密钥派生。
A better idea would be to use some slow key derivation algorithm, specially if your password is something what a human can memorize. Then use the pbkdf2function to generate this key on the node.js side, and PBEKeySpec together with a SecretKeyFactory (with algorithm PBKDF2WithHmacSHA1
) on the Java side. (Choose an iteration count which just does not make your customers complain about slowness on the most common devices.)
一个更好的主意是使用一些缓慢的密钥派生算法,特别是如果您的密码是人类可以记住的。然后使用pbkdf2函数在 node.js 端生成这个密钥PBKDF2WithHmacSHA1
,在 Java 端使用 PBEKeySpec 和 SecretKeyFactory(带算法)。(选择一个不会让您的客户抱怨最常见设备运行缓慢的迭代次数。)
For your cipher algorithm, on the Java side you are saying "use the AES algorithm with whatever is the default mode of operation and the default padding mode here". Don't do this, as it might change from provider to provider.
对于您的密码算法,在 Java 方面,您说的是“使用 AES 算法,无论是默认操作模式还是默认填充模式”。不要这样做,因为它可能会因提供者而异。
Instead, use explicit indications of the mode of operation (CBC
, in your case), and explicit indication of the padding mode. One example might be:
相反,使用操作模式的显式指示(CBC
在您的情况下)和填充模式的显式指示。一个例子可能是:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Have a look at the node.js documentation to see how to indicate a padding mode there (or which one is the default, to select the same on the Java side). (From the OpenSSL EVP documentation, it looks like the default is PKCS5Padding here, too.)
查看 node.js 文档,了解如何在那里指示填充模式(或者哪个是默认模式,在 Java 端选择相同的模式)。(从OpenSSL EVP 文档来看,这里的默认设置也是 PKCS5Padding。)
Also, instead of implementing the encryption yourself, consider using TLS for transport encryption. (Of course, this only works if you have a real-time connection between both sides.)
此外,不要自己实施加密,而是考虑使用 TLS 进行传输加密。(当然,这只适用于双方之间有实时连接的情况。)