I stumbled upon the problem that the IV value that you pass to CryptoJS.AES.encrypt function seems to be ignored. Following code:
import CryptoJS from "crypto-js";
const text = "This is secret text!";
const key = "abc";
const iv = CryptoJS.lib.WordArray.random(2);
const encryptedText = CryptoJS.AES.encrypt(text, key, { iv }).toString();
console.log(encryptedText);
const decryptedText = CryptoJS.AES.decrypt(encryptedText, key, { iv: 'whatever' }).toString(CryptoJS.enc.Utf8);
console.log(decryptedText);
Produces following output:
U2FsdGVkX1//3KxGM2FnVV4qR5KmWBxP/xyI1+YITvdQeXqrUBH7spIvi/Ny+7S2
This is secret text!
So it seems the function is able to decrypt the text no matter what IV value you are passing to the function.
What am I doing wrong here?
You can specify any IV in your code when encrypting and decrypting, because the IV is simply not used. Why is that?
Depending on the type of the key material, CryptoJS uses a key (if a WordArray is passed) or a password (if a string is passed). You pass a string, so the key material is interpreted as password (which is why key should better be called password). In this case, CryptoJS uses a key derivation function (EVP_BytesToKey()) to derive the key and the IV. The explicitly passed IV is completely ignored. This applies to both encryption and decryption. Therefore, if a password is applied, no IV should be used (and the one in the code should be removed).
The derived key and IV can be picked from the CipherParams object returned by CryptoJS.AES.encrypt() (s. here): The key can be read from the property key, the IV from the property iv and the ciphertext from the property ciphertext, s. here.
When you pass a key instead of a password, you must also pass an IV (otherwise an error message is displayed). If you specify a wrong IV during decryption, this will result in a corrupted first block for CBC (the default mode). The details are described in the other answer.
If you want to check this, you should use a Latin1 decoding (or hex encoding) instead of a UTF-8 decoding for the decrypted data, because the corrupted data is generally not Utf-8 compatible (and CryptoJS will display an error message).
Note that short plaintexts with a ciphertext of only one block in size will generally corrupt the padding, which in turn corrupts the data during unpadding, making the data non-displayable. However, it is possible to display the padded data by disabling the default PKCS#7 padding: padding: CryptoJS.pad.NoPadding).
The following code shows encryption/decryption with a password (1), with a key and IV (2) and the corruption of the first block for CBC when decrypting with a wrong IV (3):
var text = "The quick brown fox jumps over the lazy dog";
// (1) Encryption/Decryption using a password (key and IV implicitly derived via EVP_BytesToKey)
var password = "abc"; // This is a password, not a key!
var iv = CryptoJS.lib.WordArray.random(16);
var encryptedOpenSSLB64 = CryptoJS.AES.encrypt(text, password, {iv: iv}).toString(); // iv ignored (since derived via key derivation) and should be removed!
var decrypted = CryptoJS.AES.decrypt(encryptedOpenSSLB64, password, {iv: iv}); // iv ignored (since derived via key derivation) and should be removed!
console.log(encryptedOpenSSLB64);
console.log(decrypted.toString(CryptoJS.enc.Utf8));
// (2) Encryption/Decryption using a key and an IV
var key = CryptoJS.lib.WordArray.random(32);
var iv = CryptoJS.lib.WordArray.random(16);
var ciphertextB64 = CryptoJS.AES.encrypt(text, key, {iv : iv}).toString(); // iv required!
var decrypted = CryptoJS.AES.decrypt(ciphertextB64, key, {iv : iv}); // iv required!
console.log(ciphertextB64);
console.log(decrypted.toString(CryptoJS.enc.Utf8));
// (3) Corruption of the first block for CBC, if a wrong IV is applied during decryption
var wrongIv = CryptoJS.lib.WordArray.random(16);
var decrypted = CryptoJS.AES.decrypt(ciphertextB64, key, {iv : wrongIv}); // wrong IV corrupts first block for CBC
console.log(decrypted.toString(CryptoJS.enc.Latin1));
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
For completeness: In your code the CipherParams object is converted to the Base64 encoded OpenSSL format with toString(). The OpenSSL format consists of the ASCII encoding of Salted__ followed by an 8 bytes salt and the actual ciphertext. Because of the constant prefix Salted__ the Base64 encoded data always starts with U2FsdGVkX1 (like your ciphertext).
The random salt is generated during encryption. Based on salt and password, the key derivation function is used to derive key and IV. In order to be able to reconstruct key and IV during decryption, the salt is required. The salt is not secret, so it is usually concatenated with the ciphertext (as in the OpenSSL format described above). The salt is also contained in the salt property of the CipherParams object.
If no password but a key is applied, there is no salt and toString() returns only the Base64 encoding of the ciphertext property. This is also illustrated in the above code snippet.
CryptoJS.AES.decrypt() requires a CipherParams object (s. here). Alternatively (as in your code) the Base64 encoding created with toString() can be passed. This is implicitly converted into a CipherParams object.
CryptoJS is using AES-CBC. CBC is not an authenticated mode of operation, so it doesn't offer message integrity / authenticity. CBC only offers confidentiality, and only that while it is used correctly and doesn't allow for, for instance, padding oracle attacks.
Actually, the IV in CBC mode only changes the first block of plaintext during decryption, and only those bytes where the IV differs. So all the rest of the blocks simply decrypt as you'd expect. This is because the IV is only the initialization vector. The other "vectors" that are XOR'ed with the plaintext before AES block encryption are the previous blocks of ciphertext. Probably best to have a look at the Wikipedia page explaining the modes of operation.
Beware that the line:
const encryptedText = CryptoJS.AES.encrypt(text, key, { iv }).toString();
is not correct: iv should be a named field:
const encryptedText = CryptoJS.AES.encrypt(text, key, { iv: iv }).toString();
It's not entirely clear if this is deliberate or not from the rest of the context in the question.
Related
Trying to encrypt a string using CryptoJS and an actual key (not passphrase).
Documentation says:
For the key, when you pass a string, it's treated as a passphrase and
used to derive an actual key and IV. Or you can pass a WordArray that
represents the actual key. If you pass the actual key, you must also
pass the actual IV.
But I could not find any examples on how to pass a WordArray and IV.
I need it to be a key because it will be pre-shared. I'm trying to validate by decrypting using https://www.devglan.com/online-tools/aes-encryption-decryption
In JavaScript side I use:
CryptoJS.DES.encrypt('Content', 'password').toString()
The result:
U2FsdGVkX1/25rW2q0X7/pOtExFyP7MD
In Java side I try to decrypt it:
public static void main(String[] args) throws Exception {
String password = "password";
String encryptedString = "U2FsdGVkX1/25rW2q0X7/pOtExFyP7MD";
DESKeySpec key = new DESKeySpec(password.getBytes());
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
SecureRandom secureRandom = new SecureRandom();
byte[] ivspec = new byte[cipher.getBlockSize()];
secureRandom.nextBytes(ivspec);
IvParameterSpec iv = new IvParameterSpec(ivspec);
cipher.init(Cipher.DECRYPT_MODE, keyFactory.generateSecret(key), iv);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedString.getBytes()));
System.out.println(new String(Base64.getEncoder().encode(decryptedBytes)));
}
But I'm getting the bad padding error:
Exception in thread "main" javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.
Can anyone tell me what went wrong and what is the proper way to decrypt it? Assuming that the JavaScript side code cannot be changed (i.e the way to encrypt the string using DES). Thank you very much.
The IV must be the same for both encryption and decryption. in the example a new random IV is being created for decryption: secureRandom.nextBytes(ivspec);.
You need to carefully and fully review the CryptoJS documentation to determine how the IV is being handled. Often the IV is prepended to the encrypted data for use during decryption.
The encryptedString seems to be Base64 encoded and the decoded length is 32-bytes, just right for a 16-byte IV and 16-byte encrypted data+padding.
I want decode an encrypted string using cryptoJS. I get how to decode the encrypted object but couldnt understand how to decrypt the string.
Heres what I tried:
var enc = CryptoJS.AES.encrypt('hellloooo', 'secretpassphrase');
console.log('encrypted', enc.salt.toString());
console.log('decrypted', CryptoJS.AES.decrypt(CryptoJS.enc.salt.parse(enc.salt.toString()), 'secretpassphrase').toString(CryptoJS.enc.Utf8));
The salt is some random value that is randomly generated during encryption in order to derive the actual key and IV from the given password. It doesn't hold the secret, so trying to decrypt it won't give you anything useful.
Here are two ways to decrypt the ciphertext
CryptoJS.AES.decrypt(enc, 'secretpassphrase').toString(CryptoJS.enc.Utf8);
CryptoJS.AES.decrypt(enc.toString(), 'secretpassphrase').toString(CryptoJS.enc.Utf8);
The salt is still present in the enc object, so the decrypt() function can use it to recreate the key and IV to decrypt the string.
I have a code snippet on javascript that I'm trying to convert to python
var cipherAlgorithm = 'aes256';
var decipher = crypto.createDecipher(cipherAlgorithm, cipher);
password = decipher.update(encryptedpass, 'hex', 'utf8')+ decipher.final('utf8');
I'm trying to rewrite that in python using pythoncrypto and I'm just getting the wrong values no matter what I do
cyphertext=faaafaaa"
cipher=AES.new(key, AES.MODE_CBC, "\0"*16)
cipher.decrypt(cypertext)
and it returns the wrong value. (didn't decrypt correctly)
in the javascript code (I'm not a js expert ) I noticed that the output encoding is utf8
so I tries something like this
unicode(cipher.decrypt(cyphertext), "utf-8")
but I'm getting an error
'utf8' codec can't decode byte 0x81 in position 6: invalid start byte
what's the solution?
I am using NodeJS to interact with Amazon Web Services (specifically s3). I am attempting to use Server side encryption with customer keys. Only AES256 is allowed as an encryption method. The API specifies that the keys be base64 encoded.
At the moment I am merely testing the AWS api, I am using throwaway test files, so security (and secure key generation) are not an issue at the moment.
My problem is as follows: Given that I am in posession of a 256bit hexadecimal string, how do I obtain a base64 encoded string of the integer that that represents?
My first instinct was to first parse the Hexadecimal string as an integer, and convert it to a string using toString(radix) specifying a radix of 64. However toString() accepts a maximum radix of 36. Is there another way of doing this?
And even if I do this, is that a base64 encoded string of 256bit encryption key? The API reference just says that it expects a key that is "appropriate for use with the algorithm specified". (I am using the putObject method).
To convert a hex string to a base64 string in node.js, you can very easily use a Buffer;
var key_in_hex = '11223344556677881122334455667788'
var buf = new Buffer(key_in_hex, 'hex')
var str = buf.toString('base64')
...which will set str to the base64 equivalent of the hex string passed in ('112233...')
You could also of course combine it to a one liner;
var key_in_hex = '11223344556677881122334455667788'
var str = new Buffer(key_in_hex, 'hex').toString('base64')