Why can I decrypt text encrypted with key using keyFake?
Source code (using bcrypt
and aes-js
):
const bcrypt = require('bcrypt');
const aesjs = require('aes-js');
(async () => {
let myPlaintextPassword = "pass";
let myPlaintextPasswordFake = "sdfs6654df";
let saltRounds = 10;
let hash = await bcrypt.hash(myPlaintextPassword, saltRounds);
let key = Buffer.from({ arrayBuffer: hash, length: 32 });
let hashFake = await bcrypt.hash(myPlaintextPasswordFake, saltRounds);
let keyFake = Buffer.from({ arrayBuffer: hashFake, length: 32 });
// Convert text to bytes
var text = "ЧЕРТ ВОЗЬМИ, КАК ЖЕ ЭТО СЕКРЕТНО!";
console.log(text);
var textBytes = aesjs.utils.utf8.toBytes(text);
// The counter is optional, and if omitted will begin at 1
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
var encryptedBytes = aesCtr.encrypt(textBytes);
// To print or store the binary data, you may convert it to hex
var encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes);
console.log("encrypted " + encryptedHex);
// "a338eda3874ed884b6199150d36f49988c90f5c47fe7792b0cf8c7f77eeffd87
// ea145b73e82aefcf2076f881c88879e4e25b1d7b24ba2788"
// When ready to decrypt the hex string, convert it back to bytes
var encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex);
// The counter mode of operation maintains internal state, so to
// decrypt a new instance must be instantiated.
var aesCtr = new aesjs.ModeOfOperation.ctr(keyFake, new aesjs.Counter(5));
var decryptedBytes = aesCtr.decrypt(encryptedBytes);
// Convert our bytes back into text
var decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes);
console.log("decrypted " + decryptedText);
})();
And here is result:
Can anyone explain why code behave so? Shouldn't I see nonsense if I use another key?
There are several issues here.
First: FRIENDS DON'T LET FRIENDS ROLL THEIR OWN CRYPTO (at least not if you want to end up with something secure). Use the high-level primitives from some off-the-shelf encryption library instead.
Then, the other stuff:
Buffer.from()
wrong and you end up with the key <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
. Changing that invocation to Buffer.from(hash).slice(0, 32)
gives you a key kind-of derived from the bcrypt.hash()
result, but:bcrypt.hash()
, it never returns the same string for the same input string, making it impossible to decrypt data even if you know the correct passphrase. In addition, the function returns a string like $2b$10$z3X6QVxZtl4JmrkH2u7rV.bVq0vFUY9XSrTKVnoyZ7s8X4cybmox6
, and as things stand (even with the correct usage of Buffer.from()
), you'd end up using the first 32 characters only, which is probably not what you want.Buffer.from()
invocation, I get (e.g. – this is always random –) decrypted 嶥,벗Jꢣ틣FMnZhH줰]}H㥋z⮕gL⎕
as the output, and without knowing the original plaintext, I can't know whether it's the correct decryption result.Here's the refactored code I used.
Changing saltRounds
to a salt string that's derived with const salt = await bcrypt.genSalt(10);
or similar in the main function makes the decryption reversible, but the code still won't be secure.
"use strict";
const bcrypt = require("bcrypt");
const aesjs = require("aes-js");
async function deriveKey(password, saltRounds) {
const hash = await bcrypt.hash(password, saltRounds);
console.log("Hash:", hash);
return Buffer.from(hash).slice(0, 32);
}
async function getEncryptionObject(password, saltRounds, counter) {
const key = await deriveKey(password, saltRounds);
console.log("Key:", key);
return new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(counter));
}
async function encrypt(text, password, saltRounds, counter = 5) {
const aesCtr = await getEncryptionObject(password, saltRounds, counter);
const textBytes = aesjs.utils.utf8.toBytes(text);
const encryptedBytes = aesCtr.encrypt(textBytes);
return aesjs.utils.hex.fromBytes(encryptedBytes);
}
async function decrypt(encryptedHex, password, saltRounds, counter = 5) {
const aesCtr = await getEncryptionObject(password, saltRounds, counter);
const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex);
const decryptedBytes = aesCtr.decrypt(encryptedBytes);
return aesjs.utils.utf8.fromBytes(decryptedBytes);
}
(async () => {
const encryptionPassword = "pass";
const decryptionPassword = "sdfs6654df";
const saltRounds = 10;
const text = "ЧЕРТ ВОЗЬМИ, КАК ЖЕ ЭТО СЕКРЕТНО!";
console.log("original: " + text);
const encryptedHex = await encrypt(text, encryptionPassword, saltRounds, 5);
console.log("encrypted " + encryptedHex);
const decryptedText = await decrypt(
encryptedHex,
decryptionPassword,
saltRounds,
5,
);
console.log("decrypted " + decryptedText);
})();