mirror of
https://github.com/discourse/discourse.git
synced 2026-08-11 13:35:20 -05:00
FEATURE: Persist password hashing algorithm/params in database (#20980)
Previously, Discourse's password hashing was hard-coded to a specific algorithm and parameters. Any changes to the algorithm or parameters would essentially invalidate all existing user passwords. This commit introduces a new `password_algorithm` column on the `users` table. This persists the algorithm/parameters which were use to generate the hash for a given user. All existing rows in the users table are assumed to be using Discourse's current algorithm/parameters. With this data stored per-user in the database, we'll be able to keep existing passwords working while adjusting the algorithm/parameters for newly hashed passwords. Passwords which were hashed with an old algorithm will be automatically re-hashed with the new algorithm when the user next logs in. Values in the `password_algorithm` column are based on the PHC string format (https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md). Discourse's existing algorithm is described by the string `$pbkdf2-sha256$i=64000,l=32$` To introduce a new algorithm and start using it, make sure it's implemented in the `PasswordHasher` library, then update `User::TARGET_PASSWORD_ALGORITHM`.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PasswordHasher
|
||||
class InvalidAlgorithmError < StandardError
|
||||
end
|
||||
|
||||
class UnsupportedAlgorithmError < StandardError
|
||||
end
|
||||
|
||||
HANDLERS = {}
|
||||
|
||||
def self.register_handler(id, &blk)
|
||||
HANDLERS[id] = blk
|
||||
end
|
||||
|
||||
# Algorithm should be specified according to the id/params parts of the
|
||||
# PHC string format.
|
||||
# https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
|
||||
def self.hash_password(password:, salt:, algorithm:)
|
||||
algorithm = algorithm.delete_prefix("$").delete_suffix("$")
|
||||
|
||||
parts = algorithm.split("$")
|
||||
raise InvalidAlgorithmError if parts.length != 2
|
||||
|
||||
algorithm_id, algorithm_params = parts
|
||||
|
||||
algorithm_params = algorithm_params.split(",").map { |pair| pair.split("=") }.to_h
|
||||
|
||||
handler = HANDLERS[algorithm_id]
|
||||
if handler.nil?
|
||||
raise UnsupportedAlgorithmError.new "#{algorithm_id} is not a supported password algorithm"
|
||||
end
|
||||
|
||||
handler.call(password: password, salt: salt, params: algorithm_params)
|
||||
end
|
||||
|
||||
register_handler("pbkdf2-sha256") do |password:, salt:, params:|
|
||||
raise ArgumentError.new("Salt and password must be supplied") if password.blank? || salt.blank?
|
||||
|
||||
if params["l"].to_i != 32
|
||||
raise UnsupportedAlgorithmError.new("pbkdf2 implementation only supports l=32")
|
||||
end
|
||||
|
||||
if params["i"].to_i < 1
|
||||
raise UnsupportedAlgorithmError.new("pbkdf2 iterations must be 1 or more")
|
||||
end
|
||||
|
||||
Pbkdf2.hash_password(password, salt, params["i"].to_i, "sha256")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user