-->

How to encrypt a message at client side using cryp

2020-06-30 04:26发布

问题:

Background: the application that I am working on is supposed to work offline. I have an HTML5 page and the data keyed in by the user is encrypted using crypto-js library. And I want the encrypted message sent to java webserver and then decrypt it at the server side.

What am doing I am able to encrypt the message using Crypto-js

<code>
var message = "my message text";
var password = "user password";
var encrypted = CryptoJS.AES.encrypt( message ,password );
console.log(encrypted.toString());
// this prints an encrypted text "D0GBMGzxKXU757RKI8hDuQ=="
</code>

What I would like to do is pass the encrypted text "D0GBMGzxKXU757RKI8hDuQ== " to a java server side code and get the necrypted message decrypted.

I tried many options to decrypt the crypto-js encrypted message at the java server side. Please find below my code at the server side that is supposed to do the decryption of the encrypted text.

<code>
public static String decrypt(String keyText,String encryptedText) 
{
// generate key 
Key key = new SecretKeySpec(keyText.getBytes(), "AES");
Cipher chiper = Cipher.getInstance("AES");
chiper.init(Cipher.DECRYPT_MODE, key);
byte[] decordedValue = new BASE64Decoder().decodeBuffer(encryptedText);
byte[] decValue = chiper.doFinal(decordedValue);
String decryptedValue = new String(decValue);
return decryptedValue;
}  
</code>

I call the java method decrypt from below code

<code>
// performs decryption 
public static void main(String[] args) throws Exception 
{
String decryptedText = CrypterUtil.decrypt("user password","D0GBMGzxKXU757RKI8hDuQ==");
}
</code>

But i get the following exception when i run the java decrypt code

<code>
Exception in thread "main" java.security.InvalidKeyException: Invalid AES key length: 13 bytes
at com.sun.crypto.provider.AESCipher.engineGetKeySize(AESCipher.java:372)
at javax.crypto.Cipher.passCryptoPermCheck(Cipher.java:1052)
at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1010)
at javax.crypto.Cipher.implInit(Cipher.java:786)
at javax.crypto.Cipher.chooseProvider(Cipher.java:849)
at javax.crypto.Cipher.init(Cipher.java:1213)
at javax.crypto.Cipher.init(Cipher.java:1153)
at au.gov.daff.pems.model.utils.CrypterUtil.decrypt(CrypterUtil.java:34)
at au.gov.daff.pems.model.utils.CrypterUtil.main(CrypterUtil.java:47)
Process exited with exit code 1.
</code>

Am not sure what am I doing wrong ?... What is the best way to encrypt a message using the crypto-js library so that it can be decripted else where using user keyed in password.

回答1:

You have to understand that a password is not a key. A password usually goes through some hashing function to result in a bit string or byte array which is a key. It cannot be printed, so it is represented as hex or base64.

In JavaScript you use a password, but in Java you assume the same password is the key which it isn't. You could determine how CryptoJS hashes the password to arrive at the key and recreate this in Java, but it seems that it is implemented in such a way that a fresh salt is generated every time something is encrypted with a password and there is no way to change the salt.

If you really want to work will password from the user then you need to derive the key yourself. CryptoJS provides PBKDF2 for this, but it also takes a salt. You can generate one for your application and add it to the code. You would generate it this way once:

CryptoJS.lib.WordArray.random(128/8).toString();

To derive the key everytime you would pass the static salt into the password-based key derivation function (here for AES-256)

var key = CryptoJS.PBKDF2(userPassword,
        CryptoJS.enc.Hex.parse(salt),
        { keySize: 256/32, iterations: 1000 });
var iv = CryptoJS.lib.WordArray.random(256/8); // random IV
var encrypted = CryptoJS.AES.encrypt("Message", key, { iv: iv });

On the server you need to convert the hex key string into a byte array. You will also need to tweak the scheme on the server from AES to AES/CBC/PKCS5Padding as it is the default in CryptoJS. Note PKCS5 and PKCS7 are the same for AES.

Also note that you will need to pass the IV from client to server and init it as

chiper.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivBytes));

You can of course recreate the key from the password and the salt on the server using a Java implementation of PBKDF or just save the key for a known password and salt. You can play around with the iterations of the PBKDF what is acceptable for your users.



回答2:

Thanks to Artjom B and Isaac Potoczny-Jones for the prompt response and advice. I am giving the complete solution that worked for me below for the benefit of others.

Java code to do the decryption of the cryptojs encrypted message at the Java server side

public static void main(String args[]) throws Exception{

    String password = "Secret Passphrase";
    String salt = "222f51f42e744981cf7ce4240eeffc3a";
    String iv = "2b69947b95f3a4bb422d1475b7dc90ea";
    String encrypted = "CQVXTPM2ecOuZk+9Oy7OyGJ1M6d9rW2D/00Bzn9lkkehNra65nRZUkiCgA3qlpzL";

    byte[] saltBytes = hexStringToByteArray(salt);
    byte[] ivBytes = hexStringToByteArray(iv);
    IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);        
    SecretKeySpec sKey = (SecretKeySpec) generateKeyFromPassword(password, saltBytes);
    System.out.println( decrypt( encrypted , sKey ,ivParameterSpec));
}

public static SecretKey generateKeyFromPassword(String password, byte[] saltBytes) throws GeneralSecurityException {

    KeySpec keySpec = new PBEKeySpec(password.toCharArray(), saltBytes, 100, 128);
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    SecretKey secretKey = keyFactory.generateSecret(keySpec);

    return new SecretKeySpec(secretKey.getEncoded(), "AES");
}

public static byte[] hexStringToByteArray(String s) {

    int len = s.length();
    byte[] data = new byte[len / 2];

    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                + Character.digit(s.charAt(i+1), 16));
    }

    return data;
}

public static String decrypt(String encryptedData, SecretKeySpec sKey, IvParameterSpec ivParameterSpec) throws Exception { 

    Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
    c.init(Cipher.DECRYPT_MODE, sKey, ivParameterSpec);
    byte[] decordedValue = new BASE64Decoder().decodeBuffer(encryptedData);
    byte[] decValue = c.doFinal(decordedValue);
    String decryptedValue = new String(decValue);

    return decryptedValue;
}

The cryptojs javascript code that can do the encryption and decryption at the client side

function  generateKey(){
    var salt = CryptoJS.lib.WordArray.random(128/8);
    var iv = CryptoJS.lib.WordArray.random(128/8);
    console.log('salt  '+ salt );
    console.log('iv  '+ iv );
    var key128Bits100Iterations = CryptoJS.PBKDF2("Secret Passphrase", salt, { keySize: 128/32, iterations: 100 });
    console.log( 'key128Bits100Iterations '+ key128Bits100Iterations);
    var encrypted = CryptoJS.AES.encrypt("Message", key128Bits100Iterations, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7  });
}

function  decrypt(){
    var salt = CryptoJS.enc.Hex.parse("4acfedc7dc72a9003a0dd721d7642bde");
    var iv = CryptoJS.enc.Hex.parse("69135769514102d0eded589ff874cacd");
    var encrypted = "PU7jfTmkyvD71ZtISKFcUQ==";
    var key = CryptoJS.PBKDF2("Secret Passphrase", salt, { keySize: 128/32, iterations: 100 });
    console.log( 'key '+ key);
    var decrypt = CryptoJS.AES.decrypt(encrypted, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
    var ddd = decrypt.toString(CryptoJS.enc.Utf8); 
    console.log('ddd '+ddd);
}


回答3:

AES and the related algorithms can be used in many different ways, and when mixing languages, it can always be a little tricky to figure out what modes the client is using and match them to the modes of the server.

The first problem with your Java code is that you cannot use the bytes of a string as an AES key. There are lots of examples on the Internet of people doing this, but it's terribly wrong. Just like @artjom-B showed with the CryptoJS code, you need to use a "Password-based key derivation function" and it needs to also be parametrized exactly the same on the client & server.

Also, the client needs to generate salt and send it along with the crypto text; otherwise, the server cannot generate the same key from the given password. I'm not sure exactly how CryptoJS does this here's something reasonable in Java, and you can tweak the parameters as you learn how cryptoJS works:

public static SecretKey generateKeyFromPassword(String password, byte[] salt) throws GeneralSecurityException {
    KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, 1000, 256);
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
    return new SecretKeySpec(keyBytes, "AES");
}

With AES CBC, you also need to randomly generate an IV and send that along with the crypto text.

So in summary:

  • Figure out the AES parameters used by CryptoJS. Not sure what they are, but it sounds like: key size (256), padding (pkcs5), mode (CBC), PBE algorithm (PBKDF2), salt (random), iteration count (100)
  • Configure your server with the same parameters
  • Use a PBE key generator, along with a non-secret (but random) salt
  • Use AES CBC with a non-secret (but random) IV
  • Send the cipher text, the IV, and the salt to the server
  • Then on the server side, use the salt, iteration count, and the password to generate the AES key
  • Then base64 decode and decrypt it