Skip to content

Commit

Permalink
Use scrypt instead of sha256 to hash password
Browse files Browse the repository at this point in the history
  • Loading branch information
emlun committed Aug 15, 2023
1 parent 9d5b05e commit f5f1049
Showing 1 changed file with 98 additions and 11 deletions.
109 changes: 98 additions & 11 deletions src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Err, Ok, Result } from "ts-results";
import { Entity, PrimaryGeneratedColumn, Column, Repository} from "typeorm"
import AppDataSource from "../AppDataSource";
import crypto from "node:crypto";
import base64url from "base64url";


@Entity({ name: "user" })
class UserEntity {
@PrimaryGeneratedColumn()
Expand Down Expand Up @@ -52,6 +55,69 @@ enum UpdateFcmError {
DB_ERR = "Failed to update FCM token list"
}

// Settings for new password hashes
// Best practice guidelines from https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#scrypt
const scryptKeyLen: number = 64;
const scryptCost: number = 131072; // 2^17
const scryptBlockSize: number = 8;
const scryptMaxMem: number = 128 * scryptCost * scryptBlockSize * 2;

function parseScryptParams(passwordHash: string): Result<{ digest: Buffer, salt: Buffer, keyLen: number, cost: number, blockSize: number }, void> {
try {
if (!(passwordHash.startsWith("$") && passwordHash.endsWith("$"))) {
return Err.EMPTY;
}

const splits = passwordHash.split('$');
const keyLen = parseInt(splits[1], 10);
const cost = parseInt(splits[2], 10);
const blockSize = parseInt(splits[3], 10);
const salt = base64url.toBuffer(splits[4]);
const digest = base64url.toBuffer(splits[4]);
return Ok({ digest, salt, keyLen, cost, blockSize });

} catch (e) {
return Err.EMPTY;
}
}

async function computeScrypt(password: string, salt: Buffer, keyLen: number, cost: number, blockSize: number): Promise<string> {
return new Promise((resolve, reject) => {
crypto.scrypt(
Buffer.from(password, "utf8"),
salt,
keyLen,
{ cost, blockSize, maxmem: scryptMaxMem },
(err, derivedKey) => {
if (err) {
reject(err);
} else {
const result = "$" + [keyLen, cost, blockSize, base64url.encode(salt), base64url.encode(derivedKey)].join("$") + "$";
resolve(result);
}
},
);
});
}

async function createScryptHash(password: string): Promise<string> {
return await computeScrypt(password, crypto.randomBytes(32), scryptKeyLen, scryptCost, scryptBlockSize);
}

/**
* @return Ok(true) if password matches; Ok(false) if password is scrypt-hashed but does not match; Err(void) if password is not scrypt-hashed.
*/
async function verifyScryptHash(password: string, scryptHash: string): Promise<Result<boolean, void>> {
const decodeRes = parseScryptParams(scryptHash);
if (decodeRes.ok) {
const { salt, keyLen, cost, blockSize } = decodeRes.val;
const encoded = await computeScrypt(password, salt, keyLen, cost, blockSize);
return Ok(encoded === scryptHash);
} else {
return Err.EMPTY;
}
}


const userRepository: Repository<UserEntity> = AppDataSource.getRepository(UserEntity);

Expand Down Expand Up @@ -99,18 +165,39 @@ async function getUserByDID(did: string): Promise<Result<UserEntity, GetUserErr>

async function getUserByCredentials(username: string, password: string): Promise<Result<UserEntity, GetUserErr>> {
try {
return await userRepository.manager.transaction(async (manager) => {
const user = await manager.findOne(UserEntity, { where: { username } });
if (user) {
const scryptRes = await verifyScryptHash(password, user.passwordHash);
if (scryptRes.ok) {
if (scryptRes.val) {
return Ok(user);
} else {
return Err(GetUserErr.NOT_EXISTS);
}

const passwordHash = crypto.createHash('sha256').update(password).digest('base64');
const res = await AppDataSource.getRepository(UserEntity)
.createQueryBuilder("user")
.where("user.username = :username and user.passwordHash = :passwordHash", { username: username, passwordHash: passwordHash })
.getOne();
if (!res) {
return Err(GetUserErr.NOT_EXISTS);
}
return Ok(res);
}
catch(e) {
} else {
// User isn't migrated to sha256 yet - fall back to sha256
const sha256Hash = crypto.createHash('sha256').update(password).digest('base64');

if (user.passwordHash === sha256Hash) {
// Upgrade the user to scrypt
user.passwordHash = await createScryptHash(password);
await manager.save(user);

return Ok(user);
} else {
return Err(GetUserErr.NOT_EXISTS);
}
}

} else {
// Compute a throwaway hash anyway so we don't leak timing information
await createScryptHash(password);
return Err(GetUserErr.NOT_EXISTS);
}
});
} catch (e) {
console.log(e);
return Err(GetUserErr.DB_ERR)
}
Expand Down

0 comments on commit f5f1049

Please sign in to comment.