module NaClPassword

Constants

INVALID_OPTION_MESSAGE
UNCOERCEABLE_OPTION_MESSAGE
VERSION

Public Class Methods

authenticate(encoded, password) click to toggle source
# File lib/nacl_password.rb, line 85
def authenticate(encoded, password)
  digest, *metadata = decode(encoded)
  RbNaCl::PasswordHash.argon2id(password, *metadata) == digest
end
const_missing(name) click to toggle source
Calls superclass method
# File lib/nacl_password.rb, line 40
def self.const_missing(name)
  NaClPassword.setup
  if const_defined?(name)
    const_get(name)
  else
    super
  end
end
digest_size=(value) click to toggle source
# File lib/nacl_password.rb, line 62
def digest_size=(value)
  @digest_size = get_digest_size(value)
  digest_size
end
generate(password) click to toggle source
# File lib/nacl_password.rb, line 67
def generate(password)
  salt = RbNaCl::Random.random_bytes(Argon2::SALTBYTES)
  ops = get_ops_limit
  mem = get_mem_limit
  size = get_digest_size
  "#{
    Base64.strict_encode64(encrypt(password, salt, ops, mem, size))
  }.#{
    Base64.strict_encode64(salt)
  }.#{
    ops
  }.#{
    mem
  }.#{
    size
  }"
end
load_gem() click to toggle source

Load rbnacl gem only when nacl_password is used. This is to avoid the entire app using this gem being dependent on a binary library.

# File lib/nacl_password.rb, line 12
  def self.load_gem
    require "rbnacl"
  rescue LoadError, NameError
    $stderr.puts <<-ERROR
      You don't have rbnacl installed in your application.
      Please add it to your Gemfile and run bundle install
    ERROR

    raise
  end
mem_limit=(value) click to toggle source
# File lib/nacl_password.rb, line 58
def mem_limit=(value)
  @mem_limit = get_mem_limit(value)
end
ops_limit=(value) click to toggle source
# File lib/nacl_password.rb, line 54
def ops_limit=(value)
  @ops_limit = get_ops_limit(value)
end
setup() click to toggle source

The Argon2 hash function can handle maximum 2^32 bytes, but system available memory probably cannot. A password of length 1024 bytes is way more than any user would ever need. Put a restriction on password length that keeps memory usage in the available range, but is more than anyone would ever need. other defaults are tested ranges of functional values for libsodium that allow maximum possible security without crashing the system

# File lib/nacl_password.rb, line 30
def self.setup
  load_gem
  ::NaClPassword::Argon2              ||= RbNaCl::PasswordHash::Argon2
  ::NaClPassword::MAX_PASSWORD_LENGTH ||= 1024
  ::NaClPassword::OPS_LIMIT_RANGE     ||= 3..20
  ::NaClPassword::MEM_LIMIT_RANGE     ||= (2**25)..(2**32) #32 MB - 4 GB
  ::NaClPassword::DIGEST_SIZE_RANGE   ||= 64..512
  self
end

Private Class Methods

decode(encoded) click to toggle source
# File lib/nacl_password.rb, line 99
def decode(encoded)
  digest_64, salt_64, ops, mem, size = encoded.split(".")
  digest = Base64.strict_decode64(digest_64)
  salt = Base64.strict_decode64(salt_64)
  ops  = get_ops_limit(ops)
  mem  = get_mem_limit(mem)
  size = get_digest_size(size)
  [ digest, salt, ops, mem, size ]
end
encrypt(...) click to toggle source
# File lib/nacl_password.rb, line 95
def encrypt(...)
  RbNaCl::PasswordHash.argon2id(...)
end
get_digest_size(size = NaClPassword.digest_size) click to toggle source
# File lib/nacl_password.rb, line 157
def get_digest_size(size = NaClPassword.digest_size)
  size = :moderate unless CoerceBoolean.from(size)

  case size
  when :min         then DIGEST_SIZE_RANGE.min
  when :interactive then DIGEST_SIZE_RANGE.min * 2
  when :moderate    then DIGEST_SIZE_RANGE.min * 4
  when :sensitive   then DIGEST_SIZE_RANGE.min * 8
  when :max         then DIGEST_SIZE_RANGE.max
  when Symbol
    raise ArgumentError, INVALID_OPTION_MESSAGE
  else
    case size = size.to_i
    when DIGEST_SIZE_RANGE
      unless size % 64 == 0
        raise ArgumentError, "digest_size must be a multiple of 64"
      end

      size
    else
      raise ArgumentError, "digest_size must be within the range #{DIGEST_SIZE_RANGE}"
    end
  end
rescue NoMethodError
  raise ArgumentError, UNCOERCEABLE_OPTION_MESSAGE
end
get_mem_limit(mem = NaClPassword.mem_limit) click to toggle source
# File lib/nacl_password.rb, line 133
def get_mem_limit(mem = NaClPassword.mem_limit)
  mem = :moderate unless CoerceBoolean.from(mem)

  case mem
  when :min         then MEM_LIMIT_RANGE.min # 32mb
  when :interactive then (2**26)             # 64mb
  when :moderate    then (2**28)             # 256mb
  when :sensitive   then (2**30)             # 1024mb
  when :max         then MEM_LIMIT_RANGE.max # 4096mb
  when Symbol
    raise ArgumentError, INVALID_OPTION_MESSAGE
  else
    case mem = mem.to_i
    when MEM_LIMIT_RANGE then mem
    else
      raise \
        ArgumentError,
        "mem_limit must be within the range #{MEM_LIMIT_RANGE}"
    end
  end
rescue NoMethodError
  raise ArgumentError, UNCOERCEABLE_OPTION_MESSAGE
end
get_ops_limit(ops = NaClPassword.ops_limit) click to toggle source
# File lib/nacl_password.rb, line 109
def get_ops_limit(ops = NaClPassword.ops_limit)
  ops = :moderate unless CoerceBoolean.from(ops)

  case ops
  when :min         then OPS_LIMIT_RANGE.min
  when :interactive then 5
  when :moderate    then 10
  when :sensitive   then 15
  when :max         then OPS_LIMIT_RANGE.max
  when Symbol
    raise ArgumentError, INVALID_OPTION_MESSAGE
  else
    case ops = ops.to_i
    when OPS_LIMIT_RANGE then ops
    else
      raise \
        ArgumentError,
        "ops_limit must be within the range #{OPS_LIMIT_RANGE}"
    end
  end
rescue NoMethodError
  raise ArgumentError, UNCOERCEABLE_OPTION_MESSAGE
end