jakubzapletal/crypto-js

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);
};