# philiprehberger-password [![Tests](https://github.com/philiprehberger/rb-password/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-password/actions/workflows/ci.yml) [![Gem Version](https://badge.fury.io/rb/philiprehberger-password.svg)](https://rubygems.org/gems/philiprehberger-password) [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-password)](https://github.com/philiprehberger/rb-password/commits/main) ![philiprehberger-password](https://raw.githubusercontent.com/philiprehberger/rb-password/main/package-card.webp) 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: ```ruby gem "philiprehberger-password" ``` Or install directly: ```bash gem install philiprehberger-password ``` ## Usage ### Strength Scoring ```ruby 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": ```ruby 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: ```ruby 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: ```ruby Philiprehberger::Password.score("password") # => 0 Philiprehberger::Password.score("MyP@ssw0rd!") # => 4 ``` ### Strong Enough Predicate ```ruby 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 ```ruby Philiprehberger::Password.common?("password") # => true Philiprehberger::Password.common?("xK9#mZ2!pQ") # => false ``` ### Policy Validation ```ruby 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 ```ruby 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 ```ruby 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 ```ruby # 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 ```ruby # 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 ```ruby 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. ```ruby results = Philiprehberger::Password.batch_strength([ "hunter2", "P@ssw0rd!", "C0rr3ctH0rseB4tt3ryStapl3" ]) results.map { |r| r[:score] } # => [0, 2, 4] ``` ### Masking for Display ```ruby 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: ```ruby 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 ```bash bundle install bundle exec rspec bundle exec rubocop ``` ## Support If you find this project useful: ⭐ [Star the repo](https://github.com/philiprehberger/rb-password) 🐛 [Report issues](https://github.com/philiprehberger/rb-password/issues?q=is%3Aissue+is%3Aopen+label%3Abug) 💡 [Suggest features](https://github.com/philiprehberger/rb-password/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) ❤️ [Sponsor development](https://github.com/sponsors/philiprehberger) 🌐 [All Open Source Projects](https://philiprehberger.com/open-source-packages) 💻 [GitHub Profile](https://github.com/philiprehberger) 🔗 [LinkedIn Profile](https://www.linkedin.com/in/philiprehberger) ## License [MIT](LICENSE)