Skip to content

Commit

Permalink
Merge pull request #7 from gunet/scrypt
Browse files Browse the repository at this point in the history
Use scrypt instead of sha256 to hash password
  • Loading branch information
kkmanos authored Aug 24, 2023
2 parents fde0eeb + b7cee66 commit 5017c5e
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 13 deletions.
51 changes: 38 additions & 13 deletions src/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Err, Ok, Result } from "ts-results";
import { Entity, PrimaryGeneratedColumn, Column, Repository} from "typeorm"
import AppDataSource from "../AppDataSource";
import crypto from "node:crypto";

import AppDataSource from "../AppDataSource";
import * as scrypt from "../scrypt";


@Entity({ name: "user" })
class UserEntity {
@PrimaryGeneratedColumn()
Expand Down Expand Up @@ -101,18 +105,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 scrypt.verifyHash(password, user.passwordHash);
if (scryptRes.ok) {
if (scryptRes.val) {
return Ok(user);
} else {
return Err(GetUserErr.NOT_EXISTS);
}

} 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 scrypt.createHash(password);
await manager.save(user);

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 {
// Compute a throwaway hash anyway so we don't leak timing information
await scrypt.createHash(password);
return Err(GetUserErr.NOT_EXISTS);
}
});
} catch (e) {
console.log(e);
return Err(GetUserErr.DB_ERR)
}
Expand Down Expand Up @@ -184,4 +209,4 @@ export {
UpdateFcmError,
getUserByUsername,
getAllUsers
}
}
68 changes: 68 additions & 0 deletions src/scrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import crypto from "node:crypto";
import base64url from "base64url";
import { Err, Ok, Result } from "ts-results";


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


function parseParams(passwordHash: string): Result<{ salt: Buffer, keyLen: number, cost: number, blockSize: number }, void> {
try {
if (!passwordHash.startsWith("$")) {
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]);
return Ok({ 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 },
(err, derivedKey) => {
if (err) {
console.error("Failed to compute scrypt hash", err);
reject(err);
} else {
const result = "$" + [keyLen, cost, blockSize, base64url.encode(salt), base64url.encode(derivedKey)].join("$");
resolve(result);
}
},
);
});
}

export async function createHash(password: string): Promise<string> {
return await computeScrypt(password, crypto.randomBytes(32), keyLen, cost, blockSize);
}

/**
* @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.
*/
export async function verifyHash(password: string, scryptHash: string): Promise<Result<boolean, void>> {
const decodeRes = parseParams(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;
}
}

0 comments on commit 5017c5e

Please sign in to comment.