-
-
Save garbados/29ca945d5964ef85e7936804c23edb9d to your computer and use it in GitHub Desktop.
How to store passwords in a database, with PouchDB and native crypto.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
How to securely store a password: by salting and hashing! | |
Obviously it's risky to store passwords in plaintext. It's even risky to store | |
them encrypted, because then someone might decrypt them. What you really want to | |
do is hash the password so that even if someone gets the hash, they can't get | |
the password. | |
If you just hash the raw password, an attacker could use a rainbow table to | |
crack the hash instantly. So you want to modify the hash using some random | |
values called a *salt*, so an attacker would have to regenerate their rainbow | |
table using these random values in order to try brute-forcing your password. As | |
this regeneration process is very, very expensive, this is considered a good way | |
to obfuscate passwords. | |
However, regenerating rainbow tables is less expensive than you might imagine. | |
So, it pays to hash something many many times, which is what `crypto.pbkdf2` | |
does. In this example code, a password is hashed some hundred thousand times in | |
order to derive a suitably obscured value -- a *key*. By using this key and its | |
associated salt, a password can be easily verified without having the password | |
stored anywhere. | |
Lastly, it's worth noting you can't just compare these keys with a normal | |
equality operator like `===`. You need to use a timing-safe equality comparison, | |
like `crypto.timingSafeEqual`. This prevents what are called *timing attacks*. | |
*/ | |
const PouchDB = require('pouchdb') | |
const crypto = require('crypto') | |
const { promisify } = require('util') | |
const SALT_LENGTH = 32 | |
const ITERATIONS = 1e5 // 1 and 5 zeroes aka 100,000 | |
const HASH_ALGO = 'sha512' | |
const KEY_LENGTH = 64 | |
// good ol' pouchdb | |
const db = new PouchDB('pass-hash-example') | |
// we need the salt to be VERY RANDOM so we use crypto.randomBytes | |
const randomBytes = promisify(crypto.randomBytes) | |
// PBKDF2 turns a password into a cryptographic key for encryption and decryption | |
// remember: password-based key derivation function! just what it says on the tin. | |
async function pbkdf2 (password, salt) { | |
return new Promise((resolve, reject) => { | |
crypto.pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, HASH_ALGO, (err, key) => { | |
if (err) { return reject(err) } else { return resolve(key) } | |
}) | |
}) | |
} | |
// converts a password into a random salt and a secure, derived key | |
async function obfuscate (password) { | |
const salt = await randomBytes(SALT_LENGTH) | |
const key = await pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, HASH_ALGO) | |
// convert values from buffers to utf8 hex strings for portability | |
return { salt: salt.toString('hex'), key: key.toString('hex') } | |
} | |
// verify that a password matches the key associated with a given salt | |
async function verify (password, salt, key) { | |
// cryptography functions work on bytes. remember to decode from hex! | |
if (typeof key === 'string') { key = Buffer.from(key, 'hex') } | |
if (typeof salt === 'string') { salt = Buffer.from(salt, 'hex') } | |
const derivedKey = await pbkdf2(password, salt) | |
return crypto.timingSafeEqual(key, derivedKey) | |
} | |
Promise.resolve().then(async () => { | |
// let's mimic a sign-in process! | |
// garbados creates an account using a very secure password: | |
const username = 'garbados' | |
const password = 'horses resources and grundlepick pie' | |
const { salt, key } = await obfuscate(password) | |
// now we store the salt and the derived key in our db: | |
await db.put({ _id: username, salt, key }) | |
// then garbados logs out. | |
// later, she tries to log back in, so we verify her password using the | |
// stored salt and key: | |
const doc = await db.get(username) | |
const ok = await verify(password, doc.salt, doc.key) | |
console.log(ok) | |
}).catch((err) => { | |
console.trace(err) | |
}).then(() => { | |
return db.destroy() | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment