2

I'm trying to implement a aes/cbc/pkcs5padding algorithm. I started off with some CryptoJS in JS and found out how wonderful the encryption and decryption works in some seconds. I searched for many compatible solutions because the value that got returned was always wrong.

Unimportant: My goal is the following concept:

  1. The user account is created server-side and the password is hashed with SHA-256
  2. The user tries to log-in on the webpage (client-side JS) where a packet is sent to the server beginning with his plain username followed by an AES encrypted JSONObject with a simple success message. The key for the AES is the SHA-256 of the user's password hashed locally
  3. The server receives this packet and checks for all user accounts if any of the names match with the beginning of the packet. In that case, the application separates the AES part of it and tries to decrypt it with the SHA-256 hashed password the server knows since the creation of the user
  4. If the decryption succeeded, client and server will continue using this key and have a nice communication without transmitting the plain AES key with the websocket at any time. If the decryption fails, the user gets an error message "credentials incorrect"

That's just for your information to help me ;) Because I could not know what CryptoJS exactly does with the data it gets I started with Java and ended up with the following Java implementation:

import io.cloudsystem.module.network.NetworkModule;
import io.netty.handler.codec.DecoderException;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;

public class Crypto {

public static String getSHA256(String password) {

    try {

        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        digest.update(password.getBytes("UTF-8"));

        byte[] dig = digest.digest();
        String base = Base64.getEncoder().encodeToString(dig);
        System.out.println("SHA-KEY (size): " + dig.length);
        System.out.println("SHA-KEY (raw): " + new String(dig, "UTF-8"));
        System.out.println("SHA-KEY (base): " + base);

        return base;

    } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
        NetworkModule.handleException(e);
    }

    return null;

}

public static String encrypt(String plainText, String key) {

    try {

        byte[] clean = plainText.getBytes("UTF-8");

        int ivSize = 16;
        byte[] iv = new byte[ivSize];
        SecureRandom random = new SecureRandom();
        random.nextBytes(iv);
        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

        System.out.println("ENC-IV (length): " + ivParameterSpec.getIV().length);
        System.out.println("ENC-IV (raw): " + new String(ivParameterSpec.getIV(), "UTF-8"));

        byte[] keyFetch = key.getBytes("UTF-8");
        byte[] keyBytes = new byte[16];
        System.arraycopy(keyFetch, 0, keyBytes, 0, keyBytes.length);
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");

        System.out.println("ENC-KEY (length): " + secretKeySpec.getEncoded().length);
        System.out.println("ENC-KEY (raw): " + new String(secretKeySpec.getEncoded(), "UTF-8"));

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
        byte[] encrypted = cipher.doFinal(clean);

        System.out.println("ENC-RAW (length): " + encrypted.length);
        System.out.println("ENC-RAW (raw): " + new String(encrypted, "UTF-8"));

        byte[] encryptedIVAndText = new byte[ivSize + encrypted.length];
        System.arraycopy(iv, 0, encryptedIVAndText, 0, ivSize);
        System.arraycopy(encrypted, 0, encryptedIVAndText, ivSize, encrypted.length);

        System.out.println("ENC-FET (length): " + encryptedIVAndText.length);
        System.out.println("ENC-FET (raw): " + new String(encryptedIVAndText, "UTF-8"));

        String base = Base64.getEncoder().encodeToString(encryptedIVAndText);
        System.out.println("ENC-BASE: " + base);
        return base;

    } catch (NoSuchAlgorithmException | NoSuchPaddingException | UnsupportedEncodingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
        NetworkModule.handleException(e);
    }

    return null;

}

public static String decrypt(String encryped, String key) {

    byte[] encryptedIvTextBytes = Base64.getDecoder().decode(encryped);
    int ivSize = 16;
    int keySize = 16;
    try {
    byte[] iv = new byte[ivSize];
    System.arraycopy(encryptedIvTextBytes, 0, iv, 0, iv.length);
    IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

    System.out.println("DEC-IV (length): " + ivParameterSpec.getIV().length);
    System.out.println("DEC-IV (raw): " + new String(ivParameterSpec.getIV(), "UTF-8"));

    int encryptedSize = encryptedIvTextBytes.length - ivSize;
    byte[] encryptedBytes = new byte[encryptedSize];
    System.arraycopy(encryptedIvTextBytes, ivSize, encryptedBytes, 0, encryptedSize);

        System.out.println("DEC-ENC (length): " + encryptedBytes.length);
        System.out.println("DEC-ENC (raw): " + new String(encryptedBytes, "UTF-8"));

    byte[] keyFetch = key.getBytes();
    byte[] keyBytes = new byte[keySize];
    System.arraycopy(keyFetch, 0, keyBytes, 0, keyBytes.length);
    SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");

        System.out.println("DEC-KEY (length): " + secretKeySpec.getEncoded().length);
        System.out.println("DEC-KEY (raw): " + new String(secretKeySpec.getEncoded(), "UTF-8"));

        Cipher cipherDecrypt = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipherDecrypt.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
        byte[] decrypted = cipherDecrypt.doFinal(encryptedBytes);

        System.out.println("DEC (length): " + decrypted.length);
        System.out.println("DEC: " + new String(decrypted));

        return new String(decrypted);

    } catch (UnsupportedEncodingException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException | InvalidKeyException e) {
        NetworkModule.handleException(e);
    }

        return null;

    }

}

Sorry for the missleading paddings in the code btw. An example scenario could end up like this:

SHA-KEY (size): 32
SHA-KEY (raw): �F os���>P�`��o�8e5*gf�
 �
SHA-KEY (base): 4kYAb3O+EQTB9hE+UJxg9rNv8AQ4ZTUeKmdmzxoKAJE=
ENC-IV (length): 16
ENC-IV (raw): �I?dm�@�ܹTa؞�
ENC-KEY (length): 16
ENC-KEY (raw): 4kYAb3O+EQTB9hE+
ENC-RAW (length): 16
ENC-RAW (raw): B;��\`A0��z��
ENC-FET (length): 32
ENC-FET (raw): �I?dm�@�ܹTa؞�B;��\`A0��z��
ENC-BASE: FfVJP2RtuED53LlUYdie9EI7uJITXGBBMN7OepQeAqU=
DEC-IV (length): 16
DEC-IV (raw): �I?dm�@�ܹTa؞�
DEC-ENC (length): 16
DEC-ENC (raw): B;��\`A0��z��
DEC-KEY (length): 16
DEC-KEY (raw): 4kYAb3O+EQTB9hE+
DEC (length): 10
DEC: helloworld

My result: encryption and decryption works in java. Now I need to implement it to javascript. After alot of time of testing, encoding, decoding, getting stuck with the difference of UTF16 (JS default) and UTF8 (Java Charset) I ended up with the same values I got in java with this code:

var key = new Buffer("4kYAb3O+EQTB9hE+UJxg9rNv8AQ4ZTUeKmdmzxoKAJE=").subarray(0, 16)
var cryptobase = "FfVJP2RtuED53LlUYdie9EI7uJITXGBBMN7OepQeAqU="
varr crypto = new Buffer(cryptobase, 'base64')
var iv = crypto.subarray(0, 16)
var text = crypto.subarray(16, crypto.length)

console.log("Key: " + new TextDecoder("utf-8").decode(key))
console.log("IV: " + new TextDecoder("utf-8").decode(iv))
console.log("Text: " + new TextDecoder("utf-8").decode(text))

console.log(CryptoJS.AES.decrypt(text, key, {iv: iv}).toString(CryptoJS.enc.Utf8))

and here are the results:

Key: 4kYAb3O+EQTB9hE+
IV: �I?dm�@�ܹTa؞�
Text: B;��\`A0��z��

Exactly the same values while decryption we got on the Java side. Except of one: The CryptoJS result. This is "" (emptystring) Now here is my big problem: how to continue now, and can I use CryptoJS? Did I do something wrong, insecure or not-failsafe?

Please recognize:

  • I've seen https://github.com/mpetersen/aes-example but it didn't work for me and I want to use my own key. Tested it again. Still different values using pbkdf2

  • I don't want to use AES-256 because I don't want to require the users of the software to install JCE

  • I know that SHA-256 is not the way to go to hash a password. I am using pbkdf2 in production

9
  • Well I'm doing as you can see. 16 Bytes of the SHA-256 = 16*8=128. But that is not by problem / question Commented Jan 8, 2018 at 23:31
  • 1. Just use AES with a 128 bit key, it is essentially as secure as as with a 256 bit key, neither are susceptible to a brute force attack. 2. Don't use SHA-256 for the password, use PBKDF2, theme says it all: Password Based Key Derivation Function 2. Commented Jan 8, 2018 at 23:32
  • Okay but PBKDF2 still does not solve the issue Commented Jan 8, 2018 at 23:34
  • Does it? Yes its more secure dir hashing but how solves it the problem? Commented Jan 9, 2018 at 8:42
  • Your key is not base 64 decoded in JS. There is also something terribly wrong with your IV in the Java output. According to the code it should be random, but it is printed out as hexadecimals if I must believe your output. You're also still using getBytes() for the plaintext, which doesn't include an explicit character set (such as UTF-8). Commented Jan 9, 2018 at 17:29

1 Answer 1

4

I got it working after implementing the aes example from https://github.com/mpetersen/aes-example as in the sourcecode, not the description. I modified it to pass the data within one string, so you only need the password and the key for the encryption/decryption. Here is my sourcecode (modified version of mpetersens aes-example):

JavaScript:

const CryptoJS = require("crypto-js")
var Crypto = new AES()

function AES() {}

AES.prototype.generateKey = function(salt, passPhrase) {
    var key = CryptoJS.PBKDF2(passPhrase, CryptoJS.enc.Hex.parse(salt), { keySize: 4, iterations: 1000 });
    return key;
}

AES.prototype.encrypt = function(password, message) {
    var salt = CryptoJS.lib.WordArray.random(128/8).toString(CryptoJS.enc.Hex)
    var iv = CryptoJS.lib.WordArray.random(128/8).toString(CryptoJS.enc.Hex)
    var encrypted = CryptoJS.AES.encrypt(message, this.generateKey(salt, password), { iv: CryptoJS.enc.Hex.parse(iv) })
    var base64 = encrypted.ciphertext.toString(CryptoJS.enc.Base64)
    return salt + base64.substring(0, base64.length-2) + iv
}

AES.prototype.decrypt = function(password, message) {
    var salt = message.substring(0, 32)
    var iv = message.substring(message.length-32, message.length)
    var cipherParams = CryptoJS.lib.CipherParams.create({
        ciphertext: CryptoJS.enc.Base64.parse(message.substring(32, message.length-32) + "==")
    });
    var decrypted = CryptoJS.AES.decrypt(cipherParams, this.generateKey(salt, password), { iv: CryptoJS.enc.Hex.parse(iv) })
    return decrypted.toString(CryptoJS.enc.Utf8)
}

Java:

private static final char[] HEX = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'b', 'c', 'D', 'e', 'F'};
private static Cipher cipher;

public static void init() {

    try {
        cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    } catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
        NetworkModule.handleException(e);
    }

}

public static String encrypt(String password, String message) {

    try {

        String salt = random(16);
        String iv = random(16);
        SecretKey key = generateKey(salt, password);
        byte[] encrypted = doFinal(Cipher.ENCRYPT_MODE, key, iv, message.getBytes("UTF-8"));
        String code = Base64.getEncoder().encodeToString(encrypted);
        return salt + code.substring(0, code.length() - 2) + iv;

    } catch (UnsupportedEncodingException e) {
        NetworkModule.handleException(e);
        return null;
    }

}

public static String decrypt(String password, String message) {

    try {

        String salt = message.substring(0, 32);
        String iv = message.substring(message.length() - 32, message.length());
        String base = message = message.substring(32, message.length() - 32) + "==";
        SecretKey key = generateKey(salt, password);
        byte[] decrypted = doFinal(Cipher.DECRYPT_MODE, key, iv, Base64.getDecoder().decode(base));
        return new String(decrypted, "UTF-8");

    } catch (UnsupportedEncodingException e) {
        NetworkModule.handleException(e);
        return null;
    }

}

private static byte[] doFinal(int encryptMode, SecretKey key, String iv, byte[] bytes) {

    try {

        cipher.init(encryptMode, key, new IvParameterSpec(hex(iv)));
        return cipher.doFinal(bytes);

    } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
        NetworkModule.handleException(e);
        return null;
    }

}

private static SecretKey generateKey(String salt, String passphrase) {

    try {

        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        KeySpec spec = new PBEKeySpec(passphrase.toCharArray(), hex(salt), 1000, 128);
        SecretKey key = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
        return key;

    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
        NetworkModule.handleException(e);
        return null;
    }

}

private static String random(int length) {
    byte[] salt = new byte[length];
    new SecureRandom().nextBytes(salt);
    return hex(salt);
}

private static String hex(byte[] data) {

    int l = data.length;
    char[] out = new char[l << 1];
    int i = 0;

    for (int var5 = 0; i < l; ++i) {
        out[var5++] = HEX[(240 & data[i]) >>> 4];
        out[var5++] = HEX[15 & data[i]];
    }

    return new String(out);

}

private static byte[] hex(String hex) {

    char[] data = hex.toCharArray();
    int len = data.length;

    if ((len & 1) != 0) {
        return null;
    } else {

        byte[] out = new byte[len >> 1];
        int i = 0;

        for (int j = 0; j < len; ++i) {

            int f = Character.digit(data[j], 16) << 4;
            ++j;
            f |= Character.digit(data[j], 16);
            ++j;
            out[i] = (byte) (f & 255);

        }

        return out;

    }

}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.