Crypto JS: Can you slow down decryption, without an exponential file size increase?

I have the following:

let original = 'something'
let passphrase = uuidv4()

// will take place on the server
let encrypted = CryptoJS.AES.encrypt(original, passphrase)

// will take place on the browser
// I want this part to take  ≈ 10 minutes *minimum*
let decrypted = CryptoJS.AES.decrypt(encrypted, passphrase)

I tried an iterative approach. Controlling the decrypting iterations needed means you can also control the decryption time to some degree:

// increase this until we reach the desired decryption time on the browser
let numberOfEncryptions = 2

// will take place on the server
let encrypted = CryptoJS.AES.encrypt(original, passphrase).toString()

let i = 0

while (i < numberOfEncryptions) {
  encrypted = CryptoJS.AES.encrypt(encrypted, passphrase).toString()
  i++
}


// will take place on the browser:

let decrypted = CryptoJS.AES.decrypt(encrypted, passphrase).toString(CryptoJS.enc.Utf8)

i = 0

while (i < numberOfEncryptions) {
  decrypted = CryptoJS.AES.decrypt(decrypted, passphrase).toString(CryptoJS.enc.Utf8)
  i++
}

await checkWithServer(decrypted) // returns true

The results were disappointing.

Increasing the number of encryptions on the server also increases the time of decryption on the browser which is great because this is what I want.

But it also increases the size of the encrypted file exponentially which is horrible as the user cannot possibly download such a huge file in order to the decrypt it.

Is there some other solution?

UPDATE:

@SlavaKnyazev suggested I should instead encrypt the passphrase used to encrypt the data and send a hint to the end-user for them to brute-force for the passphrase.

Instead of spending time decrypting the data itself the user will spend time brute-forcing for the passphrase.

This is how I tried to implement this (as a test):

  const KEY = 'ab' // uuidv4()
  const dataToEncrypt = 'The Message'
  const md5key = CryptoJS.MD5(KEY).toString()
  const encrypted = CryptoJS.AES.encrypt(dataToEncrypt, md5key)

  const sha1keyHint = CryptoJS.SHA1(KEY).toString()

  let pool = 'abcdefghijklmnopqrstuvwxyz'.split('')
  let before = Date.now()
  let after
  let md5keyFromHint

  bruteForce(pool, (value) => {
    if(CryptoJS.SHA1(value).toString() === sha1keyHint) {
      md5keyFromHint = CryptoJS.MD5(value).toString()
      after = Date.now()
      console.log(`KEY is ${value}`)
      console.log(`Found after ${(after - before) / 1000} seconds`)
      return true
    }
    return false
  })

  const decrypted = CryptoJS.AES.decrypt(encrypted, md5keyFromHint).toString(CryptoJS.enc.Utf8)
  console.log(dataToEncrypt === decrypted) // returns true

I turns out KEY has to be rather “simple” and not a uuidv4() as I thought initially. Otherwise, it could take forever. Also, the brute)forcing method I’m using needs a “pool” of characters to look into, I guess the bigger the pool, the longer it will take.

Once again my problem is that the user will not be spending time decrypting the actual data. So it feels like PoW, i.e. faking it.

But I’ll consider the question answered this time and will stop here. 🙂

My initial goal apparently it not possible. Thank you for your help.

Answer

Don’t give the key, give a hash of the key. The length of the key to brute-force will give you the granularity you’re looking for.

Encryption process:

  1. Generate a key (ex: hunter2) and encrypt your data using it.
  2. Hash hunter2 using an algorithm such as SHA1 (f3bbbd66a63d4bf1747940578ec3d0103530e21d)
  3. Make the client brute-force it for the key

The longer the key, the exponentially longer it will take to find it, while the size of the payload remains constant.

This has a flaw however — brute-forcing is not necessary, as brute-forcing the AES encryption will be just as straight forward. This can be defeated by salting by making the key a hash as well.

Encrypt your data using not “hunter2”, but using MD5(hunter2) (use a different algorithm).

Throw in some salt to the hashes to prevent the effective usage of rainbow tables.

Pseudo code:

// Encrypt
let key = "password";
let aesKey = md5(password);
let hint = sha1(password);

let encryptedData = data.encrypt(aesKey);

let decryptedData = data.decrypt(md5(bruteForceSha1(hint));