require 'dionysus'
require 'active_support/secure_random'
##
# Encapsulates a 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