I wrote some simplified code for sign-up and sign-in. You can see whole source code at the bottom of this gist.
Assuming an id-password based login, the general flow of sign-up would look like this:
- Check that the user information is in the database with the user ID. Return if already exists.
- Create
salt
withrandomBytes
function. - Pass the
salt
value and the user's raw password(password
) to the hash function (herederivePassword
). - Insert the user data into the
db
and savehash
andsalt
in association with user data. - After user data creation is completed, proceed with the subsequent procedure(here
postSignUp
which only logs user data).
import { scrypt, randomBytes, randomUUID as uuid } from "node:crypto";
// [...]
async function signUp(userId, password) {
const user = getUserByUserId(userId);
if (user) {
throw Error(`Cannot create a user with the userId: ${userId}`);
}
// The salt should be as unique as possible. It is recommended that a salt is random and at least 16 bytes long.
// See NIST SP 800-132(https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf) for details.
const salt = randomBytes(saltSize).toString('hex');
const hash = await derivePassword(password, salt);
return postSignUp(createUser({ userId, hash, salt }));
}
// [...]
// Password derivation function
async function derivePassword(rawPassword, salt) {
return new Promise((resolve, reject) => {
scrypt(rawPassword, salt, keyLength, scryptOptions, (err, dk) => {
if (err) {
return reject(err);
}
return resolve(dk.toString("hex"));
});
});
}
// [...]
// A dummy function to denote a procedure after signup
async function postSignUp(user) {
console.log(
`SignUp Success, (userId/hash/salt): (${user.userId}/${user.hash}/${user.salt})`
);
}
Login will flow in the following order:
- We need to get user data first. To be precise, we need to get the
hash
andsalt
associated with the user data. - We need to verify that the hash value created with the
rawPassword
passed by the user equal to thehash
value stored in the database.
- In this case, the
compare
function is used to verify, and the following two values are compared to check whether therawPassword
is correct.hash
:hash
stored in the database (a hash value created with the password provided by the user when signing up)derivePassword(rawPassword, salt)
: A hash value created from therawPassword
provided by the user when logging in
- If the two values do not match return
- After the login is completed, proceed with the subsequent procedure(here
postSignIn
which only logs user data).
async function signIn(userId, rawPassword) {
const user = getUserByUserId(userId);
if (!user) {
throw Error(`Signin failed!`);
}
const { hash, salt } = user;
const ok = await compare(rawPassword, hash, salt);
if (!ok) {
throw Error("Signin faield");
}
return postSignIn(user);
}
// [...]
// password verification function
async function compare(rawPassword, hash, salt) {
return hash === (await derivePassword(rawPassword, salt));
}
// [...]
// A dummy function to denote a procedure after signin
async function postSignIn(user) {
console.log(
`SignIn Success, (userId/hash/salt): (${user.userId}/${user.hash}/${user.salt})`
);
}
As you can see, scrypt
is used for password generation. Tune parameters in scryptOptions
as you need.
// see scrypt from node built-ins
import { scrypt, randomBytes, randomUUID as uuid } from "node:crypto";
const scryptOptions = {
N: 8192, // Alias for cost. Only one of both may be specified. Default: 16384.
r: 8, // Alias for blockSize. Only one of both may be specified. Default: 8.
p: 1, // Alias for parallelization. Only one of both may be specified. Default: 1.
maxmem: 32 * 1024 * 1024, // Memory upper bound. It is an error when (approximately) 128 * N * r > maxmem.
};
// [...]
async function derivePassword(rawPassword, salt) {
return new Promise((resolve, reject) => {
scrypt(rawPassword, salt, keyLength, scryptOptions, (err, dk) => {
if (err) {
return reject(err);
}
return resolve(dk.toString("hex"));
});
});
}
- node.js
scrypt
documentation - scrypt: A new key derivation function - Doing our best to thwart TLAs armed with ASICs
- paper - STRONGER KEY DERIVATION VIA SEQUENTIAL MEMORY-HARD FUNCTIONS
- NIST SP 800-132
import { scrypt, randomBytes, randomUUID as uuid } from "node:crypto";
const saltSize = 32;
const keyLength = 64;
/**
* Scrypt paramters
*
* @see https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback
* @see http://www.tarsnap.com/scrypt/scrypt-slides.pdf
* @see https://www.tarsnap.com/scrypt/scrypt.pdf
*/
const scryptOptions = {
N: 8192, // Alias for cost. Only one of both may be specified. Default: 16384.
r: 8, // Alias for blockSize. Only one of both may be specified. Default: 8.
p: 1, // Alias for parallelization. Only one of both may be specified. Default: 1.
maxmem: 32 * 1024 * 1024, // Memory upper bound. It is an error when (approximately) 128 * N * r > maxmem.
};
// Simplified version of database
const db = {};
/**
* Simplified version of getting credential of a user
*/
function getUserByUserId(userId) {
return Object.keys(db)
.map((uuid) => db[uuid])
.find((data) => data.userId === userId);
}
function createUser(data) {
const id = uuid();
db[id] = {
id,
...data,
createdAt: new Date(),
};
return db[id];
}
// A dummy function to denote a procedure after signup
async function postSignUp(user) {
console.log(
`SignUp Success, (userId/hash/salt): (${user.userId}/${user.hash}/${user.salt})`
);
}
// A dummy function to denote a procedure after signin
async function postSignIn(user) {
console.log(
`SignIn Success, (userId/hash/salt): (${user.userId}/${user.hash}/${user.salt})`
);
}
async function signUp(userId, password) {
const user = getUserByUserId(userId);
if (user) {
throw Error(`Cannot create a user with the userId: ${userId}`);
}
// The salt should be as unique as possible. It is recommended that a salt is random and at least 16 bytes long.
// See NIST SP 800-132(https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf) for details.
const salt = randomBytes(saltSize).toString("hex");
const hash = await derivePassword(password, salt);
return postSignUp(createUser({ userId, hash, salt }));
}
async function signIn(userId, rawPassword) {
const user = getUserByUserId(userId);
if (!user) {
throw Error(`Signin failed!`);
}
const { hash, salt } = user;
const ok = await compare(rawPassword, hash, salt);
if (!ok) {
throw Error("Signin faield");
}
return postSignIn(user);
}
// password derivation function
async function derivePassword(rawPassword, salt) {
return new Promise((resolve, reject) => {
scrypt(rawPassword, salt, keyLength, scryptOptions, (err, dk) => {
if (err) {
return reject(err);
}
return resolve(dk.toString("hex"));
});
});
}
// password verification function
async function compare(rawPassword, hash, salt) {
return hash === (await derivePassword(rawPassword, salt));
}
(async () => {
await signUp("jaysok", "superpassword!");
await signIn("jaysok", "superpassword!");
})();