require 'dionysus' require 'active_support/secure_random' ## # Encapsulates a password salt. This is usually not required specifically, # instead it's required as a part of the security module: # # require 'dionysus/security' # # However, you may require it specifically: # # require 'dionysus/security/password_salt' # # Examples # # salt = PasswordSalt.new # #=> generates random salt of 8 characters # salt.salt_password('foobar') #=> 'foobar0PD0oKAj' # # salt = PasswordSalt.new(:length => 20) # #=> generates random salt of 20 characters # salt.salt_password('foobar') #=> 'foobar7qvFpfi+3jGVFaA5TaE7' # # salt = PasswordSalt.new('ABCDEFG', :beginning) # #=> generates salt 'abcdef' with beginning placement # salt.salt_password('foobar') # #=> 'ABCDEFGfoobar' # # salt = PasswordSalt.new(:split, :length => 10) # #=> generates random salt of 10 characters with split placement # salt.salt_password('foobar') # #=> 'WyVjpfoobarjGYXJ' class PasswordSalt PLACEMENTS = [:before, :after, :split] DEFAULT_LENGTH = 8 DEFAULT_PLACEMENT = :after @@secure_random = SecureRandom cattr_accessor :secure_random attr_accessor :string attr_reader :placement ## # Generate a salt string of the given length (Default: 8) with the base64 # character set. Optionally, you may pass the format as :binary to generate # a binary salt. def self.generate( length = DEFAULT_LENGTH, format = :base64 ) if length < 0 raise ArgumentError, "Invalid length: #{length}" end case format.to_sym when :base64 self.secure_random.base64(length)[0...length] when :binary self.secure_random.random_bytes(length) else raise ArgumentError, "Invalid format: #{format}" end end ## # Initialize a new PasswordSalt # # If you pass the first argument as a string, the string will be set as # the salt: # PasswordSalt.new('ABCDEFG') # # If the first argument is :new or is an option hash, a random salt # will be generated: # PasswordSalt.new(:new) # PasswordSalt.new(:length => 20) # PasswordSalt.new # # If the first or second argument is a Symbol (other than :new), it # will be interpreted as the placement: # PasswordSalt.new(:before) # PasswordSalt.new(:new, :before) # PasswordSalt.new('ABCDEFG', :before) # # You may always pass in an options hash as the last argument. # [length] Length of salt to be generated. # Default: 8 def initialize( *args ) options = args.extract_options! self.string = args.detect { |val| val.is_a?(String) or val == :new } self.placement = args.detect { |val| PLACEMENTS.include?(val) } || DEFAULT_PLACEMENT if self.string == :new or self.string.nil? self.string = self.class.generate(options[:length] || DEFAULT_LENGTH) end end ## # Returns the given password, but salted. def salt_password( password ) case self.placement when :after password.to_s + string when :before string + password.to_s when :split string[0...(string.length/2).floor] + password.to_s + string[(string.length/2).floor...string.length] else raise "Invalid salt placement: #{self.placement}" end end ## # Set the salt's placement # # [after] Place the salt after the password. # [before] Place the salt before the password. # [split] Place half of the salt before and half after the password. For # salts of odd length, the shorter half will be in front. def placement=( sym ) unless PLACEMENTS.include?(sym.to_sym) raise ArgumentError, "Invalid salt placement: #{sym}" end @placement = sym.to_sym end ## # Returns the salt string. def to_s self.string end ## # Returns +true+ if the given salt is equivilent to this salt, +false+ otherwise def eql?( salt ) self.to_s.eql?(salt.to_s) && self.placement.eql?(salt.placement) end end