Skip to content

Introduction

Passwords should never be stored as is, but as a hash, so that they cannot be recovered if the hashes leak. Doing so securely requires a slow, salted hashing algorithm such as bcrypt, as fast general purpose hash functions can be brute forced.

The Athena::PasswordHasher component hashes passwords, verifies them, and tells when a hash should be updated, e.g. after increasing the cost of the algorithm. It provides an APH::Interface with the following implementations:

  • APH::Native - Hashes passwords with the Crystal standard libraries' bcrypt
  • APH::Migrating - Hashes passwords with one hasher, while still verifying the passwords other hashers created

Installation#

First, install the component by adding the following to your shard.yml, then running shards install:

dependencies:
  athena-password_hasher:
    github: athena-framework/password-hasher
    version: ~> 0.1.0

Usage#

An APH::Interface can be used on its own to hash a plain password, and later verify a plain password against it:

hasher = APH::Native.new

hash = hasher.hash "password" # => "$2a$13$..."

hasher.verify hash, "password"       # => true
hasher.verify hash, "wrong_password" # => false

Hashing User Passwords#

Most of the time, the passwords being hashed belong to users of the application. Including APH::PasswordAuthenticatedUserInterface into the user type allows an APH::UserPasswordHasher to work with its password:

class User
  include APH::PasswordAuthenticatedUserInterface

  # The hashed password.
  property password : String?
end

hasher = APH::UserPasswordHasher.new APH::Factory.new({User => APH::Native.new})

user = User.new
user.password = hasher.hash_password user, "password"

hasher.password_valid? user, "password" # => true

The APH::Factory decides which hasher to use for a user. Hashers are configured per type, given as is or by its fully qualified name, which also matches the subtypes and includers of that type. For example, APH::PasswordAuthenticatedUserInterface may be used to configure the hasher of every user type. A user may also choose the hasher used for its password by name, via the APH::PasswordHasherAwareInterface.

Upgrading Hashes#

Over time, the cost of a hashing algorithm should be increased as hardware gets faster, or the algorithm itself replaced by a stronger one. Hashes can only be updated when the plain password is known, so the best time to do it is when a user logs in:

if hasher.password_valid? user, plain_password
  if hasher.needs_rehash? user
    user.password = hasher.hash_password user, plain_password
    # Persist the user
  end

  # Log the user in
end

Increasing the cost of an APH::Native hasher is enough to make existing hashes need a rehash, as the cost is stored within the hash. To switch to a different algorithm, an APH::Migrating hasher verifies the existing hashes with the previous hasher, while new hashes are created with the new one.

Custom Hashers#

Other algorithms, such as argon2, can be used by implementing APH::Interface, for example with a third-party shard providing the algorithm itself. The custom hasher may then be used with any of the types of the component.

class Argon2Hasher
  include APH::Interface

  # :inherit:
  def hash(plain_password : String) : String
    # ...
  end

  # :inherit:
  def verify(hashed_password : String, plain_password : String) : Bool
    # ...
  end

  # :inherit:
  def needs_rehash?(hashed_password : String) : Bool
    # ...
  end
end

hasher = APH::UserPasswordHasher.new APH::Factory.new({User => APH::Migrating.new(Argon2Hasher.new, APH::Native.new)})

Hashing Passwords from the Command Line#

When the Console component is installed, the component also provides the security:hash-password command via APH::Commands::UserPasswordHash. It is useful to create the hash of a password while developing an application, e.g. to insert a user into the database.

$ ./bin/console security:hash-password

Athena Password Hash Utility
============================

 Type in your password to be hashed:
 >
 --------------- --------------------------------------------------------------
  Key             Value
 --------------- --------------------------------------------------------------
  Hasher used     Athena::PasswordHasher::Native
  Password hash   $2a$13$OXGeRAmufse10/PKTENJsODMUyfiCsiPYod9od/8N9LUitZB49Qq6
 --------------- --------------------------------------------------------------

 ! [NOTE] Self-salting hasher used: the hasher generated its own built-in salt.

 [OK] Password hashing succeeded