philiprehberger-password

Tests Gem Version Last updated

philiprehberger-password

Password strength checking, policy validation, pattern detection, hashing, and secure generation

Requirements

  • Ruby >= 3.1
  • bcrypt gem (optional, for password hashing only)

Installation

Add to your Gemfile:

gem "philiprehberger-password"

Or install directly:

gem install philiprehberger-password

Usage

Strength Scoring

require "philiprehberger/password"

result = Philiprehberger::Password.strength("MyP@ssw0rd!")
result[:score]    # => 3
result[:label]    # => :strong
result[:entropy]  # => 72.08
result[:feedback] # => [] (empty for strong passwords)

The :feedback key holds an array of actionable suggestions built from length, character-class, sequence, and common-password analysis. It is empty for passwords that already score "strong" or "excellent":

Philiprehberger::Password.strength("helloworld")[:feedback]
# => ["Use at least 12 characters", "Add uppercase letters, digits, and symbols"]

Philiprehberger::Password.strength("password")[:feedback]
# => ["Avoid using a common password", "Use at least 12 characters", "Add uppercase letters, digits, and symbols"]

Entropy Estimate

Estimated password entropy in bits — length * log2(pool_size) where the pool is inferred from the character classes present:

Philiprehberger::Password.entropy("aaaaaa")       # => 28.20
Philiprehberger::Password.entropy("MyP@ssw0rd!")  # => 72.08
Philiprehberger::Password.entropy("")             # => 0.0

Score Only

Return the 0-4 integer score without the full strength hash:

Philiprehberger::Password.score("password")    # => 0
Philiprehberger::Password.score("MyP@ssw0rd!") # => 4

Strong Enough Predicate

Philiprehberger::Password.strong?("password")                  # => false
Philiprehberger::Password.strong?("MyP@ssw0rd!")               # => true
Philiprehberger::Password.strong?("MyP@ssw0rd!", threshold: 4) # => true / false

Default threshold: is 3 (the "strong" tier on the 0-4 scale: terrible/weak/fair/strong/excellent). Raise it to 4 for stricter gating.

Common Password Check

Philiprehberger::Password.common?("password")   # => true
Philiprehberger::Password.common?("xK9#mZ2!pQ") # => false

Policy Validation

policy = Philiprehberger::Password::Policy.new(
  min_length: 12,
  require_uppercase: true,
  require_digit: true,
  require_symbol: true,
  reject_common: true,
  custom_passwords: ["companyname", "internalpass"]
)

result = policy.validate("short")
result.valid?  # => false
result.errors  # => ["must be at least 12 characters", ...]
result.score   # => 0

Context-Aware Validation

policy = Philiprehberger::Password::Policy.new

result = policy.validate("johndoe2024!", context: {
  username: "johndoe",
  email: "johndoe@example.com",
  app_name: "myapp"
})
result.valid?  # => false
result.errors  # => ["must not contain your username", "must not contain your email username"]

Keyboard Pattern Detection

patterns = Philiprehberger::Password.keyboard_patterns("qwertyaaa123456")
# => [
#   { type: :keyboard_row, token: "qwerty", start: 0, length: 6, direction: :forward },
#   { type: :repeated, token: "aaa", start: 6, length: 3, repeated_char: "a" },
#   { type: :sequence, token: "123456", start: 9, length: 6, sequence_type: :numeric, direction: :ascending }
# ]

Password Hashing

# Requires bcrypt gem: gem install bcrypt
hash = Philiprehberger::Password.hash("my-secret-password", cost: 12)
# => "$2a$12$..."

Philiprehberger::Password.verify("my-secret-password", hash)
# => true

Philiprehberger::Password.verify("wrong-password", hash)
# => false

Password Generation

# Random password
Philiprehberger::Password.generate(length: 20)
# => "kX9#mZ2!pQ7@wR4bN5&j"

# Passphrase (200+ word list)
Philiprehberger::Password.generate(style: :passphrase, words: 4, separator: "-")
# => "correct-horse-battery-staple"

# PIN
Philiprehberger::Password.generate(style: :pin, length: 6)
# => "482917"

# Pronounceable (alternating consonant/vowel syllables)
Philiprehberger::Password.generate(style: :pronounceable, length: 12)
# => "kotelu5mapib"

# Exclude visually ambiguous characters (0 O o l I 1)
Philiprehberger::Password.generate(length: 20, exclude_ambiguous: true)
# => "kX9mZ2pQ7wR4bN5vTgHj"

# Restrict to a custom symbol set
Philiprehberger::Password.generate(length: 16, symbols: ["#", "$", "%"])
Philiprehberger::Password.generate(length: 16, symbol_set: "#$%")

zxcvbn-Style Strength Estimation

result = Philiprehberger::Password.zxcvbn("p@ssw0rd123")
result[:score]              # => 1
result[:crack_time_display] # => "minutes"
result[:patterns]           # => [{ type: :leet, token: "p@ssw0rd", ... }, ...]

Batch Strength

Grade many passwords in one call. Useful for password audits across user lists or seeded test fixtures. Results are returned in input order; each element is coerced via to_s so non-string entries don't raise.

results = Philiprehberger::Password.batch_strength([
  "hunter2",
  "P@ssw0rd!",
  "C0rr3ctH0rseB4tt3ryStapl3"
])
results.map { |r| r[:score] }
# => [0, 2, 4]

Masking for Display

Philiprehberger::Password.mask("hunter2")                 # => "*******"
Philiprehberger::Password.mask("hunter2", visible: 2)     # => "*****r2"
Philiprehberger::Password.mask("hunter2", mask: "")      # => "•••••••"

Timing-Safe Comparison

Compare secrets (tokens, hashes, API keys) without leaking timing information:

Philiprehberger::Password.secure_compare("s3cr3t-token", "s3cr3t-token") # => true
Philiprehberger::Password.secure_compare("s3cr3t-token", "wrong-token")  # => false

Uses the constant-time OpenSSL.fixed_length_secure_compare when both inputs share a byte length, and a length-masked constant-time fallback (returning false without early-exiting on the length difference) otherwise. No extra dependencies — OpenSSL ships with Ruby.

API

Philiprehberger::Password

Method Description
.common?(password) Returns true if password is in the common password dictionary
.strength(password) Returns hash with :score (0-4), :label, :entropy, and :feedback (array of suggestions)
.batch_strength(passwords) Returns array of strength hashes, one per password, in input order
.entropy(password) Estimated entropy in bits (Float)
.score(password) Strength score as integer 0-4
.strong?(password, threshold: 3) Returns true when score >= threshold
.generate(**options) Generate a password (see options below)
.keyboard_patterns(password) Returns array of detected keyboard/sequence/repeat patterns
.hash(password, cost: 12) Hash password with bcrypt (requires bcrypt gem)
.verify(password, hash) Verify password against bcrypt hash (requires bcrypt gem)
.zxcvbn(password) Returns hash with :score (0-4), :patterns, :crack_time_display
.mask(password, visible: 0, mask: '*') Redact password for display; reveals trailing visible characters
.secure_compare(a, b) Timing-safe equality comparison for secrets (returns true/false)

Generate Options

Option Default Description
length 16 Password length
uppercase true Include uppercase letters
lowercase true Include lowercase letters
digits true Include digits
symbols true Include symbols; pass an array to use a custom symbol set
symbol_set nil String of characters forming a custom symbol pool
exclude_ambiguous false Drop visually ambiguous characters (0 O o l I 1)
style nil :passphrase, :pin, or :pronounceable for alternative styles
words 4 Word count for passphrase style
separator "-" Separator for passphrase style

Philiprehberger::Password::Policy

Method Description
.new(**options) Create policy (min_length, max_length, require_uppercase, require_lowercase, require_digit, require_symbol, reject_common, custom_passwords)
#validate(password, context: {}) Returns Result with .valid?, .errors, .score. Context accepts :username, :email, :app_name

Strength Labels

Score Label Entropy
0 :terrible < 28 bits
1 :weak < 36 bits
2 :fair < 60 bits
3 :strong < 80 bits
4 :excellent >= 80 bits

zxcvbn Pattern Types

Type Description
:dictionary Common password or known word detected
:leet L33t-speak substitution of a known word
:spatial QWERTY keyboard adjacency pattern
:date Date pattern (yyyy, mm/dd/yyyy, etc.)
:sequence Alphabetic or numeric sequence
:repeated Repeated characters

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT