Skip to content

Instantly share code, notes, and snippets.

@mudge
Last active November 9, 2024 12:35
Show Gist options
  • Save mudge/2ca7c691f4bb5862b71dc2c51c8d5a08 to your computer and use it in GitHub Desktop.
Save mudge/2ca7c691f4bb5862b71dc2c51c8d5a08 to your computer and use it in GitHub Desktop.
Transparently upgrade from bcrypt to Argon2id with Active Record
require "argon2id"
require "bcrypt"
# Schema: User(name: string, password_digest:string)
class User < ApplicationRecord
attr_reader :password
validates :password_digest, presence: true
validates :password, confirmation: true, allow_blank: true
def password=(unencrypted_password)
if unencrypted_password.nil?
@password = nil
self.password_digest = nil
elsif !unencrypted_password.empty?
@password = unencrypted_password
self.password_digest = Argon2id::Password.create(unencrypted_password)
end
end
def authenticate_password(unencrypted_password)
return false unless password_digest?
if argon2id_password_digest?
Argon2id::Password.new(password_digest).is_password?(unencrypted_password) && self
elsif BCrypt::Password.new(password_digest).is_password?(unencrypted_password)
update_attribute(:password, unencrypted_password)
self
else
false
end
end
def password_salt
return unless password_digest?
if argon2id_password_digest?
Argon2id::Password.new(password_digest).salt
else
BCrypt::Password.new(password_digest).salt
end
end
def argon2id_password_digest?
Argon2id::Password.valid_hash?(password_digest)
end
alias_method :authenticate, :authenticate_password
end
require "test_helper"
class UserTest < ActiveSupport::TestCase
test "is invalid if password digest is blank" do
user = User.new
user.password_digest = ""
assert user.invalid?
end
test "is invalid if password confirmation does not match" do
user = User.new
user.password = "password"
user.password_confirmation = "diffpassword"
assert user.invalid?
end
test "is valid if password is set" do
user = User.new
user.password = "password"
assert user.valid?
end
test "is valid if password and confirmation match" do
user = User.new
user.password = "password"
user.password_confirmation = "password"
assert user.valid?
end
test "setting password to a non-empty string sets the password" do
user = User.new
user.password = "password"
assert_equal "password", user.password
end
test "setting password to a non-empty string sets the password digest" do
user = User.new
user.password = "password"
assert_not_empty "password", user.password_digest
end
test "setting password to nil clears the password" do
user = User.new(password: "password")
user.password = nil
assert_nil user.password
end
test "setting password to nil clears the password digest" do
user = User.new(password_digest: "$password$digest")
user.password = nil
assert_nil user.password_digest
end
test "setting the password to an empty string doesn't change the password" do
user = User.new(password: "password")
user.password = ""
assert_equal "password", user.password
end
test "setting the password to an empty string doesn't change the password digest" do
user = User.new(password_digest: "$password$digest")
user.password = ""
assert_equal "$password$digest", user.password_digest
end
test "authenticating Argon2id hash with correct password returns the user" do
user = User.new(
password_digest: "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
)
assert_equal user, user.authenticate("password")
end
test "authenticating Argon2id hash with incorrect password returns false" do
user = User.new(
password_digest: "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
)
assert_not user.authenticate("diffpassword")
end
test "authenticating bcrypt hash with correct password returns user" do
user = User.new(
password_digest: "$2a$12$stsRn7Mi9r02.keRyF4OK.Aq4UWOU185lWggfUQfcupAi.b7AI/nS"
)
assert_equal user, user.authenticate("password")
end
test "authenticating bcrypt hash with incorrect password returns false" do
user = User.new(
password_digest: "$2a$12$stsRn7Mi9r02.keRyF4OK.Aq4UWOU185lWggfUQfcupAi.b7AI/nS"
)
assert_not user.authenticate("diffpassword")
end
test "authenticating bcrypt hash with correct password rehashes password with Argon2id" do
user = User.new(
password_digest: "$2a$12$stsRn7Mi9r02.keRyF4OK.Aq4UWOU185lWggfUQfcupAi.b7AI/nS"
)
user.authenticate("password")
assert user.password_digest.start_with?("$argon2id$")
end
test "rehashed password still matches old plain text password" do
user = User.new(
password_digest: "$2a$12$stsRn7Mi9r02.keRyF4OK.Aq4UWOU185lWggfUQfcupAi.b7AI/nS"
)
user.authenticate("password")
assert_equal user, user.authenticate("password")
end
test "authenticating bcrypt hash with incorrect password does not rehash password" do
user = User.new(
password_digest: "$2a$12$stsRn7Mi9r02.keRyF4OK.Aq4UWOU185lWggfUQfcupAi.b7AI/nS"
)
user.authenticate("diffpassword")
assert_equal "$2a$12$stsRn7Mi9r02.keRyF4OK.Aq4UWOU185lWggfUQfcupAi.b7AI/nS", user.password_digest
end
test "password salt is nil if there is no password digest" do
user = User.new
assert_nil user.password_salt
end
test "password salt is nil if the password digest is blank" do
user = User.new
user.password_digest = ""
assert_nil user.password_salt
end
test "password salt returns the salt from an Argon2id hash" do
user = User.new(
password_digest: "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
)
assert_equal "somesalt", user.password_salt
end
test "password salt returns the salt from bcrypt hash" do
user = User.new(
password_digest: "$2a$12$stsRn7Mi9r02.keRyF4OK.Aq4UWOU185lWggfUQfcupAi.b7AI/nS"
)
assert_equal "$2a$12$stsRn7Mi9r02.keRyF4OK.", user.password_salt
end
test "is an Argon2id password digest with a valid Argon2id hash" do
user = User.new(
password_digest: "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
)
assert user.argon2id_password_digest?
end
test "is not an Argon2id password digest with a bcrypt hash" do
user = User.new(
password_digest: "$2a$12$stsRn7Mi9r02.keRyF4OK.Aq4UWOU185lWggfUQfcupAi.b7AI/nS"
)
assert_not user.argon2id_password_digest?
end
test "is not an Argon2id password digest with no password digest" do
user = User.new(password_digest: nil)
assert_not user.argon2id_password_digest?
end
test "is not an Argon2id password digest with a blank password digest" do
user = User.new(password_digest: "")
assert_not user.argon2id_password_digest?
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment