deterministic encryption fails
measwel opened this issue · 4 comments
Good day,
I am trying to encrypt emails deterministically before saving them to my database. However, the resulting cipher-text is different every time.
function encryptCustomerEmail(email) {
let encryptedEmail = CryptoJS.AES.encrypt(email, process.env.SERVER_ENCRYPTION_KEY, {mode: CryptoJS.mode.ECB}).toString();
return encryptedEmail;
}
SERVER_ENCRYPTION_KEY is just a string here.
Can somebody please explain to me, how I should properly encrypt emails deterministically and also build a reverse function to decrypt them using the same encryption key?
Thank you.
Update. I currently have this implementation, which seems to produce a deterministic and reversible cypher-text. Can somebody please verify that I am doing this correctly?
function getBytes(e) {
var bytes = [];
for (var i = 0; i < e.length; ++i) {
bytes.push(e.charCodeAt(i));
}
return bytes;
}
function getHex(e) {
return e.map(function (byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
}
function hash(e) {
return CryptoJS.SHA256(e);
}
function encryptCustomerEmail(email) {
let iv = CryptoJS.enc.Hex.parse(getHex(getBytes(process.env.IV)));
let encryptedEmail = CryptoJS.AES.encrypt(email, hash(process.env.SERVER_ENCRYPTION_KEY), {iv: iv, mode: CryptoJS.mode.CBC}).toString();
return encryptedEmail;
}
function decryptCustomerEmail(encryptedEmail) {
let iv = CryptoJS.enc.Hex.parse(getHex(getBytes(process.env.IV)));
let decryptedBytes = CryptoJS.AES.decrypt(encryptedEmail, hash(process.env.SERVER_ENCRYPTION_KEY), {iv: iv, mode: CryptoJS.mode.CBC});
let email = decryptedBytes.toString(CryptoJS.enc.Utf8);
return email;
}
I can confirm the indeterministic behavior for { mode: CryptoJS.mode.ECB }
as well. Here's a test written in typescript 3.9.7
// ./src/crypto.ts
import crypto from "crypto-js";
export const encrypt = (clearText: string, env = process.env) => {
const pw = usePw(env);
const deterministic = { mode: crypto.mode.ECB };
return crypto.AES.encrypt(clearText, pw, deterministic).toString();
};
const usePw = (env = process.env) => {
const pw = env.PASSWORD;
if (!pw) {
throw new Error("Missing env var: PASSWORD");
}
return pw;
};
Jest 26.4.1 test
// ./test/crypto.test.ts
import { encrypt } from "../src/crypto";
it("encrypt is deterministic", () => {
const env = {
PASSWORD: "secret",
};
const clearText = "hello there";
const cipherText = encrypt(clearText, env);
const cipherText2 = encrypt(clearText, env);
expect(cipherText).toBe(cipherText2); // fails
});
Output
● encrypt is deterministic
expect(received).toBe(expected) // Object.is equality
Expected: "U2FsdGVkX192xaA8MFbqEZnkY0j8HnsJALPAqYN47XI="
Received: "U2FsdGVkX1+a/trOsp+aOicmCz1meUFVcwRD9fzsPP8="
13 | const cipherText2 = encrypt(clearText, env);
14 |
> 15 | expect(cipherText).toBe(cipherText2);
| ^
16 | });
17 |
18 | it("decrypt after encrypt should yield the same result | deterministic", () => {
at Object.<anonymous> (test/crypto.test.ts:15:24)
Any ideas how to implement deterministic encryption in JavaScript would be helpful. Thank you.
Came up with a work though this should work:
const encrypt = (text: string, key: string) => {
const hash = CryptoJS.SHA256(key);
const ciphertext = CryptoJS.AES.encrypt(text, hash, {
mode: CryptoJS.mode.ECB,
});
return ciphertext.toString();
};
const decrypt = (ciphertext: string, key: string) => {
const hash = CryptoJS.SHA256(key);
const bytes = CryptoJS.AES.decrypt(ciphertext, hash, {
mode: CryptoJS.mode.ECB,
});
return bytes.toString(CryptoJS.enc.Utf8);
};