与节点加密。js加密模块和Java解密(Android app)

时间:2020-11-24 18:27:11

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

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

4 个解决方案

#1


8  

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(),它使用OpenSSL的evp_bykey()来获取密钥。您可以传递一个原始字节缓冲区并使用它初始化Java的SecretKey,或者在Java代码中模拟evp_by睾丸素键()。更多细节请使用$ man evp_by睾丸素,但本质上它会用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 =加密。createCipheriv(“aes-128-ecb”,新缓冲区(“000102030506080808080a0b0b0c0d0d0f101112”,“hex”).toString(“二进制”),“”);

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.

强制警告:自己实现密码协议很少是一个好主意。即使您让它工作,您是否将对所有消息使用相同的键?多长时间?如果您决定旋转键,那么如何管理它。等,等。

#2


10  

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 (for trial and error)来完成的

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安卓应用作为客户。

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

#3


5  

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/ pkcs7填充”不能使用。

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

#4


4  

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).

对于键,在Java方面,您使用了相当多的工作来从字符串中派生出一个键——在节点上没有这样的操作。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)

加密。createCipher(算法,密码)

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 Buffers for more information).

算法依赖于OpenSSL,示例有“aes192”等。在最近的版本中,OpenSSL列表密码算法将显示可用的密码算法。密码用于派生key和IV,它们必须是“二进制”编码的字符串(更多信息请参见缓冲区)。

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(它直接接受键和初始化向量,并且不做任何修改就使用它们),或者查看源代码。

createCipher will somehow invoke the C++ function CipherInit in 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_by睾丸素函数从提供的字符串(使用MD5、空盐和计数1)派生键,然后执行与CipherInitiv相同的操作(由createCipheriv调用,并直接使用IV和key)。

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 pbkdf2 function 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函数在节点上生成这个键。在Java端,和PBEKeySpec以及一个SecretKeyFactory(带有算法PBKDF2WithHmacSHA1)。(选择一个迭代计数,它不会让您的客户抱怨最常见的设备运行缓慢。)

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.)

看一下节点。通过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进行传输加密。(当然,这只有在双方都有实时连接时才有效。)

#1


8  

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(),它使用OpenSSL的evp_bykey()来获取密钥。您可以传递一个原始字节缓冲区并使用它初始化Java的SecretKey,或者在Java代码中模拟evp_by睾丸素键()。更多细节请使用$ man evp_by睾丸素,但本质上它会用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 =加密。createCipheriv(“aes-128-ecb”,新缓冲区(“000102030506080808080a0b0b0c0d0d0f101112”,“hex”).toString(“二进制”),“”);

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.

强制警告:自己实现密码协议很少是一个好主意。即使您让它工作,您是否将对所有消息使用相同的键?多长时间?如果您决定旋转键,那么如何管理它。等,等。

#2


10  

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 (for trial and error)来完成的

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安卓应用作为客户。

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

#3


5  

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/ pkcs7填充”不能使用。

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

#4


4  

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).

对于键,在Java方面,您使用了相当多的工作来从字符串中派生出一个键——在节点上没有这样的操作。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)

加密。createCipher(算法,密码)

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 Buffers for more information).

算法依赖于OpenSSL,示例有“aes192”等。在最近的版本中,OpenSSL列表密码算法将显示可用的密码算法。密码用于派生key和IV,它们必须是“二进制”编码的字符串(更多信息请参见缓冲区)。

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(它直接接受键和初始化向量,并且不做任何修改就使用它们),或者查看源代码。

createCipher will somehow invoke the C++ function CipherInit in 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_by睾丸素函数从提供的字符串(使用MD5、空盐和计数1)派生键,然后执行与CipherInitiv相同的操作(由createCipheriv调用,并直接使用IV和key)。

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 pbkdf2 function 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函数在节点上生成这个键。在Java端,和PBEKeySpec以及一个SecretKeyFactory(带有算法PBKDF2WithHmacSHA1)。(选择一个迭代计数,它不会让您的客户抱怨最常见的设备运行缓慢。)

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.)

看一下节点。通过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进行传输加密。(当然,这只有在双方都有实时连接时才有效。)