Skip to content

Hashing

This page shows how to hash passwords with Hash and check them later.

A hash is one-way. You can not turn it back into the password. Hash uses bcrypt with a new random salt each time. It is in package:dox_core/dox_core.dart.

Hash a password

Hash the password before you save it.

user.password = await Hash.makeAsync(body.password);
await user.save();

makeAsync runs bcrypt in another isolate. See Without blocking the server.

The result looks like $2a$10$.... It holds the salt, so you only store this one string. The same password gives a different hash each time.

Check a password

bool ok = Hash.verify('secret', user.password!);

Hash.verify(password, hashedPassword) returns true when the password matches. Hash.check is the same, with the Laravel name. It returns false when the password does not match, or when hashedPassword is not a valid bcrypt hash (for example an empty string).

Without blocking the server

Hash.make and Hash.verify block the request isolate while bcrypt runs. In a request handler, use the async versions. They run bcrypt in another isolate.

user.password = await Hash.makeAsync(body.password);
bool ok = await Hash.verifyAsync('secret', user.password!);

Cost

Hash.rounds is the bcrypt cost for new hashes. The default is 10. Each step up doubles the time. The cost must be from 4 to 31. Do not go below 10.

Hash.rounds = 12;
String hash = Hash.make('secret', rounds: 12); // or per call

Old hashes keep their own cost, so they still verify. If you change it, also set hashRounds on JwtAuthDriver or SessionAuthDriver to the same value. Then a login with an unknown email takes as long as a wrong password. See Authentication.

Long passwords

bcrypt reads at most 72 bytes. Hash.make and Hash.makeAsync throw an ArgumentError when the password is longer than 72 bytes (UTF-8). The error text does not hold the password, so it is safe in logs. Hash.verify returns false for such a password.

So limit the length in your DTO, like @Length(min: 8, max: 72). @Length counts characters, and a letter outside ASCII can use up to 4 bytes. So a password with such letters can still be too long. Catch the error and answer 422, or use a smaller max.

Compare secrets

To check an API key or a token, use Hash.equals(given, expected), not ==. == stops at the first wrong character, so the time can tell how much of the key was right. Hash.equals takes the same time for every value of the same length.

bool ok = Hash.equals(req.header('X-Api-Key') ?? '', Env.get<String>('API_KEY'));

Store tokens

For a long random secret that you keep in the database (an API key, an access token), store only Hash.sha256(token). Find the row by the hash of what the client sends. SHA-256 is fast and gives the same hash for the same input, so a unique index works on it.

String stored = Hash.sha256(token);          // save this, never the token
Token? row = await Token().where('hash', Hash.sha256(given)).getFirst();

Never use Hash.sha256 for passwords. A password is short and can be guessed, so use Hash.make (bcrypt).

With login

You do not need Hash.verify for normal login. Auth().attempt() from dox_auth finds the user and checks the password with bcrypt. See Authentication.

Things to know

  • Never store or encrypt a plain password. Hash it.
  • Never send the hash to the client. Put the column in the model hidden list.
  • bcrypt is slow on purpose, so guessing is slow too. Do not hash in a loop on hot paths.