Encrypt with Node.js Crypto module and decrypt with Java (in Android app)
Looking for a way to encrypt data (mainly strings) in node and decrypt in an android app (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.
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
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 r开发者_如何学Goesult;
}
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
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.
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.
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
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:
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;
}
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.
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"), "");
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.)
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.
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.
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;
}
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).
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 Buffers for more information).
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.
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.)
As AES uses 128 bits of key and initialization vector and MD5 has 128 bits of output, this in effect means
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.
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.)
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.
Instead, use explicit indications of the mode of operation (CBC
, in your case), and explicit indication of the padding mode. One example might be:
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.)
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.)
精彩评论