This article: Life in a post-database world: using crypto to avoid DB writes

Prompted me to write this url-crypt Node.js module to convert a Secret into secure urlsafe base64 strings.

Think of an email verification link that doesn't need to store it's data in the database. Or, a JWT token whose claims are secret instead of base64.

The idea is to encrypt and store a reasonable amount of data (<512 chars) in < 2k chars of base64 (a URL) so it can be decrypted later.

I published this on npm and then thought. Is it secure? Insecure would be unhelpful to others :-/

How it works:

var urlCrypt = require('url-crypt')('super-secret-key');

var data = { hello: 'world', this: 'is a test', of: 'url-crypt' };
var base64 = urlCrypt.cryptObj(data);
var backAgain = urlCrypt.decryptObj(base64);

What it does: urlCrypt.cryptObj(obj):

  • Converts obj to JSON
  • Gzips the JSON
  • Creates 30 bytes of salt
  • Encrypts [salt][gzip] with aes-256-cbc and a pbkdf2 of key
  • Converts to base64

And back again.

The code is 2 functions all one file.

Some specific questions:

  1. Here it adds some jitter as a protection against dictionary attacks. Does this make it stronger?

  2. Here it uses pbkdf2 to make the aes key. The salt is constant. Does pbkdf2 this way make it stronger? Adding a salt configuration parameter seems like just making a longer key.

  3. Here it puts some random data in front of the real data. If I knew the real data ({email:"me@example.com"}) I thought it would be easier to guess the secret. This random data is to protect against that. Does this salt help?

  4. Here it repeats the aes-256-cbs. Is this helpful? Repetition makes the ending token longer (which makes for less data in the URL), so is this extraneous?

  5. What's the best way to make computationally harder, w/o making the token longer?

  6. Is there a way to make any of this better? Or anything complexity that could be removed.

Thanks! It's my first security focused module, so I wanted to get it sanity checked.

  • 2
    Salts cannot be constant: you need a different salt for every time there's a new key (if it's always the same, it's not a salt, because part of the definition of a salt is every password has a different one). What sort of key is it? PBKDF2 is for passwords; if it's a strong key already (i.e. 128-bit random string), you instead use a KBKDF. – cpast Feb 21 '15 at 8:02
  • Also: You aren't saying what you want to defend against. If you want to prevent an attacker from forging a password reset, you're using the wrong types of algorithm to do that (you're using encryption, which is not designed to provide authenticity). While the blog post doesn't properly describe HMAC (it's not a signature, and isn't just any cryptographic hash), there are specific properties HMAC has that are absolutely critical for this situation. AES-CBC is not designed to have them. Throwing algorithms at a problem without understanding what they do and why does not generally work in crypto. – cpast Feb 21 '15 at 8:15
  • Which leads to the main point: crypto is hard, don't roll your own. If you're writing your first security module, don't. Or do, but only use it for a toy project. As a bare minimum, you need to know exactly why you're doing everything you're doing; crypto is a subject where it's very easy to go badly wrong with minor-seeming mistakes. – cpast Feb 21 '15 at 8:21
  • This problem has already been solved. See: npmjs.com/package/fernet – user10211 Feb 21 '15 at 8:36
  • 2
    Unless you have profiling information that this matters, I'd consider avoiding the database premature optimization. I recommend using the database. – CodesInChaos Feb 21 '15 at 10:18

There are several fundamental issues.

First of all, do not invent your own security protocols, especially when you're a layman. I know this has been said hundreds of times before, I know we're all tired of hearing it, but it's a very important rule. If you actually consider to use your code in production (which seems to be the case), you're already making a big mistake.

Secondly, the premise is flawed. In the article, this approach is marketed as an easy, lightweight alternative to storing data in a database. But it's neither easy nor lightweight. Getting cryptography right is extremely difficult, it requires expert knowledge and hard work. On the other hand, setting up a database is a matter of a few minutes and can be done by anybody with basic SQL knowledge. In some special cases, you might get a performance benefit from avoiding a server-side database, but this needs to be measured in the concrete application and weighed against the disadvantages. I see no indication that you've actually done this.

Third, there are conceptual issues. You talk about secrets and encryption, but it seems what you actually want is authentication and integrity, that is, you want to make sure that the data in the URL indeed comes from you and hasn't been manipulated. Encryption generally does not prevent manipulation, it only provides confidentiality (I don't see why you'd need that in your case). If you want all three properties at the same time, things get even more complex. Now you need advanced cipher modes like the Galois Counter Mode of AES which may or may not be supported in your environment. Coming up with your own encryption/MAC combination is not recommended. Even big software projects have screwed this up, so as a single programmer with no crypto background, the chances of making a mistake are enormous.

You also haven't thought about the problem of revoking a token. What if you find out that a token has been compromised? What if a user asks you to cancel it immediately? Since you have no control over the tokens once you've issued them, this isn't possible. All you can do is wait for the token to expire, but this is hardly an option. So you probably do need a server-side database of some kind, at which point the whole exercise becomes rather useless.

Last but not least, there are plenty of technical issues in your code:

  • Using a constant salt is plain wrong (as has been said already). It's not even clear to me why you'd want to use a password-based key instead of a purely random key.
  • There's no random initialization vector, which again is a fatal flaw. NodeJS does try to derive an IV from the key material, but since your PBKDF2 salt is constant, the IV is constant as well.
  • You're using the wrong methods. If you read the manual, you'll see that the crypto.createCipher() method actually expects a password, not a key. It then derives a key and an initialization vector from this password (using a rather weak algorithm). To encrypt data with a custom key, crypto.createCipheriv() is used.
  • No, adding garbage data or throwing in a bunch of repetitions doesn't help. Doing random stuff in the hopes that it will increase security generally isn't helpful in cryptography.
| improve this answer | |
  • Hey @fleche, thanks for your answer. I think some extra requirements got into the mix. Maybe I'm using the wrong vocabulary. Anyways, Yes about the salt and IV, thanks for the info on crypto.createCypher, and yes about the repetition. – Michael Cole Feb 21 '15 at 13:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.