require "../bcrypt" require "../subtle" # Generate, read and verify `Crypto::Bcrypt` hashes. # # NOTE: To use `Password`, you must explicitly import it with `require "crypto/bcrypt/password"` # # ``` # require "crypto/bcrypt/password" # # password = Crypto::Bcrypt::Password.create("super secret", cost: 10) # # => $2a$10$rI4xRiuAN2fyiKwynO6PPuorfuoM4L2PVv6hlnVJEmNLjqcibAfHq # # password.verify("wrong secret") # => false # password.verify("super secret") # => true # ``` # # See `Crypto::Bcrypt` for hints to select the cost when generating hashes. class Crypto::Bcrypt::Password private SUPPORTED_VERSIONS = ["2", "2a", "2b", "2y"] # Hashes a password. # # ``` # require "crypto/bcrypt/password" # # password = Crypto::Bcrypt::Password.create("super secret", cost: 10) # # => $2a$10$rI4xRiuAN2fyiKwynO6PPuorfuoM4L2PVv6hlnVJEmNLjqcibAfHq # ``` def self.create(password : String, cost : Int32 = DEFAULT_COST) : self new(Bcrypt.hash_secret(password, cost).to_s) end getter version : String getter cost : Int32 getter salt : String getter digest : String # Loads a bcrypt hash. # # ``` # require "crypto/bcrypt/password" # # password = Crypto::Bcrypt::Password.new("$2a$10$X6rw/jDiLBuzHV./JjBNXe8/Po4wTL0fhdDNdAdjcKN/Fup8tGCya") # password.version # => "2a" # password.salt # => "X6rw/jDiLBuzHV./JjBNXe" # password.digest # => "8/Po4wTL0fhdDNdAdjcKN/Fup8tGCya" # ``` def initialize(@raw_hash : String) parts = @raw_hash.split('$') raise Error.new("Invalid hash string") unless parts.size == 4 raise Error.new("Invalid hash version") unless SUPPORTED_VERSIONS.includes?(parts[1]) @version = parts[1] @cost = parts[2].to_i @salt = parts[3][0..21] @digest = parts[3][22..-1] raise Error.new("Invalid cost") unless COST_RANGE.includes?(cost) raise Error.new("Invalid salt size") unless salt.size == 22 raise Error.new("Invalid digest size") unless digest.size == 31 end # Verifies a password against the hash. # # ``` # require "crypto/bcrypt/password" # # password = Crypto::Bcrypt::Password.create("super secret") # password.verify("wrong secret") # => false # password.verify("super secret") # => true # ``` def verify(password : String) : Bool hashed_password = Bcrypt.new(password, salt, cost) hashed_password_digest = Base64.encode(hashed_password.digest, hashed_password.digest.size - 1) Crypto::Subtle.constant_time_compare(@digest, hashed_password_digest) end # Returns the bcrypt hash, suitable for storage and use in `Crypto::Bcrypt::Password.new`. # # ``` # require "crypto/bcrypt/password" # # password = Crypto::Bcrypt::Password.create("super secret") # password.to_s # => "$2a$11$zs8yeubYXMGGJmWyIYdFtO9aOrx44g5rarvixyBfl1klr3dZPG8Ma" # ``` def to_s(io : IO) : Nil io << @raw_hash end def inspect(io : IO) : Nil to_s(io) end end