Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

Security

Hashing

The Hash facade hashes your users' passwords with bcrypt or Argon2, and checks a password against its hash.

Introduction#

A password is stored as a hash, which can't be turned back into the password: to sign a user in, Oriel hashes what they typed and compares the two. Bcrypt, the default, is slow on purpose, and its work factor can rise as computers get faster, so guessing passwords from a stolen hash stays costly. The starter kits hash with bcrypt when users register, and check the hash when they sign in.

Configuration#

config/hashing.ts names the driver, bcrypt unless HASH_DRIVER names another, and each driver's settings:

config/hashing.ts
import { env } from '@orieljs/framework'export default {  driver: env('HASH_DRIVER', 'bcrypt'),  bcrypt: { rounds: Number(env('BCRYPT_ROUNDS', 12)), verify: true },  argon: { memory: 65536, threads: 1, time: 4, verify: true },  rehash_on_login: true,}

The drivers are bcrypt, argon (Argon2i) and argon2id. Bcrypt's rounds is its work factor; Argon2's memory (in KiB), time and threads are its. The settings may be numbers or the strings env() gives, such as '12'.

Your tests run with BCRYPT_ROUNDS=4, so the hashing in them is quick.

Basic usage#

Hashing passwords#

Hash.make() hashes a value with your driver. It returns a promise:

app/Http/Controllers/PasswordController.ts
import { Hash, redirect, request } from '@orieljs/framework'import type { User } from '../../Models/User'import { Controller } from './Controller'export class PasswordController extends Controller {  /** Update the password for the user. */  async update() {    // Validate the new password length...    const user = (await request().user<User>())!    await user.fill({ password: await Hash.make(request().string('newPassword').toString()) }).save()    return redirect('/profile')  }}

A model can do this for you: an attribute with the hashed cast is hashed when the model is saved, and a value that is already a hash made with your current settings is kept. Hashed casting describes it.

Adjusting the work factor#

Hash.make() takes the value only. Set the work factor in config/hashing.ts, or, for bcrypt, pass it to the bcrypt() helper, which hashes with bcrypt whatever your driver is:

const hashed = await bcrypt('password', { rounds: 12 })

Verifying that a password matches a hash#

Hash.check() hashes a password and compares it with a hash. It returns a promise too:

if (await Hash.check('plain-text', hashedPassword)) {  // The passwords match...}

An empty hash never matches.

Determining if a password needs to be rehashed#

Hash.needsRehash() tells you whether a hash was made with other settings than your current ones, such as fewer rounds or another algorithm. It is synchronous:

if (Hash.needsRehash(hashed)) {  hashed = await Hash.make('plain-text')}

With rehash_on_login on, as in a new app, signing a user in with Auth.attempt() does this for you: when the password matches and its hash needs rehashing, the user's password is hashed again and saved.

Other methods#

  • Hash.info(hashed) gives a hash's algorithm and options, such as { algorithm: 'bcrypt', options: { cost: 12 } }.
  • Hash.isHashed(value) tells you whether a value is a bcrypt or Argon2 hash.
  • Hash.driver('argon2id') returns another driver, to hash or check with it.

Bcrypt hashes start with $2y$, as PHP's password_hash() writes them, so the hashes of a Laravel app's users check in Oriel, and the other way round.

Hash algorithm verification#

With verify on, as in a new app, Hash.check() refuses a hash made by another algorithm than the driver's: it throws a HashAlgorithmError, "This password does not use the Bcrypt algorithm.", rather than return false. This stops a hash of another kind from being checked in a way it wasn't meant for. When you move your users from one algorithm to another, set verify: false in the driver's settings while their old hashes remain.

Privacy policy ·

esc