I'm using Node's crypto native api to encode an id because it's going to be shown in an URL parameter, the problem is, it encodes in a way it can be guessed and I can't comprehend why, here's my code.
const encrypt = (key, value) => {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-ctr', Buffer.from(key), iv);
let encrypted = cipher.update(value);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return `${iv.toString('hex')}-${encrypted.toString('hex')}`;
}
what happens is that the AES encoded id outputs in a predictable way, for example, a number 5 outputs the code 160f20bea36be22f90b092f876f1abdd-55 and note that if I change the last two numbers to 56, the output became 6, or to 54, the id's discovered because it outputs 7.
What I want it to do is to give me a secure encoded id.
Your problem with predictability of output comes from the fact that you are using CTR mode. Let's see how CTR mode works:
CTR is a streaming mode. As you see, CTR mode works based on encrypting a counter which starts from 1 and XORing result with your plain data. Nonce in this picture is IV. This mechanism has a drawback as you mentioned. If we assume that C(i) is encrypted counter i, we have:
Cipher(i) = Plain(i) xor C(i)
Since C(i) does not depend on input, if you change Plain(i), Cipher(i) will be changed based on your Plain(i) change:
Cipher(i) = Plain(i) xor C(i) => C(i) = Cipher(i) xor Plain(i)
then for block i:
Cipher(x) = Plain(x) xor C(i) => Cipher(x) = Plain(x) xor Cipher(i) xor Plain(i)
XOR is add without carry. That's why you feel that your result is predictable. But this only happens IF your IV is fixed and as a result, shows importance of IV in CTR mode.
I should say that your code in general does not have any main issue since you use:
const iv = crypto.randomBytes(16);
to generate a new IV each time for encrypting and add that IV to your encoded result. Since you change IV every time, C(i) will be different for each encrypted data and no one can guess the new encrypted data. As an example:
input data encrypted output
---------------------------------------------------
5 160f20bea36be22f90b092f876f1abdd-16
6 160f20bea36be22f90b092f876f1abdd-15 // Feels predictable since IV is similar
6 160f20bea36be22f90b092f876f1abde-ee // Sample input, but completely deferent
// output since I changed 1 bit in IV
So in general, your code does not have any major problems cryptography-wise. But since you need to keep IV in your result for decryption, your encrypted data result will be large.
Related
Consider the code:
const CryptoJS = require("crypto-js");
var key = CryptoJS.enc.Hex.parse("000102030405060708090a0b0c0d0e0f");
var iv = CryptoJS.enc.Hex.parse("101112131415161718191a1b1c1d1e1f");
// encrypt
var aesEncryptor = CryptoJS.algo.AES.createEncryptor(key, { iv: iv });
var ciphertextPart1 = aesEncryptor.process("Message Part 1");
var ciphertextPart2 = aesEncryptor.process("Message Part 2");
var ciphertextPart3 = aesEncryptor.process("Message Part 3");
var ciphertextPart4 = aesEncryptor.finalize();
// decrypt
var aesDecryptor = CryptoJS.algo.AES.createDecryptor(key, { iv: iv });
var plaintextPart1 = aesDecryptor.process(ciphertextPart1);
var plaintextPart2 = aesDecryptor.process(ciphertextPart2);
var plaintextPart3 = aesDecryptor.process(ciphertextPart3);
var plaintextPart4 = aesDecryptor.process(ciphertextPart4);
var plaintextPart5 = aesDecryptor.finalize();
console.log(plaintextPart5.toString());
Source: https://cryptojs.gitbook.io/docs/#ciphers
Maybe I am wrong, but I expected the messages to be decrypted.
The output is actually:
61676520506172742033
I have no idea what that output means and where that comes from.
If I print out another part, the same issue:
console.log(plaintextPart4.toString());
Output:
7373616765205061727420324d657373
Discussion
One comment says that I am wrong on my assumption regarding how the method works, a nice answer could nicely correct me as well! I know the difference, from my studies, between one-way encryption and by parts: it makes no sense to me the way it is actually working, it seems a bug. My ideia is that you could keep adding message to be encrypted, as we can do with hashing, in the case of hashing (from the same abovementioned source), it seems to work, why not for encryption as well?
I honestly expected this example to work since it is from the official documentation, and they do not mention anything.
Comprehensive documentation is a rare beast.
it makes no sense to me the way it is actually working, it seems a bug
It works like this:
const CryptoJS = require("crypto-js");
const original_message = " ...0, ...1, ...2, ...3, ...4, ...5, ...6, ...7, ...8, ...9, ...The End!"
var key = CryptoJS.enc.Hex.parse("000102030405060708090a0b0c0d0e0f");
var iv = CryptoJS.enc.Hex.parse("101112131415161718191a1b1c1d1e1f");
const aesEncryptor = CryptoJS.algo.AES.createEncryptor(key, { iv: iv });
const aesDecryptor = CryptoJS.algo.AES.createDecryptor(key, { iv: iv });
const fragment_size = 25
const e_acc = []
for(let i = 0 ; i*fragment_size < original_message.length ; ++i ) {
const slice = original_message.slice(i*fragment_size, i*fragment_size + fragment_size)
console.log("slice to encrypt", slice)
e_acc.push(aesEncryptor.process(slice));
}
e_acc.push(aesEncryptor.finalize());
let message = ""
for(let i = 0 ; i !== e_acc.length ; ++i ) {
message += aesDecryptor.process(e_acc[i]).toString(CryptoJS.enc.Utf8)
console.log("intermidiate message", message)
}
message += aesDecryptor.finalize().toString(CryptoJS.enc.Utf8)
console.log("full message", message)
[...] it supposes to be a trick that you can add information by parts
[...]
My idea is that you could keep adding message to be encrypted
And you can, the only thing is - you can't decipher parts of messages that do not fill a block. You can only get as much as fits into a chain of complete blocks.
Consider the code
const sum1 = solver.process("1+22+3")
const sum2 = solver.process("03")
const sum3 = solver.finalize()
Here you can get the intermediate sum1 - sum of 1 and 22. But you can't add 3 right away, because, as you can see later on, it is not actually 3 but 303. Same thing with sum2 - solver do not know yet if the 303 should be added or maybe it would become 3031234.... So the only thing the solver can do is to remember the 3 and then the 303, but not use it. Until you call finalize.
aesEncryptor is like the solver but much more complicated.
Let's get back to the aesDecryptor and why all of plaintextPart* contain only the parts of the message... Why do you need "Progressive Ciphering" in the first place? Probably because you are sending fragments over the net? The key word is fragments - they purposely do not contain all the previous parts of the message as they could weight gigabytes!
in the case of hashing [...], it seems to work, why not for encryption as well?
Hashing is a quite different thing. You can not expect that AES has all the same properties as hashing, as you can not expect that hashing has all the same properties as, for example, summation.
The .process method returns any new chunks of ciphertext that were generated by processing the plaintext given as the argument to .process along with any plaintext that was left over from earlier encryptor activity.
In this case the cipher algorithm is AES. Specifically it's AES-128, because the key used to create the encryptor is 128 bits long. AES-128 consumes plaintext in 128-bit (16-byte) blocks and emits 128 bits (16 bytes) of ciphertext for each plaintext block. This block-based processing is what produces the results that you didn't understand.
What happens with your program is:
var ciphertextPart1 = aesEncryptor.process("Message Part 1");
The encryptor is given 14 bytes of input. That's not enough to allow it to generate a ciphertext block, so .process returns an empty ciphertext result which you store in ciphertextPart1. The encryptor stores the unprocessed 14 bytes internally.
var ciphertextPart2 = aesEncryptor.process("Message Part 2");
This gives the encryptor a further 14 bytes. It appends those to the 14 bytes left over from the previous call, so it now has a total of 28 unprocessed bytes. It processes as many of those bytes as it can. That is, it processes the first 16 of those bytes ("Message Part1Me") and returns the ciphertext block for those 16 bytes, which you store in cipherText2. The encryptor now contains 12 unprocessed bytes.
var ciphertextPart3 = aesEncryptor.process("Message Part 3");
This gives the encryptor another 14 bytes. It now has 26 unprocessed bytes. It processes the first 16 of those bytes ("ssage Part2Mess") and returns the ciphertext block for those 16 bytes, which you store in cipherText3. The encryptor now contains 10 unprocessed bytes.
var ciphertextPart4 = aesEncryptor.finalize();
This forces the encryptor to process any unprocessed bytes. It can only work on 16-byte blocks, so it adds 6 bytes of padding to the remaining 10 unprocessed plaintext bytes ("age Part 3"), encrypts that block and returns the ciphertext for that block. You store that ciphertext block as ciphertextPart4.
And now you decrypt the ciphertext blocks.
var plaintextPart1 = aesDecryptor.process(ciphertextPart1);
cipherTextPart1 was an empty block, so plaintextPart1 will be empty and obviously the decryptor will retain no unprocessed ciphertext.
var plaintextPart2 = aesDecryptor.process(ciphertextPart2);
cipherTextPart2 contained the encrypted version of the first 16 bytes of the plaintext, so plaintextPart2 will contain "Message Part 1Me". The ciphertext input was exactly 16 bytes long, therefore the decryptor contains no unprocessed ciphertext.
var plaintextPart3 = aesDecryptor.process(ciphertextPart3);
cipherTextPart3 contained the encrypted version of the next 16 bytes of the plaintext, so plaintextPart3 will contain "ssage Part 2Mess". Again the decryptor is holding no unprocessed ciphertext.
var plaintextPart4 = aesDecryptor.process(ciphertextPart4);
cipherTextPart4 contained the encrypted version of the final 10 bytes of the plaintext, so plaintextPart3 will contain "age Part 3". No unprocessed ciphertext remains in the decryptor.
var plaintextPart5 = aesDecryptor.finalize();
The is no unprocessed ciphertext remaining in the decryptor, so finalize has no work to do and plaintextPart5 will be empty.
I have some difficulties reproducing AES encryption and decryption in python.
Context: A year ago, I created a small django based application using this javascript library for client side encryption. Basically, some users' inputs are encrypted with a key and sent as hexadecimal strings to be stored.
For the illustration, I'll focus on bd45bcccd0 (a.k.a 'Masha' encrypted with john's key: 3ed8bd71327aafd855aac37921519767)
Encryption and decryption with the current js library
encryption utf-8 -> bytes -> encrypted bytes -> hex
decryption hex -> encrypted bytes -> bytes -> utf-8
id_password is a MD5 hash of the user's password. It is stored in the session storage and is used as a key
function encrypt(t){
var key = aesjs.utils.hex.toBytes(sessionStorage.getItem("id_password"));
var textBytes = aesjs.utils.utf8.toBytes(t);
var aesCtr = new aesjs.ModeOfOperation.ctr(key);
var encryptedBytes = aesCtr.encrypt(textBytes);
var encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes);
return encryptedHex;
}
function decrypt(t){
var key = aesjs.utils.hex.toBytes(sessionStorage.getItem("id_password"));
var textBytes = aesjs.utils.hex.toBytes(t);
var aesCtr = new aesjs.ModeOfOperation.ctr(key);
var decriptedBytes = aesCtr.decrypt(textBytes);
var decrypted_utf8 = aesjs.utils.utf8.fromBytes(decriptedBytes);
return decrypted_utf8;
}
Once loaded in key, I get a 16 items array (So I guess a AES 128bits CTR is performed):
var key = aesjs.utils.hex.toBytes(sessionStorage.getItem("id_password"));
console.log(key)
Array(16) [ 62, 216, 189, 113, 50, 122, 175, 216, 85, 170, … ]
With the current code, encryption and decryption work
Python implementation
For unit-testing purposes, I wanted to be able to decrypt. I am using this library. To mimic the client side as much as possible, I tried the following:
john_key = "3ed8bd71327aafd855aac37921519767"
cipher = AES.new(codecs.decode(john_key,'hex_codec'), AES.MODE_CTR)
d = cipher.decrypt(codecs.decode('bd45bcccd0', 'hex_codec'))
d.decode('utf-8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xac in position 0: invalid start byte
Here is the problem but I am not sure at which stage it occurs. Here is what I checked:
# key's length is correct
k = codecs.decode(john_key,'hex_codec')
k
b'>\xd8\xbdq2z\xaf\xd8U\xaa\xc3y!Q\x97g'
len(k)
16
# decoded message's length is correct
d = cipher.decrypt(codecs.decode('bd45bcccd0', 'hex_codec'))
len(d)
5
Since I can't rely on a library I can't reproduce the results, I wonder whether I misused PyCryptodome or whether the way this javascript library implements AES CTR encryption is reliable. Any insights?
The CTR-mode requires an IV. Since you do not explicitly create the IV, an implicitly created IV is used. However, both codes generate different IVs, so that the decryption fails. In the Python-code, a random IV is generated, in the aes-js-code a fixed IV (1) is used.
So that the decryption is possible with the Python-code, the same IV must be used here as in the aes-js-code (here and here). For this purpose:
cipher = AES.new(codecs.decode(john_key,'hex_codec'), AES.MODE_CTR)
has to be replaced by
counter = Counter.new(128, initial_value = 1)
cipher = AES.new(codecs.decode(john_key,'hex_codec'), AES.MODE_CTR, counter = counter)
which decrypts the ciphertext to Maria (however not Masha).
For security reasons it is mandatory for CTR that key/IV pairs may only be used once, i.e. if the same key is applied, a new IV must be generated for each encryption. The current code has the weakness that key/IV pairs would be repeated when using the same key. A better way would be to generate a random IV for each encryption, send this IV together with the ciphertext to the recipient (the IV isn't secret, so it is usually prepended to the ciphertext), where it can be used for the decryption.
Im trying to convert the java library - AESCrypt-Java
to javascript.
This is my implementation so far for the decrypt function. Im not able to decrypt the text. Can someone figure out where I'm going wrong?
function decrypt(password, base64text) {
key = generateKey(password);
var decodedCipherText = new Buffer(base64text, 'base64')
var iv = new Buffer(16);
iv.fill(0);
var decipher = crypto.createDecipheriv("aes-256-cbc", key, iv)
let decrypted = decipher.update(decodedCipherText, 'base64', 'utf-8');
decrypted += decipher.final('utf-8')
return decryptedBytes
}
function generateKey(password) {
return crypto.createHash('sha256').update(usr_id).digest();
}
var encryptedText = '1+2yFMDH1C/uIc1huwezbrsQ==';
var password = '8AVrWtyabQ';
decrypt(password, encryptedText)
The expected plaintext output is Wordpress.
You are making a few decisions that will adversely affect the security of your sensitive values:
You are using a static, all-zero IV. The IV must be unique and non-predictable for every message encrypted with a specific key. The IV can then be prepended to the cipher text and transmitted unprotected to the recipient, where it is sliced and used for decryption.
Your key derivation function (KDF) is weak -- SHA-256 can be cracked at 23 billion attempts per second on commodity hardware. Use a key-stretching algorithm like PBKDF2 with a high iteration count, or bcrypt or scrypt for memory hardness.
Your cipher text is not authenticated -- AES/CBC provides confidentiality, but not integrity or authentication. An interceptor can manipulate the cipher text in transmission and attempt to decrypt it. This can result in unauthorized decryption (i.e. injecting malicious plaintext into your application) or a padding oracle attack, and eventually cipher text recovery. Use an authenticated encryption (with associated data) (AE or AEAD) cipher mode to mitigate this, or add a strong HMAC construction using a separate key over the cipher text and verify prior to decryption with a constant-time equals method.
new Buffer(string, encoding) and new Buffer(size) are deprecated and Buffer.from(string, encoding) and Buffer.alloc(size) should be used instead. You create a Buffer containing the provided cipher text which is encoded in Base64. I have a feeling there is an issue occurring with your encoding (you don't provide any example output for us to see). Here is an example of encrypting and decrypting with Buffer objects.
function encrypt(buffer){
var cipher = crypto.createCipher(algorithm,password)
var crypted = Buffer.concat([cipher.update(buffer),cipher.final()]);
return crypted;
}
function decrypt(buffer){
var decipher = crypto.createDecipher(algorithm,password)
var dec = Buffer.concat([decipher.update(buffer) , decipher.final()]);
return dec;
}
var hw = encrypt(new Buffer("hello world", "utf8"))
// outputs hello world
console.log(decrypt(hw).toString('utf8'));
As you can see, cipher.update(buffer) handles the encoding internally so you don't need to.
I'm quite new to Node and have run into an issue with the encryption object:
var des3_key = new Buffer("redacted", "base64"); // copied from key in chilk
var des3_iv = new Buffer("alsoredacted", "base64"); // copied from iv in chilk
var des3_encryption = crypto.createCipheriv("des3", des3_key, des3_iv);
// encode a string
var string_to_encode = "thisisatest";
var ciphered_string = des3_encryption.update(string_to_encode, "utf8", "base64");
console.log(string_to_encode+" "+ciphered_string);
Both in the Node console and when running on the server, line 6 causes an error node-crypto: Invalid IV length 32 instead of returning an encryption object, as expected.
The key and IV I've removed and their encryption types are copied from another file but for the sake of testing I have tried various strings and encryption types but still get the same error, though with different lengths in the error.
My knowledge of encryption is limited to what I've used previously and not much else unfortunately, and I'm having trouble finding troubleshooting resources for Node in this regard. Any help would be appreciated.
Edit: Experimenting with des and des3 yields the same result.
From OP's edit:
SOLVED:
Working code:
var string_to_decode = "encrypted string";
var des_key = new Buffer("key string", "base64");
var des_iv = new Buffer(0);
var des_decryption = Crypto.createDecipheriv("DES-EDE3", des_key, des_iv);
var deciphered_string = des_decryption.update(string_to_decode, "base64", "utf8");
console.log("["+string_to_decode+"] => ["+deciphered_string+"]");
I found this by making a script to guess combinations of key and IV lengths, and encryption types and methods, and encoding types until it resulted in the correct string. It was a last resort but it worked.
I want to create two functions encrypt(message, key) and decrypt(ciphertext, key) using the Forge library in javascript, but I dont undestand the example code.
// generate a random key and IV
var key = forge.random.getBytesSync(16);
var iv = forge.random.getBytesSync(16);
// encrypt some bytes using CBC mode
// (other modes include: CFB, OFB, and CTR)
var cipher = forge.aes.createEncryptionCipher(key, 'CBC');
cipher.start(iv);
cipher.update(forge.util.createBuffer(someBytes));
cipher.finish();
var encrypted = cipher.output;
// outputs encrypted hex
console.log(encrypted.toHex());
// decrypt some bytes using CBC mode
// (other modes include: CFB, OFB, and CTR)
var cipher = forge.aes.createDecryptionCipher(key, 'CBC');
cipher.start(iv);
cipher.update(encrypted);
cipher.finish();
// outputs decrypted hex
console.log(cipher.output.toHex());
// generate a password-based 16-byte key
var salt = forge.random.getBytesSync(128);
var derivedKey = forge.pkcs5.pbkdf2('password', salt, numIterations, 16);
Where should I use my own key?
Where can I choose 256 bit mode?
Can you give me an easier example?
Where should I use my own key?
I haven't used that library but it seems pretty straight forward. Take this part at the top:
// generate a random key and IV
var key = forge.random.getBytesSync(16);
And put your key in like this:
// generate a random key and IV
var key = neverGuessMahKeyIs1234;
Do the same for the iv if you want.
Where can I choose 256 bit mode?
Ok, so first of all your dealing with symmetric encryption which has a key length of the desired size. Because it's symmetric, it's used on both the encrypting and decrypting ends, which is what the code that you posted seems to do. I say 'seems' because I'm trusting that the library's native functions are as you posted them.
So, the code as you posted seems to use (as I showed above) 128 bits (16*8=128). If you want a random 256, then just use:
var key = forge.random.getBytesSync(32);
Or just make your own key that 256 bits long.