I tried to decrypt en encrypted string in JAVA with to code below.
SecretKey secretKey = new SecretKeySpec(build3DesKey(key), "DESede");
Cipher cipher = Cipher.getInstance("DESede");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] b = cipher.doFinal(str2ByteArray(dest));
String decoder = new String(b, "utf-8");
private static byte[] build3DesKey(String keyStr) throws Exception {
byte[] key = new byte[24];
byte[] temp = keyStr.getBytes("utf-8");
if (key.length > temp.length) {
System.arraycopy(temp, 0, key, 0, temp.length);
} else {
System.arraycopy(temp, 0, key, 0, key.length);
}
return key;
}
How can I get the same result with PHP version? I tried to write in PHP and It outputs with wrong result.
$data = '69C16E8142F2BDDE7569842BB0D68A3176624264E...';
$key = 'rpwdvbppnrvr56m123+#';
function decrypt($data, $secret)
{
//Generate a key from a hash
$key = md5(utf8_encode($secret), true);
//Take first 8 bytes of $key and append them to the end of $key.
$key .= substr($key, 0, 8);
$data = base64_decode($data);
$data = mcrypt_decrypt('tripledes', $key, $data, 'ecb');
$block = mcrypt_get_block_size('tripledes', 'ecb');
$len = strlen($data);
$pad = ord($data[$len-1]);
return substr($data, 0, strlen($data) - $pad);
}
var_dump(utf8_encode(Decrypt($data, $key)));
The build3DesKey() function expands a too short 3DES key to 24 bytes by padding the end with 0x00 values, for too long keys the end is simply truncated. build3DesKey() can be implemented in PHP as follows:
$key = substr(str_pad($key, 24, "\0"), 0, 24);
Although the str2ByteArray() function is missing, its functionality can be deduced. Since in your example the ciphertext is hexadecimal encoded, this function seems to simply perform a hex decoding. In PHP, the analog to str2ByteArray() is hex2bin().
Thus, a possible implementation for decryption is (using PHP/OpenSSL):
$key = "12345";
$ciphertext = "84b24172c57752385251d142abadbed1d9945301a3aee429ce00c1e291a605c30ad18c5e00007f6db394fc6138a2ee4c";
$key = substr(str_pad($key, 24, "\0"), 0, 24);
$plaintext = openssl_decrypt(hex2bin($ciphertext), "des-ede3", $key, OPENSSL_RAW_DATA);
print($plaintext. PHP_EOL); // The quick brown fox jumps over the lazy dog
The Java code returns the same plain text for these input data!
Differences to your code:
Your code uses the deprecated mcrypt. This should not be applied nowadays for security reasons. A better alternative is PHP/OpenSSL, which is used in the code above. Also, the implemented key derivation is wrong, e.g. it applies the digest MD5, which is not used in the Java code at all.
Security:
Even though this is probably a legacy application, a few words about security:
The key derivation build3DesKey() is insecure. If the key material is a string, it is generally not a key, but a password. Therefore a reliable key derivation function should be used, e.g. Argon2 or PBKDF2.
des-ede3 applies ECB mode, which is also insecure. Nowadays authenticated encryption, e.g. AES-GCM should be used.
3DES/TripleDES is outdated and the only not yet deprecated variant, triple-length keys or 3TDEA will be soon, and is comparatively slow. Today's standard AES should be applied here.
Related
I am finding it difficult to convert a piece of code to php from java.
I searched on the internet about the meaning of each line of code written in my java code example but I didn't find any.
I want to understand what each line does in this particular example.
This is what I tried.
function my_aes_encrypt($key, $data) {
if(16 !== strlen($key)) $key = hash('MD5', $key, true);
$padding = 16 - (strlen($data) % 16);
$data .= str_repeat(chr($padding), $padding);
return base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_ECB, str_repeat("\0", 16)));
}
function my_aes_decrypt($str, $key){
$str = base64_decode($str);
$str = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $str, MCRYPT_MODE_ECB);
$block = mcrypt_get_block_size('rijndael_128', 'ecb');
$pad = ord($str[($len = strlen($str)) - 1]);
$len = strlen($str);
$pad = ord($str[$len-1]);
return substr($str, 0, strlen($str) - $pad);
}
Convert from Java to PHP
//provided key
byte[] keyBinary = DatatypeConverter.parseBase64Binary("r/RloSflFkLj3Pq2gFmdBQ==");
SecretKey secret = new SecretKeySpec(keyBinary, "AES");
// encrypted string
byte[] bytes = DatatypeConverter.parseBase64Binary("IKWpOq9rhTAz/K1ZR0znPA==");
// iv
byte[] iv = DatatypeConverter.parseBase64Binary("yzXzUhr3OAt1A47g7zmYxw==");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
String msisdn = new String(cipher.doFinal(bytes), "UTF-8");
It would be great if you guys let me know the details of each line written in Java.
The functionalities of the Java- and the PHP-code differ significantly. First of all, the Java-code contains only the decryption part, whereas the PHP-part contains both, the encryption- and decryption part. Contrary to the Java-code, in the PHP-my_aes_decrypt-method the insecure ECB-mode (https://crypto.stackexchange.com/questions/20941/why-shouldnt-i-use-ecb-encryption) seems to be used instead of the CBC-mode and thus, no IV is involved. Less important, but nonetheless different, the key doesn't seem to be base64-encoded because it's not decoded anywhere. In addition, in the PHP-code deprecated methods like mcrypt_encrypt (http://php.net/manual/de/function.mcrypt-encrypt.php) or cryptographic weak algorithms like MD5 (https://en.wikipedia.org/wiki/MD5) are used.
If I get it right, the Java code is the reference code and you need the PHP-counterpart. Thus, I focus on the Java-code and ignore the differing and outdated PHP-code completely.
In the Java-code, the key, the data and the IV, all base64-encoded, become decoded and then, the encrypted data are decrypted using these decoded data.
A possible PHP-counterpart for the decryption could be:
<?php
$keyBinary = base64_decode('r/RloSflFkLj3Pq2gFmdBQ=='); // decode base64-encoded key in a string (internally, PHP strings are byte arrays)
$bytes = base64_decode('IKWpOq9rhTAz/K1ZR0znPA=='); // decode base64-encoded encrypted data in a string
$iv = base64_decode('yzXzUhr3OAt1A47g7zmYxw=='); // decode base64-encoded IV in a string
$msisdn = openssl_decrypt($bytes, 'AES-128-CBC', $keyBinary, OPENSSL_RAW_DATA, $iv); // decrypt data using AES-128, CBC-mode and PKCS7-Padding (default-padding)
// - when OPENSSL_RAW_DATA is specified raw data are returned, otherwise base64-encoded data (= default)
// - when OPENSSL_ZERO_PADDING is specified no padding is used, otherwise PKCS7-padding (= default)
// - The value XXX in AES-XXX-CBC is determined by the length of the key in Bit used in the Java-code,
// e.g. for a 32 Byte (256 Bit)-key AES-256-CBC has to be used.
print $msisdn."\n"; // Output: 1234567 // print decrypted data
The desired explanation for the Java-code can be found in the comments:
//provided key
byte[] keyBinary = DatatypeConverter.parseBase64Binary("r/RloSflFkLj3Pq2gFmdBQ=="); // decode base64-encoded key in a byte-array
SecretKey secret = new SecretKeySpec(keyBinary, "AES"); // create AES-key from byte-array (currently 16 Byte = 128 Bit long)
// encrypted string
byte[] bytes = DatatypeConverter.parseBase64Binary("IKWpOq9rhTAz/K1ZR0znPA=="); // decode base64-encoded encrypted data in a byte-array
// iv
byte[] iv = DatatypeConverter.parseBase64Binary("yzXzUhr3OAt1A47g7zmYxw=="); // decode base64-encoded IV in a byte-array
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // create cipher-instance for using AES in CBC-mode with PKCS5-Padding (Java counterpart to PKCS7)
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv)); // initialize cipher-instance for decryption with specified AES-key and IV (the latter created from corresponding byte-array)
String msisdn = new String(cipher.doFinal(bytes), "UTF-8"); // decrypt data using AES-128 (128 determined by length of used key in Bit), CBC-mode and PKCS5-Padding,
// and put them in a UTF-8 string
System.out.println(msisdn); // Output: 1234567 // print decrypted data
The PHP-encryption part could be:
<?php
$keyBinary = base64_decode('r/RloSflFkLj3Pq2gFmdBQ==');
$msisdn = '1234567'; // plain text
$iv = openssl_random_pseudo_bytes(16); // generate random IV
//$iv = base64_decode('yzXzUhr3OAt1A47g7zmYxw=='); // use this line for tests with your base64-encoded test-IV yzXzUhr3OAt1A47g7zmYxw==
$bytes = openssl_encrypt($msisdn, 'AES-128-CBC', $keyBinary, OPENSSL_RAW_DATA, $iv); // encrypt data using AES-128, CBC-mode and PKCS7-Padding (default-padding)
$ivBase64 = base64_encode($iv); // base64-encode IV
$bytesBase64 = base64_encode($bytes); // base64-encode encrypted data
print $ivBase64."\n".$bytesBase64."\n"; // print base64-encoded IV and encrypted data
Java AES/CBC/PKCS5PADDING function
public static String encrypt_key_data(String password, String message) {
//password = 4lt0iD3biT#2O17l8
//message = "{"key_id":"101","merchant_code":"65010A"}";
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING", "SunJCE");
MessageDigest sha = MessageDigest.getInstance("SHA-1");
byte[] hashedpassword = sha.digest(password.getBytes("UTF-8"));
hashedpassword = Arrays.copyOf(hashedpassword, 16);
SecretKeySpec key = new SecretKeySpec(hashedpassword, "AES");
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(IV.getBytes("UTF-8")));
byte[] encrypted;
encrypted = cipher.doFinal(message.getBytes());
return asHex(encrypted);
}
java function resulting value = 'bc26d620be9fa0d810e31e62b00a518f79524f6142b90550b9148d50a1ab94ba55671e68f6cf3ebc44dd6af12f566ee8'
PHP AES-256-CBC function
function encrypt($password, $iv, $data) {
$password = '4lt0iD3biT#2O17l8';
$iv = 'AAAAAAAAAAAAAAAA';
$data = '{"key_id":"101","merchant_code":"65010A"}';
$encodedEncryptedData = (openssl_encrypt(($data), 'AES-256-CBC', fixKey(sha1($password)), OPENSSL_RAW_DATA, $iv));
print_r(bin2hex($encodedEncryptedData));
}
function fixKey($key) {
if (strlen($key) < 32) {
//0 pad to len 32
return str_pad("$key", 32, "0");
}
if (strlen($key) > 32) {
//truncate to 32 bytes
return substr($key, 0, 32);
}
return $key;
}
php function resulting value = 'cf20379c95a41429d4097f0ef7982c72a0d25c014cc09d93ba4a111bb9c11c38bc75d6c9f16cd9cb6545dc8c31560985'
I use same password and same IV, and i have read that AES/CBC/PKCS5PADDING is equivalent with AES-256-CBC. But why mine is resulting different result?
Please tell me where is my fault
==============================================
solved. I need to hex2bin($key) then use the key to encrypt using aes
To do AES-256 you would need a 256 bit key, but you are only providing 128 bits - both in java with:
hashedpassword = Arrays.copyOf(hashedpassword, 16);
.. and in PHP with:
if (strlen($key) > 32) {
//truncate to 32 bytes
return substr($key, 0, 32);
}
as $key is a hexstring with only 4 bit per digit (4 * 32 = 128).
As Java deterts key length from the key provided, you end up with 128 bit encryption in Java. Exactly what PHP/Openssl ends up doing is a bit unknown as you provide conflicting information. You ask for AES-256-CBC but only provides a 128 bit key.
Also, you should not use a simple SHA1 to derive keys from passwords. Instead use a key deriving function like pbkdf2, or just use an actual binary key.
I am in a situation where a JSON is encrypted in PHP's openssl_encrypt and needs to be decrypted in JAVA.
$encrypted = "...ENCRYPTED DATA...";
$secretFile = "/path/to/secret/saved/in/text_file";
$secret = base64_decode(file_get_contents($secretFile));
var_dump(strlen($secret)); // prints : int(370)
$iv = substr($encrypted, 0, 16);
$data = substr($encrypted, 16);
$decrypted = openssl_decrypt($data, "aes-256-cbc", $secret, null, $iv);
This $decrypted has correct data which is now decrypted.
Now, the problem is when I try to do same things in Java it doesn't work :(
String path = "/path/to/secret/saved/in/text";
String payload = "...ENCRYPTED DATA...";
StringBuilder output = new StringBuilder();
String iv = payload.substring(0, 16);
byte[] secret = Base64.getDecoder().decode(Files.readAllBytes(Paths.get(path)));
String data = payload.substring(16);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(), 0, cipher.getBlockSize());
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); // This line throws exception :
cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
Here it is:
Exception in thread "main" java.security.InvalidKeyException: Invalid AES key length: 370 bytes
at com.sun.crypto.provider.AESCrypt.init(AESCrypt.java:87)
at com.sun.crypto.provider.CipherBlockChaining.init(CipherBlockChaining.java:91)
at com.sun.crypto.provider.CipherCore.init(CipherCore.java:591)
at com.sun.crypto.provider.AESCipher.engineInit(AESCipher.java:346)
at javax.crypto.Cipher.init(Cipher.java:1394)
at javax.crypto.Cipher.init(Cipher.java:1327)
at com.sample.App.main(App.java:70)
I have already visited similar question like
AES-256 CBC encrypt in php and decrypt in Java or vice-versa
openssl_encrypt 256 CBC raw_data in java
Unable to exchange data encrypted with AES-256 between Java and PHP
and list continues.... but no luck there
btw, this is how encryption is done in PHP
$secretFile = "/path/to/secret/saved/in/text_file";
$secret = base64_decode(file_get_contents($secretFile));
$iv = bin2hex(openssl_random_pseudo_bytes(8));
$enc = openssl_encrypt($plainText, "aes-256-cbc", $secret, false, $iv);
return $iv.$enc;
and yes, I forgot to mention that my JRE is already at UnlimitedJCEPolicy and I can't change PHP code.
I am totally stuck at this point and can't move forward. Please help out.
EDIT#1
byte[] payload = ....;
byte[] iv = ....;
byte[] secret = ....; // Now 370 bits
byte[] data = Base64.getDecoder().decode(payload);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec secretKeySpec = new SecretKeySpec(Arrays.copyOfRange(secret, 0, 32), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv, 0, cipher.getBlockSize());
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] output = cipher.doFinal(data);
System.out.println(new String(output).trim());
Above snippet seems to be working with openssl_encrypt
EDIT#2
I am not sure if this is correct, but following is what I have done and encryption-decryption on both side are working fine.
Encrypt in PHP, Decrypt in JAVA use AES/CBC/NoPadding
Encrypt in JAVA, Decrypt in PHP use AES/CBC/PKCS5Padding
I won't provide a complete solution, but there are a few differences you should take care of
Encoding:
String iv = payload.substring(0, 16);
String data = payload.substring(16);
are you sure the IV and data are the same in Java and PHP (The IV is string?)? If the data are encrypted, they should be treated as a byte array, not string. Just REALLY make sure they are THE SAME (print hex/base64 in php and java)
For the IV you at the end call iv.getBytes(), but the locale encoding may/will corrupt your values. The String should be use only when it's really string (text). Don't use string for binaries.
Simply treat data and iv as byte[]
Key generation according to the openssl
AES key must have length of 256 bit for aes-256-cbc used. The thing is - openssl by default doesn't use the provided secret as a key (I believe it can, but I don't know how it is to be specified in PHP).
see OpenSSL EVP_BytesToKey issue in Java
and here is the EVP_BytesToKey implementation: https://olabini.com/blog/tag/evp_bytestokey/
you should generate a 256 bit key usging the EVP_BytesToKey function (it's a key derivation function used by openssl).
Edit:
Maarten (in the comments) is right. The key parameter is the key. Seems the PHP function is accepting parameter of any length which is misleading. According to some articles (e.g. http://thefsb.tumblr.com/post/110749271235/using-opensslendecrypt-in-php-instead-of) the key is trucated or padded to necessary length (so seems 370 bit key is truncated to length of 256 bits).
According to your example, I wrote fully working code for PHP and Java:
AesCipher class: https://gist.github.com/demisang/716250080d77a7f65e66f4e813e5a636
Notes:
-By default algo is AES-128-CBC.
-By default init vector is 16 bytes.
-Encoded result = base64(initVector + aes crypt).
-Encoded/Decoded results present as itself object, it gets more helpful and get possibility to check error, get error message and get init vector value after encode/decode operations.
PHP:
$secretKey = '26kozQaKwRuNJ24t';
$text = 'Some text'
$encrypted = AesCipher::encrypt($secretKey, $text);
$decrypted = AesCipher::decrypt($secretKey, $encrypted);
$encrypted->hasError(); // TRUE if operation failed, FALSE otherwise
$encrypted->getData(); // Encoded/Decoded result
$encrypted->getInitVector(); // Get used (random if encode) init vector
// $decrypted->* has identical methods
JAVA:
String secretKey = "26kozQaKwRuNJ24t";
String text = "Some text";
AesCipher encrypted = AesCipher.encrypt(secretKey, text);
AesCipher decrypted = AesCipher.decrypt(secretKey, encrypted);
encrypted.hasError(); // TRUE if operation failed, FALSE otherwise
encrypted.getData(); // Encoded/Decoded result
encrypted.getInitVector(); // Get used (random if encode) init vector
// decrypted.* has identical methods
I'm try to be compatible Encrypt/Decrypt both C# and Java.
As I know the default mode is 'ecb/pkcs5' in Java, and 'cbc/pkcs7' in C#.
So I match these things.
1st question is that PKCS7 and PKCS5 are compatible each other??,
there is no PKCS7 in Java so I use PKCS5. but I can get same encrypted data [even the padding-way is different ,pkcs7/pkcs5,] Is it possible? or these are compatible?
2nd question is that Why I get same result even though the mode, way are all different?
I compare 'DES-ECB / DES-CBC / TripleDES-ECB' these things. and C# is working well, results are all different.
Input > HELLO Output > (ECB)/dZf3gUY150= (CBC) V17s5QLzynM= (Triple)sWGS0GMe1jE
but I get same reulst in Java ..
Input > HELLO Output > (ECB)/dZf3gUY150= (CBC)/dZf3gUY150= (Triple)/dZf3gUY150=
When debugging the flow is right.
Here is my code.
C#
public static string Encrypt_DES(string originalString, byte[] key, string mode)
{
DESCryptoServiceProvider cryptoProvider = new DESCryptoServiceProvider();
if (mode.Equals("ECB"))
cryptoProvider.Mode = CipherMode.ECB;
else if (mode.Equals("CBC"))
{
cryptoProvider.Mode = CipherMode.CBC;
cryptoProvider.IV = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
}
cryptoProvider.Padding = PaddingMode.PKCS7;
MemoryStream memoryStream = new MemoryStream();
CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoProvider.CreateEncryptor(key, key), CryptoStreamMode.Write);
StreamWriter writer = new StreamWriter(cryptoStream);
writer.Write(originalString);
writer.Flush();
cryptoStream.FlushFinalBlock();
writer.Flush();
return Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
}
public static string Encrypt_TripleDES(string source, string key)
{
TripleDESCryptoServiceProvider desCryptoProvider = new TripleDESCryptoServiceProvider();
MD5CryptoServiceProvider hashMD5Provider = new MD5CryptoServiceProvider();
byte[] byteHash;
byte[] byteBuff;
byteHash = hashMD5Provider.ComputeHash(Encoding.UTF8.GetBytes(key));
desCryptoProvider.Key = byteHash;
desCryptoProvider.Mode = CipherMode.ECB; //CBC, CFB
desCryptoProvider.Padding = PaddingMode.PKCS7;
byteBuff = Encoding.UTF8.GetBytes(source);
string encoded = Convert.ToBase64String(desCryptoProvider.CreateEncryptor().TransformFinalBlock(byteBuff, 0, byteBuff.Length));
return encoded;
}
Java(Android)
public String Encrypt(String str, String desKey, String mode) {
try {
KeySpec keySpec = null;
SecretKey key = null;
Cipher ecipher = null;
if (desKey.length() == 8) {
keySpec = new DESKeySpec(desKey.getBytes("UTF8"));
key = SecretKeyFactory.getInstance("DES").generateSecret(keySpec);
if(mode.equals(ECB)){
ecipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
ecipher.init(Cipher.ENCRYPT_MODE, key);
}else if (mode.equals(CBC)){
ecipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes);
ecipher.init(Cipher.ENCRYPT_MODE, key,ivSpec);
}
} else if (desKey.length() == 24) {
keySpec = new DESedeKeySpec(desKey.getBytes("UTF8"));
key = SecretKeyFactory.getInstance("DESede").generateSecret(keySpec);
ecipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
ecipher.init(Cipher.ENCRYPT_MODE, key);
}
byte[] data = str.getBytes("UTF-8");
byte[] crypt = ecipher.doFinal(data);
return Base64.encodeToString(crypt, 0);
} catch (Exception ex) {
Log.d("ZVM", ex.getMessage());
}
return null;
}
As I understand 'IV' is for CBC, When making password, it is mixed with IV(not the key but like key). Is it right?
Thanks.
PKCS7 and PKCS5 are compatible each other
PKCS#5 and PKCS#7 paddings are compatible (equal) for DES. For AES, Java actually uses PKCS#7 padding even though you would write AES/xyz/PKCS5Padding.
Why I get same result even though the mode, way are all different?
First, let's see how Java behaves. The ciphertexts for DES-ECB, DES-CBC and DESede-ECB are all equal. This is correct if
the key is the same (DES supports only 8 byte keys, but Triple DES supports 8, 16 and 24 byte keys where non-24 byte keys are expanded to 24 byte keys),
the plaintext is the same,
the plaintext is less than 8 bytes long (block size of DES/Triple DES) and
the IV is an all 0x00 bytes IV.
Those are all true in the Java code. If you have trouble grasping that, combine the encryption routines for the ECB and CBC modes of operation.
The result of Triple DES might be a bit confusing. I assume that you've taken your 8 byte key for DES and replicated it either twice or thrice for use in Triple DES. This is an issue, because Triple DES encryption consists of three steps of normal DES: EDE means Encryption + Decryption + Encryption. If all the three subkeys are the same, the one of the Encryption steps cancels out with the Decryption step and the whole thing is equivalent to a single DES encryption.
Let's see why C# behaves differently:
The ciphertext from DES-CBC is different from DES-ECB, because the IV is not an all 0x00 bytes IV. cryptoProvider.CreateEncryptor(key, key) creates an Encryptor with the IV set to key (the second argument). That's not what you want. Just use cryptoProvider.CreateEncryptor() instead.
The ciphertext from DESede-ECB is different from DES-ECB, because you're running the key through a hash function. The key is therefore different.
Don't use DES nowadays. It only provides 56 bit of security. AES would be a much better, because it's more secure with the lowest key size of 128 bit. There is also a practical limit on the maximum ciphertext size with DES. See Security comparison of 3DES and AES.
I'm only asking this because I have read many posts for 2 days now about crypto AES encryption, and just when I thought I was getting it, I realized I wasn't getting it at all.
This post is the closest one to my issue, I have exactly the same problem but it is unanswered:
CryptoJS AES encryption and JAVA AES decryption value mismatch
I have tried doing it in many ways but I haven't gotten it right.
First Off
I'm getting the already encrypted string (I only got the code to see how they were doing it), so modifying the encryption way is not an option. That's why all the similar questions aren't that useful to me.
Second
I do have access to the secret key and I can modify it (so adjusting length is an option if neccessary).
The encryption is done on CryptoJS and they send the encrypted string as a GET parameter.
GetParamsForAppUrl.prototype.generateUrlParams = function() {
const self = this;
return new Promise((resolve, reject) => {
const currentDateInMilliseconds = new Date().getTime();
const secret = tokenSecret.secret;
var encrypted = CryptoJS.AES.encrypt(self.authorization, secret);
encrypted = encrypted.toString();
self.urlParams = {
token: encrypted,
time: currentDateInMilliseconds
};
resolve();
});
};
I can easily decrypt this on javascript using CryptoJS with:
var decrypted = CryptoJS.AES.decrypt(encrypted_string, secret);
console.log(decrypted.toString(CryptoJS.enc.Utf8));
But I don't want to do this on Javascript, for security reasons, so I'm trying to decrypt this on Java:
String secret = "secret";
byte[] cipherText = encrypted_string.getBytes("UTF8");
SecretKey secKey = new SecretKeySpec(secret.getBytes(), "AES");
Cipher aesCipher = Cipher.getInstance("AES");
aesCipher.init(Cipher.DECRYPT_MODE, secKey);
byte[] bytePlainText = aesCipher.doFinal(byteCipherText);
String myDecryptedText = = new String(bytePlainText);
Before I had any idea of what I was doing, I tried base64 decoding, adding some IV and a lot of stuff I read, of course none of it worked.
But after I started to understand, kinda, what I was doing, I wrote that simple script above, and got me the same error on the post: Invalid AES key length
I don't know where to go from here. After reading a lot about this, the solution seems to be hashing or padding, but I have no control on the encryption method, so I can't really hash the secret or pad it.
But as I said, I can change the secret key so it can match some specific length, and I have tried changing it, but as I'm shooting in the dark here, I don't really know if this is the solution.
So, my question basically is, If I got the encrypted string (in javascript like the first script) and the secret key, is there a way to decrypt it (in Java)? If so, how to do it?
Disclaimer: Do not use encryption unless you understand encryption concepts including chaining mode, key derivation functions, IV and block size. And don't roll your own security scheme but stick to an established one. Just throwing in encryption algorithms doesn't mean an application has become any more secure.
CryptoJS implements the same key derivation function as OpenSSL and the same format to put the IV into the encrypted data. So all Java code that deals with OpenSSL encoded data applies.
Given the following Javascript code:
var text = "The quick brown fox jumps over the lazy dog. 👻 👻";
var secret = "René Über";
var encrypted = CryptoJS.AES.encrypt(text, secret);
encrypted = encrypted.toString();
console.log("Cipher text: " + encrypted);
We get the cipher text:
U2FsdGVkX1+tsmZvCEFa/iGeSA0K7gvgs9KXeZKwbCDNCs2zPo+BXjvKYLrJutMK+hxTwl/hyaQLOaD7LLIRo2I5fyeRMPnroo6k8N9uwKk=
On the Java side, we have
String secret = "René Über";
String cipherText = "U2FsdGVkX1+tsmZvCEFa/iGeSA0K7gvgs9KXeZKwbCDNCs2zPo+BXjvKYLrJutMK+hxTwl/hyaQLOaD7LLIRo2I5fyeRMPnroo6k8N9uwKk=";
byte[] cipherData = Base64.getDecoder().decode(cipherText);
byte[] saltData = Arrays.copyOfRange(cipherData, 8, 16);
MessageDigest md5 = MessageDigest.getInstance("MD5");
final byte[][] keyAndIV = GenerateKeyAndIV(32, 16, 1, saltData, secret.getBytes(StandardCharsets.UTF_8), md5);
SecretKeySpec key = new SecretKeySpec(keyAndIV[0], "AES");
IvParameterSpec iv = new IvParameterSpec(keyAndIV[1]);
byte[] encrypted = Arrays.copyOfRange(cipherData, 16, cipherData.length);
Cipher aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding");
aesCBC.init(Cipher.DECRYPT_MODE, key, iv);
byte[] decryptedData = aesCBC.doFinal(encrypted);
String decryptedText = new String(decryptedData, StandardCharsets.UTF_8);
System.out.println(decryptedText);
The result is:
The quick brown fox jumps over the lazy dog. 👻 👻
That's the text we started with. And emojis, accents and umlauts work as well.
GenerateKeyAndIV is a helper function that reimplements OpenSSL's key derivation function EVP_BytesToKey (see https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c).
/**
* Generates a key and an initialization vector (IV) with the given salt and password.
* <p>
* This method is equivalent to OpenSSL's EVP_BytesToKey function
* (see https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c).
* By default, OpenSSL uses a single iteration, MD5 as the algorithm and UTF-8 encoded password data.
* </p>
* #param keyLength the length of the generated key (in bytes)
* #param ivLength the length of the generated IV (in bytes)
* #param iterations the number of digestion rounds
* #param salt the salt data (8 bytes of data or <code>null</code>)
* #param password the password data (optional)
* #param md the message digest algorithm to use
* #return an two-element array with the generated key and IV
*/
public static byte[][] GenerateKeyAndIV(int keyLength, int ivLength, int iterations, byte[] salt, byte[] password, MessageDigest md) {
int digestLength = md.getDigestLength();
int requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength;
byte[] generatedData = new byte[requiredLength];
int generatedLength = 0;
try {
md.reset();
// Repeat process until sufficient data has been generated
while (generatedLength < keyLength + ivLength) {
// Digest data (last digest if available, password data, salt if available)
if (generatedLength > 0)
md.update(generatedData, generatedLength - digestLength, digestLength);
md.update(password);
if (salt != null)
md.update(salt, 0, 8);
md.digest(generatedData, generatedLength, digestLength);
// additional rounds
for (int i = 1; i < iterations; i++) {
md.update(generatedData, generatedLength, digestLength);
md.digest(generatedData, generatedLength, digestLength);
}
generatedLength += digestLength;
}
// Copy key and IV into separate byte arrays
byte[][] result = new byte[2][];
result[0] = Arrays.copyOfRange(generatedData, 0, keyLength);
if (ivLength > 0)
result[1] = Arrays.copyOfRange(generatedData, keyLength, keyLength + ivLength);
return result;
} catch (DigestException e) {
throw new RuntimeException(e);
} finally {
// Clean out temporary data
Arrays.fill(generatedData, (byte)0);
}
}
Note that you have to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy. Otherwise, AES with key size of 256 won't work and throw an exception:
java.security.InvalidKeyException: Illegal key size
Update
I have replaced Ola Bini's Java code of EVP_BytesToKey, which I used in the first version of my answer, with a more idiomatic and easier to understand Java code (see above).
Also see How to decrypt file in Java encrypted with openssl command using AES?.
When encrypting on one system and decrypting on another you are at the mercy of system defaults. If any system defaults do not match (and they often don't) then your decryption will fail.
Everything has to be byte for byte the same on both sides. Effectively that means specifying everything on both sides rather than relying on defaults. You can only use defaults if you are using the same system at both ends. Even then, it is better to specify exactly.
Key, IV, encryption mode, padding and string to bytes conversion all need to be the same at both ends. It is especially worth checking that the key bytes are the same. If you are using a Key Derivation Function (KDF) to generate your key, then all the parameters for that need to be the same, and hence specified exactly.
Your "Invalid AES key length" may well indicate a problem with generating your key. You use getBytes(). That is probably an error. You need to specify what sort of bytes you are getting: ANSI, UTF-8, EBCDIC, whatever. The default assumption for the string to byte conversion is the likely cause of this problem. Specify the conversion to be used explicitly at both ends. That way you can be sure that they match.
Crypto is designed to fail if the parameters do not match exactly for encryption and decryption. For example, even a one bit difference in the key will cause it to fail.