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:
- The user account is created server-side and the password is hashed with SHA-256
- 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
- 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
- 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
getBytes()for the plaintext, which doesn't include an explicit character set (such as UTF-8).