Search code examples
javascriptencryptioncryptographyaescryptojs

Crypto function that always decrypts to plain text, even with the incorrect key


I'm working on a javascript function to encrypt and store passwords on the browser using a 6 digit PIN. While this could easily be brute forced, the server side code prevents this by locking out the account after 3 incorrect attempts.

The example below using AES, only decrypts to plain text when the pin/key is correct. This allows the attacker to try the 99,9999 combinations and pick out the only plain text result, bypassing the server side restrictions.

Can someone recommend a javascript crypto function/library that always decrypts to plain text, even with the incorrect key?

var encrypted = CryptoJS.AES.encrypt("password:abcdefg", "pin:123456");
$('#1').text(encrypted);


var decryptedCorrect = CryptoJS.AES.decrypt($('#1').text(), "pin:123456")
$('#3').text(decryptedCorrect.toString(CryptoJS.enc.Utf8));

var decryptedInCorrect = CryptoJS.AES.decrypt($('#1').text(), "pin:112233")
$('#4').text(decryptedInCorrect.toString(CryptoJS.enc.Utf8));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js"></script>

<div>
  Encrypted text: <span id="1"></span>
</div>
<br />
<div>
  Decrypted with correct pin: <span id="3"></span>
</div>
<br />
<div>
  Decrypted with incorrect pin: <span id="4"></span>
</div>
(Ideally the above should be some plain text value)


Solution

  • In general, this cannot work.

    To prevent brute-force guessing attacks like these, not only would you need every key (or at least a fairly large fraction of the keys) to decrypt the ciphertext into valid plaintext, but you'd somehow have to arrange for every key to decrypt the ciphertext into plausible plaintext, convincing enough to pass for the real plaintext at least on a cursory inspection.

    In particular, the "fake" plaintext produced on incorrect encryption would need to at least be syntactically valid, so that your own code will accept it, and it would also need to have the same general structure and the same statistical properties as your real plaintext, so that the attacker cannot just use some regexps or letter frequency analysis to guess which of the plaintexts is most likely the correct one.

    Basically, if you were encrypting passwords, your decryption method would have to generate a plausible-looking password for any key. If you were encrypting JSON data, it would have to generate valid JSON. If you were encrypting poems, it would have to generate a poem.

    And they'd have to be good poems, because who'd bother encrypting bad poetry?

    Obviously, no general-purpose encryption algorithm can do this.


    So, how can you achieve something like what you want? Basically, you have two options:

    1. You can increase the length of your keys so that they cannot be practically enumerated. If you're using random decimal numbers (actually chosen randomly, not by the user!), then around 25 to 30 digits should be the minimum safe length.

      You can cut down the length a bit by using key stretching. For example, if you hash each key 100,000 times before using the result to decrypt the data, then you can cut down your keyspace size by a factor of 100,000, i.e. from 25 digits to 20.

      You can also make your keys easier to remember by encoding them as phrases. For example, you could compile a list of 1000 short, common English words (or use an existing list), and replace each group of three digits in your password with the corresponding word in the list. It's a lot easier for most people to remember, say, a sequence of five random words than a random 25-digit number.

    2. The other option, of course, is to make use of the server-side rate limiting you already have. To do that, you need to ensure that the client has to check the key with the server before it can do anything else with it.

      That is, you should store the actual encryption key (which could be, say, a random 128-bit binary string) on the server, and have the server only send it to the client after the client has successfully authenticated with its own "short key".

      You should also make sure that neither the short nor the long key can be captured by an eavesdropper, e.g. by using something like SRP for the authentication, or simply by doing the authentication over TLS/SSL.

    Neither of these solutions is perfect: the first may require inconveniently long keys, even with key stretching, while the second method won't work if the client is offline, and can fail catastrophically is the server is ever hacked. In general, though, those are pretty much the best you can do.

    (There are ways to combine the two methods for extra security, so that the attacker would have to both compromise the server and guess the client's password in order to break the system. But then you'd also have the disadvantages of both methods.)