Module: Philiprehberger::Password

Defined in:
lib/philiprehberger/password.rb,
lib/philiprehberger/password/policy.rb,
lib/philiprehberger/password/zxcvbn.rb,
lib/philiprehberger/password/hashing.rb,
lib/philiprehberger/password/version.rb,
lib/philiprehberger/password/patterns.rb,
lib/philiprehberger/password/strength.rb,
lib/philiprehberger/password/generator.rb,
lib/philiprehberger/password/common_passwords.rb

Defined Under Namespace

Modules: CommonPasswords, Generator, Hashing, Patterns, Strength, Zxcvbn Classes: Policy

Constant Summary collapse

VERSION =
'0.9.0'

Class Method Summary collapse

Class Method Details

.batch_strength(passwords) ⇒ Array<Hash>

Compute strength for a list of passwords in input order. Each element is coerced via to_s so non-string entries don't raise.

Parameters:

  • passwords (Enumerable<String>)

    passwords to grade

Returns:

  • (Array<Hash>)

    strength hashes (one per input, same order)

Raises:

  • (ArgumentError)

    if passwords is not enumerable



39
40
41
42
43
# File 'lib/philiprehberger/password.rb', line 39

def self.batch_strength(passwords)
  raise ArgumentError, 'passwords must be enumerable' unless passwords.respond_to?(:each)

  passwords.map { |p| Strength.compute(p.to_s) }
end

.common?(password) ⇒ Boolean

Check if a password appears in the common password dictionary.

Parameters:

  • password (String)

    the password to check

Returns:

  • (Boolean)

    true if the password is common



21
22
23
# File 'lib/philiprehberger/password.rb', line 21

def self.common?(password)
  CommonPasswords.include?(password.to_s.downcase)
end

.entropy(password) ⇒ Float

Estimated entropy of the password in bits (log2(pool_size ^ length)). Pool size is inferred from the character classes present.

Parameters:

  • password (String)

    the password to evaluate

Returns:

  • (Float)

    estimated entropy in bits (0.0 for empty passwords)



50
51
52
# File 'lib/philiprehberger/password.rb', line 50

def self.entropy(password)
  Strength.entropy(password)
end

.generate(**options) ⇒ String

Generate a secure random password, passphrase, PIN, or pronounceable password.

Parameters:

  • length (Integer)

    password length (default 16; ignored for passphrase style)

  • uppercase (Boolean)

    include uppercase letters (default true)

  • lowercase (Boolean)

    include lowercase letters (default true)

  • digits (Boolean)

    include digits (default true)

  • symbols (Boolean, Array<String>)

    include symbols; pass an array to use a custom symbol set

  • symbol_set (String, nil)

    characters of a custom symbol pool (alternative to symbols: array)

  • exclude_ambiguous (Boolean)

    drop visually ambiguous characters (0 O o l I 1)

  • style (Symbol, nil)

    :passphrase, :pin, or :pronounceable for alternative styles

  • words (Integer)

    word count for passphrase style (default 4)

  • separator (String)

    separator for passphrase style (default "-")

Returns:

  • (String)

    the generated password



89
90
91
# File 'lib/philiprehberger/password.rb', line 89

def self.generate(**options)
  Generator.generate(**options)
end

.hash(password, cost: 12) ⇒ String

Hash a password using bcrypt. Requires the bcrypt gem to be installed.

Parameters:

  • password (String)

    the plaintext password to hash

  • cost (Integer) (defaults to: 12)

    bcrypt cost factor (4-31, default 12)

Returns:

  • (String)

    the bcrypt hash



130
131
132
# File 'lib/philiprehberger/password.rb', line 130

def self.hash(password, cost: 12)
  Hashing.hash(password, cost: cost)
end

.keyboard_patterns(password) ⇒ Array<Hash>

Detect keyboard patterns, sequences, and repeated characters.

Parameters:

  • password (String)

    the password to inspect

Returns:

  • (Array<Hash>)

    detected pattern hashes (keyboard rows, sequences, repeats)



120
121
122
# File 'lib/philiprehberger/password.rb', line 120

def self.keyboard_patterns(password)
  Patterns.detect(password)
end

.mask(password, visible: 0, mask: '*') ⇒ String

Mask a password for safe display in logs, diagnostics, or UI surfaces. Reveals the trailing visible characters and replaces the rest with mask so that the full length of the password is still preserved. When visible is 0 (default) the entire password is masked.

Parameters:

  • password (String)

    the password to mask

  • visible (Integer) (defaults to: 0)

    number of trailing characters to expose (>= 0)

  • mask (String) (defaults to: '*')

    single-character replacement for masked positions

Returns:

  • (String)

    the masked password

Raises:

  • (ArgumentError)

    if visible is negative or mask is not one character



162
163
164
165
166
167
168
169
170
171
172
# File 'lib/philiprehberger/password.rb', line 162

def self.mask(password, visible: 0, mask: '*')
  raise ArgumentError, 'visible must be >= 0' if visible.negative?
  raise ArgumentError, 'mask must be a single character' unless mask.is_a?(String) && mask.length == 1

  str = password.to_s
  return '' if str.empty?

  reveal = [visible, str.length].min
  masked_length = str.length - reveal
  (mask * masked_length) + str[-reveal, reveal].to_s
end

.score(password) ⇒ Integer

Strength score as a 0-4 integer. Convenience accessor that returns only the :score from strength.

Parameters:

  • password (String)

    the password to evaluate

Returns:

  • (Integer)

    strength score (0 = very weak, 4 = very strong)



59
60
61
# File 'lib/philiprehberger/password.rb', line 59

def self.score(password)
  Strength.compute(password)[:score]
end

.secure_compare(left, right) ⇒ Boolean

Timing-safe string comparison. Uses the constant-time OpenSSL.fixed_length_secure_compare when both inputs are the same byte length, and a length-masked constant-time fallback (returning false without early-exiting on the length difference) otherwise.

Parameters:

  • left (String)

    first value to compare

  • right (String)

    second value to compare

Returns:

  • (Boolean)

    true when the two values are equal



101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/philiprehberger/password.rb', line 101

def self.secure_compare(left, right)
  a = left.to_s.b
  b = right.to_s.b

  return OpenSSL.fixed_length_secure_compare(a, b) if a.bytesize == b.bytesize

  # Compare equal-length digests so we never branch purely on the byte
  # size, then force false since differing lengths cannot be equal.
  OpenSSL.fixed_length_secure_compare(
    OpenSSL::Digest::SHA256.digest(a),
    OpenSSL::Digest::SHA256.digest(b)
  )
  false
end

.strength(password) ⇒ Hash

Compute strength of a single password.

Parameters:

  • password (String)

    the password to evaluate

Returns:

  • (Hash)

    strength hash with :score (0-4), :label, :entropy, and :feedback



29
30
31
# File 'lib/philiprehberger/password.rb', line 29

def self.strength(password)
  Strength.compute(password)
end

.strong?(password, threshold: 3) ⇒ Boolean

Predicate: is the password "strong enough"?

Returns true when score is at least threshold (default 3, the "strong" tier on the 0-4 scale: terrible/weak/fair/strong/excellent). Use a higher threshold (e.g. 4) for stricter gating.

Parameters:

  • password (String)

    the password to evaluate

  • threshold (Integer) (defaults to: 3)

    minimum acceptable score (0-4)

Returns:

  • (Boolean)


72
73
74
# File 'lib/philiprehberger/password.rb', line 72

def self.strong?(password, threshold: 3)
  score(password) >= threshold
end

.verify(password, hash) ⇒ Boolean

Verify a password against a bcrypt hash. Requires the bcrypt gem to be installed.

Parameters:

  • password (String)

    the plaintext password to verify

  • hash (String)

    the bcrypt hash to verify against

Returns:

  • (Boolean)

    true when the password matches the hash



140
141
142
# File 'lib/philiprehberger/password.rb', line 140

def self.verify(password, hash)
  Hashing.verify(password, hash)
end

.zxcvbn(password) ⇒ Hash

Perform zxcvbn-style strength estimation.

Parameters:

  • password (String)

    the password to evaluate

Returns:

  • (Hash)

    zxcvbn-style report with :score, :patterns, and :crack_time_display



148
149
150
# File 'lib/philiprehberger/password.rb', line 148

def self.zxcvbn(password)
  Zxcvbn.estimate(password)
end