├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── data ├── adjacency_graphs.json └── frequency_lists │ ├── english.txt │ ├── female_names.txt │ ├── male_names.txt │ ├── passwords.txt │ └── surnames.txt ├── lib ├── zxcvbn.rb └── zxcvbn │ ├── crack_time.rb │ ├── data.rb │ ├── dictionary_ranker.rb │ ├── entropy.rb │ ├── feedback.rb │ ├── feedback_giver.rb │ ├── match.rb │ ├── matchers │ ├── date.rb │ ├── dictionary.rb │ ├── digits.rb │ ├── l33t.rb │ ├── new_l33t.rb │ ├── regex_helpers.rb │ ├── repeat.rb │ ├── sequences.rb │ ├── spatial.rb │ └── year.rb │ ├── math.rb │ ├── omnimatch.rb │ ├── password_strength.rb │ ├── score.rb │ ├── scorer.rb │ ├── tester.rb │ └── version.rb ├── spec ├── dictionary_ranker_spec.rb ├── feedback_giver_spec.rb ├── matchers │ ├── date_spec.rb │ ├── dictionary_spec.rb │ ├── digits_spec.rb │ ├── l33t_spec.rb │ ├── repeat_spec.rb │ ├── sequences_spec.rb │ ├── spatial_spec.rb │ └── year_spec.rb ├── omnimatch_spec.rb ├── scorer_spec.rb ├── scoring │ ├── crack_time_spec.rb │ ├── entropy_spec.rb │ └── math_spec.rb ├── spec_helper.rb ├── support │ ├── js_helpers.rb │ ├── js_source │ │ ├── adjacency_graphs.js │ │ ├── compiled.js │ │ ├── frequency_lists.js │ │ ├── init.coffee │ │ ├── init.js │ │ ├── matching.coffee │ │ ├── matching.js │ │ ├── scoring.coffee │ │ └── scoring.js │ └── matcher.rb ├── tester_spec.rb └── zxcvbn_spec.rb └── zxcvbn-ruby.gemspec /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | ruby: ['2.5', '2.6', '2.7', '3.0', '3.1', '3.2'] 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Set up Ruby 14 | uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: ${{ matrix.ruby }} 17 | bundler-cache: true 18 | - run: bundle exec rake 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | bin/ -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | [Unreleased]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.0...HEAD 10 | 11 | ## [1.2.0] - 2021-01-05 12 | 13 | ### Added 14 | - Support for Ruby 3 (thanks [@RSO] ([#32])) 15 | 16 | ### Changed 17 | - Use [mini\_racer] for running JavaScript specs (thanks [@RSO] ([#33])) 18 | - Moved CI to GitHub Actions ([#34]) 19 | 20 | [1.2.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.1.0...v.1.2.0 21 | [@rso]: https://github.com/RSO 22 | [mini\_racer]: https://rubygems.org/gems/mini_racer/ 23 | [#32]: https://github.com/envato/zxcvbn-ruby/pull/32 24 | [#33]: https://github.com/envato/zxcvbn-ruby/pull/33 25 | [#34]: https://github.com/envato/zxcvbn-ruby/pull/34 26 | 27 | ## [1.1.0] - 2020-07-16 28 | ### Added 29 | - Support for Ruby 2.7 ([#29]) 30 | - Gem metadata ([#29]) 31 | 32 | ### Removed 33 | - Support for Ruby 2.3 ([#29]) 34 | - Support for Ruby 2.4 ([#29]) 35 | 36 | ### Fixed 37 | - Invalid user dictionaries are handled more robustly ([#28]) 38 | 39 | [1.1.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.0.0...v1.1.0 40 | [#28]: https://github.com/envato/zxcvbn-ruby/pull/28 41 | [#29]: https://github.com/envato/zxcvbn-ruby/pull/29 42 | 43 | ## [1.0.0] - 2019-05-14 44 | ### Added 45 | - License info in the gemspec ([#21]) 46 | - More ported password checking features to bring this gem more up to date. ([#22]) 47 | - spatial - Keyboard patterns 48 | - repeat - Repeated characters 49 | - sequence - easily guessable sequences 50 | - date - date associations 51 | 52 | ### Removed 53 | - This gem will no longer run on Ruby versions < 2.3 ([#25]) 54 | 55 | [1.0.0]: https://github.com/envato/zxcvbn-ruby/compare/v0.1.2...v1.0.0 56 | [#21]: https://github.com/envato/zxcvbn-ruby/pull/21 57 | [#22]: https://github.com/envato/zxcvbn-ruby/pull/22 58 | [#25]: https://github.com/envato/zxcvbn-ruby/pull/25 59 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | pete.johns@envato.com 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 121 | 122 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 123 | enforcement ladder](https://github.com/mozilla/diversity). 124 | 125 | [homepage]: https://www.contributor-covenant.org 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | https://www.contributor-covenant.org/faq. Translations are available at 129 | https://www.contributor-covenant.org/translations. 130 | 131 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem 'guard' 7 | gem 'guard-bundler', require: false 8 | gem 'guard-rspec', require: false 9 | gem 'rake' 10 | end 11 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :bundler do 2 | require 'guard/bundler' 3 | require 'guard/bundler/verify' 4 | helper = Guard::Bundler::Verify.new 5 | 6 | files = ['Gemfile'] 7 | files += Dir['*.gemspec'] if files.any? { |f| helper.uses_gemspec?(f) } 8 | 9 | # Assume files are symlinked from somewhere 10 | files.each { |file| watch(helper.real_path(file)) } 11 | end 12 | 13 | guard :rspec, cmd: "bundle exec rspec" do 14 | require "guard/rspec/dsl" 15 | dsl = Guard::RSpec::Dsl.new(self) 16 | 17 | # RSpec files 18 | rspec = dsl.rspec 19 | watch(rspec.spec_helper) { rspec.spec_dir } 20 | watch(rspec.spec_support) { rspec.spec_dir } 21 | watch(rspec.spec_files) 22 | 23 | # Ruby files 24 | ruby = dsl.ruby 25 | dsl.watch_spec_files_for(ruby.lib_files) 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Envato 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zxcvbn-ruby 2 | 3 | This is a Ruby port of Dropbox's [zxcvbn.js][zxcvbn.js] JavaScript library. 4 | 5 | ## Development status [![CI Status](https://github.com/envato/zxcvbn-ruby/workflows/CI/badge.svg)](https://github.com/envato/zxcvbn-ruby/actions?query=workflow%3ACI) 6 | 7 | `zxcvbn-ruby` is considered stable and is used in projects around [Envato][envato]. 8 | 9 | After checking out the repository, run `bundle install` to install dependencies. 10 | Then, run `rake spec` to run the tests. 11 | 12 | To install this gem onto your local machine, run `bundle exec rake install`. 13 | 14 | To release a new version, update the version number in `version.rb`, and then 15 | run `bundle exec rake release`, which will create a git tag for the version, 16 | push git commits and tags, and push the `.gem` file to 17 | [rubygems.org](https://rubygems.org). 18 | 19 | 20 | ## Getting started [![Gem version](https://img.shields.io/gem/v/zxcvbn-ruby.svg?style=flat-square)](https://github.com/envato/zxcvbn-ruby) [![Gem downloads](https://img.shields.io/gem/dt/zxcvbn-ruby.svg?style=flat-square)](https://rubygems.org/gems/zxcvbn-ruby) 21 | 22 | Add the following to your project's `Gemfile`: 23 | 24 | ```ruby 25 | gem 'zxcvbn-ruby', require: 'zxcvbn' 26 | ``` 27 | 28 | Example usage: 29 | 30 | ```ruby 31 | $ irb 32 | >> require 'zxcvbn' 33 | => true 34 | >> pp Zxcvbn.test('@lfred2004', ['alfred']) 35 | #, 46 | @match_sequence= 47 | [#"a"}, sub_display="@ -> a", base_entropy=0.0, uppercase_entropy=0.0, l33t_entropy=1, entropy=1.0>, 48 | #], 49 | @password="@lfred2004", 50 | @score=0> 51 | => # 52 | >> pp Zxcvbn.test('asdfghju7654rewq', ['alfred']) 53 | #, 64 | @match_sequence= 65 | [#], 66 | @password="asdfghju7654rewq", 67 | @score=2> 68 | => # 69 | ``` 70 | 71 | ## Testing Multiple Passwords 72 | 73 | The dictionaries used for password strength testing are loaded each request to `Zxcvbn.test`. If you you'd prefer to persist the dictionaries in memory (approx 20MB RSS) to perform lots of password tests in succession then you can use the `Zxcvbn::Tester` API: 74 | 75 | ```ruby 76 | $ irb 77 | >> require 'zxcvbn' 78 | => true 79 | >> tester = Zxcvbn::Tester.new 80 | => # 81 | >> pp tester.test('@lfred2004', ['alfred']) 82 | #, 93 | @match_sequence= 94 | [#"a"}, sub_display="@ -> a", base_entropy=0.0, uppercase_entropy=0.0, l33t_entropy=1, entropy=1.0>, 95 | #], 96 | @password="@lfred2004", 97 | @score=0> 98 | => # 99 | >> pp tester.test('@lfred2004', ['alfred']) 100 | #, 111 | @match_sequence= 112 | [#"a"}, sub_display="@ -> a", base_entropy=0.0, uppercase_entropy=0.0, l33t_entropy=1, entropy=1.0>, 113 | #], 114 | @password="@lfred2004", 115 | @score=0> 116 | => # 117 | ``` 118 | 119 | **Note**: Storing the entropy of an encrypted or hashed value provides 120 | information that can make cracking the value orders of magnitude easier for an 121 | attacker. For this reason we advise you not to store the results of 122 | `Zxcvbn::Tester#test`. Further reading: [A Tale of Security Gone Wrong](http://gavinmiller.io/2016/a-tale-of-security-gone-wrong/). 123 | 124 | ## Contact 125 | 126 | - [GitHub project](https://github.com/envato/zxcvbn-ruby) 127 | - Bug reports and feature requests are welcome via [GitHub Issues](https://github.com/envato/zxcvbn-ruby/issues) 128 | 129 | ## Maintainers 130 | 131 | - [Pete Johns](https://github.com/johnsyweb) 132 | - [Steve Hodgkiss](https://github.com/stevehodgkiss) 133 | 134 | ## Authors 135 | 136 | - [Steve Hodgkiss](https://github.com/stevehodgkiss) 137 | - [Matthieu Aussaguel](https://github.com/matthieua) 138 | - [_et al._](https://github.com/envato/zxcvbn-ruby/graphs/contributors) 139 | 140 | ## License [![license](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square)](https://github.com/envato/zxcvbn-ruby/blob/HEAD/LICENSE.txt) 141 | 142 | `zxcvbn-ruby` uses MIT license, the same as [zxcvbn.js][zxcvbn.js] itself. See 143 | [`LICENSE.txt`](https://github.com/envato/zxcvbn-ruby/blob/HEAD/LICENSE.txt) 144 | for details. 145 | 146 | ## Code of Conduct 147 | 148 | We welcome contribution from everyone. Read more about it in 149 | [`CODE_OF_CONDUCT.md`](https://github.com/envato/zxcvbn-ruby/blob/HEAD/CODE_OF_CONDUCT.md). 150 | 151 | ## Contributing [![PRs welcome](https://img.shields.io/badge/PRs-welcome-orange.svg?style=flat-square)](https://github.com/envato/zxcvbn-ruby/issues) 152 | 153 | For bug fixes, documentation changes, and features: 154 | 155 | 1. [Fork it](./fork) 156 | 1. Create your feature branch (`git checkout -b my-new-feature`) 157 | 1. Commit your changes (`git commit -am 'Add some feature'`) 158 | 1. Push to the branch (`git push origin my-new-feature`) 159 | 1. Create a new Pull Request 160 | 161 | For larger new features: Do everything as above, but first also make contact with the project maintainers to be sure your change fits with the project direction and you won't be wasting effort going in the wrong direction. 162 | 163 | ## About [![code with heart by Envato](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-Envato-ff69b4.svg?style=flat-square)](https://github.com/envato/zxcvbn-ruby) 164 | 165 | This project is maintained by the [Envato engineering team][webuild] and funded by [Envato][envato]. 166 | 167 | [Envato logo][envato] 168 | 169 | Encouraging the use and creation of open source software is one of the ways we 170 | serve our community. See [our other projects][oss] or [come work with us][careers] 171 | where you'll find an incredibly diverse, intelligent and capable group of people 172 | who help make our company succeed and make our workplace fun, friendly and 173 | happy. 174 | 175 | [careers]: https://envato.com/careers/?utm_source=github 176 | [envato]: https://envato.com?utm_source=github 177 | [oss]: https://opensource.envato.com/?utm_source=github 178 | [webuild]: https://webuild.envato.com?utm_source=github 179 | [zxcvbn.js]: https://github.com/dropbox/zxcvbn 180 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require "bundler/setup" 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new('spec') 7 | task default: [:spec] 8 | 9 | task :console do 10 | require 'zxcvbn' 11 | require './spec/support/js_helpers' 12 | include JsHelpers 13 | require 'irb' 14 | ARGV.clear 15 | IRB.start 16 | 1 17 | end 18 | 19 | task :compile_coffeescript do 20 | `coffee --compile --bare spec/support/js_source/{matching,scoring,init}.coffee` 21 | `cat spec/support/js_source/{matching,scoring,adjacency_graphs,frequency_lists,init}.js > spec/support/js_source/compiled.js` 22 | end 23 | -------------------------------------------------------------------------------- /data/adjacency_graphs.json: -------------------------------------------------------------------------------- 1 | { 2 | "qwerty": {"!": ["`~", null, null, "2@", "qQ", null], "\"": [";:", "[{", "]}", null, null, "/?"], "#": ["2@", null, null, "4$", "eE", "wW"], "$": ["3#", null, null, "5%", "rR", "eE"], "%": ["4$", null, null, "6^", "tT", "rR"], "&": ["6^", null, null, "8*", "uU", "yY"], "'": [";:", "[{", "]}", null, null, "/?"], "(": ["8*", null, null, "0)", "oO", "iI"], ")": ["9(", null, null, "-_", "pP", "oO"], "*": ["7&", null, null, "9(", "iI", "uU"], "+": ["-_", null, null, null, "]}", "[{"], ",": ["mM", "kK", "lL", ".>", null, null], "-": ["0)", null, null, "=+", "[{", "pP"], ".": [",<", "lL", ";:", "/?", null, null], "/": [".>", ";:", "'\"", null, null, null], "0": ["9(", null, null, "-_", "pP", "oO"], "1": ["`~", null, null, "2@", "qQ", null], "2": ["1!", null, null, "3#", "wW", "qQ"], "3": ["2@", null, null, "4$", "eE", "wW"], "4": ["3#", null, null, "5%", "rR", "eE"], "5": ["4$", null, null, "6^", "tT", "rR"], "6": ["5%", null, null, "7&", "yY", "tT"], "7": ["6^", null, null, "8*", "uU", "yY"], "8": ["7&", null, null, "9(", "iI", "uU"], "9": ["8*", null, null, "0)", "oO", "iI"], ":": ["lL", "pP", "[{", "'\"", "/?", ".>"], ";": ["lL", "pP", "[{", "'\"", "/?", ".>"], "<": ["mM", "kK", "lL", ".>", null, null], "=": ["-_", null, null, null, "]}", "[{"], ">": [",<", "lL", ";:", "/?", null, null], "?": [".>", ";:", "'\"", null, null, null], "@": ["1!", null, null, "3#", "wW", "qQ"], "A": [null, "qQ", "wW", "sS", "zZ", null], "B": ["vV", "gG", "hH", "nN", null, null], "C": ["xX", "dD", "fF", "vV", null, null], "D": ["sS", "eE", "rR", "fF", "cC", "xX"], "E": ["wW", "3#", "4$", "rR", "dD", "sS"], "F": ["dD", "rR", "tT", "gG", "vV", "cC"], "G": ["fF", "tT", "yY", "hH", "bB", "vV"], "H": ["gG", "yY", "uU", "jJ", "nN", "bB"], "I": ["uU", "8*", "9(", "oO", "kK", "jJ"], "J": ["hH", "uU", "iI", "kK", "mM", "nN"], "K": ["jJ", "iI", "oO", "lL", ",<", "mM"], "L": ["kK", "oO", "pP", ";:", ".>", ",<"], "M": ["nN", "jJ", "kK", ",<", null, null], "N": ["bB", "hH", "jJ", "mM", null, null], "O": ["iI", "9(", "0)", "pP", "lL", "kK"], "P": ["oO", "0)", "-_", "[{", ";:", "lL"], "Q": [null, "1!", "2@", "wW", "aA", null], "R": ["eE", "4$", "5%", "tT", "fF", "dD"], "S": ["aA", "wW", "eE", "dD", "xX", "zZ"], "T": ["rR", "5%", "6^", "yY", "gG", "fF"], "U": ["yY", "7&", "8*", "iI", "jJ", "hH"], "V": ["cC", "fF", "gG", "bB", null, null], "W": ["qQ", "2@", "3#", "eE", "sS", "aA"], "X": ["zZ", "sS", "dD", "cC", null, null], "Y": ["tT", "6^", "7&", "uU", "hH", "gG"], "Z": [null, "aA", "sS", "xX", null, null], "[": ["pP", "-_", "=+", "]}", "'\"", ";:"], "\\": ["]}", null, null, null, null, null], "]": ["[{", "=+", null, "\\|", null, "'\""], "^": ["5%", null, null, "7&", "yY", "tT"], "_": ["0)", null, null, "=+", "[{", "pP"], "`": [null, null, null, "1!", null, null], "a": [null, "qQ", "wW", "sS", "zZ", null], "b": ["vV", "gG", "hH", "nN", null, null], "c": ["xX", "dD", "fF", "vV", null, null], "d": ["sS", "eE", "rR", "fF", "cC", "xX"], "e": ["wW", "3#", "4$", "rR", "dD", "sS"], "f": ["dD", "rR", "tT", "gG", "vV", "cC"], "g": ["fF", "tT", "yY", "hH", "bB", "vV"], "h": ["gG", "yY", "uU", "jJ", "nN", "bB"], "i": ["uU", "8*", "9(", "oO", "kK", "jJ"], "j": ["hH", "uU", "iI", "kK", "mM", "nN"], "k": ["jJ", "iI", "oO", "lL", ",<", "mM"], "l": ["kK", "oO", "pP", ";:", ".>", ",<"], "m": ["nN", "jJ", "kK", ",<", null, null], "n": ["bB", "hH", "jJ", "mM", null, null], "o": ["iI", "9(", "0)", "pP", "lL", "kK"], "p": ["oO", "0)", "-_", "[{", ";:", "lL"], "q": [null, "1!", "2@", "wW", "aA", null], "r": ["eE", "4$", "5%", "tT", "fF", "dD"], "s": ["aA", "wW", "eE", "dD", "xX", "zZ"], "t": ["rR", "5%", "6^", "yY", "gG", "fF"], "u": ["yY", "7&", "8*", "iI", "jJ", "hH"], "v": ["cC", "fF", "gG", "bB", null, null], "w": ["qQ", "2@", "3#", "eE", "sS", "aA"], "x": ["zZ", "sS", "dD", "cC", null, null], "y": ["tT", "6^", "7&", "uU", "hH", "gG"], "z": [null, "aA", "sS", "xX", null, null], "{": ["pP", "-_", "=+", "]}", "'\"", ";:"], "|": ["]}", null, null, null, null, null], "}": ["[{", "=+", null, "\\|", null, "'\""], "~": [null, null, null, "1!", null, null]}, 3 | 4 | "dvorak": {"!": ["`~", null, null, "2@", "'\"", null], "\"": [null, "1!", "2@", ",<", "aA", null], "#": ["2@", null, null, "4$", ".>", ",<"], "$": ["3#", null, null, "5%", "pP", ".>"], "%": ["4$", null, null, "6^", "yY", "pP"], "&": ["6^", null, null, "8*", "gG", "fF"], "'": [null, "1!", "2@", ",<", "aA", null], "(": ["8*", null, null, "0)", "rR", "cC"], ")": ["9(", null, null, "[{", "lL", "rR"], "*": ["7&", null, null, "9(", "cC", "gG"], "+": ["/?", "]}", null, "\\|", null, "-_"], ",": ["'\"", "2@", "3#", ".>", "oO", "aA"], "-": ["sS", "/?", "=+", null, null, "zZ"], ".": [",<", "3#", "4$", "pP", "eE", "oO"], "/": ["lL", "[{", "]}", "=+", "-_", "sS"], "0": ["9(", null, null, "[{", "lL", "rR"], "1": ["`~", null, null, "2@", "'\"", null], "2": ["1!", null, null, "3#", ",<", "'\""], "3": ["2@", null, null, "4$", ".>", ",<"], "4": ["3#", null, null, "5%", "pP", ".>"], "5": ["4$", null, null, "6^", "yY", "pP"], "6": ["5%", null, null, "7&", "fF", "yY"], "7": ["6^", null, null, "8*", "gG", "fF"], "8": ["7&", null, null, "9(", "cC", "gG"], "9": ["8*", null, null, "0)", "rR", "cC"], ":": [null, "aA", "oO", "qQ", null, null], ";": [null, "aA", "oO", "qQ", null, null], "<": ["'\"", "2@", "3#", ".>", "oO", "aA"], "=": ["/?", "]}", null, "\\|", null, "-_"], ">": [",<", "3#", "4$", "pP", "eE", "oO"], "?": ["lL", "[{", "]}", "=+", "-_", "sS"], "@": ["1!", null, null, "3#", ",<", "'\""], "A": [null, "'\"", ",<", "oO", ";:", null], "B": ["xX", "dD", "hH", "mM", null, null], "C": ["gG", "8*", "9(", "rR", "tT", "hH"], "D": ["iI", "fF", "gG", "hH", "bB", "xX"], "E": ["oO", ".>", "pP", "uU", "jJ", "qQ"], "F": ["yY", "6^", "7&", "gG", "dD", "iI"], "G": ["fF", "7&", "8*", "cC", "hH", "dD"], "H": ["dD", "gG", "cC", "tT", "mM", "bB"], "I": ["uU", "yY", "fF", "dD", "xX", "kK"], "J": ["qQ", "eE", "uU", "kK", null, null], "K": ["jJ", "uU", "iI", "xX", null, null], "L": ["rR", "0)", "[{", "/?", "sS", "nN"], "M": ["bB", "hH", "tT", "wW", null, null], "N": ["tT", "rR", "lL", "sS", "vV", "wW"], "O": ["aA", ",<", ".>", "eE", "qQ", ";:"], "P": [".>", "4$", "5%", "yY", "uU", "eE"], "Q": [";:", "oO", "eE", "jJ", null, null], "R": ["cC", "9(", "0)", "lL", "nN", "tT"], "S": ["nN", "lL", "/?", "-_", "zZ", "vV"], "T": ["hH", "cC", "rR", "nN", "wW", "mM"], "U": ["eE", "pP", "yY", "iI", "kK", "jJ"], "V": ["wW", "nN", "sS", "zZ", null, null], "W": ["mM", "tT", "nN", "vV", null, null], "X": ["kK", "iI", "dD", "bB", null, null], "Y": ["pP", "5%", "6^", "fF", "iI", "uU"], "Z": ["vV", "sS", "-_", null, null, null], "[": ["0)", null, null, "]}", "/?", "lL"], "\\": ["=+", null, null, null, null, null], "]": ["[{", null, null, null, "=+", "/?"], "^": ["5%", null, null, "7&", "fF", "yY"], "_": ["sS", "/?", "=+", null, null, "zZ"], "`": [null, null, null, "1!", null, null], "a": [null, "'\"", ",<", "oO", ";:", null], "b": ["xX", "dD", "hH", "mM", null, null], "c": ["gG", "8*", "9(", "rR", "tT", "hH"], "d": ["iI", "fF", "gG", "hH", "bB", "xX"], "e": ["oO", ".>", "pP", "uU", "jJ", "qQ"], "f": ["yY", "6^", "7&", "gG", "dD", "iI"], "g": ["fF", "7&", "8*", "cC", "hH", "dD"], "h": ["dD", "gG", "cC", "tT", "mM", "bB"], "i": ["uU", "yY", "fF", "dD", "xX", "kK"], "j": ["qQ", "eE", "uU", "kK", null, null], "k": ["jJ", "uU", "iI", "xX", null, null], "l": ["rR", "0)", "[{", "/?", "sS", "nN"], "m": ["bB", "hH", "tT", "wW", null, null], "n": ["tT", "rR", "lL", "sS", "vV", "wW"], "o": ["aA", ",<", ".>", "eE", "qQ", ";:"], "p": [".>", "4$", "5%", "yY", "uU", "eE"], "q": [";:", "oO", "eE", "jJ", null, null], "r": ["cC", "9(", "0)", "lL", "nN", "tT"], "s": ["nN", "lL", "/?", "-_", "zZ", "vV"], "t": ["hH", "cC", "rR", "nN", "wW", "mM"], "u": ["eE", "pP", "yY", "iI", "kK", "jJ"], "v": ["wW", "nN", "sS", "zZ", null, null], "w": ["mM", "tT", "nN", "vV", null, null], "x": ["kK", "iI", "dD", "bB", null, null], "y": ["pP", "5%", "6^", "fF", "iI", "uU"], "z": ["vV", "sS", "-_", null, null, null], "{": ["0)", null, null, "]}", "/?", "lL"], "|": ["=+", null, null, null, null, null], "}": ["[{", null, null, null, "=+", "/?"], "~": [null, null, null, "1!", null, null]}, 5 | 6 | "keypad": {"*": ["/", null, null, null, "-", "+", "9", "8"], "+": ["9", "*", "-", null, null, null, null, "6"], "-": ["*", null, null, null, null, null, "+", "9"], ".": ["0", "2", "3", null, null, null, null, null], "/": [null, null, null, null, "*", "9", "8", "7"], "0": [null, "1", "2", "3", ".", null, null, null], "1": [null, null, "4", "5", "2", "0", null, null], "2": ["1", "4", "5", "6", "3", ".", "0", null], "3": ["2", "5", "6", null, null, null, ".", "0"], "4": [null, null, "7", "8", "5", "2", "1", null], "5": ["4", "7", "8", "9", "6", "3", "2", "1"], "6": ["5", "8", "9", "+", null, null, "3", "2"], "7": [null, null, null, "/", "8", "5", "4", null], "8": ["7", null, "/", "*", "9", "6", "5", "4"], "9": ["8", "/", "*", "-", "+", null, "6", "5"]}, 7 | 8 | "mac_keypad": {"*": ["/", null, null, null, null, null, "-", "9"], "+": ["6", "9", "-", null, null, null, null, "3"], "-": ["9", "/", "*", null, null, null, "+", "6"], ".": ["0", "2", "3", null, null, null, null, null], "/": ["=", null, null, null, "*", "-", "9", "8"], "0": [null, "1", "2", "3", ".", null, null, null], "1": [null, null, "4", "5", "2", "0", null, null], "2": ["1", "4", "5", "6", "3", ".", "0", null], "3": ["2", "5", "6", "+", null, null, ".", "0"], "4": [null, null, "7", "8", "5", "2", "1", null], "5": ["4", "7", "8", "9", "6", "3", "2", "1"], "6": ["5", "8", "9", "-", "+", null, "3", "2"], "7": [null, null, null, "=", "8", "5", "4", null], "8": ["7", null, "=", "/", "9", "6", "5", "4"], "9": ["8", "=", "/", "*", "-", "+", "6", "5"], "=": [null, null, null, null, "/", "9", "8", "7"]} 9 | } -------------------------------------------------------------------------------- /data/frequency_lists/male_names.txt: -------------------------------------------------------------------------------- 1 | james 2 | john 3 | robert 4 | michael 5 | william 6 | david 7 | richard 8 | charles 9 | joseph 10 | thomas 11 | christopher 12 | daniel 13 | paul 14 | mark 15 | donald 16 | george 17 | kenneth 18 | steven 19 | edward 20 | brian 21 | ronald 22 | anthony 23 | kevin 24 | jason 25 | matthew 26 | gary 27 | timothy 28 | jose 29 | larry 30 | jeffrey 31 | frank 32 | scott 33 | eric 34 | stephen 35 | andrew 36 | raymond 37 | gregory 38 | joshua 39 | jerry 40 | dennis 41 | walter 42 | patrick 43 | peter 44 | harold 45 | douglas 46 | henry 47 | carl 48 | arthur 49 | ryan 50 | roger 51 | joe 52 | juan 53 | jack 54 | albert 55 | jonathan 56 | justin 57 | terry 58 | gerald 59 | keith 60 | samuel 61 | willie 62 | ralph 63 | lawrence 64 | nicholas 65 | roy 66 | benjamin 67 | bruce 68 | brandon 69 | adam 70 | harry 71 | fred 72 | wayne 73 | billy 74 | steve 75 | louis 76 | jeremy 77 | aaron 78 | randy 79 | eugene 80 | carlos 81 | russell 82 | bobby 83 | victor 84 | ernest 85 | phillip 86 | todd 87 | jesse 88 | craig 89 | alan 90 | shawn 91 | clarence 92 | sean 93 | philip 94 | chris 95 | johnny 96 | earl 97 | jimmy 98 | antonio 99 | danny 100 | bryan 101 | tony 102 | luis 103 | mike 104 | stanley 105 | leonard 106 | nathan 107 | dale 108 | manuel 109 | rodney 110 | curtis 111 | norman 112 | marvin 113 | vincent 114 | glenn 115 | jeffery 116 | travis 117 | jeff 118 | chad 119 | jacob 120 | melvin 121 | alfred 122 | kyle 123 | francis 124 | bradley 125 | jesus 126 | herbert 127 | frederick 128 | ray 129 | joel 130 | edwin 131 | don 132 | eddie 133 | ricky 134 | troy 135 | randall 136 | barry 137 | bernard 138 | mario 139 | leroy 140 | francisco 141 | marcus 142 | micheal 143 | theodore 144 | clifford 145 | miguel 146 | oscar 147 | jay 148 | jim 149 | tom 150 | calvin 151 | alex 152 | jon 153 | ronnie 154 | bill 155 | lloyd 156 | tommy 157 | leon 158 | derek 159 | darrell 160 | jerome 161 | floyd 162 | leo 163 | alvin 164 | tim 165 | wesley 166 | dean 167 | greg 168 | jorge 169 | dustin 170 | pedro 171 | derrick 172 | dan 173 | zachary 174 | corey 175 | herman 176 | maurice 177 | vernon 178 | roberto 179 | clyde 180 | glen 181 | hector 182 | shane 183 | ricardo 184 | sam 185 | rick 186 | lester 187 | brent 188 | ramon 189 | tyler 190 | gilbert 191 | gene 192 | marc 193 | reginald 194 | ruben 195 | brett 196 | angel 197 | nathaniel 198 | rafael 199 | edgar 200 | milton 201 | raul 202 | ben 203 | cecil 204 | duane 205 | andre 206 | elmer 207 | brad 208 | gabriel 209 | ron 210 | roland 211 | jared 212 | adrian 213 | karl 214 | cory 215 | claude 216 | erik 217 | darryl 218 | neil 219 | christian 220 | javier 221 | fernando 222 | clinton 223 | ted 224 | mathew 225 | tyrone 226 | darren 227 | lonnie 228 | lance 229 | cody 230 | julio 231 | kurt 232 | allan 233 | clayton 234 | hugh 235 | max 236 | dwayne 237 | dwight 238 | armando 239 | felix 240 | jimmie 241 | everett 242 | ian 243 | ken 244 | bob 245 | jaime 246 | casey 247 | alfredo 248 | alberto 249 | dave 250 | ivan 251 | johnnie 252 | sidney 253 | byron 254 | julian 255 | isaac 256 | clifton 257 | willard 258 | daryl 259 | virgil 260 | andy 261 | salvador 262 | kirk 263 | sergio 264 | seth 265 | kent 266 | terrance 267 | rene 268 | eduardo 269 | terrence 270 | enrique 271 | freddie 272 | stuart 273 | fredrick 274 | arturo 275 | alejandro 276 | joey 277 | nick 278 | luther 279 | wendell 280 | jeremiah 281 | evan 282 | julius 283 | donnie 284 | otis 285 | trevor 286 | luke 287 | homer 288 | gerard 289 | doug 290 | kenny 291 | hubert 292 | angelo 293 | shaun 294 | lyle 295 | matt 296 | alfonso 297 | orlando 298 | rex 299 | carlton 300 | ernesto 301 | pablo 302 | lorenzo 303 | omar 304 | wilbur 305 | blake 306 | horace 307 | roderick 308 | kerry 309 | abraham 310 | rickey 311 | ira 312 | andres 313 | cesar 314 | johnathan 315 | malcolm 316 | rudolph 317 | damon 318 | kelvin 319 | rudy 320 | preston 321 | alton 322 | archie 323 | marco 324 | wm 325 | pete 326 | randolph 327 | garry 328 | geoffrey 329 | jonathon 330 | felipe 331 | bennie 332 | gerardo 333 | ed 334 | dominic 335 | loren 336 | delbert 337 | colin 338 | guillermo 339 | earnest 340 | benny 341 | noel 342 | rodolfo 343 | myron 344 | edmund 345 | salvatore 346 | cedric 347 | lowell 348 | gregg 349 | sherman 350 | devin 351 | sylvester 352 | roosevelt 353 | israel 354 | jermaine 355 | forrest 356 | wilbert 357 | leland 358 | simon 359 | irving 360 | owen 361 | rufus 362 | woodrow 363 | kristopher 364 | levi 365 | marcos 366 | gustavo 367 | lionel 368 | marty 369 | gilberto 370 | clint 371 | nicolas 372 | laurence 373 | ismael 374 | orville 375 | drew 376 | ervin 377 | dewey 378 | al 379 | wilfred 380 | josh 381 | hugo 382 | ignacio 383 | caleb 384 | tomas 385 | sheldon 386 | erick 387 | frankie 388 | darrel 389 | rogelio 390 | terence 391 | alonzo 392 | elias 393 | bert 394 | elbert 395 | ramiro 396 | conrad 397 | noah 398 | grady 399 | phil 400 | cornelius 401 | lamar 402 | rolando 403 | clay 404 | percy 405 | dexter 406 | bradford 407 | merle 408 | darin 409 | amos 410 | terrell 411 | moses 412 | irvin 413 | saul 414 | roman 415 | darnell 416 | randal 417 | tommie 418 | timmy 419 | darrin 420 | brendan 421 | toby 422 | van 423 | abel 424 | dominick 425 | emilio 426 | elijah 427 | cary 428 | domingo 429 | aubrey 430 | emmett 431 | marlon 432 | emanuel 433 | jerald 434 | edmond 435 | emil 436 | dewayne 437 | otto 438 | teddy 439 | reynaldo 440 | bret 441 | jess 442 | trent 443 | humberto 444 | emmanuel 445 | stephan 446 | louie 447 | vicente 448 | lamont 449 | garland 450 | micah 451 | efrain 452 | heath 453 | rodger 454 | demetrius 455 | ethan 456 | eldon 457 | rocky 458 | pierre 459 | eli 460 | bryce 461 | antoine 462 | robbie 463 | kendall 464 | royce 465 | sterling 466 | grover 467 | elton 468 | cleveland 469 | dylan 470 | chuck 471 | damian 472 | reuben 473 | stan 474 | leonardo 475 | russel 476 | erwin 477 | benito 478 | hans 479 | monte 480 | blaine 481 | ernie 482 | curt 483 | quentin 484 | agustin 485 | jamal 486 | devon 487 | adolfo 488 | tyson 489 | wilfredo 490 | bart 491 | jarrod 492 | vance 493 | denis 494 | damien 495 | joaquin 496 | harlan 497 | desmond 498 | elliot 499 | darwin 500 | gregorio 501 | kermit 502 | roscoe 503 | esteban 504 | anton 505 | solomon 506 | norbert 507 | elvin 508 | nolan 509 | carey 510 | rod 511 | quinton 512 | hal 513 | brain 514 | rob 515 | elwood 516 | kendrick 517 | darius 518 | moises 519 | marlin 520 | fidel 521 | thaddeus 522 | cliff 523 | marcel 524 | ali 525 | raphael 526 | bryon 527 | armand 528 | alvaro 529 | jeffry 530 | dane 531 | joesph 532 | thurman 533 | ned 534 | sammie 535 | rusty 536 | michel 537 | monty 538 | rory 539 | fabian 540 | reggie 541 | kris 542 | isaiah 543 | gus 544 | avery 545 | loyd 546 | diego 547 | adolph 548 | millard 549 | rocco 550 | gonzalo 551 | derick 552 | rodrigo 553 | gerry 554 | rigoberto 555 | alphonso 556 | ty 557 | rickie 558 | noe 559 | vern 560 | elvis 561 | bernardo 562 | mauricio 563 | hiram 564 | donovan 565 | basil 566 | nickolas 567 | scot 568 | vince 569 | quincy 570 | eddy 571 | sebastian 572 | federico 573 | ulysses 574 | heriberto 575 | donnell 576 | denny 577 | gavin 578 | emery 579 | romeo 580 | jayson 581 | dion 582 | dante 583 | clement 584 | coy 585 | odell 586 | jarvis 587 | bruno 588 | issac 589 | dudley 590 | sanford 591 | colby 592 | carmelo 593 | nestor 594 | hollis 595 | stefan 596 | donny 597 | art 598 | linwood 599 | beau 600 | weldon 601 | galen 602 | isidro 603 | truman 604 | delmar 605 | johnathon 606 | silas 607 | frederic 608 | irwin 609 | merrill 610 | charley 611 | marcelino 612 | carlo 613 | trenton 614 | kurtis 615 | aurelio 616 | winfred 617 | vito 618 | collin 619 | denver 620 | leonel 621 | emory 622 | pasquale 623 | mohammad 624 | mariano 625 | danial 626 | landon 627 | dirk 628 | branden 629 | adan 630 | numbers 631 | clair 632 | buford 633 | german 634 | bernie 635 | wilmer 636 | emerson 637 | zachery 638 | jacques 639 | errol 640 | josue 641 | edwardo 642 | wilford 643 | theron 644 | raymundo 645 | daren 646 | tristan 647 | robby 648 | lincoln 649 | jame 650 | genaro 651 | octavio 652 | cornell 653 | hung 654 | arron 655 | antony 656 | herschel 657 | alva 658 | giovanni 659 | garth 660 | cyrus 661 | cyril 662 | ronny 663 | stevie 664 | lon 665 | kennith 666 | carmine 667 | augustine 668 | erich 669 | chadwick 670 | wilburn 671 | russ 672 | myles 673 | jonas 674 | mitchel 675 | mervin 676 | zane 677 | jamel 678 | lazaro 679 | alphonse 680 | randell 681 | major 682 | johnie 683 | jarrett 684 | ariel 685 | abdul 686 | dusty 687 | luciano 688 | seymour 689 | scottie 690 | eugenio 691 | mohammed 692 | valentin 693 | arnulfo 694 | lucien 695 | ferdinand 696 | thad 697 | ezra 698 | aldo 699 | rubin 700 | royal 701 | mitch 702 | earle 703 | abe 704 | marquis 705 | lanny 706 | kareem 707 | jamar 708 | boris 709 | isiah 710 | emile 711 | elmo 712 | aron 713 | leopoldo 714 | everette 715 | josef 716 | eloy 717 | dorian 718 | rodrick 719 | reinaldo 720 | lucio 721 | jerrod 722 | weston 723 | hershel 724 | lemuel 725 | lavern 726 | burt 727 | jules 728 | gil 729 | eliseo 730 | ahmad 731 | nigel 732 | efren 733 | antwan 734 | alden 735 | margarito 736 | refugio 737 | dino 738 | osvaldo 739 | les 740 | deandre 741 | normand 742 | kieth 743 | ivory 744 | trey 745 | norberto 746 | napoleon 747 | jerold 748 | fritz 749 | rosendo 750 | milford 751 | sang 752 | deon 753 | christoper 754 | alfonzo 755 | lyman 756 | josiah 757 | brant 758 | wilton 759 | rico 760 | jamaal 761 | dewitt 762 | brenton 763 | yong 764 | olin 765 | faustino 766 | claudio 767 | judson 768 | gino 769 | edgardo 770 | alec 771 | jarred 772 | donn 773 | trinidad 774 | tad 775 | porfirio 776 | odis 777 | lenard 778 | chauncey 779 | tod 780 | mel 781 | marcelo 782 | kory 783 | augustus 784 | keven 785 | hilario 786 | bud 787 | sal 788 | orval 789 | mauro 790 | dannie 791 | zachariah 792 | olen 793 | anibal 794 | milo 795 | jed 796 | thanh 797 | amado 798 | lenny 799 | tory 800 | richie 801 | horacio 802 | brice 803 | mohamed 804 | delmer 805 | dario 806 | mac 807 | jonah 808 | jerrold 809 | robt 810 | hank 811 | sung 812 | rupert 813 | rolland 814 | kenton 815 | damion 816 | chi 817 | antone 818 | waldo 819 | fredric 820 | bradly 821 | kip 822 | burl 823 | tyree 824 | jefferey 825 | ahmed 826 | willy 827 | stanford 828 | oren 829 | moshe 830 | mikel 831 | enoch 832 | brendon 833 | quintin 834 | jamison 835 | florencio 836 | darrick 837 | tobias 838 | minh 839 | hassan 840 | giuseppe 841 | demarcus 842 | cletus 843 | tyrell 844 | lyndon 845 | keenan 846 | werner 847 | theo 848 | geraldo 849 | columbus 850 | chet 851 | bertram 852 | markus 853 | huey 854 | hilton 855 | dwain 856 | donte 857 | tyron 858 | omer 859 | isaias 860 | hipolito 861 | fermin 862 | chung 863 | adalberto 864 | jamey 865 | teodoro 866 | mckinley 867 | maximo 868 | sol 869 | raleigh 870 | lawerence 871 | abram 872 | rashad 873 | emmitt 874 | daron 875 | chong 876 | samual 877 | otha 878 | miquel 879 | eusebio 880 | dong 881 | domenic 882 | darron 883 | wilber 884 | renato 885 | hoyt 886 | haywood 887 | ezekiel 888 | chas 889 | florentino 890 | elroy 891 | clemente 892 | arden 893 | neville 894 | edison 895 | deshawn 896 | carrol 897 | shayne 898 | nathanial 899 | jordon 900 | danilo 901 | claud 902 | val 903 | sherwood 904 | raymon 905 | rayford 906 | cristobal 907 | ambrose 908 | titus 909 | hyman 910 | felton 911 | ezequiel 912 | erasmo 913 | lonny 914 | len 915 | ike 916 | milan 917 | lino 918 | jarod 919 | herb 920 | andreas 921 | rhett 922 | jude 923 | douglass 924 | cordell 925 | oswaldo 926 | ellsworth 927 | virgilio 928 | toney 929 | nathanael 930 | del 931 | benedict 932 | mose 933 | hong 934 | isreal 935 | garret 936 | fausto 937 | asa 938 | arlen 939 | zack 940 | modesto 941 | francesco 942 | manual 943 | jae 944 | gaylord 945 | gaston 946 | filiberto 947 | deangelo 948 | michale 949 | granville 950 | wes 951 | malik 952 | zackary 953 | tuan 954 | nicky 955 | cristopher 956 | antione 957 | malcom 958 | korey 959 | jospeh 960 | colton 961 | waylon 962 | von 963 | hosea 964 | shad 965 | santo 966 | rudolf 967 | rolf 968 | rey 969 | renaldo 970 | marcellus 971 | lucius 972 | kristofer 973 | harland 974 | arnoldo 975 | rueben 976 | leandro 977 | kraig 978 | jerrell 979 | jeromy 980 | hobert 981 | cedrick 982 | arlie 983 | winford 984 | wally 985 | luigi 986 | keneth 987 | jacinto 988 | graig 989 | franklyn 990 | edmundo 991 | sid 992 | leif 993 | jeramy 994 | willian 995 | vincenzo 996 | shon 997 | michal 998 | lynwood 999 | jere 1000 | hai 1001 | elden 1002 | darell 1003 | broderick 1004 | alonso -------------------------------------------------------------------------------- /lib/zxcvbn.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require 'zxcvbn/version' 3 | require 'zxcvbn/tester' 4 | 5 | module Zxcvbn 6 | extend self 7 | 8 | DATA_PATH = Pathname(File.expand_path('../../data', __FILE__)) 9 | 10 | # Returns a Zxcvbn::Score for the given password 11 | # 12 | # Example: 13 | # 14 | # Zxcvbn.test("password").score #=> 0 15 | def test(password, user_inputs = [], word_lists = {}) 16 | tester = Tester.new 17 | tester.add_word_lists(word_lists) 18 | tester.test(password, user_inputs) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/zxcvbn/crack_time.rb: -------------------------------------------------------------------------------- 1 | module Zxcvbn 2 | module CrackTime 3 | SINGLE_GUESS = 0.010 4 | NUM_ATTACKERS = 100 5 | 6 | SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS 7 | 8 | def entropy_to_crack_time(entropy) 9 | 0.5 * (2 ** entropy) * SECONDS_PER_GUESS 10 | end 11 | 12 | def crack_time_to_score(seconds) 13 | case 14 | when seconds < 10**2 15 | 0 16 | when seconds < 10**4 17 | 1 18 | when seconds < 10**6 19 | 2 20 | when seconds < 10**8 21 | 3 22 | else 23 | 4 24 | end 25 | end 26 | 27 | def display_time(seconds) 28 | minute = 60 29 | hour = minute * 60 30 | day = hour * 24 31 | month = day * 31 32 | year = month * 12 33 | century = year * 100 34 | 35 | case 36 | when seconds < minute 37 | 'instant' 38 | when seconds < hour 39 | "#{1 + (seconds / minute).ceil} minutes" 40 | when seconds < day 41 | "#{1 + (seconds / hour).ceil} hours" 42 | when seconds < month 43 | "#{1 + (seconds / day).ceil} days" 44 | when seconds < year 45 | "#{1 + (seconds / month).ceil} months" 46 | when seconds < century 47 | "#{1 + (seconds / year).ceil} years" 48 | else 49 | 'centuries' 50 | end 51 | end 52 | end 53 | end -------------------------------------------------------------------------------- /lib/zxcvbn/data.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'zxcvbn/dictionary_ranker' 3 | 4 | module Zxcvbn 5 | class Data 6 | def initialize 7 | @ranked_dictionaries = DictionaryRanker.rank_dictionaries( 8 | "english" => read_word_list("english.txt"), 9 | "female_names" => read_word_list("female_names.txt"), 10 | "male_names" => read_word_list("male_names.txt"), 11 | "passwords" => read_word_list("passwords.txt"), 12 | "surnames" => read_word_list("surnames.txt") 13 | ) 14 | @adjacency_graphs = JSON.load(DATA_PATH.join('adjacency_graphs.json').read) 15 | end 16 | 17 | attr_reader :ranked_dictionaries, :adjacency_graphs 18 | 19 | def add_word_list(name, list) 20 | @ranked_dictionaries[name] = DictionaryRanker.rank_dictionary(list) 21 | end 22 | 23 | private 24 | 25 | def read_word_list(file) 26 | DATA_PATH.join("frequency_lists", file).read.split 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/zxcvbn/dictionary_ranker.rb: -------------------------------------------------------------------------------- 1 | module Zxcvbn 2 | class DictionaryRanker 3 | def self.rank_dictionaries(lists) 4 | lists.each_with_object({}) do |(dict_name, words), dictionaries| 5 | dictionaries[dict_name] = rank_dictionary(words) 6 | end 7 | end 8 | 9 | def self.rank_dictionary(words) 10 | words.each_with_index 11 | .with_object({}) do |(word, i), dictionary| 12 | dictionary[word.downcase] = i + 1 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/zxcvbn/entropy.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/math' 2 | 3 | module Zxcvbn::Entropy 4 | include Zxcvbn::Math 5 | 6 | def calc_entropy(match) 7 | return match.entropy unless match.entropy.nil? 8 | 9 | match.entropy = case match.pattern 10 | when 'repeat' 11 | repeat_entropy(match) 12 | when 'sequence' 13 | sequence_entropy(match) 14 | when 'digits' 15 | digits_entropy(match) 16 | when 'year' 17 | year_entropy(match) 18 | when 'date' 19 | date_entropy(match) 20 | when 'spatial' 21 | spatial_entropy(match) 22 | when 'dictionary' 23 | dictionary_entropy(match) 24 | else 25 | 0 26 | end 27 | end 28 | 29 | def repeat_entropy(match) 30 | cardinality = bruteforce_cardinality match.token 31 | lg(cardinality * match.token.length) 32 | end 33 | 34 | def sequence_entropy(match) 35 | first_char = match.token[0] 36 | base_entropy = if ['a', '1'].include?(first_char) 37 | 1 38 | elsif first_char.match(/\d/) 39 | lg(10) 40 | elsif first_char.match(/[a-z]/) 41 | lg(26) 42 | else 43 | lg(26) + 1 44 | end 45 | base_entropy += 1 unless match.ascending 46 | base_entropy + lg(match.token.length) 47 | end 48 | 49 | def digits_entropy(match) 50 | lg(10 ** match.token.length) 51 | end 52 | 53 | NUM_YEARS = 119 # years match against 1900 - 2019 54 | NUM_MONTHS = 12 55 | NUM_DAYS = 31 56 | 57 | def year_entropy(match) 58 | lg(NUM_YEARS) 59 | end 60 | 61 | def date_entropy(match) 62 | if match.year < 100 63 | entropy = lg(NUM_DAYS * NUM_MONTHS * 100) 64 | else 65 | entropy = lg(NUM_DAYS * NUM_MONTHS * NUM_YEARS) 66 | end 67 | 68 | if match.separator 69 | entropy += 2 70 | end 71 | 72 | entropy 73 | end 74 | 75 | def dictionary_entropy(match) 76 | match.base_entropy = lg(match.rank) 77 | match.uppercase_entropy = extra_uppercase_entropy(match) 78 | match.l33t_entropy = extra_l33t_entropy(match) 79 | 80 | match.base_entropy + match.uppercase_entropy + match.l33t_entropy 81 | end 82 | 83 | START_UPPER = /^[A-Z][^A-Z]+$/ 84 | END_UPPER = /^[^A-Z]+[A-Z]$/ 85 | ALL_UPPER = /^[A-Z]+$/ 86 | ALL_LOWER = /^[a-z]+$/ 87 | 88 | def extra_uppercase_entropy(match) 89 | word = match.token 90 | [START_UPPER, END_UPPER, ALL_UPPER].each do |regex| 91 | return 1 if word.match(regex) 92 | end 93 | num_upper = word.chars.count{|c| c.match(/[A-Z]/) } 94 | num_lower = word.chars.count{|c| c.match(/[a-z]/) } 95 | possibilities = 0 96 | (0..[num_upper, num_lower].min).each do |i| 97 | possibilities += nCk(num_upper + num_lower, i) 98 | end 99 | lg(possibilities) 100 | end 101 | 102 | def extra_l33t_entropy(match) 103 | word = match.token 104 | return 0 unless match.l33t 105 | possibilities = 0 106 | match.sub.each do |subbed, unsubbed| 107 | num_subbed = word.chars.count{|c| c == subbed} 108 | num_unsubbed = word.chars.count{|c| c == unsubbed} 109 | (0..[num_subbed, num_unsubbed].min).each do |i| 110 | possibilities += nCk(num_subbed + num_unsubbed, i) 111 | end 112 | end 113 | entropy = lg(possibilities) 114 | entropy == 0 ? 1 : entropy 115 | end 116 | 117 | def spatial_entropy(match) 118 | if %w|qwerty dvorak|.include? match.graph 119 | starting_positions = starting_positions_for_graph('qwerty') 120 | average_degree = average_degree_for_graph('qwerty') 121 | else 122 | starting_positions = starting_positions_for_graph('keypad') 123 | average_degree = average_degree_for_graph('keypad') 124 | end 125 | 126 | possibilities = 0 127 | token_length = match.token.length 128 | turns = match.turns 129 | 130 | # estimate the ngpumber of possible patterns w/ token length or less with number of turns or less. 131 | (2..token_length).each do |i| 132 | possible_turns = [turns, i - 1].min 133 | (1..possible_turns).each do |j| 134 | possibilities += nCk(i - 1, j - 1) * starting_positions * average_degree ** j 135 | end 136 | end 137 | 138 | entropy = lg possibilities 139 | # add extra entropy for shifted keys. (% instead of 5, A instead of a.) 140 | # math is similar to extra entropy from uppercase letters in dictionary matches. 141 | 142 | if match.shifted_count 143 | shiffted_count = match.shifted_count 144 | unshifted_count = match.token.length - match.shifted_count 145 | possibilities = 0 146 | 147 | (0..[shiffted_count, unshifted_count].min).each do |i| 148 | possibilities += nCk(shiffted_count + unshifted_count, i) 149 | end 150 | entropy += lg possibilities 151 | end 152 | entropy 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /lib/zxcvbn/feedback.rb: -------------------------------------------------------------------------------- 1 | module Zxcvbn 2 | class Feedback 3 | attr_accessor :warning, :suggestions 4 | 5 | def initialize(options = {}) 6 | @warning = options[:warning] 7 | @suggestions = options[:suggestions] || [] 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/zxcvbn/feedback_giver.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/entropy' 2 | require 'zxcvbn/feedback' 3 | 4 | module Zxcvbn 5 | class FeedbackGiver 6 | NAME_DICTIONARIES = %w[surnames male_names female_names].freeze 7 | 8 | DEFAULT_FEEDBACK = Feedback.new( 9 | suggestions: [ 10 | 'Use a few words, avoid common phrases', 11 | 'No need for symbols, digits, or uppercase letters' 12 | ] 13 | ).freeze 14 | 15 | EMPTY_FEEDBACK = Feedback.new.freeze 16 | 17 | def self.get_feedback(score, sequence) 18 | # starting feedback 19 | return DEFAULT_FEEDBACK if sequence.length.zero? 20 | 21 | # no feedback if score is good or great. 22 | return EMPTY_FEEDBACK if score > 2 23 | 24 | # tie feedback to the longest match for longer sequences 25 | longest_match = sequence[0] 26 | for match in sequence[1..-1] 27 | longest_match = match if match.token.length > longest_match.token.length 28 | end 29 | 30 | feedback = get_match_feedback(longest_match, sequence.length == 1) 31 | extra_feedback = 'Add another word or two. Uncommon words are better.' 32 | 33 | if feedback.nil? 34 | feedback = Feedback.new(suggestions: [extra_feedback]) 35 | else 36 | feedback.suggestions.unshift extra_feedback 37 | end 38 | 39 | feedback 40 | end 41 | 42 | def self.get_match_feedback(match, is_sole_match) 43 | case match.pattern 44 | when 'dictionary' 45 | get_dictionary_match_feedback match, is_sole_match 46 | 47 | when 'spatial' 48 | layout = match.graph.upcase 49 | warning = if match.turns == 1 50 | 'Straight rows of keys are easy to guess' 51 | else 52 | 'Short keyboard patterns are easy to guess' 53 | end 54 | 55 | Feedback.new( 56 | warning: warning, 57 | suggestions: [ 58 | 'Use a longer keyboard pattern with more turns' 59 | ] 60 | ) 61 | 62 | when 'repeat' 63 | Feedback.new( 64 | warning: 'Repeats like "aaa" are easy to guess', 65 | suggestions: [ 66 | 'Avoid repeated words and characters' 67 | ] 68 | ) 69 | 70 | when 'sequence' 71 | Feedback.new( 72 | warning: 'Sequences like abc or 6543 are easy to guess', 73 | suggestions: [ 74 | 'Avoid sequences' 75 | ] 76 | ) 77 | 78 | when 'date' 79 | Feedback.new( 80 | warning: 'Dates are often easy to guess', 81 | suggestions: [ 82 | 'Avoid dates and years that are associated with you' 83 | ] 84 | ) 85 | end 86 | end 87 | 88 | def self.get_dictionary_match_feedback(match, is_sole_match) 89 | warning = if match.dictionary_name == 'passwords' 90 | if is_sole_match && !match.l33t && !match.reversed 91 | if match.rank <= 10 92 | 'This is a top-10 common password' 93 | elsif match.rank <= 100 94 | 'This is a top-100 common password' 95 | else 96 | 'This is a very common password' 97 | end 98 | else 99 | 'This is similar to a commonly used password' 100 | end 101 | elsif NAME_DICTIONARIES.include? match.dictionary_name 102 | if is_sole_match 103 | 'Names and surnames by themselves are easy to guess' 104 | else 105 | 'Common names and surnames are easy to guess' 106 | end 107 | end 108 | 109 | suggestions = [] 110 | word = match.token 111 | 112 | if word =~ Zxcvbn::Entropy::START_UPPER 113 | suggestions.push "Capitalization doesn't help very much" 114 | elsif word =~ Zxcvbn::Entropy::ALL_UPPER && word.downcase != word 115 | suggestions.push( 116 | 'All-uppercase is almost as easy to guess as all-lowercase' 117 | ) 118 | end 119 | 120 | if match.l33t 121 | suggestions.push( 122 | "Predictable substitutions like '@' instead of 'a' \ 123 | don't help very much" 124 | ) 125 | end 126 | 127 | Feedback.new( 128 | warning: warning, 129 | suggestions: suggestions 130 | ) 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/zxcvbn/match.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module Zxcvbn 4 | class Match < OpenStruct 5 | def to_hash 6 | @table.keys.sort.each_with_object({}) do |key, hash| 7 | hash[key.to_s] = @table[key] 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/zxcvbn/matchers/date.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/matchers/regex_helpers' 2 | 3 | module Zxcvbn 4 | module Matchers 5 | class Date 6 | include RegexHelpers 7 | 8 | YEAR_SUFFIX = / 9 | ( \d{1,2} ) # day or month 10 | ( \s | \- | \/ | \\ | \_ | \. ) # separator 11 | ( \d{1,2} ) # month or day 12 | \2 # same separator 13 | ( 19\d{2} | 200\d | 201\d | \d{2} ) # year 14 | /x 15 | 16 | YEAR_PREFIX = / 17 | ( 19\d{2} | 200\d | 201\d | \d{2} ) # year 18 | ( \s | - | \/ | \\ | _ | \. ) # separator 19 | ( \d{1,2} ) # day or month 20 | \2 # same separator 21 | ( \d{1,2} ) # month or day 22 | /x 23 | 24 | WITHOUT_SEPARATOR = /\d{4,8}/ 25 | 26 | def matches(password) 27 | match_with_separator(password) + match_without_separator(password) 28 | end 29 | 30 | def match_with_separator(password) 31 | result = [] 32 | re_match_all(YEAR_SUFFIX, password) do |match, re_match| 33 | match.pattern = 'date' 34 | match.separator = re_match[2] 35 | match.year = re_match[4].to_i 36 | 37 | day = re_match[1].to_i 38 | month = re_match[3].to_i 39 | 40 | if month <= 12 41 | match.day = day 42 | match.month = month 43 | else 44 | match.day = month 45 | match.month = day 46 | end 47 | 48 | result << match if valid_date?(match.day, match.month, match.year) 49 | end 50 | result 51 | end 52 | 53 | def match_without_separator(password) 54 | result = [] 55 | re_match_all(WITHOUT_SEPARATOR, password) do |match, re_match| 56 | extract_dates(match.token).each do |candidate| 57 | day, month, year = candidate[:day], candidate[:month], candidate[:year] 58 | 59 | match.pattern = 'date' 60 | match.day = day 61 | match.month = month 62 | match.year = year 63 | match.separator = '' 64 | result << match 65 | end 66 | end 67 | result 68 | end 69 | 70 | def extract_dates(token) 71 | dates = [] 72 | date_patterns_for_length(token.length).map do |pattern| 73 | candidate = { 74 | :year => '', 75 | :month => '', 76 | :day => '' 77 | } 78 | for i in 0...token.length 79 | candidate[PATTERN_CHAR_TO_SYM[pattern[i]]] << token[i] 80 | end 81 | candidate.each do |component, value| 82 | candidate[component] = value.to_i 83 | end 84 | 85 | candidate[:year] = expand_year(candidate[:year]) 86 | 87 | if valid_date?(candidate[:day], candidate[:month], candidate[:year]) && !matches_year?(token) 88 | dates << candidate 89 | end 90 | end 91 | dates 92 | end 93 | 94 | DATE_PATTERN_FOR_LENGTH = { 95 | 8 => %w[ yyyymmdd ddmmyyyy mmddyyyy ], 96 | 7 => %w[ yyyymdd yyyymmd ddmyyyy dmmyyyy ], 97 | 6 => %w[ yymmdd ddmmyy mmddyy ], 98 | 5 => %w[ yymdd yymmd ddmyy dmmyy mmdyy mddyy ], 99 | 4 => %w[ yymd dmyy mdyy ] 100 | } 101 | 102 | PATTERN_CHAR_TO_SYM = { 103 | 'y' => :year, 104 | 'm' => :month, 105 | 'd' => :day 106 | } 107 | 108 | def date_patterns_for_length(length) 109 | DATE_PATTERN_FOR_LENGTH[length] || [] 110 | end 111 | 112 | def valid_date?(day, month, year) 113 | return false if day > 31 || month > 12 114 | return false unless year >= 1900 && year <= 2019 115 | true 116 | end 117 | 118 | def matches_year?(token) 119 | token.size == 4 && Year::YEAR_REGEX.match(token) 120 | end 121 | 122 | def expand_year(year) 123 | return year 124 | # Block dates with 2 digit years for now to be compatible with the JS version 125 | # return year unless year < 100 126 | # now = Time.now.year 127 | # if year <= 19 128 | # year + 2000 129 | # else 130 | # year + 1900 131 | # end 132 | end 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/zxcvbn/matchers/dictionary.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/match' 2 | 3 | module Zxcvbn 4 | module Matchers 5 | # Given a password and a dictionary, match on any sequential segment of 6 | # the lowercased password in the dictionary 7 | 8 | class Dictionary 9 | def initialize(name, ranked_dictionary) 10 | @name = name 11 | @ranked_dictionary = ranked_dictionary 12 | end 13 | 14 | def matches(password) 15 | results = [] 16 | password_length = password.length 17 | lowercased_password = password.downcase 18 | (0..password_length).each do |i| 19 | (i...password_length).each do |j| 20 | word = lowercased_password[i..j] 21 | if @ranked_dictionary.has_key?(word) 22 | results << Match.new(:matched_word => word, 23 | :token => password[i..j], 24 | :i => i, 25 | :j => j, 26 | :rank => @ranked_dictionary[word], 27 | :pattern => 'dictionary', 28 | :dictionary_name => @name) 29 | end 30 | end 31 | end 32 | results 33 | end 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /lib/zxcvbn/matchers/digits.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/matchers/regex_helpers' 2 | 3 | module Zxcvbn 4 | module Matchers 5 | class Digits 6 | include RegexHelpers 7 | 8 | DIGITS_REGEX = /\d{3,}/ 9 | 10 | def matches(password) 11 | result = [] 12 | re_match_all(DIGITS_REGEX, password) do |match| 13 | match.pattern = 'digits' 14 | result << match 15 | end 16 | result 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/zxcvbn/matchers/l33t.rb: -------------------------------------------------------------------------------- 1 | module Zxcvbn 2 | module Matchers 3 | class L33t 4 | L33T_TABLE = { 5 | 'a' => ['4', '@'], 6 | 'b' => ['8'], 7 | 'c' => ['(', '{', '[', '<'], 8 | 'e' => ['3'], 9 | 'g' => ['6', '9'], 10 | 'i' => ['1', '!', '|'], 11 | 'l' => ['1', '|', '7'], 12 | 'o' => ['0'], 13 | 's' => ['$', '5'], 14 | 't' => ['+', '7'], 15 | 'x' => ['%'], 16 | 'z' => ['2'] 17 | } 18 | 19 | def initialize(dictionary_matchers) 20 | @dictionary_matchers = dictionary_matchers 21 | end 22 | 23 | def matches(password) 24 | matches = [] 25 | lowercased_password = password.downcase 26 | combinations_to_try = l33t_subs(relevent_l33t_subtable(lowercased_password)) 27 | combinations_to_try.each do |substitution| 28 | @dictionary_matchers.each do |matcher| 29 | subbed_password = translate(lowercased_password, substitution) 30 | matcher.matches(subbed_password).each do |match| 31 | token = password[match.i..match.j] 32 | next if token.downcase == match.matched_word.downcase 33 | match_substitutions = {} 34 | substitution.each do |s, letter| 35 | match_substitutions[s] = letter if token.include?(s) 36 | end 37 | match.l33t = true 38 | match.token = password[match.i..match.j] 39 | match.sub = match_substitutions 40 | match.sub_display = match_substitutions.map do |k, v| 41 | "#{k} -> #{v}" 42 | end.join(', ') 43 | matches << match 44 | end 45 | end 46 | end 47 | matches 48 | end 49 | 50 | def translate(password, sub) 51 | password.split('').map do |chr| 52 | sub[chr] || chr 53 | end.join 54 | end 55 | 56 | def relevent_l33t_subtable(password) 57 | filtered = {} 58 | L33T_TABLE.each do |letter, subs| 59 | relevent_subs = subs.select { |s| password.include?(s) } 60 | filtered[letter] = relevent_subs unless relevent_subs.empty? 61 | end 62 | filtered 63 | end 64 | 65 | def l33t_subs(table) 66 | keys = table.keys 67 | subs = [[]] 68 | subs = find_substitutions(subs, table, keys) 69 | new_subs = [] 70 | subs.each do |sub| 71 | hash = {} 72 | sub.each do |l33t_char, chr| 73 | hash[l33t_char] = chr 74 | end 75 | new_subs << hash 76 | end 77 | new_subs 78 | end 79 | 80 | def find_substitutions(subs, table, keys) 81 | return subs if keys.empty? 82 | first_key = keys[0] 83 | rest_keys = keys[1..-1] 84 | next_subs = [] 85 | table[first_key].each do |l33t_char| 86 | subs.each do |sub| 87 | dup_l33t_index = -1 88 | (0...sub.length).each do |i| 89 | if sub[i][0] == l33t_char 90 | dup_l33t_index = i 91 | break 92 | end 93 | end 94 | 95 | if dup_l33t_index == -1 96 | sub_extension = sub + [[l33t_char, first_key]] 97 | next_subs << sub_extension 98 | else 99 | sub_alternative = sub.dup 100 | sub_alternative[dup_l33t_index, 1] = [[l33t_char, first_key]] 101 | next_subs << sub 102 | next_subs << sub_alternative 103 | end 104 | end 105 | end 106 | subs = dedup(next_subs) 107 | find_substitutions(subs, table, rest_keys) 108 | end 109 | 110 | def dedup(subs) 111 | deduped = [] 112 | members = [] 113 | subs.each do |sub| 114 | assoc = sub.dup 115 | 116 | assoc.sort! rescue debugger 117 | label = assoc.map{|k, v| "#{k},#{v}"}.join('-') 118 | unless members.include?(label) 119 | members << label 120 | deduped << sub 121 | end 122 | end 123 | deduped 124 | end 125 | end 126 | end 127 | end -------------------------------------------------------------------------------- /lib/zxcvbn/matchers/new_l33t.rb: -------------------------------------------------------------------------------- 1 | module Zxcvbn 2 | module Matchers 3 | class L33t 4 | L33T_TABLE = { 5 | 'a' => ['4', '@'], 6 | 'b' => ['8'], 7 | 'c' => ['(', '{', '[', '<'], 8 | 'e' => ['3'], 9 | 'g' => ['6', '9'], 10 | 'i' => ['1', '!', '|'], 11 | 'l' => ['1', '|', '7'], 12 | 'o' => ['0'], 13 | 's' => ['$', '5'], 14 | 't' => ['+', '7'], 15 | 'x' => ['%'], 16 | 'z' => ['2'] 17 | } 18 | 19 | def initialize(dictionary_matchers) 20 | @dictionary_matchers = dictionary_matchers 21 | end 22 | 23 | def matches(password) 24 | matches = [] 25 | lowercased_password = password.downcase 26 | combinations_to_try = substitution_combinations(relevant_l33t_substitutions(lowercased_password)) 27 | # debugger if password == 'abcdefghijk987654321' 28 | combinations_to_try.each do |substitution| 29 | @dictionary_matchers.each do |matcher| 30 | subbed_password = substitute(lowercased_password, substitution) 31 | matcher.matches(subbed_password).each do |match| 32 | token = lowercased_password[match.i..match.j] 33 | next if token == match.matched_word.downcase 34 | # debugger if token == '1' 35 | match_substitutions = {} 36 | substitution.each do |letter, substitution| 37 | match_substitutions[substitution] = letter if token.include?(substitution) 38 | end 39 | match.l33t = true 40 | match.token = password[match.i..match.j] 41 | match.sub = match_substitutions 42 | match.sub_display = match_substitutions.map do |k, v| 43 | "#{k} -> #{v}" 44 | end.join(', ') 45 | matches << match 46 | end 47 | end 48 | end 49 | matches 50 | end 51 | 52 | def substitute(password, substitution) 53 | subbed_password = password.dup 54 | substitution.each do |letter, substitution| 55 | subbed_password.gsub!(substitution, letter) 56 | end 57 | subbed_password 58 | end 59 | 60 | # produces a l33t table of substitutions present in the given password 61 | def relevant_l33t_substitutions(password) 62 | subs = Hash.new do |hash, key| 63 | hash[key] = [] 64 | end 65 | L33T_TABLE.each do |letter, substibutions| 66 | password.each_char do |password_char| 67 | if substibutions.include?(password_char) 68 | subs[letter] << password_char 69 | end 70 | end 71 | end 72 | subs 73 | end 74 | 75 | # takes a character substitutions hash and produces an array of all 76 | # possible substitution combinations 77 | def substitution_combinations(subs_hash) 78 | combinations = [] 79 | expanded_substitutions = expanded_substitutions(subs_hash) 80 | 81 | # build an array of all possible combinations 82 | expanded_substitutions.each do |substitution_hash| 83 | # convert a hash to an array of hashes with 1 key each 84 | subs_array = substitution_hash.map do |letter, substitutions| 85 | {letter => substitutions} 86 | end 87 | combinations << subs_array 88 | 89 | # find all possible combinations for each number of combinations available 90 | subs_array.combination(subs_array.size).each do |combination| 91 | # Don't add duplicates 92 | combinations << combination unless combinations.include?(combination) 93 | end 94 | end 95 | 96 | # convert back to simple hash per substitution combination 97 | combination_hashes = combinations.map do |combination_set| 98 | hash = {} 99 | combination_set.each do |combination_hash| 100 | hash.merge!(combination_hash) 101 | end 102 | hash 103 | end 104 | 105 | combination_hashes 106 | end 107 | 108 | # expand possible combinations if multiple characters can be substituted 109 | # e.g. {'a' => ['4', '@'], 'i' => ['1']} expands to 110 | # [{'a' => '4', 'i' => 1}, {'a' => '@', 'i' => '1'}] 111 | def expanded_substitutions(hash) 112 | return {} if hash.empty? 113 | values = hash.values 114 | product_values = values[0].product(*values[1..-1]) 115 | product_values.map{ |p| Hash[hash.keys.zip(p)] } 116 | end 117 | 118 | end 119 | end 120 | end -------------------------------------------------------------------------------- /lib/zxcvbn/matchers/regex_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/match' 2 | 3 | module Zxcvbn 4 | module Matchers 5 | module RegexHelpers 6 | def re_match_all(regex, password) 7 | pos = 0 8 | while re_match = regex.match(password, pos) 9 | i, j = re_match.offset(0) 10 | pos = j 11 | j -= 1 12 | 13 | match = Match.new( 14 | :i => i, 15 | :j => j, 16 | :token => password[i..j] 17 | ) 18 | yield match, re_match 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/zxcvbn/matchers/repeat.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/match' 2 | 3 | module Zxcvbn 4 | module Matchers 5 | class Repeat 6 | def matches(password) 7 | result = [] 8 | i = 0 9 | while i < password.length 10 | cur_char = password[i] 11 | j = i + 1 12 | while cur_char == password[j] 13 | j += 1 14 | end 15 | 16 | if j - i > 2 # don't consider length 1 or 2 chains. 17 | result << Match.new( 18 | :pattern => 'repeat', 19 | :i => i, 20 | :j => j-1, 21 | :token => password[i...j], 22 | :repeated_char => cur_char 23 | ) 24 | end 25 | 26 | i = j 27 | end 28 | result 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/zxcvbn/matchers/sequences.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/match' 2 | 3 | module Zxcvbn 4 | module Matchers 5 | class Sequences 6 | SEQUENCES = { 7 | 'lower' => 'abcdefghijklmnopqrstuvwxyz', 8 | 'upper' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 9 | 'digits' => '01234567890' 10 | } 11 | 12 | def seq_match_length(password, from, direction, seq) 13 | index_from = seq.index(password[from]) 14 | j = 1 15 | while from + j < password.length && 16 | password[from + j] == seq[index_from + direction * j] 17 | j+= 1 18 | end 19 | j 20 | end 21 | 22 | # find the first matching sequence, and return with 23 | # direction, if characters are one apart in the sequence 24 | def applicable_sequence(password, i) 25 | SEQUENCES.each do |name, sequence| 26 | index1 = sequence.index(password[i]) 27 | index2 = sequence.index(password[i+1]) 28 | if index1 and index2 29 | seq_direction = index2 - index1 30 | if [-1, 1].include?(seq_direction) 31 | return [name, sequence, seq_direction] 32 | else 33 | return nil 34 | end 35 | end 36 | end 37 | end 38 | 39 | def matches(password) 40 | result = [] 41 | i = 0 42 | while i < password.length - 1 43 | seq_name, seq, seq_direction = applicable_sequence(password, i) 44 | 45 | if seq 46 | length = seq_match_length(password, i, seq_direction, seq) 47 | if length > 2 48 | result << Match.new( 49 | :pattern => 'sequence', 50 | :i => i, 51 | :j => i + length - 1, 52 | :token => password[i, length], 53 | :sequence_name => seq_name, 54 | :sequence_space => seq.length, 55 | :ascending => seq_direction == 1 56 | ) 57 | end 58 | i += length - 1 59 | else 60 | i += 1 61 | end 62 | end 63 | result 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/zxcvbn/matchers/spatial.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/match' 2 | 3 | module Zxcvbn 4 | module Matchers 5 | class Spatial 6 | def initialize(graphs) 7 | @graphs = graphs 8 | end 9 | 10 | def matches(password) 11 | results = [] 12 | @graphs.each do |graph_name, graph| 13 | results += matches_for_graph(graph, graph_name, password) 14 | end 15 | results 16 | end 17 | 18 | def matches_for_graph(graph, graph_name, password) 19 | result = [] 20 | i = 0 21 | while i < password.length - 1 22 | j = i + 1 23 | last_direction = nil 24 | turns = 0 25 | shifted_count = 0 26 | loop do 27 | prev_char = password[j-1] 28 | found = false 29 | found_direction = -1 30 | cur_direction = -1 31 | adjacents = graph[prev_char] || [] 32 | # consider growing pattern by one character if j hasn't gone over the edge. 33 | if j < password.length 34 | cur_char = password[j] 35 | adjacents.each do |adj| 36 | cur_direction += 1 37 | if adj && adj.index(cur_char) 38 | found = true 39 | found_direction = cur_direction 40 | if adj.index(cur_char) == 1 41 | # index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc. 42 | # for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted. 43 | shifted_count += 1 44 | end 45 | if last_direction != found_direction 46 | # adding a turn is correct even in the initial case when last_direction is null: 47 | # every spatial pattern starts with a turn. 48 | turns += 1 49 | last_direction = found_direction 50 | end 51 | break 52 | end 53 | end 54 | end 55 | # if the current pattern continued, extend j and try to grow again 56 | if found 57 | j += 1 58 | else 59 | # otherwise push the pattern discovered so far, if any... 60 | if j - i > 2 # don't consider length 1 or 2 chains. 61 | result << Match.new( 62 | :pattern => 'spatial', 63 | :i => i, 64 | :j => j-1, 65 | :token => password[i...j], 66 | :graph => graph_name, 67 | :turns => turns, 68 | :shifted_count => shifted_count 69 | ) 70 | end 71 | # ...and then start a new search for the rest of the password. 72 | i = j 73 | break 74 | end 75 | end 76 | end 77 | result 78 | end 79 | end 80 | end 81 | end -------------------------------------------------------------------------------- /lib/zxcvbn/matchers/year.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/matchers/regex_helpers' 2 | 3 | module Zxcvbn 4 | module Matchers 5 | class Year 6 | include RegexHelpers 7 | 8 | YEAR_REGEX = /19\d\d|200\d|201\d/ 9 | 10 | def matches(password) 11 | result = [] 12 | re_match_all(YEAR_REGEX, password) do |match| 13 | match.pattern = 'year' 14 | result << match 15 | end 16 | result 17 | end 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /lib/zxcvbn/math.rb: -------------------------------------------------------------------------------- 1 | module Zxcvbn 2 | module Math 3 | def bruteforce_cardinality(password) 4 | is_type_of = {} 5 | 6 | password.each_byte do |ordinal| 7 | case ordinal 8 | when (48..57) 9 | is_type_of['digits'] = true 10 | when (65..90) 11 | is_type_of['upper'] = true 12 | when (97..122) 13 | is_type_of['lower'] = true 14 | else 15 | is_type_of['symbols'] = true 16 | end 17 | end 18 | 19 | cardinality = 0 20 | cardinality += 10 if is_type_of['digits'] 21 | cardinality += 26 if is_type_of['upper'] 22 | cardinality += 26 if is_type_of['lower'] 23 | cardinality += 33 if is_type_of['symbols'] 24 | cardinality 25 | end 26 | 27 | def lg(n) 28 | ::Math.log(n, 2) 29 | end 30 | 31 | def nCk(n, k) 32 | return 0 if k > n 33 | return 1 if k == 0 34 | r = 1 35 | (1..k).each do |d| 36 | r = r * n 37 | r = r / d 38 | n -= 1 39 | end 40 | r 41 | end 42 | 43 | def average_degree_for_graph(graph_name) 44 | graph = data.adjacency_graphs[graph_name] 45 | degrees = graph.map { |_, neighbors| neighbors.compact.size } 46 | sum = degrees.inject(0, :+) 47 | sum.to_f / graph.size 48 | end 49 | 50 | def starting_positions_for_graph(graph_name) 51 | data.adjacency_graphs[graph_name].length 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/zxcvbn/omnimatch.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/dictionary_ranker' 2 | require 'zxcvbn/matchers/dictionary' 3 | require 'zxcvbn/matchers/l33t' 4 | require 'zxcvbn/matchers/spatial' 5 | require 'zxcvbn/matchers/digits' 6 | require 'zxcvbn/matchers/repeat' 7 | require 'zxcvbn/matchers/sequences' 8 | require 'zxcvbn/matchers/year' 9 | require 'zxcvbn/matchers/date' 10 | 11 | module Zxcvbn 12 | class Omnimatch 13 | def initialize(data) 14 | @data = data 15 | @matchers = build_matchers 16 | end 17 | 18 | def matches(password, user_inputs = []) 19 | matchers = @matchers + user_input_matchers(user_inputs) 20 | matchers.map do |matcher| 21 | matcher.matches(password) 22 | end.inject(&:+) 23 | end 24 | 25 | private 26 | 27 | def user_input_matchers(user_inputs) 28 | return [] unless user_inputs.any? 29 | user_ranked_dictionary = DictionaryRanker.rank_dictionary(user_inputs) 30 | dictionary_matcher = Matchers::Dictionary.new('user_inputs', user_ranked_dictionary) 31 | l33t_matcher = Matchers::L33t.new([dictionary_matcher]) 32 | [dictionary_matcher, l33t_matcher] 33 | end 34 | 35 | 36 | def build_matchers 37 | matchers = [] 38 | dictionary_matchers = @data.ranked_dictionaries.map do |name, dictionary| 39 | Matchers::Dictionary.new(name, dictionary) 40 | end 41 | l33t_matcher = Matchers::L33t.new(dictionary_matchers) 42 | matchers += dictionary_matchers 43 | matchers += [ 44 | l33t_matcher, 45 | Matchers::Spatial.new(@data.adjacency_graphs), 46 | Matchers::Digits.new, 47 | Matchers::Repeat.new, 48 | Matchers::Sequences.new, 49 | Matchers::Year.new, 50 | Matchers::Date.new 51 | ] 52 | matchers 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/zxcvbn/password_strength.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | require 'zxcvbn/feedback_giver' 3 | require 'zxcvbn/omnimatch' 4 | require 'zxcvbn/scorer' 5 | 6 | module Zxcvbn 7 | class PasswordStrength 8 | def initialize(data) 9 | @omnimatch = Omnimatch.new(data) 10 | @scorer = Scorer.new(data) 11 | end 12 | 13 | def test(password, user_inputs = []) 14 | password = password || '' 15 | result = nil 16 | calc_time = Benchmark.realtime do 17 | matches = @omnimatch.matches(password, user_inputs) 18 | result = @scorer.minimum_entropy_match_sequence(password, matches) 19 | end 20 | result.calc_time = calc_time 21 | result.feedback = FeedbackGiver.get_feedback(result.score, result.match_sequence) 22 | result 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /lib/zxcvbn/score.rb: -------------------------------------------------------------------------------- 1 | module Zxcvbn 2 | class Score 3 | attr_accessor :entropy, :crack_time, :crack_time_display, :score, :pattern, 4 | :match_sequence, :password, :calc_time, :feedback 5 | 6 | def initialize(options = {}) 7 | @entropy = options[:entropy] 8 | @crack_time = options[:crack_time] 9 | @crack_time_display = options[:crack_time_display] 10 | @score = options[:score] 11 | @match_sequence = options[:match_sequence] 12 | @password = options[:password] 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /lib/zxcvbn/scorer.rb: -------------------------------------------------------------------------------- 1 | require 'zxcvbn/entropy' 2 | require 'zxcvbn/crack_time' 3 | require 'zxcvbn/score' 4 | require 'zxcvbn/match' 5 | 6 | module Zxcvbn 7 | class Scorer 8 | def initialize(data) 9 | @data = data 10 | end 11 | 12 | attr_reader :data 13 | 14 | include Entropy 15 | include CrackTime 16 | 17 | def minimum_entropy_match_sequence(password, matches) 18 | bruteforce_cardinality = bruteforce_cardinality(password) # e.g. 26 for lowercase 19 | up_to_k = [] # minimum entropy up to k. 20 | backpointers = [] # for the optimal sequence of matches up to k, holds the final match (match.j == k). null means the sequence ends w/ a brute-force character. 21 | (0...password.length).each do |k| 22 | # starting scenario to try and beat: adding a brute-force character to the minimum entropy sequence at k-1. 23 | previous_k_entropy = k > 0 ? up_to_k[k-1] : 0 24 | up_to_k[k] = previous_k_entropy + lg(bruteforce_cardinality) 25 | backpointers[k] = nil 26 | matches.select do |match| 27 | match.j == k 28 | end.each do |match| 29 | i, j = match.i, match.j 30 | # see if best entropy up to i-1 + entropy of this match is less than the current minimum at j. 31 | previous_i_entropy = i > 0 ? up_to_k[i-1] : 0 32 | candidate_entropy = previous_i_entropy + calc_entropy(match) 33 | if up_to_k[j] && candidate_entropy < up_to_k[j] 34 | up_to_k[j] = candidate_entropy 35 | backpointers[j] = match 36 | end 37 | end 38 | end 39 | 40 | # walk backwards and decode the best sequence 41 | match_sequence = [] 42 | k = password.length - 1 43 | while k >= 0 44 | match = backpointers[k] 45 | if match 46 | match_sequence.unshift match 47 | k = match.i - 1 48 | else 49 | k -= 1 50 | end 51 | end 52 | 53 | match_sequence = pad_with_bruteforce_matches(match_sequence, password, bruteforce_cardinality) 54 | score_for(password, match_sequence, up_to_k) 55 | end 56 | 57 | def score_for password, match_sequence, up_to_k 58 | min_entropy = up_to_k[password.length - 1] || 0 # or 0 corner case is for an empty password '' 59 | crack_time = entropy_to_crack_time(min_entropy) 60 | 61 | # final result object 62 | Score.new( 63 | :password => password, 64 | :entropy => min_entropy.round(3), 65 | :match_sequence => match_sequence, 66 | :crack_time => crack_time.round(3), 67 | :crack_time_display => display_time(crack_time), 68 | :score => crack_time_to_score(crack_time) 69 | ) 70 | end 71 | 72 | 73 | def pad_with_bruteforce_matches(match_sequence, password, bruteforce_cardinality) 74 | k = 0 75 | match_sequence_copy = [] 76 | match_sequence.each do |match| 77 | if match.i > k 78 | match_sequence_copy << make_bruteforce_match(password, k, match.i - 1, bruteforce_cardinality) 79 | end 80 | k = match.j + 1 81 | match_sequence_copy << match 82 | end 83 | if k < password.length 84 | match_sequence_copy << make_bruteforce_match(password, k, password.length - 1, bruteforce_cardinality) 85 | end 86 | match_sequence_copy 87 | end 88 | # fill in the blanks between pattern matches with bruteforce "matches" 89 | # that way the match sequence fully covers the password: 90 | # match1.j == match2.i - 1 for every adjacent match1, match2. 91 | def make_bruteforce_match(password, i, j, bruteforce_cardinality) 92 | Match.new( 93 | :pattern => 'bruteforce', 94 | :i => i, 95 | :j => j, 96 | :token => password[i..j], 97 | :entropy => lg(bruteforce_cardinality ** (j - i + 1)), 98 | :cardinality => bruteforce_cardinality 99 | ) 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/zxcvbn/tester.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "zxcvbn/data" 4 | require "zxcvbn/password_strength" 5 | 6 | module Zxcvbn 7 | # Allows you to test the strength of multiple passwords without reading and 8 | # parsing the dictionary data from disk each test. Dictionary data is read 9 | # once from disk and stored in memory for the life of the Tester object. 10 | # 11 | # Example: 12 | # 13 | # tester = Zxcvbn::Tester.new 14 | # 15 | # tester.add_word_lists("custom" => ["words"]) 16 | # 17 | # tester.test("password 1") 18 | # tester.test("password 2") 19 | # tester.test("password 3") 20 | class Tester 21 | def initialize 22 | @data = Data.new 23 | end 24 | 25 | def test(password, user_inputs = []) 26 | PasswordStrength.new(@data).test(password, sanitize(user_inputs)) 27 | end 28 | 29 | def add_word_lists(lists) 30 | lists.each_pair { |name, words| @data.add_word_list(name, sanitize(words)) } 31 | end 32 | 33 | def inspect 34 | "#<#{self.class}:0x#{__id__.to_s(16)}>" 35 | end 36 | 37 | private 38 | 39 | def sanitize(user_inputs) 40 | user_inputs.select { |i| i.respond_to?(:downcase) } 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/zxcvbn/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Zxcvbn 4 | VERSION = '1.2.0' 5 | end 6 | -------------------------------------------------------------------------------- /spec/dictionary_ranker_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::DictionaryRanker do 4 | describe '.rank_dictionaries' do 5 | it 'ranks word lists' do 6 | result = Zxcvbn::DictionaryRanker.rank_dictionaries({:test => ['ABC', 'def'], 7 | :test2 => ['def', 'ABC']}) 8 | expect(result[:test]).to eq({'abc' => 1, 'def' => 2}) 9 | expect(result[:test2]).to eq({'def' => 1, 'abc' => 2}) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/feedback_giver_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::FeedbackGiver do 4 | # NOTE: We go in via the tester because the `FeedbackGiver` relies on both 5 | # Omnimatch and the Scorer, which are troublesome to wire up for tests 6 | let(:tester) { Zxcvbn::Tester.new } 7 | 8 | describe '.get_feedback' do 9 | it "gives empty feedback when a password's score is good" do 10 | feedback = tester.test('5815A30BE798').feedback 11 | 12 | expect(feedback).to be_a Zxcvbn::Feedback 13 | expect(feedback.warning).to be_nil 14 | expect(feedback.suggestions).to be_empty 15 | end 16 | 17 | it 'gives general feedback when a password is empty' do 18 | feedback = tester.test('').feedback 19 | 20 | expect(feedback).to be_a Zxcvbn::Feedback 21 | expect(feedback.warning).to be_nil 22 | expect(feedback.suggestions).to contain_exactly( 23 | 'Use a few words, avoid common phrases', 24 | 'No need for symbols, digits, or uppercase letters' 25 | ) 26 | end 27 | 28 | it "gives general feedback when a password is poor but doesn't match any heuristics" do 29 | feedback = tester.test(':005:0').feedback 30 | 31 | expect(feedback).to be_a Zxcvbn::Feedback 32 | expect(feedback.warning).to be_nil 33 | expect(feedback.suggestions).to contain_exactly( 34 | 'Add another word or two. Uncommon words are better.' 35 | ) 36 | end 37 | 38 | describe 'gives specific feedback' do 39 | describe 'for dictionary passwords' do 40 | it 'that are extremely common' do 41 | feedback = tester.test('password').feedback 42 | 43 | expect(feedback).to be_a Zxcvbn::Feedback 44 | expect(feedback.warning).to eql('This is a top-10 common password') 45 | expect(feedback.suggestions).to contain_exactly( 46 | 'Add another word or two. Uncommon words are better.' 47 | ) 48 | end 49 | 50 | it 'that are very, very common' do 51 | feedback = tester.test('letmein').feedback 52 | 53 | expect(feedback).to be_a Zxcvbn::Feedback 54 | expect(feedback.warning).to eql('This is a top-100 common password') 55 | expect(feedback.suggestions).to contain_exactly( 56 | 'Add another word or two. Uncommon words are better.' 57 | ) 58 | end 59 | 60 | it 'that are very common' do 61 | feedback = tester.test('playstation').feedback 62 | 63 | expect(feedback).to be_a Zxcvbn::Feedback 64 | expect(feedback.warning).to eql('This is a very common password') 65 | expect(feedback.suggestions).to contain_exactly( 66 | 'Add another word or two. Uncommon words are better.' 67 | ) 68 | end 69 | 70 | it 'that are common and you tried to use l33tsp33k' do 71 | feedback = tester.test('pl4yst4ti0n').feedback 72 | 73 | expect(feedback).to be_a Zxcvbn::Feedback 74 | expect(feedback.warning).to eql( 75 | 'This is similar to a commonly used password' 76 | ) 77 | expect(feedback.suggestions).to contain_exactly( 78 | 'Add another word or two. Uncommon words are better.', 79 | "Predictable substitutions like '@' instead of 'a' don't help very much" 80 | ) 81 | end 82 | 83 | it 'that are common and you capitalised the start' do 84 | feedback = tester.test('Password').feedback 85 | 86 | expect(feedback).to be_a Zxcvbn::Feedback 87 | expect(feedback.warning).to eql( 88 | 'This is a top-10 common password' 89 | ) 90 | expect(feedback.suggestions).to contain_exactly( 91 | 'Add another word or two. Uncommon words are better.', 92 | "Capitalization doesn't help very much" 93 | ) 94 | end 95 | 96 | it 'that are common and you capitalised the whole thing' do 97 | feedback = tester.test('PASSWORD').feedback 98 | 99 | expect(feedback).to be_a Zxcvbn::Feedback 100 | expect(feedback.warning).to eql( 101 | 'This is a top-10 common password' 102 | ) 103 | expect(feedback.suggestions).to contain_exactly( 104 | 'Add another word or two. Uncommon words are better.', 105 | 'All-uppercase is almost as easy to guess as all-lowercase' 106 | ) 107 | end 108 | 109 | it 'that contain a common first name or last name' do 110 | feedback = tester.test('jessica').feedback 111 | 112 | expect(feedback).to be_a Zxcvbn::Feedback 113 | expect(feedback.warning).to eql( 114 | 'Names and surnames by themselves are easy to guess' 115 | ) 116 | expect(feedback.suggestions).to contain_exactly( 117 | 'Add another word or two. Uncommon words are better.' 118 | ) 119 | 120 | feedback = tester.test('smith').feedback 121 | 122 | expect(feedback).to be_a Zxcvbn::Feedback 123 | expect(feedback.warning).to eql( 124 | 'Names and surnames by themselves are easy to guess' 125 | ) 126 | expect(feedback.suggestions).to contain_exactly( 127 | 'Add another word or two. Uncommon words are better.' 128 | ) 129 | end 130 | 131 | it 'that contain a common name and surname' do 132 | feedback = tester.test('jessica smith').feedback 133 | 134 | expect(feedback).to be_a Zxcvbn::Feedback 135 | expect(feedback.warning).to eql( 136 | 'Common names and surnames are easy to guess' 137 | ) 138 | expect(feedback.suggestions).to contain_exactly( 139 | 'Add another word or two. Uncommon words are better.' 140 | ) 141 | end 142 | end 143 | 144 | describe 'for spatial passwords' do 145 | it 'that contain a straight keyboard row' do 146 | feedback = tester.test('1qaz').feedback 147 | 148 | expect(feedback).to be_a Zxcvbn::Feedback 149 | expect(feedback.warning).to eql( 150 | 'Straight rows of keys are easy to guess' 151 | ) 152 | expect(feedback.suggestions).to contain_exactly( 153 | 'Add another word or two. Uncommon words are better.', 154 | 'Use a longer keyboard pattern with more turns' 155 | ) 156 | end 157 | 158 | it 'that contain a keyboard pattern with one turn' do 159 | feedback = tester.test('zaqwer').feedback 160 | 161 | expect(feedback).to be_a Zxcvbn::Feedback 162 | expect(feedback.warning).to eql( 163 | 'Short keyboard patterns are easy to guess' 164 | ) 165 | expect(feedback.suggestions).to contain_exactly( 166 | 'Add another word or two. Uncommon words are better.', 167 | 'Use a longer keyboard pattern with more turns' 168 | ) 169 | end 170 | end 171 | 172 | it 'for passwords with repeated characters' do 173 | feedback = tester.test('zzz').feedback 174 | 175 | expect(feedback).to be_a Zxcvbn::Feedback 176 | expect(feedback.warning).to eql( 177 | 'Repeats like "aaa" are easy to guess' 178 | ) 179 | expect(feedback.suggestions).to contain_exactly( 180 | 'Add another word or two. Uncommon words are better.', 181 | 'Avoid repeated words and characters' 182 | ) 183 | end 184 | 185 | it 'for passwords with sequential characters' do 186 | feedback = tester.test('pqrpqrpqr').feedback 187 | 188 | expect(feedback).to be_a Zxcvbn::Feedback 189 | expect(feedback.warning).to eql( 190 | 'Sequences like abc or 6543 are easy to guess' 191 | ) 192 | expect(feedback.suggestions).to contain_exactly( 193 | 'Add another word or two. Uncommon words are better.', 194 | 'Avoid sequences' 195 | ) 196 | end 197 | 198 | it 'for passwords containing dates' do 199 | feedback = tester.test('testing02\12\1997').feedback 200 | 201 | expect(feedback).to be_a Zxcvbn::Feedback 202 | expect(feedback.warning).to eql( 203 | 'Dates are often easy to guess' 204 | ) 205 | expect(feedback.suggestions).to contain_exactly( 206 | 'Add another word or two. Uncommon words are better.', 207 | 'Avoid dates and years that are associated with you' 208 | ) 209 | end 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /spec/matchers/date_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::Matchers::Date do 4 | let(:matcher) { subject } 5 | 6 | { 7 | ' ' => 'testing02 12 1997', 8 | '-' => 'testing02-12-1997', 9 | '/' => 'testing02/12/1997', 10 | '\\' => 'testing02\12\1997', 11 | '_' => 'testing02_12_1997', 12 | '.' => 'testing02.12.1997' 13 | }.each do |separator, password| 14 | context "with #{separator} seperator" do 15 | let(:matches) { matcher.matches(password) } 16 | 17 | it 'finds matches' do 18 | expect(matches).not_to be_empty 19 | end 20 | 21 | it 'finds the correct matches' do 22 | expect(matches.count).to eq 1 23 | expect(matches[0].token).to eq %w[ 02 12 1997 ].join(separator) 24 | expect(matches[0].separator).to eq separator 25 | expect(matches[0].day).to eq 2 26 | expect(matches[0].month).to eq 12 27 | expect(matches[0].year).to eq 1997 28 | end 29 | end 30 | end 31 | 32 | # context 'without separator' do 33 | # context '5 digit date' do 34 | # let(:matches) { matcher.matches('13192boo') } 35 | 36 | # it 'finds matches' do 37 | # matches.should_not be_empty 38 | # end 39 | 40 | # it 'finds the correct matches' do 41 | # matches.count.should eq 2 42 | # matches[0].token.should eq '13192' 43 | # matches[0].separator.should eq '' 44 | # matches[0].day.should eq 13 45 | # matches[0].month.should eq 1 46 | # matches[0].year.should eq 1992 47 | 48 | # matches[1].token.should eq '13192' 49 | # matches[1].separator.should eq '' 50 | # matches[1].day.should eq 31 51 | # matches[1].month.should eq 1 52 | # matches[1].year.should eq 1992 53 | # end 54 | # end 55 | # end 56 | 57 | # describe '#extract_dates' do 58 | # { 59 | # '1234' => [ 60 | # {:year => 2012, :month => 3, :day => 4}, 61 | # {:year => 1934, :month => 2, :day => 1}, 62 | # {:year => 1934, :month => 1, :day => 2} 63 | # ], 64 | # '12345' => [ 65 | # {:year => 1945, :month => 3, :day => 12}, 66 | # {:year => 1945, :month => 12, :day => 3}, 67 | # {:year => 1945, :month => 1, :day => 23} 68 | # ], 69 | # '54321' => [ 70 | # {:year => 1954, :month => 3, :day => 21} 71 | # ], 72 | # '151290' => [ 73 | # {:year => 1990, :month => 12, :day => 15} 74 | # ], 75 | # '901215' => [ 76 | # {:year => 1990, :month => 12, :day => 15} 77 | # ], 78 | # '1511990' => [ 79 | # {:year => 1990, :month => 1, :day => 15} 80 | # ] 81 | # }.each do |token, expected_candidates| 82 | # it "finds the correct candidates for #{token}" do 83 | # matcher.extract_dates(token).should match_array expected_candidates 84 | # end 85 | # end 86 | # end 87 | 88 | # describe '#expand_year' do 89 | # { 90 | # 12 => 2012, 91 | # 01 => 2001, 92 | # 15 => 2015, 93 | # 19 => 2019, 94 | # 20 => 1920 95 | # }.each do |small, expanded| 96 | # it "expands #{small} to #{expanded}" do 97 | # matcher.expand_year(small).should eq expanded 98 | # end 99 | # end 100 | # end 101 | 102 | context 'invalid date' do 103 | let(:matches) { matcher.matches('testing0.x.1997') } 104 | 105 | it 'doesnt match' do 106 | expect(matches).to be_empty 107 | end 108 | end 109 | end -------------------------------------------------------------------------------- /spec/matchers/dictionary_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe Zxcvbn::Matchers::Dictionary do 6 | subject(:matcher) { described_class.new("Test dictionary", dictionary) } 7 | 8 | describe "#matches" do 9 | let(:matches) { matcher.matches(password) } 10 | let(:matched_words) { matches.map(&:matched_word) } 11 | 12 | context "Given a dictionary of English words" do 13 | let(:dictionary) { Zxcvbn::Data.new.ranked_dictionaries["english"] } 14 | let(:password) { "whatisinit" } 15 | 16 | it "finds all the matches" do 17 | expect(matched_words).to match_array %w[wha what ha hat a at tis i is sin i in i it] 18 | end 19 | end 20 | 21 | context "Given a custom dictionary" do 22 | let(:dictionary) { Zxcvbn::DictionaryRanker.rank_dictionary(%w[test AB10CD]) } 23 | let(:password) { "AB10CD" } 24 | 25 | it "matches uppercase passwords with normalised dictionary entries" do 26 | expect(matched_words).to match_array(%w[ab10cd]) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/matchers/digits_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::Matchers::Digits do 4 | let(:matcher) { subject } 5 | let(:matches) { matcher.matches('testing1239xx9712') } 6 | 7 | it 'sets the pattern name' do 8 | expect(matches.all? { |m| m.pattern == 'digits' }).to eql(true) 9 | end 10 | 11 | it 'finds the correct matches' do 12 | expect(matches.count).to eq(2) 13 | expect(matches[0].token).to eq '1239' 14 | end 15 | end -------------------------------------------------------------------------------- /spec/matchers/l33t_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::Matchers::L33t do 4 | let(:matcher) { described_class.new([dictionary_matcher]) } 5 | let(:dictionary) { Zxcvbn::Data.new.ranked_dictionaries['english'] } 6 | let(:dictionary_matcher) { Zxcvbn::Matchers::Dictionary.new('english', dictionary) } 7 | 8 | describe '#relevant_l33t_substitutions' do 9 | it 'returns relevant l33t substitutions' do 10 | expect(matcher.relevent_l33t_subtable('p@ssw1rd24')).to eq( 11 | {'a' => ['4', '@'], 'i' => ['1'], 'l' => ['1'], 'z' => ['2']} 12 | ) 13 | end 14 | end 15 | 16 | describe 'possible l33t substitutions' do 17 | context 'with 2 possible substitutions' do 18 | it 'returns the correct possible substitutions' do 19 | substitutions = {'a' => ['@'], 'i' => ['1']} 20 | expect(matcher.l33t_subs(substitutions)).to match_array([ 21 | {'@' => 'a', '1' => 'i'} 22 | ]) 23 | end 24 | 25 | it 'returns the correct possible substitutions with multiple options' do 26 | substitutions = {'a' => ['@', '4'], 'i' => ['1']} 27 | expect(matcher.l33t_subs(substitutions)).to match_array([ 28 | {'@' => 'a', '1' => 'i'}, 29 | {'4' => 'a', '1' => 'i'} 30 | ]) 31 | end 32 | end 33 | 34 | context 'with 3 possible substitutions' do 35 | it 'returns the correct possible substitutions' do 36 | substitutions = {'a' => ['@'], 'i' => ['1'], 'z' => ['3']} 37 | expect(matcher.l33t_subs(substitutions)).to match_array([ 38 | {'@' => 'a', '1' => 'i', '3' => 'z'} 39 | ]) 40 | end 41 | end 42 | 43 | context 'with 4 possible substitutions' do 44 | it 'returns the correct possible substitutions' do 45 | substitutions = {'a' => ['@'], 'i' => ['1'], 'z' => ['3'], 'b' => ['8']} 46 | expect(matcher.l33t_subs(substitutions)).to match_array([ 47 | {'@' => 'a', '1' => 'i', '3' => 'z', '8' => 'b'} 48 | ]) 49 | end 50 | end 51 | end 52 | 53 | describe '#matches' do 54 | subject(:matches) { matcher.matches('p@ssword') } 55 | 56 | it "doesn't find 'password' because it's not in english.txt" do 57 | expect(matches.map(&:matched_word)).not_to include "password" 58 | end 59 | 60 | it 'finds the correct matches' do 61 | expect(matches.map(&:matched_word)).to match_array([ 62 | 'pas', 63 | 'a', 64 | 'as', 65 | 'ass' 66 | ]) 67 | end 68 | 69 | it 'sets the token correctly on those matches' do 70 | expect(matches.map(&:token)).to match_array([ 71 | 'p@s', 72 | '@', 73 | '@s', 74 | '@ss' 75 | ]) 76 | end 77 | 78 | it 'sets the substituions used' do 79 | expect(matches.map(&:sub)).to match_array([ 80 | {'@' => 'a'}, 81 | {'@' => 'a'}, 82 | {'@' => 'a'}, 83 | {'@' => 'a'} 84 | ]) 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/matchers/repeat_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::Matchers::Repeat do 4 | let(:matcher) { subject } 5 | let(:matches) { matcher.matches('bbbbbtestingaaa') } 6 | 7 | it 'sets the pattern name' do 8 | expect(matches.all? { |m| m.pattern == 'repeat' }).to eql(true) 9 | end 10 | 11 | it 'finds the repeated patterns' do 12 | expect(matches.count).to eq 2 13 | expect(matches[0].token).to eq 'bbbbb' 14 | expect(matches[0].repeated_char).to eq 'b' 15 | expect(matches[1].token).to eq 'aaa' 16 | expect(matches[1].repeated_char).to eq 'a' 17 | end 18 | end -------------------------------------------------------------------------------- /spec/matchers/sequences_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::Matchers::Sequences do 4 | let(:matcher) { subject } 5 | let(:matches) { matcher.matches('abcde87654') } 6 | 7 | it 'sets the pattern name' do 8 | expect(matches.all? { |m| m.pattern == 'sequence' }).to eql(true) 9 | end 10 | 11 | it 'finds the correct matches' do 12 | expect(matches.count).to eq(2) 13 | expect(matches[0].token).to eq 'abcde' 14 | expect(matches[1].token).to eq '87654' 15 | end 16 | 17 | it 'finds overlapping matches' do 18 | matches = matcher.matches('abcba') 19 | expect(matches.map(&:token)).to eq ['abc', 'cba'] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/matchers/spatial_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::Matchers::Spatial do 4 | let(:matcher) { Zxcvbn::Matchers::Spatial.new(graphs) } 5 | let(:graphs) { Zxcvbn::Data.new.adjacency_graphs } 6 | 7 | describe '#matches' do 8 | let(:matches) { matcher.matches('rtyikm') } 9 | 10 | it 'finds the correct of matches' do 11 | expect(matches.count).to eq 3 12 | expect(matches[0].token).to eq 'rty' 13 | expect(matches[0].graph).to eq 'qwerty' 14 | expect(matches[1].token).to eq 'ikm' 15 | expect(matches[1].graph).to eq 'qwerty' 16 | expect(matches[2].token).to eq 'yik' 17 | expect(matches[2].graph).to eq 'dvorak' 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /spec/matchers/year_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::Matchers::Year do 4 | let(:matcher) { subject } 5 | let(:matches) { matcher.matches('testing1998') } 6 | 7 | it 'sets the pattern name' do 8 | expect(matches.all? { |m| m.pattern == 'year' }).to eql(true) 9 | end 10 | 11 | it 'finds the correct matches' do 12 | expect(matches.count).to eq(1) 13 | expect(matches[0].token).to eq '1998' 14 | end 15 | end -------------------------------------------------------------------------------- /spec/omnimatch_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::Omnimatch do 4 | before(:all) do 5 | @omnimatch = described_class.new(Zxcvbn::Data.new) 6 | end 7 | 8 | def omnimatch(password) 9 | @omnimatch.matches(password) 10 | end 11 | 12 | def js_omnimatch(password) 13 | run_js(%'omnimatch("#{password}")') 14 | end 15 | 16 | TEST_PASSWORDS.each do |password| 17 | it "gives back the same results for #{password}" do 18 | js_results = js_omnimatch(password) 19 | ruby_results = omnimatch(password) 20 | 21 | expect(ruby_results).to match_js_results js_results 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /spec/scorer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::Scorer do 4 | 5 | end -------------------------------------------------------------------------------- /spec/scoring/crack_time_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::CrackTime do 4 | include Zxcvbn::CrackTime 5 | 6 | describe '#entropy_to_crack_time' do 7 | specify do 8 | expect(entropy_to_crack_time(15.433976574415976)).to eq 2.2134000000000014 9 | end 10 | end 11 | 12 | describe '#crack_time_to_score' do 13 | context 'crack time less than 10 to the power 2' do 14 | it 'returns 0' do 15 | expect(crack_time_to_score(90)).to eq 0 16 | end 17 | end 18 | 19 | context 'crack time in between 10**2 and 10**4' do 20 | it 'returns 1' do 21 | expect(crack_time_to_score(5000)).to eq 1 22 | end 23 | end 24 | 25 | context 'crack time in between 10**4 and 10**6' do 26 | it 'returns 2' do 27 | expect(crack_time_to_score(500_000)).to eq 2 28 | end 29 | end 30 | 31 | context 'crack time in between 10**6 and 10**8' do 32 | it 'returns 3' do 33 | expect(crack_time_to_score(50_000_000)).to eq 3 34 | end 35 | end 36 | 37 | context 'crack time above 10**8' do 38 | it 'returns 4' do 39 | expect(crack_time_to_score(110_000_000)).to eq 4 40 | end 41 | end 42 | end 43 | 44 | describe '#display_time' do 45 | let(:minute_to_seconds) { 60 } 46 | let(:hour_to_seconds) { minute_to_seconds * 60 } 47 | let(:day_to_seconds) { hour_to_seconds * 24 } 48 | let(:month_to_seconds) { day_to_seconds * 31 } 49 | let(:year_to_seconds) { month_to_seconds * 12 } 50 | let(:century_to_seconds) { year_to_seconds * 100 } 51 | 52 | context 'when less than a minute' do 53 | it 'should return instant' do 54 | [0, minute_to_seconds - 1].each do |seconds| 55 | expect(display_time(seconds)).to eql 'instant' 56 | end 57 | end 58 | end 59 | 60 | context 'when less than an hour' do 61 | it 'should return a readable time in minutes' do 62 | [60, (hour_to_seconds - 1)].each do |seconds| 63 | expect(display_time(seconds)).to match(/[0-9]+ minutes$/) 64 | end 65 | end 66 | end 67 | 68 | context 'when less than a day' do 69 | it 'should return a readable time in hours' do 70 | [hour_to_seconds, (day_to_seconds - 1)].each do |seconds| 71 | expect(display_time(seconds)).to match(/[0-9]+ hours$/) 72 | end 73 | end 74 | end 75 | 76 | context 'when less than 31 days' do 77 | it 'should return a readable time in days' do 78 | [day_to_seconds, month_to_seconds - 1].each do |seconds| 79 | expect(display_time(seconds)).to match(/[0-9]+ days$/) 80 | end 81 | end 82 | end 83 | 84 | context 'when less than 1 year' do 85 | it 'should return a readable time in days' do 86 | [month_to_seconds, (year_to_seconds - 1)].each do |seconds| 87 | expect(display_time(seconds)).to match(/[0-9]+ months$/) 88 | end 89 | end 90 | end 91 | 92 | context 'when less than a century' do 93 | it 'should return a readable time in days' do 94 | [year_to_seconds, (century_to_seconds - 1)].each do |seconds| 95 | expect(display_time(seconds)).to match(/[0-9]+ years$/) 96 | end 97 | end 98 | end 99 | 100 | context 'when a century or more' do 101 | it 'should return centuries' do 102 | expect(display_time(century_to_seconds)).to eql 'centuries' 103 | end 104 | end 105 | end 106 | end -------------------------------------------------------------------------------- /spec/scoring/entropy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::Entropy do 4 | include Zxcvbn::Math 5 | 6 | let(:entropy) { 7 | Class.new do 8 | include Zxcvbn::Entropy 9 | def data 10 | Zxcvbn::Data.new 11 | end 12 | end.new 13 | } 14 | 15 | describe '#repeat_entropy' do 16 | it 'returns the correct value' do 17 | match = Zxcvbn::Match.new(:token => '2222') 18 | expect(entropy.repeat_entropy(match)).to eq 5.321928094887363 19 | end 20 | end 21 | 22 | describe '#sequence_entropy' do 23 | let(:match) { Zxcvbn::Match.new(:token => token, :ascending => true) } 24 | 25 | {'a' => 'abcdefg', '1' => '1234567'}.each do |first_char, token| 26 | context "when the first char is #{first_char}" do 27 | let(:token) { token } 28 | 29 | it 'returns the correct value' do 30 | expect(entropy.sequence_entropy(match)).to eq 3.807354922057604 31 | end 32 | end 33 | end 34 | 35 | context 'when the first character is a digit' do 36 | let(:token) { '23456' } 37 | 38 | it 'returns the correct value' do 39 | expect(entropy.sequence_entropy(match)).to eq 5.643856189774725 40 | end 41 | end 42 | 43 | context 'when the first character is a lowercase letter' do 44 | let(:token) { 'bcdef' } 45 | 46 | it 'returns the correct value' do 47 | expect(entropy.sequence_entropy(match)).to eq 7.022367813028454 48 | end 49 | end 50 | 51 | context 'when the first character is an uppercase letter' do 52 | let(:token) { 'BCDEF' } 53 | 54 | it 'returns the correct value' do 55 | expect(entropy.sequence_entropy(match)).to eq 8.022367813028454 56 | end 57 | end 58 | 59 | context 'when the match is ascending' do 60 | before { match.ascending = false } 61 | let(:token) { 'bcdef' } 62 | 63 | it 'returns the correct value' do 64 | expect(entropy.sequence_entropy(match)).to eq 8.022367813028454 65 | end 66 | end 67 | end 68 | 69 | describe '#digits_entropy' do 70 | it 'returns the correct value' do 71 | match = Zxcvbn::Match.new(:token => '12345678') 72 | expect(entropy.digits_entropy(match)).to eq 26.5754247590989 73 | end 74 | end 75 | 76 | describe '#year_entropy' do 77 | it 'returns the correct value' do 78 | expect(entropy.year_entropy(nil)).to eq 6.894817763307944 79 | end 80 | end 81 | 82 | describe '#date_entropy' do 83 | context 'with a two digit year' do 84 | it 'returns the correct value' do 85 | match = Zxcvbn::Match.new(:year => 98) 86 | expect(entropy.date_entropy(match)).to eq 15.183015000882756 87 | end 88 | end 89 | 90 | context 'with a four digit year' do 91 | it 'returns the correct value' do 92 | match = Zxcvbn::Match.new(:year => 2012) 93 | expect(entropy.date_entropy(match)).to eq 15.433976574415976 94 | end 95 | end 96 | 97 | context 'with a separator' do 98 | it 'returns the correct value' do 99 | match = Zxcvbn::Match.new(:year => 2012, :separator => '/') 100 | expect(entropy.date_entropy(match)).to eq 17.433976574415976 101 | end 102 | end 103 | end 104 | 105 | describe '#dictionary_entropy' do 106 | let(:match) { Zxcvbn::Match.new(:token => token, :rank => rank, :l33t => l33t, :sub => sub) } 107 | let(:l33t) { false } 108 | let(:sub) { {} } 109 | let(:calculated_entropy) { entropy.dictionary_entropy(match) } 110 | 111 | context 'a simple dictionary word, all lower case and no l33t subs' do 112 | let(:token) { 'you' } 113 | let(:rank) { 1 } 114 | 115 | specify { expect(calculated_entropy).to eq 0 } 116 | end 117 | 118 | context 'with all upper case characters' do 119 | let(:token) { 'YOU' } 120 | let(:rank) { 1 } 121 | 122 | specify { expect(calculated_entropy).to eq 1 } 123 | end 124 | 125 | context 'starting with uppercase' do 126 | let(:token) { 'You' } 127 | let(:rank) { 1 } 128 | 129 | specify { expect(calculated_entropy).to eq 1 } 130 | end 131 | 132 | context 'starting with uppercase' do 133 | let(:token) { 'yoU' } 134 | let(:rank) { 1 } 135 | 136 | specify { expect(calculated_entropy).to eq 1 } 137 | end 138 | 139 | context 'mixed upper and lower' do 140 | let(:token) { 'tEsTiNg' } 141 | let(:rank) { 1 } 142 | 143 | specify { expect(calculated_entropy).to eq 6 } 144 | end 145 | 146 | context 'starting with digits' do 147 | let(:token) { '12345' } 148 | let(:rank) { 1 } 149 | 150 | specify { expect(calculated_entropy).to eq 0 } 151 | end 152 | 153 | context 'extra l33t entropy' do 154 | let(:token) { 'p3rs0n' } 155 | let(:rank) { 1 } 156 | let(:l33t) { true } 157 | let(:sub) { {'3' => 'e', '0' => 'o'} } 158 | 159 | specify { expect(calculated_entropy).to eq 1 } 160 | end 161 | end 162 | 163 | describe '#spatial_entropy' do 164 | let(:match) { Zxcvbn::Match.new(:token => '123wsclf', :turns => 1) } 165 | 166 | context 'when keyboard is qwerty' do 167 | it 'should return the correct entropy' do 168 | match.graph = 'qwerty' 169 | 170 | expect(entropy.spatial_entropy(match)).to eql 11.562242424221074 171 | end 172 | end 173 | 174 | context 'when keyboard is dvorak' do 175 | it 'should return the correct entropy' do 176 | match.graph = 'dvorak' 177 | 178 | expect(entropy.spatial_entropy(match)).to eql 11.562242424221074 179 | end 180 | end 181 | 182 | context 'when keyboard is not qwerty or dvorak' do 183 | it 'should return the correct entropy' do 184 | match.graph = 'keypad' 185 | 186 | expect(entropy.spatial_entropy(match)).to eql 9.05528243550119 187 | end 188 | end 189 | 190 | context 'when match includes several turns' do 191 | it 'should return the correct entropy' do 192 | match.turns = 5 193 | 194 | expect(entropy.spatial_entropy(match)).to eql 21.761397858718993 195 | end 196 | end 197 | 198 | context 'when match includes shifted count' do 199 | it 'should return the correct entropy' do 200 | match.shiffted_count = 5 201 | 202 | expect(entropy.spatial_entropy(match)).to eql 9.05528243550119 203 | end 204 | end 205 | 206 | context 'when match includes shifted count and several turns' do 207 | it 'should return the correct entropy' do 208 | match.shiffted_count = 5 209 | match.turns = 5 210 | 211 | expect(entropy.spatial_entropy(match)).to eql 21.761397858718993 212 | end 213 | end 214 | end 215 | 216 | end -------------------------------------------------------------------------------- /spec/scoring/math_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Zxcvbn::Math do 4 | include Zxcvbn::Math 5 | 6 | def data 7 | Zxcvbn::Data.new 8 | end 9 | 10 | describe '#bruteforce_cardinality' do 11 | context 'when empty password' do 12 | it 'should return 0 if empty password' do 13 | expect(bruteforce_cardinality('')).to eql 0 14 | end 15 | end 16 | 17 | context 'when password is one character long' do 18 | context 'and a digit' do 19 | it 'should return 10' do 20 | (0..9).each do |digit| 21 | expect(bruteforce_cardinality(digit.to_s)).to eql 10 22 | end 23 | end 24 | end 25 | 26 | context 'and an upper case character' do 27 | it 'should return 26' do 28 | ('A'..'Z').each do |character| 29 | expect(bruteforce_cardinality(character)).to eql 26 30 | end 31 | end 32 | end 33 | 34 | context 'and a lower case character' do 35 | it 'should return 26' do 36 | ('a'..'z').each do |character| 37 | expect(bruteforce_cardinality(character)).to eql 26 38 | end 39 | end 40 | end 41 | 42 | context 'and a symbol' do 43 | it 'should return 33' do 44 | %w|/ [ ` {|.each do |symbol| 45 | expect(bruteforce_cardinality(symbol)).to eql 33 46 | end 47 | end 48 | end 49 | end 50 | 51 | context 'when password is more than one character long' do 52 | context 'and only digits' do 53 | it 'should return 10' do 54 | expect(bruteforce_cardinality('123456789')).to eql 10 55 | end 56 | end 57 | 58 | context 'and only lowercase characters' do 59 | it 'should return 26' do 60 | expect(bruteforce_cardinality('password')).to eql 26 61 | end 62 | end 63 | 64 | context 'and only uppercase characters' do 65 | it 'should return 26' do 66 | expect(bruteforce_cardinality('PASSWORD')).to eql 26 67 | end 68 | end 69 | 70 | context 'and only symbols' do 71 | it 'should return 33' do 72 | expect(bruteforce_cardinality('/ [ ` {')).to eql 33 73 | end 74 | end 75 | 76 | context 'and a mixed of character types' do 77 | it 'should add up every character type cardinality' do 78 | expect(bruteforce_cardinality('p1SsWorD!')).to eql 95 79 | end 80 | end 81 | end 82 | end 83 | 84 | describe '#average_degree_for_graph' do 85 | context 'when keyboard is qwerty' do 86 | it 'returns the correct average degree over all keys' do 87 | expect(average_degree_for_graph('qwerty')).to eql 4.595744680851064 88 | end 89 | end 90 | 91 | context 'when keyboard is dvorak' do 92 | it 'returns the correct average degree over all keys' do 93 | expect(average_degree_for_graph('dvorak')).to eql 4.595744680851064 94 | end 95 | end 96 | 97 | context 'when keyboard is keypad' do 98 | it 'returns the correct average degree over all keys' do 99 | expect(average_degree_for_graph('keypad')).to eql 5.066666666666666 100 | end 101 | end 102 | 103 | context 'when keyboard is mac keypad' do 104 | it 'returns the correct average degree over all keys' do 105 | expect(average_degree_for_graph('mac_keypad')).to eql 5.25 106 | end 107 | end 108 | end 109 | 110 | describe '#starting_positions_for_graph' do 111 | context 'when keyboard is qwerty' do 112 | it 'returns the correct average degree over all keys' do 113 | expect(starting_positions_for_graph('qwerty')).to eql 94 114 | end 115 | end 116 | 117 | context 'when keyboard is dvorak' do 118 | it 'returns the correct average degree over all keys' do 119 | expect(starting_positions_for_graph('dvorak')).to eql 94 120 | end 121 | end 122 | 123 | context 'when keyboard is keypad' do 124 | it 'returns the correct average degree over all keys' do 125 | expect(starting_positions_for_graph('keypad')).to eql 15 126 | end 127 | end 128 | 129 | context 'when keyboard is mac keypad' do 130 | it 'returns the correct average degree over all keys' do 131 | expect(starting_positions_for_graph('mac_keypad')).to eql 16 132 | end 133 | end 134 | end 135 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rspec' 3 | require 'zxcvbn' 4 | 5 | Dir[Pathname.new(File.expand_path('../', __FILE__)).join('support/**/*.rb')].each {|f| require f} 6 | 7 | RSpec.configure do |config| 8 | config.include JsHelpers 9 | end 10 | 11 | TEST_PASSWORDS = [ 12 | 'zxcvbn', 13 | 'qwER43@!', 14 | 'Tr0ub4dour&3', 15 | 'correcthorsebatterystaple', 16 | 'coRrecth0rseba++ery9.23.2007staple$', 17 | 18 | 'D0g..................', 19 | 'abcdefghijk987654321', 20 | 'neverforget13/3/1997', 21 | '1qaz2wsx3edc', 22 | 23 | 'temppass22', 24 | 'briansmith', 25 | 'briansmith4mayor', 26 | 'password1', 27 | 'viking', 28 | 'thx1138', 29 | 'ScoRpi0ns', 30 | 'do you know', 31 | 32 | 'ryanhunter2000', 33 | 'rianhunter2000', 34 | 35 | 'asdfghju7654rewq', 36 | 'AOEUIDHG&*()LS_', 37 | 38 | '12345678', 39 | 'defghi6789', 40 | 41 | 'rosebud', 42 | 'Rosebud', 43 | 'ROSEBUD', 44 | 'rosebuD', 45 | 'ros3bud99', 46 | 'r0s3bud99', 47 | 'R0$38uD99', 48 | 49 | 'verlineVANDERMARK', 50 | 51 | 'eheuczkqyq', 52 | 'rWibMFACxAUGZmxhVncy', 53 | 'Ba9ZyWABu99[BK#6MBgbH88Tofv)vs$w' 54 | ] -------------------------------------------------------------------------------- /spec/support/js_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'mini_racer' 2 | require 'json' 3 | 4 | module JsHelpers 5 | class JsMethodInvoker 6 | JS_SOURCE_PATH = Pathname(File.expand_path('../js_source/', __FILE__)) 7 | 8 | def initialize 9 | @ctx = MiniRacer::Context.new 10 | @ctx.eval(JS_SOURCE_PATH.join('compiled.js').read) 11 | end 12 | 13 | def eval(string) 14 | @ctx.eval(string) 15 | end 16 | 17 | def eval_convert_object(string) 18 | serialized = eval("JSON.stringify(#{string})") 19 | JSON.parse(serialized) 20 | end 21 | end 22 | 23 | def method_invoker 24 | $method_invoker ||= JsMethodInvoker.new 25 | end 26 | 27 | def run_js(javascript) 28 | method_invoker.eval_convert_object(javascript) 29 | end 30 | 31 | def js_zxcvbn(password) 32 | run_js("zxcvbn('#{password}')") 33 | end 34 | end -------------------------------------------------------------------------------- /spec/support/js_source/adjacency_graphs.js: -------------------------------------------------------------------------------- 1 | var qwerty = {"!": ["`~", null, null, "2@", "qQ", null], "\"": [";:", "[{", "]}", null, null, "/?"], "#": ["2@", null, null, "4$", "eE", "wW"], "$": ["3#", null, null, "5%", "rR", "eE"], "%": ["4$", null, null, "6^", "tT", "rR"], "&": ["6^", null, null, "8*", "uU", "yY"], "'": [";:", "[{", "]}", null, null, "/?"], "(": ["8*", null, null, "0)", "oO", "iI"], ")": ["9(", null, null, "-_", "pP", "oO"], "*": ["7&", null, null, "9(", "iI", "uU"], "+": ["-_", null, null, null, "]}", "[{"], ",": ["mM", "kK", "lL", ".>", null, null], "-": ["0)", null, null, "=+", "[{", "pP"], ".": [",<", "lL", ";:", "/?", null, null], "/": [".>", ";:", "'\"", null, null, null], "0": ["9(", null, null, "-_", "pP", "oO"], "1": ["`~", null, null, "2@", "qQ", null], "2": ["1!", null, null, "3#", "wW", "qQ"], "3": ["2@", null, null, "4$", "eE", "wW"], "4": ["3#", null, null, "5%", "rR", "eE"], "5": ["4$", null, null, "6^", "tT", "rR"], "6": ["5%", null, null, "7&", "yY", "tT"], "7": ["6^", null, null, "8*", "uU", "yY"], "8": ["7&", null, null, "9(", "iI", "uU"], "9": ["8*", null, null, "0)", "oO", "iI"], ":": ["lL", "pP", "[{", "'\"", "/?", ".>"], ";": ["lL", "pP", "[{", "'\"", "/?", ".>"], "<": ["mM", "kK", "lL", ".>", null, null], "=": ["-_", null, null, null, "]}", "[{"], ">": [",<", "lL", ";:", "/?", null, null], "?": [".>", ";:", "'\"", null, null, null], "@": ["1!", null, null, "3#", "wW", "qQ"], "A": [null, "qQ", "wW", "sS", "zZ", null], "B": ["vV", "gG", "hH", "nN", null, null], "C": ["xX", "dD", "fF", "vV", null, null], "D": ["sS", "eE", "rR", "fF", "cC", "xX"], "E": ["wW", "3#", "4$", "rR", "dD", "sS"], "F": ["dD", "rR", "tT", "gG", "vV", "cC"], "G": ["fF", "tT", "yY", "hH", "bB", "vV"], "H": ["gG", "yY", "uU", "jJ", "nN", "bB"], "I": ["uU", "8*", "9(", "oO", "kK", "jJ"], "J": ["hH", "uU", "iI", "kK", "mM", "nN"], "K": ["jJ", "iI", "oO", "lL", ",<", "mM"], "L": ["kK", "oO", "pP", ";:", ".>", ",<"], "M": ["nN", "jJ", "kK", ",<", null, null], "N": ["bB", "hH", "jJ", "mM", null, null], "O": ["iI", "9(", "0)", "pP", "lL", "kK"], "P": ["oO", "0)", "-_", "[{", ";:", "lL"], "Q": [null, "1!", "2@", "wW", "aA", null], "R": ["eE", "4$", "5%", "tT", "fF", "dD"], "S": ["aA", "wW", "eE", "dD", "xX", "zZ"], "T": ["rR", "5%", "6^", "yY", "gG", "fF"], "U": ["yY", "7&", "8*", "iI", "jJ", "hH"], "V": ["cC", "fF", "gG", "bB", null, null], "W": ["qQ", "2@", "3#", "eE", "sS", "aA"], "X": ["zZ", "sS", "dD", "cC", null, null], "Y": ["tT", "6^", "7&", "uU", "hH", "gG"], "Z": [null, "aA", "sS", "xX", null, null], "[": ["pP", "-_", "=+", "]}", "'\"", ";:"], "\\": ["]}", null, null, null, null, null], "]": ["[{", "=+", null, "\\|", null, "'\""], "^": ["5%", null, null, "7&", "yY", "tT"], "_": ["0)", null, null, "=+", "[{", "pP"], "`": [null, null, null, "1!", null, null], "a": [null, "qQ", "wW", "sS", "zZ", null], "b": ["vV", "gG", "hH", "nN", null, null], "c": ["xX", "dD", "fF", "vV", null, null], "d": ["sS", "eE", "rR", "fF", "cC", "xX"], "e": ["wW", "3#", "4$", "rR", "dD", "sS"], "f": ["dD", "rR", "tT", "gG", "vV", "cC"], "g": ["fF", "tT", "yY", "hH", "bB", "vV"], "h": ["gG", "yY", "uU", "jJ", "nN", "bB"], "i": ["uU", "8*", "9(", "oO", "kK", "jJ"], "j": ["hH", "uU", "iI", "kK", "mM", "nN"], "k": ["jJ", "iI", "oO", "lL", ",<", "mM"], "l": ["kK", "oO", "pP", ";:", ".>", ",<"], "m": ["nN", "jJ", "kK", ",<", null, null], "n": ["bB", "hH", "jJ", "mM", null, null], "o": ["iI", "9(", "0)", "pP", "lL", "kK"], "p": ["oO", "0)", "-_", "[{", ";:", "lL"], "q": [null, "1!", "2@", "wW", "aA", null], "r": ["eE", "4$", "5%", "tT", "fF", "dD"], "s": ["aA", "wW", "eE", "dD", "xX", "zZ"], "t": ["rR", "5%", "6^", "yY", "gG", "fF"], "u": ["yY", "7&", "8*", "iI", "jJ", "hH"], "v": ["cC", "fF", "gG", "bB", null, null], "w": ["qQ", "2@", "3#", "eE", "sS", "aA"], "x": ["zZ", "sS", "dD", "cC", null, null], "y": ["tT", "6^", "7&", "uU", "hH", "gG"], "z": [null, "aA", "sS", "xX", null, null], "{": ["pP", "-_", "=+", "]}", "'\"", ";:"], "|": ["]}", null, null, null, null, null], "}": ["[{", "=+", null, "\\|", null, "'\""], "~": [null, null, null, "1!", null, null]}; 2 | 3 | var dvorak = {"!": ["`~", null, null, "2@", "'\"", null], "\"": [null, "1!", "2@", ",<", "aA", null], "#": ["2@", null, null, "4$", ".>", ",<"], "$": ["3#", null, null, "5%", "pP", ".>"], "%": ["4$", null, null, "6^", "yY", "pP"], "&": ["6^", null, null, "8*", "gG", "fF"], "'": [null, "1!", "2@", ",<", "aA", null], "(": ["8*", null, null, "0)", "rR", "cC"], ")": ["9(", null, null, "[{", "lL", "rR"], "*": ["7&", null, null, "9(", "cC", "gG"], "+": ["/?", "]}", null, "\\|", null, "-_"], ",": ["'\"", "2@", "3#", ".>", "oO", "aA"], "-": ["sS", "/?", "=+", null, null, "zZ"], ".": [",<", "3#", "4$", "pP", "eE", "oO"], "/": ["lL", "[{", "]}", "=+", "-_", "sS"], "0": ["9(", null, null, "[{", "lL", "rR"], "1": ["`~", null, null, "2@", "'\"", null], "2": ["1!", null, null, "3#", ",<", "'\""], "3": ["2@", null, null, "4$", ".>", ",<"], "4": ["3#", null, null, "5%", "pP", ".>"], "5": ["4$", null, null, "6^", "yY", "pP"], "6": ["5%", null, null, "7&", "fF", "yY"], "7": ["6^", null, null, "8*", "gG", "fF"], "8": ["7&", null, null, "9(", "cC", "gG"], "9": ["8*", null, null, "0)", "rR", "cC"], ":": [null, "aA", "oO", "qQ", null, null], ";": [null, "aA", "oO", "qQ", null, null], "<": ["'\"", "2@", "3#", ".>", "oO", "aA"], "=": ["/?", "]}", null, "\\|", null, "-_"], ">": [",<", "3#", "4$", "pP", "eE", "oO"], "?": ["lL", "[{", "]}", "=+", "-_", "sS"], "@": ["1!", null, null, "3#", ",<", "'\""], "A": [null, "'\"", ",<", "oO", ";:", null], "B": ["xX", "dD", "hH", "mM", null, null], "C": ["gG", "8*", "9(", "rR", "tT", "hH"], "D": ["iI", "fF", "gG", "hH", "bB", "xX"], "E": ["oO", ".>", "pP", "uU", "jJ", "qQ"], "F": ["yY", "6^", "7&", "gG", "dD", "iI"], "G": ["fF", "7&", "8*", "cC", "hH", "dD"], "H": ["dD", "gG", "cC", "tT", "mM", "bB"], "I": ["uU", "yY", "fF", "dD", "xX", "kK"], "J": ["qQ", "eE", "uU", "kK", null, null], "K": ["jJ", "uU", "iI", "xX", null, null], "L": ["rR", "0)", "[{", "/?", "sS", "nN"], "M": ["bB", "hH", "tT", "wW", null, null], "N": ["tT", "rR", "lL", "sS", "vV", "wW"], "O": ["aA", ",<", ".>", "eE", "qQ", ";:"], "P": [".>", "4$", "5%", "yY", "uU", "eE"], "Q": [";:", "oO", "eE", "jJ", null, null], "R": ["cC", "9(", "0)", "lL", "nN", "tT"], "S": ["nN", "lL", "/?", "-_", "zZ", "vV"], "T": ["hH", "cC", "rR", "nN", "wW", "mM"], "U": ["eE", "pP", "yY", "iI", "kK", "jJ"], "V": ["wW", "nN", "sS", "zZ", null, null], "W": ["mM", "tT", "nN", "vV", null, null], "X": ["kK", "iI", "dD", "bB", null, null], "Y": ["pP", "5%", "6^", "fF", "iI", "uU"], "Z": ["vV", "sS", "-_", null, null, null], "[": ["0)", null, null, "]}", "/?", "lL"], "\\": ["=+", null, null, null, null, null], "]": ["[{", null, null, null, "=+", "/?"], "^": ["5%", null, null, "7&", "fF", "yY"], "_": ["sS", "/?", "=+", null, null, "zZ"], "`": [null, null, null, "1!", null, null], "a": [null, "'\"", ",<", "oO", ";:", null], "b": ["xX", "dD", "hH", "mM", null, null], "c": ["gG", "8*", "9(", "rR", "tT", "hH"], "d": ["iI", "fF", "gG", "hH", "bB", "xX"], "e": ["oO", ".>", "pP", "uU", "jJ", "qQ"], "f": ["yY", "6^", "7&", "gG", "dD", "iI"], "g": ["fF", "7&", "8*", "cC", "hH", "dD"], "h": ["dD", "gG", "cC", "tT", "mM", "bB"], "i": ["uU", "yY", "fF", "dD", "xX", "kK"], "j": ["qQ", "eE", "uU", "kK", null, null], "k": ["jJ", "uU", "iI", "xX", null, null], "l": ["rR", "0)", "[{", "/?", "sS", "nN"], "m": ["bB", "hH", "tT", "wW", null, null], "n": ["tT", "rR", "lL", "sS", "vV", "wW"], "o": ["aA", ",<", ".>", "eE", "qQ", ";:"], "p": [".>", "4$", "5%", "yY", "uU", "eE"], "q": [";:", "oO", "eE", "jJ", null, null], "r": ["cC", "9(", "0)", "lL", "nN", "tT"], "s": ["nN", "lL", "/?", "-_", "zZ", "vV"], "t": ["hH", "cC", "rR", "nN", "wW", "mM"], "u": ["eE", "pP", "yY", "iI", "kK", "jJ"], "v": ["wW", "nN", "sS", "zZ", null, null], "w": ["mM", "tT", "nN", "vV", null, null], "x": ["kK", "iI", "dD", "bB", null, null], "y": ["pP", "5%", "6^", "fF", "iI", "uU"], "z": ["vV", "sS", "-_", null, null, null], "{": ["0)", null, null, "]}", "/?", "lL"], "|": ["=+", null, null, null, null, null], "}": ["[{", null, null, null, "=+", "/?"], "~": [null, null, null, "1!", null, null]}; 4 | 5 | var keypad = {"*": ["/", null, null, null, "-", "+", "9", "8"], "+": ["9", "*", "-", null, null, null, null, "6"], "-": ["*", null, null, null, null, null, "+", "9"], ".": ["0", "2", "3", null, null, null, null, null], "/": [null, null, null, null, "*", "9", "8", "7"], "0": [null, "1", "2", "3", ".", null, null, null], "1": [null, null, "4", "5", "2", "0", null, null], "2": ["1", "4", "5", "6", "3", ".", "0", null], "3": ["2", "5", "6", null, null, null, ".", "0"], "4": [null, null, "7", "8", "5", "2", "1", null], "5": ["4", "7", "8", "9", "6", "3", "2", "1"], "6": ["5", "8", "9", "+", null, null, "3", "2"], "7": [null, null, null, "/", "8", "5", "4", null], "8": ["7", null, "/", "*", "9", "6", "5", "4"], "9": ["8", "/", "*", "-", "+", null, "6", "5"]}; 6 | 7 | var mac_keypad = {"*": ["/", null, null, null, null, null, "-", "9"], "+": ["6", "9", "-", null, null, null, null, "3"], "-": ["9", "/", "*", null, null, null, "+", "6"], ".": ["0", "2", "3", null, null, null, null, null], "/": ["=", null, null, null, "*", "-", "9", "8"], "0": [null, "1", "2", "3", ".", null, null, null], "1": [null, null, "4", "5", "2", "0", null, null], "2": ["1", "4", "5", "6", "3", ".", "0", null], "3": ["2", "5", "6", "+", null, null, ".", "0"], "4": [null, null, "7", "8", "5", "2", "1", null], "5": ["4", "7", "8", "9", "6", "3", "2", "1"], "6": ["5", "8", "9", "-", "+", null, "3", "2"], "7": [null, null, null, "=", "8", "5", "4", null], "8": ["7", null, "=", "/", "9", "6", "5", "4"], "9": ["8", "=", "/", "*", "-", "+", "6", "5"], "=": [null, null, null, null, "/", "9", "8", "7"]}; 8 | 9 | -------------------------------------------------------------------------------- /spec/support/js_source/init.coffee: -------------------------------------------------------------------------------- 1 | 2 | ranked_user_inputs_dict = {} 3 | 4 | # initialize matcher lists 5 | DICTIONARY_MATCHERS = [ 6 | build_dict_matcher('passwords', build_ranked_dict(passwords)), 7 | build_dict_matcher('english', build_ranked_dict(english)), 8 | build_dict_matcher('male_names', build_ranked_dict(male_names)), 9 | build_dict_matcher('female_names', build_ranked_dict(female_names)), 10 | build_dict_matcher('surnames', build_ranked_dict(surnames)), 11 | build_dict_matcher('user_inputs', ranked_user_inputs_dict), 12 | ] 13 | 14 | MATCHERS = DICTIONARY_MATCHERS.concat [ 15 | l33t_match, 16 | digits_match, year_match, date_match, 17 | repeat_match, sequence_match, 18 | spatial_match 19 | ] 20 | 21 | GRAPHS = 22 | 'qwerty': qwerty 23 | 'dvorak': dvorak 24 | 'keypad': keypad 25 | 'mac_keypad': mac_keypad 26 | 27 | # on qwerty, 'g' has degree 6, being adjacent to 'ftyhbv'. '\' has degree 1. 28 | # this calculates the average over all keys. 29 | calc_average_degree = (graph) -> 30 | average = 0 31 | for key, neighbors of graph 32 | average += (n for n in neighbors when n).length 33 | average /= (k for k,v of graph).length 34 | average 35 | 36 | KEYBOARD_AVERAGE_DEGREE = calc_average_degree(qwerty) 37 | KEYPAD_AVERAGE_DEGREE = calc_average_degree(keypad) # slightly different for keypad/mac keypad, but close enough 38 | 39 | KEYBOARD_STARTING_POSITIONS = (k for k,v of qwerty).length 40 | KEYPAD_STARTING_POSITIONS = (k for k,v of keypad).length 41 | 42 | time = -> (new Date()).getTime() 43 | 44 | # now that frequency lists are loaded, replace zxcvbn stub function. 45 | zxcvbn = (password, user_inputs) -> 46 | start = time() 47 | if user_inputs? 48 | for i in [0...user_inputs.length] 49 | # update ranked_user_inputs_dict. 50 | # i+1 instead of i b/c rank starts at 1. 51 | ranked_user_inputs_dict[user_inputs[i]] = i + 1 52 | matches = omnimatch password 53 | result = minimum_entropy_match_sequence password, matches 54 | result.calc_time = time() - start 55 | result 56 | 57 | # make zxcvbn function globally available 58 | # via window or exports object, depending on the environment 59 | if window? 60 | window.zxcvbn = zxcvbn 61 | window.zxcvbn_load_hook?() # run load hook from user, if defined 62 | else if exports? 63 | exports.zxcvbn = zxcvbn 64 | -------------------------------------------------------------------------------- /spec/support/js_source/init.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.3.3 2 | var DICTIONARY_MATCHERS, GRAPHS, KEYBOARD_AVERAGE_DEGREE, KEYBOARD_STARTING_POSITIONS, KEYPAD_AVERAGE_DEGREE, KEYPAD_STARTING_POSITIONS, MATCHERS, calc_average_degree, k, ranked_user_inputs_dict, time, v, zxcvbn; 3 | 4 | ranked_user_inputs_dict = {}; 5 | 6 | DICTIONARY_MATCHERS = [build_dict_matcher('passwords', build_ranked_dict(passwords)), build_dict_matcher('english', build_ranked_dict(english)), build_dict_matcher('male_names', build_ranked_dict(male_names)), build_dict_matcher('female_names', build_ranked_dict(female_names)), build_dict_matcher('surnames', build_ranked_dict(surnames)), build_dict_matcher('user_inputs', ranked_user_inputs_dict)]; 7 | 8 | MATCHERS = DICTIONARY_MATCHERS.concat([l33t_match, digits_match, year_match, date_match, repeat_match, sequence_match, spatial_match]); 9 | 10 | GRAPHS = { 11 | 'qwerty': qwerty, 12 | 'dvorak': dvorak, 13 | 'keypad': keypad, 14 | 'mac_keypad': mac_keypad 15 | }; 16 | 17 | calc_average_degree = function(graph) { 18 | var average, k, key, n, neighbors, v; 19 | average = 0; 20 | for (key in graph) { 21 | neighbors = graph[key]; 22 | average += ((function() { 23 | var _i, _len, _results; 24 | _results = []; 25 | for (_i = 0, _len = neighbors.length; _i < _len; _i++) { 26 | n = neighbors[_i]; 27 | if (n) { 28 | _results.push(n); 29 | } 30 | } 31 | return _results; 32 | })()).length; 33 | } 34 | average /= ((function() { 35 | var _results; 36 | _results = []; 37 | for (k in graph) { 38 | v = graph[k]; 39 | _results.push(k); 40 | } 41 | return _results; 42 | })()).length; 43 | return average; 44 | }; 45 | 46 | KEYBOARD_AVERAGE_DEGREE = calc_average_degree(qwerty); 47 | 48 | KEYPAD_AVERAGE_DEGREE = calc_average_degree(keypad); 49 | 50 | KEYBOARD_STARTING_POSITIONS = ((function() { 51 | var _results; 52 | _results = []; 53 | for (k in qwerty) { 54 | v = qwerty[k]; 55 | _results.push(k); 56 | } 57 | return _results; 58 | })()).length; 59 | 60 | KEYPAD_STARTING_POSITIONS = ((function() { 61 | var _results; 62 | _results = []; 63 | for (k in keypad) { 64 | v = keypad[k]; 65 | _results.push(k); 66 | } 67 | return _results; 68 | })()).length; 69 | 70 | time = function() { 71 | return (new Date()).getTime(); 72 | }; 73 | 74 | zxcvbn = function(password, user_inputs) { 75 | var i, matches, result, start, _i, _ref; 76 | start = time(); 77 | if (user_inputs != null) { 78 | for (i = _i = 0, _ref = user_inputs.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { 79 | ranked_user_inputs_dict[user_inputs[i]] = i + 1; 80 | } 81 | } 82 | matches = omnimatch(password); 83 | result = minimum_entropy_match_sequence(password, matches); 84 | result.calc_time = time() - start; 85 | return result; 86 | }; 87 | 88 | if (typeof window !== "undefined" && window !== null) { 89 | window.zxcvbn = zxcvbn; 90 | if (typeof window.zxcvbn_load_hook === "function") { 91 | window.zxcvbn_load_hook(); 92 | } 93 | } else if (typeof exports !== "undefined" && exports !== null) { 94 | exports.zxcvbn = zxcvbn; 95 | } 96 | -------------------------------------------------------------------------------- /spec/support/js_source/matching.coffee: -------------------------------------------------------------------------------- 1 | 2 | empty = (obj) -> (k for k of obj).length == 0 3 | extend = (lst, lst2) -> lst.push.apply lst, lst2 4 | translate = (string, chr_map) -> (chr_map[chr] or chr for chr in string.split('')).join('') 5 | 6 | # ------------------------------------------------------------------------------ 7 | # omnimatch -- combine everything ---------------------------------------------- 8 | # ------------------------------------------------------------------------------ 9 | 10 | omnimatch = (password) -> 11 | matches = [] 12 | for matcher in MATCHERS 13 | extend matches, matcher(password) 14 | matches.sort (match1, match2) -> 15 | (match1.i - match2.i) or (match1.j - match2.j) 16 | 17 | #------------------------------------------------------------------------------- 18 | # dictionary match (common passwords, english, last names, etc) ---------------- 19 | #------------------------------------------------------------------------------- 20 | 21 | dictionary_match = (password, ranked_dict) -> 22 | result = [] 23 | len = password.length 24 | password_lower = password.toLowerCase() 25 | for i in [0...len] 26 | for j in [i...len] 27 | if password_lower[i..j] of ranked_dict 28 | word = password_lower[i..j] 29 | rank = ranked_dict[word] 30 | result.push( 31 | pattern: 'dictionary' 32 | i: i 33 | j: j 34 | token: password[i..j] 35 | matched_word: word 36 | rank: rank 37 | ) 38 | result 39 | 40 | build_ranked_dict = (unranked_list) -> 41 | result = {} 42 | i = 1 # rank starts at 1, not 0 43 | for word in unranked_list 44 | result[word] = i 45 | i += 1 46 | result 47 | 48 | build_dict_matcher = (dict_name, ranked_dict) -> 49 | (password) -> 50 | matches = dictionary_match(password, ranked_dict) 51 | match.dictionary_name = dict_name for match in matches 52 | matches 53 | 54 | #------------------------------------------------------------------------------- 55 | # dictionary match with common l33t substitutions ------------------------------ 56 | #------------------------------------------------------------------------------- 57 | 58 | l33t_table = 59 | a: ['4', '@'] 60 | b: ['8'] 61 | c: ['(', '{', '[', '<'] 62 | e: ['3'] 63 | g: ['6', '9'] 64 | i: ['1', '!', '|'] 65 | l: ['1', '|', '7'] 66 | o: ['0'] 67 | s: ['$', '5'] 68 | t: ['+', '7'] 69 | x: ['%'] 70 | z: ['2'] 71 | 72 | # makes a pruned copy of l33t_table that only includes password's possible substitutions 73 | relevent_l33t_subtable = (password) -> 74 | password_chars = {} 75 | for chr in password.split('') 76 | password_chars[chr] = true 77 | filtered = {} 78 | for letter, subs of l33t_table 79 | relevent_subs = (sub for sub in subs when sub of password_chars) 80 | if relevent_subs.length > 0 81 | filtered[letter] = relevent_subs 82 | filtered 83 | 84 | # returns the list of possible 1337 replacement dictionaries for a given password 85 | enumerate_l33t_subs = (table) -> 86 | keys = (k for k of table) 87 | subs = [[]] 88 | 89 | dedup = (subs) -> 90 | deduped = [] 91 | members = {} 92 | for sub in subs 93 | assoc = ([k,v] for k,v in sub) 94 | assoc.sort() 95 | label = (k+','+v for k,v in assoc).join('-') 96 | unless label of members 97 | members[label] = true 98 | deduped.push sub 99 | deduped 100 | 101 | helper = (keys) -> 102 | return if not keys.length 103 | first_key = keys[0] 104 | rest_keys = keys[1..] 105 | next_subs = [] 106 | for l33t_chr in table[first_key] 107 | for sub in subs 108 | dup_l33t_index = -1 109 | for i in [0...sub.length] 110 | if sub[i][0] == l33t_chr 111 | dup_l33t_index = i 112 | break 113 | if dup_l33t_index == -1 114 | sub_extension = sub.concat [[l33t_chr, first_key]] 115 | next_subs.push sub_extension 116 | else 117 | sub_alternative = sub.slice(0) 118 | sub_alternative.splice(dup_l33t_index, 1) 119 | sub_alternative.push [l33t_chr, first_key] 120 | next_subs.push sub 121 | next_subs.push sub_alternative 122 | subs = dedup next_subs 123 | helper(rest_keys) 124 | 125 | helper(keys) 126 | sub_dicts = [] # convert from assoc lists to dicts 127 | for sub in subs 128 | sub_dict = {} 129 | for [l33t_chr, chr] in sub 130 | sub_dict[l33t_chr] = chr 131 | sub_dicts.push sub_dict 132 | sub_dicts 133 | 134 | l33t_match = (password) -> 135 | matches = [] 136 | for sub in enumerate_l33t_subs relevent_l33t_subtable password 137 | break if empty sub # corner case: password has no relevent subs. 138 | for matcher in DICTIONARY_MATCHERS 139 | subbed_password = translate password, sub 140 | for match in matcher(subbed_password) 141 | token = password[match.i..match.j] 142 | if token.toLowerCase() == match.matched_word 143 | continue # only return the matches that contain an actual substitution 144 | match_sub = {} # subset of mappings in sub that are in use for this match 145 | for subbed_chr, chr of sub when token.indexOf(subbed_chr) != -1 146 | match_sub[subbed_chr] = chr 147 | match.l33t = true 148 | match.token = token 149 | match.sub = match_sub 150 | match.sub_display = ("#{k} -> #{v}" for k,v of match_sub).join(', ') 151 | matches.push match 152 | matches 153 | 154 | # ------------------------------------------------------------------------------ 155 | # spatial match (qwerty/dvorak/keypad) ----------------------------------------- 156 | # ------------------------------------------------------------------------------ 157 | 158 | spatial_match = (password) -> 159 | matches = [] 160 | for graph_name, graph of GRAPHS 161 | extend matches, spatial_match_helper(password, graph, graph_name) 162 | matches 163 | 164 | spatial_match_helper = (password, graph, graph_name) -> 165 | result = [] 166 | i = 0 167 | while i < password.length - 1 168 | j = i + 1 169 | last_direction = null 170 | turns = 0 171 | shifted_count = 0 172 | loop 173 | prev_char = password.charAt(j-1) 174 | found = false 175 | found_direction = -1 176 | cur_direction = -1 177 | adjacents = graph[prev_char] or [] 178 | # consider growing pattern by one character if j hasn't gone over the edge. 179 | if j < password.length 180 | cur_char = password.charAt(j) 181 | for adj in adjacents 182 | cur_direction += 1 183 | if adj and adj.indexOf(cur_char) != -1 184 | found = true 185 | found_direction = cur_direction 186 | if adj.indexOf(cur_char) == 1 187 | # index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc. 188 | # for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted. 189 | shifted_count += 1 190 | if last_direction != found_direction 191 | # adding a turn is correct even in the initial case when last_direction is null: 192 | # every spatial pattern starts with a turn. 193 | turns += 1 194 | last_direction = found_direction 195 | break 196 | # if the current pattern continued, extend j and try to grow again 197 | if found 198 | j += 1 199 | # otherwise push the pattern discovered so far, if any... 200 | else 201 | if j - i > 2 # don't consider length 1 or 2 chains. 202 | result.push 203 | pattern: 'spatial' 204 | i: i 205 | j: j-1 206 | token: password[i...j] 207 | graph: graph_name 208 | turns: turns 209 | shifted_count: shifted_count 210 | # ...and then start a new search for the rest of the password. 211 | i = j 212 | break 213 | result 214 | 215 | #------------------------------------------------------------------------------- 216 | # repeats (aaa) and sequences (abcdef) ----------------------------------------- 217 | #------------------------------------------------------------------------------- 218 | 219 | repeat_match = (password) -> 220 | result = [] 221 | i = 0 222 | while i < password.length 223 | j = i + 1 224 | loop 225 | [prev_char, cur_char] = password[j-1..j] 226 | if password.charAt(j-1) == password.charAt(j) 227 | j += 1 228 | else 229 | if j - i > 2 # don't consider length 1 or 2 chains. 230 | result.push 231 | pattern: 'repeat' 232 | i: i 233 | j: j-1 234 | token: password[i...j] 235 | repeated_char: password.charAt(i) 236 | break 237 | i = j 238 | result 239 | 240 | SEQUENCES = 241 | lower: 'abcdefghijklmnopqrstuvwxyz' 242 | upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 243 | digits: '01234567890' 244 | 245 | sequence_match = (password) -> 246 | result = [] 247 | i = 0 248 | while i < password.length 249 | j = i + 1 250 | seq = null # either lower, upper, or digits 251 | seq_name = null 252 | seq_direction = null # 1 for ascending seq abcd, -1 for dcba 253 | for seq_candidate_name, seq_candidate of SEQUENCES 254 | [i_n, j_n] = (seq_candidate.indexOf(chr) for chr in [password.charAt(i), password.charAt(j)]) 255 | if i_n > -1 and j_n > -1 256 | direction = j_n - i_n 257 | if direction in [1, -1] 258 | seq = seq_candidate 259 | seq_name = seq_candidate_name 260 | seq_direction = direction 261 | break 262 | if seq 263 | loop 264 | [prev_char, cur_char] = password[j-1..j] 265 | [prev_n, cur_n] = (seq_candidate.indexOf(chr) for chr in [prev_char, cur_char]) 266 | # Bug fix. 'ba+' was falsly being reported as a sequence due to 1 - null working in JS. 267 | # TODO: Submit PR to zxcvbn.js 268 | if !!cur_n && (cur_n - prev_n == seq_direction) 269 | j += 1 270 | else 271 | if j - i > 2 # don't consider length 1 or 2 chains. 272 | result.push 273 | pattern: 'sequence' 274 | i: i 275 | j: j-1 276 | token: password[i...j] 277 | sequence_name: seq_name 278 | sequence_space: seq.length 279 | ascending: seq_direction == 1 280 | break 281 | i = j 282 | result 283 | 284 | #------------------------------------------------------------------------------- 285 | # digits, years, dates --------------------------------------------------------- 286 | #------------------------------------------------------------------------------- 287 | 288 | repeat = (chr, n) -> (chr for i in [1..n]).join('') 289 | 290 | findall = (password, rx) -> 291 | matches = [] 292 | loop 293 | match = password.match rx 294 | break if not match 295 | match.i = match.index 296 | match.j = match.index + match[0].length - 1 297 | matches.push match 298 | password = password.replace match[0], repeat(' ', match[0].length) 299 | matches 300 | 301 | digits_rx = /\d{3,}/ 302 | digits_match = (password) -> 303 | for match in findall password, digits_rx 304 | [i, j] = [match.i, match.j] 305 | pattern: 'digits' 306 | i: i 307 | j: j 308 | token: password[i..j] 309 | 310 | # 4-digit years only. 2-digit years have the same entropy as 2-digit brute force. 311 | year_rx = /19\d\d|200\d|201\d/ 312 | year_match = (password) -> 313 | for match in findall password, year_rx 314 | [i, j] = [match.i, match.j] 315 | pattern: 'year' 316 | i: i 317 | j: j 318 | token: password[i..j] 319 | 320 | date_match = (password) -> 321 | # match dates with separators 1/1/1911 and dates without 111997 322 | date_without_sep_match(password).concat date_sep_match(password) 323 | 324 | date_without_sep_match = (password) -> 325 | date_matches = [] 326 | for digit_match in findall password, /\d{4,8}/ # 1197 is length-4, 01011997 is length 8 327 | [i, j] = [digit_match.i, digit_match.j] 328 | token = password[i..j] 329 | end = token.length 330 | candidates_round_1 = [] # parse year alternatives 331 | if token.length <= 6 332 | candidates_round_1.push # 2-digit year prefix 333 | daymonth: token[2..] 334 | year: token[0..1] 335 | i: i 336 | j: j 337 | candidates_round_1.push # 2-digit year suffix 338 | daymonth: token[0...end-2] 339 | year: token[end-2..] 340 | i: i 341 | j: j 342 | if token.length >= 6 343 | candidates_round_1.push # 4-digit year prefix 344 | daymonth: token[4..] 345 | year: token[0..3] 346 | i: i 347 | j: j 348 | candidates_round_1.push # 4-digit year suffix 349 | daymonth: token[0...end-4] 350 | year: token[end-4..] 351 | i: i 352 | j: j 353 | candidates_round_2 = [] # parse day/month alternatives 354 | for candidate in candidates_round_1 355 | switch candidate.daymonth.length 356 | when 2 # ex. 1 1 97 357 | candidates_round_2.push 358 | day: candidate.daymonth[0] 359 | month: candidate.daymonth[1] 360 | year: candidate.year 361 | i: candidate.i 362 | j: candidate.j 363 | when 3 # ex. 11 1 97 or 1 11 97 364 | candidates_round_2.push 365 | day: candidate.daymonth[0..1] 366 | month: candidate.daymonth[2] 367 | year: candidate.year 368 | i: candidate.i 369 | j: candidate.j 370 | candidates_round_2.push 371 | day: candidate.daymonth[0] 372 | month: candidate.daymonth[1..2] 373 | year: candidate.year 374 | i: candidate.i 375 | j: candidate.j 376 | when 4 # ex. 11 11 97 377 | candidates_round_2.push 378 | day: candidate.daymonth[0..1] 379 | month: candidate.daymonth[2..3] 380 | year: candidate.year 381 | i: candidate.i 382 | j: candidate.j 383 | # final loop: reject invalid dates 384 | for candidate in candidates_round_2 385 | day = parseInt(candidate.day) 386 | month = parseInt(candidate.month) 387 | year = parseInt(candidate.year) 388 | [valid, [day, month, year]] = check_date(day, month, year) 389 | continue unless valid 390 | date_matches.push 391 | pattern: 'date' 392 | i: candidate.i 393 | j: candidate.j 394 | token: password[i..j] 395 | separator: '' 396 | day: day 397 | month: month 398 | year: year 399 | date_matches 400 | 401 | date_rx_year_suffix = /// 402 | ( \d{1,2} ) # day or month 403 | ( \s | - | / | \\ | _ | \. ) # separator 404 | ( \d{1,2} ) # month or day 405 | \2 # same separator 406 | ( 19\d{2} | 200\d | 201\d | \d{2} ) # year 407 | /// 408 | date_rx_year_prefix = /// 409 | ( 19\d{2} | 200\d | 201\d | \d{2} ) # year 410 | ( \s | - | / | \\ | _ | \. ) # separator 411 | ( \d{1,2} ) # day or month 412 | \2 # same separator 413 | ( \d{1,2} ) # month or day 414 | /// 415 | date_sep_match = (password) -> 416 | matches = [] 417 | for match in findall password, date_rx_year_suffix 418 | [match.day, match.month, match.year] = (parseInt(match[k]) for k in [1,3,4]) 419 | match.sep = match[2] 420 | matches.push match 421 | for match in findall password, date_rx_year_prefix 422 | [match.day, match.month, match.year] = (parseInt(match[k]) for k in [4,3,1]) 423 | match.sep = match[2] 424 | matches.push match 425 | for match in matches 426 | [valid, [day, month, year]] = check_date(match.day, match.month, match.year) 427 | continue unless valid 428 | pattern: 'date' 429 | i: match.i 430 | j: match.j 431 | token: password[match.i..match.j] 432 | separator: match.sep 433 | day: day 434 | month: month 435 | year: year 436 | 437 | check_date = (day, month, year) -> 438 | if 12 <= month <= 31 and day <= 12 # tolerate both day-month and month-day order 439 | [day, month] = [month, day] 440 | if day > 31 or month > 12 441 | return [false, []] 442 | unless 1900 <= year <= 2019 443 | return [false, []] 444 | [true, [day, month, year]] 445 | -------------------------------------------------------------------------------- /spec/support/js_source/matching.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.3.3 2 | var SEQUENCES, build_dict_matcher, build_ranked_dict, check_date, date_match, date_rx_year_prefix, date_rx_year_suffix, date_sep_match, date_without_sep_match, dictionary_match, digits_match, digits_rx, empty, enumerate_l33t_subs, extend, findall, l33t_match, l33t_table, omnimatch, relevent_l33t_subtable, repeat, repeat_match, sequence_match, spatial_match, spatial_match_helper, translate, year_match, year_rx; 3 | 4 | empty = function(obj) { 5 | var k; 6 | return ((function() { 7 | var _results; 8 | _results = []; 9 | for (k in obj) { 10 | _results.push(k); 11 | } 12 | return _results; 13 | })()).length === 0; 14 | }; 15 | 16 | extend = function(lst, lst2) { 17 | return lst.push.apply(lst, lst2); 18 | }; 19 | 20 | translate = function(string, chr_map) { 21 | var chr; 22 | return ((function() { 23 | var _i, _len, _ref, _results; 24 | _ref = string.split(''); 25 | _results = []; 26 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 27 | chr = _ref[_i]; 28 | _results.push(chr_map[chr] || chr); 29 | } 30 | return _results; 31 | })()).join(''); 32 | }; 33 | 34 | omnimatch = function(password) { 35 | var matcher, matches, _i, _len; 36 | matches = []; 37 | for (_i = 0, _len = MATCHERS.length; _i < _len; _i++) { 38 | matcher = MATCHERS[_i]; 39 | extend(matches, matcher(password)); 40 | } 41 | return matches.sort(function(match1, match2) { 42 | return (match1.i - match2.i) || (match1.j - match2.j); 43 | }); 44 | }; 45 | 46 | dictionary_match = function(password, ranked_dict) { 47 | var i, j, len, password_lower, rank, result, word, _i, _j; 48 | result = []; 49 | len = password.length; 50 | password_lower = password.toLowerCase(); 51 | for (i = _i = 0; 0 <= len ? _i < len : _i > len; i = 0 <= len ? ++_i : --_i) { 52 | for (j = _j = i; i <= len ? _j < len : _j > len; j = i <= len ? ++_j : --_j) { 53 | if (password_lower.slice(i, j + 1 || 9e9) in ranked_dict) { 54 | word = password_lower.slice(i, j + 1 || 9e9); 55 | rank = ranked_dict[word]; 56 | result.push({ 57 | pattern: 'dictionary', 58 | i: i, 59 | j: j, 60 | token: password.slice(i, j + 1 || 9e9), 61 | matched_word: word, 62 | rank: rank 63 | }); 64 | } 65 | } 66 | } 67 | return result; 68 | }; 69 | 70 | build_ranked_dict = function(unranked_list) { 71 | var i, result, word, _i, _len; 72 | result = {}; 73 | i = 1; 74 | for (_i = 0, _len = unranked_list.length; _i < _len; _i++) { 75 | word = unranked_list[_i]; 76 | result[word] = i; 77 | i += 1; 78 | } 79 | return result; 80 | }; 81 | 82 | build_dict_matcher = function(dict_name, ranked_dict) { 83 | return function(password) { 84 | var match, matches, _i, _len; 85 | matches = dictionary_match(password, ranked_dict); 86 | for (_i = 0, _len = matches.length; _i < _len; _i++) { 87 | match = matches[_i]; 88 | match.dictionary_name = dict_name; 89 | } 90 | return matches; 91 | }; 92 | }; 93 | 94 | l33t_table = { 95 | a: ['4', '@'], 96 | b: ['8'], 97 | c: ['(', '{', '[', '<'], 98 | e: ['3'], 99 | g: ['6', '9'], 100 | i: ['1', '!', '|'], 101 | l: ['1', '|', '7'], 102 | o: ['0'], 103 | s: ['$', '5'], 104 | t: ['+', '7'], 105 | x: ['%'], 106 | z: ['2'] 107 | }; 108 | 109 | relevent_l33t_subtable = function(password) { 110 | var chr, filtered, letter, password_chars, relevent_subs, sub, subs, _i, _len, _ref; 111 | password_chars = {}; 112 | _ref = password.split(''); 113 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 114 | chr = _ref[_i]; 115 | password_chars[chr] = true; 116 | } 117 | filtered = {}; 118 | for (letter in l33t_table) { 119 | subs = l33t_table[letter]; 120 | relevent_subs = (function() { 121 | var _j, _len1, _results; 122 | _results = []; 123 | for (_j = 0, _len1 = subs.length; _j < _len1; _j++) { 124 | sub = subs[_j]; 125 | if (sub in password_chars) { 126 | _results.push(sub); 127 | } 128 | } 129 | return _results; 130 | })(); 131 | if (relevent_subs.length > 0) { 132 | filtered[letter] = relevent_subs; 133 | } 134 | } 135 | return filtered; 136 | }; 137 | 138 | enumerate_l33t_subs = function(table) { 139 | var chr, dedup, helper, k, keys, l33t_chr, sub, sub_dict, sub_dicts, subs, _i, _j, _len, _len1, _ref; 140 | keys = (function() { 141 | var _results; 142 | _results = []; 143 | for (k in table) { 144 | _results.push(k); 145 | } 146 | return _results; 147 | })(); 148 | subs = [[]]; 149 | dedup = function(subs) { 150 | var assoc, deduped, label, members, sub, v, _i, _len; 151 | deduped = []; 152 | members = {}; 153 | for (_i = 0, _len = subs.length; _i < _len; _i++) { 154 | sub = subs[_i]; 155 | assoc = (function() { 156 | var _j, _len1, _results; 157 | _results = []; 158 | for (v = _j = 0, _len1 = sub.length; _j < _len1; v = ++_j) { 159 | k = sub[v]; 160 | _results.push([k, v]); 161 | } 162 | return _results; 163 | })(); 164 | assoc.sort(); 165 | label = ((function() { 166 | var _j, _len1, _results; 167 | _results = []; 168 | for (v = _j = 0, _len1 = assoc.length; _j < _len1; v = ++_j) { 169 | k = assoc[v]; 170 | _results.push(k + ',' + v); 171 | } 172 | return _results; 173 | })()).join('-'); 174 | if (!(label in members)) { 175 | members[label] = true; 176 | deduped.push(sub); 177 | } 178 | } 179 | return deduped; 180 | }; 181 | helper = function(keys) { 182 | var dup_l33t_index, first_key, i, l33t_chr, next_subs, rest_keys, sub, sub_alternative, sub_extension, _i, _j, _k, _len, _len1, _ref, _ref1; 183 | if (!keys.length) { 184 | return; 185 | } 186 | first_key = keys[0]; 187 | rest_keys = keys.slice(1); 188 | next_subs = []; 189 | _ref = table[first_key]; 190 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 191 | l33t_chr = _ref[_i]; 192 | for (_j = 0, _len1 = subs.length; _j < _len1; _j++) { 193 | sub = subs[_j]; 194 | dup_l33t_index = -1; 195 | for (i = _k = 0, _ref1 = sub.length; 0 <= _ref1 ? _k < _ref1 : _k > _ref1; i = 0 <= _ref1 ? ++_k : --_k) { 196 | if (sub[i][0] === l33t_chr) { 197 | dup_l33t_index = i; 198 | break; 199 | } 200 | } 201 | if (dup_l33t_index === -1) { 202 | sub_extension = sub.concat([[l33t_chr, first_key]]); 203 | next_subs.push(sub_extension); 204 | } else { 205 | sub_alternative = sub.slice(0); 206 | sub_alternative.splice(dup_l33t_index, 1); 207 | sub_alternative.push([l33t_chr, first_key]); 208 | next_subs.push(sub); 209 | next_subs.push(sub_alternative); 210 | } 211 | } 212 | } 213 | subs = dedup(next_subs); 214 | return helper(rest_keys); 215 | }; 216 | helper(keys); 217 | sub_dicts = []; 218 | for (_i = 0, _len = subs.length; _i < _len; _i++) { 219 | sub = subs[_i]; 220 | sub_dict = {}; 221 | for (_j = 0, _len1 = sub.length; _j < _len1; _j++) { 222 | _ref = sub[_j], l33t_chr = _ref[0], chr = _ref[1]; 223 | sub_dict[l33t_chr] = chr; 224 | } 225 | sub_dicts.push(sub_dict); 226 | } 227 | return sub_dicts; 228 | }; 229 | 230 | l33t_match = function(password) { 231 | var chr, k, match, match_sub, matcher, matches, sub, subbed_chr, subbed_password, token, v, _i, _j, _k, _len, _len1, _len2, _ref, _ref1; 232 | matches = []; 233 | _ref = enumerate_l33t_subs(relevent_l33t_subtable(password)); 234 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 235 | sub = _ref[_i]; 236 | if (empty(sub)) { 237 | break; 238 | } 239 | for (_j = 0, _len1 = DICTIONARY_MATCHERS.length; _j < _len1; _j++) { 240 | matcher = DICTIONARY_MATCHERS[_j]; 241 | subbed_password = translate(password, sub); 242 | _ref1 = matcher(subbed_password); 243 | for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { 244 | match = _ref1[_k]; 245 | token = password.slice(match.i, match.j + 1 || 9e9); 246 | if (token.toLowerCase() === match.matched_word) { 247 | continue; 248 | } 249 | match_sub = {}; 250 | for (subbed_chr in sub) { 251 | chr = sub[subbed_chr]; 252 | if (token.indexOf(subbed_chr) !== -1) { 253 | match_sub[subbed_chr] = chr; 254 | } 255 | } 256 | match.l33t = true; 257 | match.token = token; 258 | match.sub = match_sub; 259 | match.sub_display = ((function() { 260 | var _results; 261 | _results = []; 262 | for (k in match_sub) { 263 | v = match_sub[k]; 264 | _results.push("" + k + " -> " + v); 265 | } 266 | return _results; 267 | })()).join(', '); 268 | matches.push(match); 269 | } 270 | } 271 | } 272 | return matches; 273 | }; 274 | 275 | spatial_match = function(password) { 276 | var graph, graph_name, matches; 277 | matches = []; 278 | for (graph_name in GRAPHS) { 279 | graph = GRAPHS[graph_name]; 280 | extend(matches, spatial_match_helper(password, graph, graph_name)); 281 | } 282 | return matches; 283 | }; 284 | 285 | spatial_match_helper = function(password, graph, graph_name) { 286 | var adj, adjacents, cur_char, cur_direction, found, found_direction, i, j, last_direction, prev_char, result, shifted_count, turns, _i, _len; 287 | result = []; 288 | i = 0; 289 | while (i < password.length - 1) { 290 | j = i + 1; 291 | last_direction = null; 292 | turns = 0; 293 | shifted_count = 0; 294 | while (true) { 295 | prev_char = password.charAt(j - 1); 296 | found = false; 297 | found_direction = -1; 298 | cur_direction = -1; 299 | adjacents = graph[prev_char] || []; 300 | if (j < password.length) { 301 | cur_char = password.charAt(j); 302 | for (_i = 0, _len = adjacents.length; _i < _len; _i++) { 303 | adj = adjacents[_i]; 304 | cur_direction += 1; 305 | if (adj && adj.indexOf(cur_char) !== -1) { 306 | found = true; 307 | found_direction = cur_direction; 308 | if (adj.indexOf(cur_char) === 1) { 309 | shifted_count += 1; 310 | } 311 | if (last_direction !== found_direction) { 312 | turns += 1; 313 | last_direction = found_direction; 314 | } 315 | break; 316 | } 317 | } 318 | } 319 | if (found) { 320 | j += 1; 321 | } else { 322 | if (j - i > 2) { 323 | result.push({ 324 | pattern: 'spatial', 325 | i: i, 326 | j: j - 1, 327 | token: password.slice(i, j), 328 | graph: graph_name, 329 | turns: turns, 330 | shifted_count: shifted_count 331 | }); 332 | } 333 | i = j; 334 | break; 335 | } 336 | } 337 | } 338 | return result; 339 | }; 340 | 341 | repeat_match = function(password) { 342 | var cur_char, i, j, prev_char, result, _ref; 343 | result = []; 344 | i = 0; 345 | while (i < password.length) { 346 | j = i + 1; 347 | while (true) { 348 | _ref = password.slice(j - 1, j + 1 || 9e9), prev_char = _ref[0], cur_char = _ref[1]; 349 | if (password.charAt(j - 1) === password.charAt(j)) { 350 | j += 1; 351 | } else { 352 | if (j - i > 2) { 353 | result.push({ 354 | pattern: 'repeat', 355 | i: i, 356 | j: j - 1, 357 | token: password.slice(i, j), 358 | repeated_char: password.charAt(i) 359 | }); 360 | } 361 | break; 362 | } 363 | } 364 | i = j; 365 | } 366 | return result; 367 | }; 368 | 369 | SEQUENCES = { 370 | lower: 'abcdefghijklmnopqrstuvwxyz', 371 | upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 372 | digits: '01234567890' 373 | }; 374 | 375 | sequence_match = function(password) { 376 | var chr, cur_char, cur_n, direction, i, i_n, j, j_n, prev_char, prev_n, result, seq, seq_candidate, seq_candidate_name, seq_direction, seq_name, _ref, _ref1, _ref2; 377 | result = []; 378 | i = 0; 379 | while (i < password.length) { 380 | j = i + 1; 381 | seq = null; 382 | seq_name = null; 383 | seq_direction = null; 384 | for (seq_candidate_name in SEQUENCES) { 385 | seq_candidate = SEQUENCES[seq_candidate_name]; 386 | _ref = (function() { 387 | var _i, _len, _ref, _results; 388 | _ref = [password.charAt(i), password.charAt(j)]; 389 | _results = []; 390 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 391 | chr = _ref[_i]; 392 | _results.push(seq_candidate.indexOf(chr)); 393 | } 394 | return _results; 395 | })(), i_n = _ref[0], j_n = _ref[1]; 396 | if (i_n > -1 && j_n > -1) { 397 | direction = j_n - i_n; 398 | if (direction === 1 || direction === (-1)) { 399 | seq = seq_candidate; 400 | seq_name = seq_candidate_name; 401 | seq_direction = direction; 402 | break; 403 | } 404 | } 405 | } 406 | if (seq) { 407 | while (true) { 408 | _ref1 = password.slice(j - 1, j + 1 || 9e9), prev_char = _ref1[0], cur_char = _ref1[1]; 409 | _ref2 = (function() { 410 | var _i, _len, _ref2, _results; 411 | _ref2 = [prev_char, cur_char]; 412 | _results = []; 413 | for (_i = 0, _len = _ref2.length; _i < _len; _i++) { 414 | chr = _ref2[_i]; 415 | _results.push(seq_candidate.indexOf(chr)); 416 | } 417 | return _results; 418 | })(), prev_n = _ref2[0], cur_n = _ref2[1]; 419 | if (!!cur_n && (cur_n - prev_n === seq_direction)) { 420 | j += 1; 421 | } else { 422 | if (j - i > 2) { 423 | result.push({ 424 | pattern: 'sequence', 425 | i: i, 426 | j: j - 1, 427 | token: password.slice(i, j), 428 | sequence_name: seq_name, 429 | sequence_space: seq.length, 430 | ascending: seq_direction === 1 431 | }); 432 | } 433 | break; 434 | } 435 | } 436 | } 437 | i = j; 438 | } 439 | return result; 440 | }; 441 | 442 | repeat = function(chr, n) { 443 | var i; 444 | return ((function() { 445 | var _i, _results; 446 | _results = []; 447 | for (i = _i = 1; 1 <= n ? _i <= n : _i >= n; i = 1 <= n ? ++_i : --_i) { 448 | _results.push(chr); 449 | } 450 | return _results; 451 | })()).join(''); 452 | }; 453 | 454 | findall = function(password, rx) { 455 | var match, matches; 456 | matches = []; 457 | while (true) { 458 | match = password.match(rx); 459 | if (!match) { 460 | break; 461 | } 462 | match.i = match.index; 463 | match.j = match.index + match[0].length - 1; 464 | matches.push(match); 465 | password = password.replace(match[0], repeat(' ', match[0].length)); 466 | } 467 | return matches; 468 | }; 469 | 470 | digits_rx = /\d{3,}/; 471 | 472 | digits_match = function(password) { 473 | var i, j, match, _i, _len, _ref, _ref1, _results; 474 | _ref = findall(password, digits_rx); 475 | _results = []; 476 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 477 | match = _ref[_i]; 478 | _ref1 = [match.i, match.j], i = _ref1[0], j = _ref1[1]; 479 | _results.push({ 480 | pattern: 'digits', 481 | i: i, 482 | j: j, 483 | token: password.slice(i, j + 1 || 9e9) 484 | }); 485 | } 486 | return _results; 487 | }; 488 | 489 | year_rx = /19\d\d|200\d|201\d/; 490 | 491 | year_match = function(password) { 492 | var i, j, match, _i, _len, _ref, _ref1, _results; 493 | _ref = findall(password, year_rx); 494 | _results = []; 495 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 496 | match = _ref[_i]; 497 | _ref1 = [match.i, match.j], i = _ref1[0], j = _ref1[1]; 498 | _results.push({ 499 | pattern: 'year', 500 | i: i, 501 | j: j, 502 | token: password.slice(i, j + 1 || 9e9) 503 | }); 504 | } 505 | return _results; 506 | }; 507 | 508 | date_match = function(password) { 509 | return date_without_sep_match(password).concat(date_sep_match(password)); 510 | }; 511 | 512 | date_without_sep_match = function(password) { 513 | var candidate, candidates_round_1, candidates_round_2, date_matches, day, digit_match, end, i, j, month, token, valid, year, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3; 514 | date_matches = []; 515 | _ref = findall(password, /\d{4,8}/); 516 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 517 | digit_match = _ref[_i]; 518 | _ref1 = [digit_match.i, digit_match.j], i = _ref1[0], j = _ref1[1]; 519 | token = password.slice(i, j + 1 || 9e9); 520 | end = token.length; 521 | candidates_round_1 = []; 522 | if (token.length <= 6) { 523 | candidates_round_1.push({ 524 | daymonth: token.slice(2), 525 | year: token.slice(0, 2), 526 | i: i, 527 | j: j 528 | }); 529 | candidates_round_1.push({ 530 | daymonth: token.slice(0, end - 2), 531 | year: token.slice(end - 2), 532 | i: i, 533 | j: j 534 | }); 535 | } 536 | if (token.length >= 6) { 537 | candidates_round_1.push({ 538 | daymonth: token.slice(4), 539 | year: token.slice(0, 4), 540 | i: i, 541 | j: j 542 | }); 543 | candidates_round_1.push({ 544 | daymonth: token.slice(0, end - 4), 545 | year: token.slice(end - 4), 546 | i: i, 547 | j: j 548 | }); 549 | } 550 | candidates_round_2 = []; 551 | for (_j = 0, _len1 = candidates_round_1.length; _j < _len1; _j++) { 552 | candidate = candidates_round_1[_j]; 553 | switch (candidate.daymonth.length) { 554 | case 2: 555 | candidates_round_2.push({ 556 | day: candidate.daymonth[0], 557 | month: candidate.daymonth[1], 558 | year: candidate.year, 559 | i: candidate.i, 560 | j: candidate.j 561 | }); 562 | break; 563 | case 3: 564 | candidates_round_2.push({ 565 | day: candidate.daymonth.slice(0, 2), 566 | month: candidate.daymonth[2], 567 | year: candidate.year, 568 | i: candidate.i, 569 | j: candidate.j 570 | }); 571 | candidates_round_2.push({ 572 | day: candidate.daymonth[0], 573 | month: candidate.daymonth.slice(1, 3), 574 | year: candidate.year, 575 | i: candidate.i, 576 | j: candidate.j 577 | }); 578 | break; 579 | case 4: 580 | candidates_round_2.push({ 581 | day: candidate.daymonth.slice(0, 2), 582 | month: candidate.daymonth.slice(2, 4), 583 | year: candidate.year, 584 | i: candidate.i, 585 | j: candidate.j 586 | }); 587 | } 588 | } 589 | for (_k = 0, _len2 = candidates_round_2.length; _k < _len2; _k++) { 590 | candidate = candidates_round_2[_k]; 591 | day = parseInt(candidate.day); 592 | month = parseInt(candidate.month); 593 | year = parseInt(candidate.year); 594 | _ref2 = check_date(day, month, year), valid = _ref2[0], (_ref3 = _ref2[1], day = _ref3[0], month = _ref3[1], year = _ref3[2]); 595 | if (!valid) { 596 | continue; 597 | } 598 | date_matches.push({ 599 | pattern: 'date', 600 | i: candidate.i, 601 | j: candidate.j, 602 | token: password.slice(i, j + 1 || 9e9), 603 | separator: '', 604 | day: day, 605 | month: month, 606 | year: year 607 | }); 608 | } 609 | } 610 | return date_matches; 611 | }; 612 | 613 | date_rx_year_suffix = /(\d{1,2})(\s|-|\/|\\|_|\.)(\d{1,2})\2(19\d{2}|200\d|201\d|\d{2})/; 614 | 615 | date_rx_year_prefix = /(19\d{2}|200\d|201\d|\d{2})(\s|-|\/|\\|_|\.)(\d{1,2})\2(\d{1,2})/; 616 | 617 | date_sep_match = function(password) { 618 | var day, k, match, matches, month, valid, year, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _results; 619 | matches = []; 620 | _ref = findall(password, date_rx_year_suffix); 621 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 622 | match = _ref[_i]; 623 | _ref1 = (function() { 624 | var _j, _len1, _ref1, _results; 625 | _ref1 = [1, 3, 4]; 626 | _results = []; 627 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 628 | k = _ref1[_j]; 629 | _results.push(parseInt(match[k])); 630 | } 631 | return _results; 632 | })(), match.day = _ref1[0], match.month = _ref1[1], match.year = _ref1[2]; 633 | match.sep = match[2]; 634 | matches.push(match); 635 | } 636 | _ref2 = findall(password, date_rx_year_prefix); 637 | for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { 638 | match = _ref2[_j]; 639 | _ref3 = (function() { 640 | var _k, _len2, _ref3, _results; 641 | _ref3 = [4, 3, 1]; 642 | _results = []; 643 | for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) { 644 | k = _ref3[_k]; 645 | _results.push(parseInt(match[k])); 646 | } 647 | return _results; 648 | })(), match.day = _ref3[0], match.month = _ref3[1], match.year = _ref3[2]; 649 | match.sep = match[2]; 650 | matches.push(match); 651 | } 652 | _results = []; 653 | for (_k = 0, _len2 = matches.length; _k < _len2; _k++) { 654 | match = matches[_k]; 655 | _ref4 = check_date(match.day, match.month, match.year), valid = _ref4[0], (_ref5 = _ref4[1], day = _ref5[0], month = _ref5[1], year = _ref5[2]); 656 | if (!valid) { 657 | continue; 658 | } 659 | _results.push({ 660 | pattern: 'date', 661 | i: match.i, 662 | j: match.j, 663 | token: password.slice(match.i, match.j + 1 || 9e9), 664 | separator: match.sep, 665 | day: day, 666 | month: month, 667 | year: year 668 | }); 669 | } 670 | return _results; 671 | }; 672 | 673 | check_date = function(day, month, year) { 674 | var _ref; 675 | if ((12 <= month && month <= 31) && day <= 12) { 676 | _ref = [month, day], day = _ref[0], month = _ref[1]; 677 | } 678 | if (day > 31 || month > 12) { 679 | return [false, []]; 680 | } 681 | if (!((1900 <= year && year <= 2019))) { 682 | return [false, []]; 683 | } 684 | return [true, [day, month, year]]; 685 | }; 686 | -------------------------------------------------------------------------------- /spec/support/js_source/scoring.coffee: -------------------------------------------------------------------------------- 1 | 2 | nCk = (n, k) -> 3 | # http://blog.plover.com/math/choose.html 4 | return 0 if k > n 5 | return 1 if k == 0 6 | r = 1 7 | for d in [1..k] 8 | r *= n 9 | r /= d 10 | n -= 1 11 | r 12 | 13 | lg = (n) -> Math.log(n) / Math.log(2) 14 | 15 | # ------------------------------------------------------------------------------ 16 | # minimum entropy search ------------------------------------------------------- 17 | # ------------------------------------------------------------------------------ 18 | # 19 | # takes a list of overlapping matches, returns the non-overlapping sublist with 20 | # minimum entropy. O(nm) dp alg for length-n password with m candidate matches. 21 | # ------------------------------------------------------------------------------ 22 | 23 | minimum_entropy_match_sequence = (password, matches) -> 24 | bruteforce_cardinality = calc_bruteforce_cardinality password # e.g. 26 for lowercase 25 | up_to_k = [] # minimum entropy up to k. 26 | backpointers = [] # for the optimal sequence of matches up to k, holds the final match (match.j == k). null means the sequence ends w/ a brute-force character. 27 | for k in [0...password.length] 28 | # starting scenario to try and beat: adding a brute-force character to the minimum entropy sequence at k-1. 29 | up_to_k[k] = (up_to_k[k-1] or 0) + lg bruteforce_cardinality 30 | backpointers[k] = null 31 | for match in matches when match.j == k 32 | [i, j] = [match.i, match.j] 33 | # see if best entropy up to i-1 + entropy of this match is less than the current minimum at j. 34 | candidate_entropy = (up_to_k[i-1] or 0) + calc_entropy(match) 35 | if candidate_entropy < up_to_k[j] 36 | up_to_k[j] = candidate_entropy 37 | backpointers[j] = match 38 | 39 | # walk backwards and decode the best sequence 40 | match_sequence = [] 41 | k = password.length - 1 42 | while k >= 0 43 | match = backpointers[k] 44 | if match 45 | match_sequence.push match 46 | k = match.i - 1 47 | else 48 | k -= 1 49 | match_sequence.reverse() 50 | 51 | # fill in the blanks between pattern matches with bruteforce "matches" 52 | # that way the match sequence fully covers the password: match1.j == match2.i - 1 for every adjacent match1, match2. 53 | make_bruteforce_match = (i, j) -> 54 | pattern: 'bruteforce' 55 | i: i 56 | j: j 57 | token: password[i..j] 58 | entropy: lg Math.pow(bruteforce_cardinality, j - i + 1) 59 | cardinality: bruteforce_cardinality 60 | k = 0 61 | match_sequence_copy = [] 62 | for match in match_sequence 63 | [i, j] = [match.i, match.j] 64 | if i - k > 0 65 | match_sequence_copy.push make_bruteforce_match(k, i - 1) 66 | k = j + 1 67 | match_sequence_copy.push match 68 | if k < password.length 69 | match_sequence_copy.push make_bruteforce_match(k, password.length - 1) 70 | match_sequence = match_sequence_copy 71 | 72 | min_entropy = up_to_k[password.length - 1] or 0 # or 0 corner case is for an empty password '' 73 | crack_time = entropy_to_crack_time min_entropy 74 | 75 | # final result object 76 | password: password 77 | entropy: round_to_x_digits(min_entropy, 3) 78 | match_sequence: match_sequence 79 | crack_time: round_to_x_digits(crack_time, 3) 80 | crack_time_display: display_time crack_time 81 | score: crack_time_to_score crack_time 82 | 83 | round_to_x_digits = (n, x) -> Math.round(n * Math.pow(10, x)) / Math.pow(10, x) 84 | 85 | # ------------------------------------------------------------------------------ 86 | # threat model -- stolen hash catastrophe scenario ----------------------------- 87 | # ------------------------------------------------------------------------------ 88 | # 89 | # assumes: 90 | # * passwords are stored as salted hashes, different random salt per user. 91 | # (making rainbow attacks infeasable.) 92 | # * hashes and salts were stolen. attacker is guessing passwords at max rate. 93 | # * attacker has several CPUs at their disposal. 94 | # ------------------------------------------------------------------------------ 95 | 96 | # for a hash function like bcrypt/scrypt/PBKDF2, 10ms per guess is a safe lower bound. 97 | # (usually a guess would take longer -- this assumes fast hardware and a small work factor.) 98 | # adjust for your site accordingly if you use another hash function, possibly by 99 | # several orders of magnitude! 100 | SINGLE_GUESS = .010 101 | NUM_ATTACKERS = 100 # number of cores guessing in parallel. 102 | 103 | SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS 104 | 105 | entropy_to_crack_time = (entropy) -> .5 * Math.pow(2, entropy) * SECONDS_PER_GUESS # average, not total 106 | 107 | crack_time_to_score = (seconds) -> 108 | return 0 if seconds < Math.pow(10, 2) 109 | return 1 if seconds < Math.pow(10, 4) 110 | return 2 if seconds < Math.pow(10, 6) 111 | return 3 if seconds < Math.pow(10, 8) 112 | return 4 113 | 114 | # ------------------------------------------------------------------------------ 115 | # entropy calcs -- one function per match pattern ------------------------------ 116 | # ------------------------------------------------------------------------------ 117 | 118 | calc_entropy = (match) -> 119 | return match.entropy if match.entropy? # a match's entropy doesn't change. cache it. 120 | entropy_func = switch match.pattern 121 | when 'repeat' then repeat_entropy 122 | when 'sequence' then sequence_entropy 123 | when 'digits' then digits_entropy 124 | when 'year' then year_entropy 125 | when 'date' then date_entropy 126 | when 'spatial' then spatial_entropy 127 | when 'dictionary' then dictionary_entropy 128 | match.entropy = entropy_func match 129 | 130 | repeat_entropy = (match) -> 131 | cardinality = calc_bruteforce_cardinality match.token 132 | lg (cardinality * match.token.length) 133 | 134 | sequence_entropy = (match) -> 135 | first_chr = match.token.charAt(0) 136 | if first_chr in ['a', '1'] 137 | base_entropy = 1 138 | else 139 | if first_chr.match /\d/ 140 | base_entropy = lg(10) # digits 141 | else if first_chr.match /[a-z]/ 142 | base_entropy = lg(26) # lower 143 | else 144 | base_entropy = lg(26) + 1 # extra bit for uppercase 145 | if not match.ascending 146 | base_entropy += 1 # extra bit for descending instead of ascending 147 | base_entropy + lg match.token.length 148 | 149 | digits_entropy = (match) -> lg Math.pow(10, match.token.length) 150 | 151 | NUM_YEARS = 119 # years match against 1900 - 2019 152 | NUM_MONTHS = 12 153 | NUM_DAYS = 31 154 | 155 | year_entropy = (match) -> lg NUM_YEARS 156 | 157 | date_entropy = (match) -> 158 | if match.year < 100 159 | entropy = lg(NUM_DAYS * NUM_MONTHS * 100) # two-digit year 160 | else 161 | entropy = lg(NUM_DAYS * NUM_MONTHS * NUM_YEARS) # four-digit year 162 | if match.separator 163 | entropy += 2 # add two bits for separator selection [/,-,.,etc] 164 | entropy 165 | 166 | spatial_entropy = (match) -> 167 | if match.graph in ['qwerty', 'dvorak'] 168 | s = KEYBOARD_STARTING_POSITIONS 169 | d = KEYBOARD_AVERAGE_DEGREE 170 | else 171 | s = KEYPAD_STARTING_POSITIONS 172 | d = KEYPAD_AVERAGE_DEGREE 173 | possibilities = 0 174 | L = match.token.length 175 | t = match.turns 176 | # estimate the number of possible patterns w/ length L or less with t turns or less. 177 | for i in [2..L] 178 | possible_turns = Math.min(t, i - 1) 179 | for j in [1..possible_turns] 180 | possibilities += nCk(i - 1, j - 1) * s * Math.pow(d, j) 181 | entropy = lg possibilities 182 | # add extra entropy for shifted keys. (% instead of 5, A instead of a.) 183 | # math is similar to extra entropy from uppercase letters in dictionary matches. 184 | if match.shifted_count 185 | S = match.shifted_count 186 | U = match.token.length - match.shifted_count # unshifted count 187 | possibilities = 0 188 | possibilities += nCk(S + U, i) for i in [0..Math.min(S, U)] 189 | entropy += lg possibilities 190 | entropy 191 | 192 | dictionary_entropy = (match) -> 193 | match.base_entropy = lg match.rank # keep these as properties for display purposes 194 | match.uppercase_entropy = extra_uppercase_entropy match 195 | match.l33t_entropy = extra_l33t_entropy match 196 | match.base_entropy + match.uppercase_entropy + match.l33t_entropy 197 | 198 | START_UPPER = /^[A-Z][^A-Z]+$/ 199 | END_UPPER = /^[^A-Z]+[A-Z]$/ 200 | ALL_UPPER = /^[^a-z]+$/ 201 | ALL_LOWER = /^[^A-Z]+$/ 202 | 203 | extra_uppercase_entropy = (match) -> 204 | word = match.token 205 | return 0 if word.match ALL_LOWER 206 | # a capitalized word is the most common capitalization scheme, 207 | # so it only doubles the search space (uncapitalized + capitalized): 1 extra bit of entropy. 208 | # allcaps and end-capitalized are common enough too, underestimate as 1 extra bit to be safe. 209 | for regex in [START_UPPER, END_UPPER, ALL_UPPER] 210 | return 1 if word.match regex 211 | # otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters with U uppercase letters or less. 212 | # or, if there's more uppercase than lower (for e.g. PASSwORD), the number of ways to lowercase U+L letters with L lowercase letters or less. 213 | U = (chr for chr in word.split('') when chr.match /[A-Z]/).length 214 | L = (chr for chr in word.split('') when chr.match /[a-z]/).length 215 | possibilities = 0 216 | possibilities += nCk(U + L, i) for i in [0..Math.min(U, L)] 217 | lg possibilities 218 | 219 | extra_l33t_entropy = (match) -> 220 | return 0 if not match.l33t 221 | possibilities = 0 222 | for subbed, unsubbed of match.sub 223 | S = (chr for chr in match.token.split('') when chr == subbed).length # number of subbed characters. 224 | U = (chr for chr in match.token.split('') when chr == unsubbed).length # number of unsubbed characters. 225 | possibilities += nCk(U + S, i) for i in [0..Math.min(U, S)] 226 | # corner: return 1 bit for single-letter subs, like 4pple -> apple, instead of 0. 227 | lg(possibilities) or 1 228 | 229 | # utilities -------------------------------------------------------------------- 230 | 231 | calc_bruteforce_cardinality = (password) -> 232 | [lower, upper, digits, symbols] = [false, false, false, false] 233 | for chr in password.split('') 234 | ord = chr.charCodeAt(0) 235 | if 0x30 <= ord <= 0x39 236 | digits = true 237 | else if 0x41 <= ord <= 0x5a 238 | upper = true 239 | else if 0x61 <= ord <= 0x7a 240 | lower = true 241 | else 242 | symbols = true 243 | c = 0 244 | c += 10 if digits 245 | c += 26 if upper 246 | c += 26 if lower 247 | c += 33 if symbols 248 | c 249 | 250 | display_time = (seconds) -> 251 | minute = 60 252 | hour = minute * 60 253 | day = hour * 24 254 | month = day * 31 255 | year = month * 12 256 | century = year * 100 257 | if seconds < minute 258 | 'instant' 259 | else if seconds < hour 260 | "#{1 + Math.ceil(seconds / minute)} minutes" 261 | else if seconds < day 262 | "#{1 + Math.ceil(seconds / hour)} hours" 263 | else if seconds < month 264 | "#{1 + Math.ceil(seconds / day)} days" 265 | else if seconds < year 266 | "#{1 + Math.ceil(seconds / month)} months" 267 | else if seconds < century 268 | "#{1 + Math.ceil(seconds / year)} years" 269 | else 270 | 'centuries' 271 | -------------------------------------------------------------------------------- /spec/support/js_source/scoring.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.3.3 2 | var ALL_LOWER, ALL_UPPER, END_UPPER, NUM_ATTACKERS, NUM_DAYS, NUM_MONTHS, NUM_YEARS, SECONDS_PER_GUESS, SINGLE_GUESS, START_UPPER, calc_bruteforce_cardinality, calc_entropy, crack_time_to_score, date_entropy, dictionary_entropy, digits_entropy, display_time, entropy_to_crack_time, extra_l33t_entropy, extra_uppercase_entropy, lg, minimum_entropy_match_sequence, nCk, repeat_entropy, round_to_x_digits, sequence_entropy, spatial_entropy, year_entropy; 3 | 4 | nCk = function(n, k) { 5 | var d, r, _i; 6 | if (k > n) { 7 | return 0; 8 | } 9 | if (k === 0) { 10 | return 1; 11 | } 12 | r = 1; 13 | for (d = _i = 1; 1 <= k ? _i <= k : _i >= k; d = 1 <= k ? ++_i : --_i) { 14 | r *= n; 15 | r /= d; 16 | n -= 1; 17 | } 18 | return r; 19 | }; 20 | 21 | lg = function(n) { 22 | return Math.log(n) / Math.log(2); 23 | }; 24 | 25 | minimum_entropy_match_sequence = function(password, matches) { 26 | var backpointers, bruteforce_cardinality, candidate_entropy, crack_time, i, j, k, make_bruteforce_match, match, match_sequence, match_sequence_copy, min_entropy, up_to_k, _i, _j, _k, _len, _len1, _ref, _ref1, _ref2; 27 | bruteforce_cardinality = calc_bruteforce_cardinality(password); 28 | up_to_k = []; 29 | backpointers = []; 30 | for (k = _i = 0, _ref = password.length; 0 <= _ref ? _i < _ref : _i > _ref; k = 0 <= _ref ? ++_i : --_i) { 31 | up_to_k[k] = (up_to_k[k - 1] || 0) + lg(bruteforce_cardinality); 32 | backpointers[k] = null; 33 | for (_j = 0, _len = matches.length; _j < _len; _j++) { 34 | match = matches[_j]; 35 | if (!(match.j === k)) { 36 | continue; 37 | } 38 | _ref1 = [match.i, match.j], i = _ref1[0], j = _ref1[1]; 39 | candidate_entropy = (up_to_k[i - 1] || 0) + calc_entropy(match); 40 | if (candidate_entropy < up_to_k[j]) { 41 | up_to_k[j] = candidate_entropy; 42 | backpointers[j] = match; 43 | } 44 | } 45 | } 46 | match_sequence = []; 47 | k = password.length - 1; 48 | while (k >= 0) { 49 | match = backpointers[k]; 50 | if (match) { 51 | match_sequence.push(match); 52 | k = match.i - 1; 53 | } else { 54 | k -= 1; 55 | } 56 | } 57 | match_sequence.reverse(); 58 | make_bruteforce_match = function(i, j) { 59 | return { 60 | pattern: 'bruteforce', 61 | i: i, 62 | j: j, 63 | token: password.slice(i, j + 1 || 9e9), 64 | entropy: lg(Math.pow(bruteforce_cardinality, j - i + 1)), 65 | cardinality: bruteforce_cardinality 66 | }; 67 | }; 68 | k = 0; 69 | match_sequence_copy = []; 70 | for (_k = 0, _len1 = match_sequence.length; _k < _len1; _k++) { 71 | match = match_sequence[_k]; 72 | _ref2 = [match.i, match.j], i = _ref2[0], j = _ref2[1]; 73 | if (i - k > 0) { 74 | match_sequence_copy.push(make_bruteforce_match(k, i - 1)); 75 | } 76 | k = j + 1; 77 | match_sequence_copy.push(match); 78 | } 79 | if (k < password.length) { 80 | match_sequence_copy.push(make_bruteforce_match(k, password.length - 1)); 81 | } 82 | match_sequence = match_sequence_copy; 83 | min_entropy = up_to_k[password.length - 1] || 0; 84 | crack_time = entropy_to_crack_time(min_entropy); 85 | return { 86 | password: password, 87 | entropy: round_to_x_digits(min_entropy, 3), 88 | match_sequence: match_sequence, 89 | crack_time: round_to_x_digits(crack_time, 3), 90 | crack_time_display: display_time(crack_time), 91 | score: crack_time_to_score(crack_time) 92 | }; 93 | }; 94 | 95 | round_to_x_digits = function(n, x) { 96 | return Math.round(n * Math.pow(10, x)) / Math.pow(10, x); 97 | }; 98 | 99 | SINGLE_GUESS = .010; 100 | 101 | NUM_ATTACKERS = 100; 102 | 103 | SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS; 104 | 105 | entropy_to_crack_time = function(entropy) { 106 | return .5 * Math.pow(2, entropy) * SECONDS_PER_GUESS; 107 | }; 108 | 109 | crack_time_to_score = function(seconds) { 110 | if (seconds < Math.pow(10, 2)) { 111 | return 0; 112 | } 113 | if (seconds < Math.pow(10, 4)) { 114 | return 1; 115 | } 116 | if (seconds < Math.pow(10, 6)) { 117 | return 2; 118 | } 119 | if (seconds < Math.pow(10, 8)) { 120 | return 3; 121 | } 122 | return 4; 123 | }; 124 | 125 | calc_entropy = function(match) { 126 | var entropy_func; 127 | if (match.entropy != null) { 128 | return match.entropy; 129 | } 130 | entropy_func = (function() { 131 | switch (match.pattern) { 132 | case 'repeat': 133 | return repeat_entropy; 134 | case 'sequence': 135 | return sequence_entropy; 136 | case 'digits': 137 | return digits_entropy; 138 | case 'year': 139 | return year_entropy; 140 | case 'date': 141 | return date_entropy; 142 | case 'spatial': 143 | return spatial_entropy; 144 | case 'dictionary': 145 | return dictionary_entropy; 146 | } 147 | })(); 148 | return match.entropy = entropy_func(match); 149 | }; 150 | 151 | repeat_entropy = function(match) { 152 | var cardinality; 153 | cardinality = calc_bruteforce_cardinality(match.token); 154 | return lg(cardinality * match.token.length); 155 | }; 156 | 157 | sequence_entropy = function(match) { 158 | var base_entropy, first_chr; 159 | first_chr = match.token.charAt(0); 160 | if (first_chr === 'a' || first_chr === '1') { 161 | base_entropy = 1; 162 | } else { 163 | if (first_chr.match(/\d/)) { 164 | base_entropy = lg(10); 165 | } else if (first_chr.match(/[a-z]/)) { 166 | base_entropy = lg(26); 167 | } else { 168 | base_entropy = lg(26) + 1; 169 | } 170 | } 171 | if (!match.ascending) { 172 | base_entropy += 1; 173 | } 174 | return base_entropy + lg(match.token.length); 175 | }; 176 | 177 | digits_entropy = function(match) { 178 | return lg(Math.pow(10, match.token.length)); 179 | }; 180 | 181 | NUM_YEARS = 119; 182 | 183 | NUM_MONTHS = 12; 184 | 185 | NUM_DAYS = 31; 186 | 187 | year_entropy = function(match) { 188 | return lg(NUM_YEARS); 189 | }; 190 | 191 | date_entropy = function(match) { 192 | var entropy; 193 | if (match.year < 100) { 194 | entropy = lg(NUM_DAYS * NUM_MONTHS * 100); 195 | } else { 196 | entropy = lg(NUM_DAYS * NUM_MONTHS * NUM_YEARS); 197 | } 198 | if (match.separator) { 199 | entropy += 2; 200 | } 201 | return entropy; 202 | }; 203 | 204 | spatial_entropy = function(match) { 205 | var L, S, U, d, entropy, i, j, possibilities, possible_turns, s, t, _i, _j, _k, _ref, _ref1; 206 | if ((_ref = match.graph) === 'qwerty' || _ref === 'dvorak') { 207 | s = KEYBOARD_STARTING_POSITIONS; 208 | d = KEYBOARD_AVERAGE_DEGREE; 209 | } else { 210 | s = KEYPAD_STARTING_POSITIONS; 211 | d = KEYPAD_AVERAGE_DEGREE; 212 | } 213 | possibilities = 0; 214 | L = match.token.length; 215 | t = match.turns; 216 | for (i = _i = 2; 2 <= L ? _i <= L : _i >= L; i = 2 <= L ? ++_i : --_i) { 217 | possible_turns = Math.min(t, i - 1); 218 | for (j = _j = 1; 1 <= possible_turns ? _j <= possible_turns : _j >= possible_turns; j = 1 <= possible_turns ? ++_j : --_j) { 219 | possibilities += nCk(i - 1, j - 1) * s * Math.pow(d, j); 220 | } 221 | } 222 | entropy = lg(possibilities); 223 | if (match.shifted_count) { 224 | S = match.shifted_count; 225 | U = match.token.length - match.shifted_count; 226 | possibilities = 0; 227 | for (i = _k = 0, _ref1 = Math.min(S, U); 0 <= _ref1 ? _k <= _ref1 : _k >= _ref1; i = 0 <= _ref1 ? ++_k : --_k) { 228 | possibilities += nCk(S + U, i); 229 | } 230 | entropy += lg(possibilities); 231 | } 232 | return entropy; 233 | }; 234 | 235 | dictionary_entropy = function(match) { 236 | match.base_entropy = lg(match.rank); 237 | match.uppercase_entropy = extra_uppercase_entropy(match); 238 | match.l33t_entropy = extra_l33t_entropy(match); 239 | return match.base_entropy + match.uppercase_entropy + match.l33t_entropy; 240 | }; 241 | 242 | START_UPPER = /^[A-Z][^A-Z]+$/; 243 | 244 | END_UPPER = /^[^A-Z]+[A-Z]$/; 245 | 246 | ALL_UPPER = /^[^a-z]+$/; 247 | 248 | ALL_LOWER = /^[^A-Z]+$/; 249 | 250 | extra_uppercase_entropy = function(match) { 251 | var L, U, chr, i, possibilities, regex, word, _i, _j, _len, _ref, _ref1; 252 | word = match.token; 253 | if (word.match(ALL_LOWER)) { 254 | return 0; 255 | } 256 | _ref = [START_UPPER, END_UPPER, ALL_UPPER]; 257 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 258 | regex = _ref[_i]; 259 | if (word.match(regex)) { 260 | return 1; 261 | } 262 | } 263 | U = ((function() { 264 | var _j, _len1, _ref1, _results; 265 | _ref1 = word.split(''); 266 | _results = []; 267 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 268 | chr = _ref1[_j]; 269 | if (chr.match(/[A-Z]/)) { 270 | _results.push(chr); 271 | } 272 | } 273 | return _results; 274 | })()).length; 275 | L = ((function() { 276 | var _j, _len1, _ref1, _results; 277 | _ref1 = word.split(''); 278 | _results = []; 279 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 280 | chr = _ref1[_j]; 281 | if (chr.match(/[a-z]/)) { 282 | _results.push(chr); 283 | } 284 | } 285 | return _results; 286 | })()).length; 287 | possibilities = 0; 288 | for (i = _j = 0, _ref1 = Math.min(U, L); 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) { 289 | possibilities += nCk(U + L, i); 290 | } 291 | return lg(possibilities); 292 | }; 293 | 294 | extra_l33t_entropy = function(match) { 295 | var S, U, chr, i, possibilities, subbed, unsubbed, _i, _ref, _ref1; 296 | if (!match.l33t) { 297 | return 0; 298 | } 299 | possibilities = 0; 300 | _ref = match.sub; 301 | for (subbed in _ref) { 302 | unsubbed = _ref[subbed]; 303 | S = ((function() { 304 | var _i, _len, _ref1, _results; 305 | _ref1 = match.token.split(''); 306 | _results = []; 307 | for (_i = 0, _len = _ref1.length; _i < _len; _i++) { 308 | chr = _ref1[_i]; 309 | if (chr === subbed) { 310 | _results.push(chr); 311 | } 312 | } 313 | return _results; 314 | })()).length; 315 | U = ((function() { 316 | var _i, _len, _ref1, _results; 317 | _ref1 = match.token.split(''); 318 | _results = []; 319 | for (_i = 0, _len = _ref1.length; _i < _len; _i++) { 320 | chr = _ref1[_i]; 321 | if (chr === unsubbed) { 322 | _results.push(chr); 323 | } 324 | } 325 | return _results; 326 | })()).length; 327 | for (i = _i = 0, _ref1 = Math.min(U, S); 0 <= _ref1 ? _i <= _ref1 : _i >= _ref1; i = 0 <= _ref1 ? ++_i : --_i) { 328 | possibilities += nCk(U + S, i); 329 | } 330 | } 331 | return lg(possibilities) || 1; 332 | }; 333 | 334 | calc_bruteforce_cardinality = function(password) { 335 | var c, chr, digits, lower, ord, symbols, upper, _i, _len, _ref, _ref1; 336 | _ref = [false, false, false, false], lower = _ref[0], upper = _ref[1], digits = _ref[2], symbols = _ref[3]; 337 | _ref1 = password.split(''); 338 | for (_i = 0, _len = _ref1.length; _i < _len; _i++) { 339 | chr = _ref1[_i]; 340 | ord = chr.charCodeAt(0); 341 | if ((0x30 <= ord && ord <= 0x39)) { 342 | digits = true; 343 | } else if ((0x41 <= ord && ord <= 0x5a)) { 344 | upper = true; 345 | } else if ((0x61 <= ord && ord <= 0x7a)) { 346 | lower = true; 347 | } else { 348 | symbols = true; 349 | } 350 | } 351 | c = 0; 352 | if (digits) { 353 | c += 10; 354 | } 355 | if (upper) { 356 | c += 26; 357 | } 358 | if (lower) { 359 | c += 26; 360 | } 361 | if (symbols) { 362 | c += 33; 363 | } 364 | return c; 365 | }; 366 | 367 | display_time = function(seconds) { 368 | var century, day, hour, minute, month, year; 369 | minute = 60; 370 | hour = minute * 60; 371 | day = hour * 24; 372 | month = day * 31; 373 | year = month * 12; 374 | century = year * 100; 375 | if (seconds < minute) { 376 | return 'instant'; 377 | } else if (seconds < hour) { 378 | return "" + (1 + Math.ceil(seconds / minute)) + " minutes"; 379 | } else if (seconds < day) { 380 | return "" + (1 + Math.ceil(seconds / hour)) + " hours"; 381 | } else if (seconds < month) { 382 | return "" + (1 + Math.ceil(seconds / day)) + " days"; 383 | } else if (seconds < year) { 384 | return "" + (1 + Math.ceil(seconds / month)) + " months"; 385 | } else if (seconds < century) { 386 | return "" + (1 + Math.ceil(seconds / year)) + " years"; 387 | } else { 388 | return 'centuries'; 389 | } 390 | }; 391 | -------------------------------------------------------------------------------- /spec/support/matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :match_js_results do |expected_js_results| 2 | match do |actual_ruby_results| 3 | actual_ruby_results = reduce(actual_ruby_results.map(&:to_hash)) 4 | expected_js_results = reduce(expected_js_results) 5 | 6 | @missing, @extra = [], [] 7 | expected_js_results.each do |js_result| 8 | unless actual_ruby_results.include?(js_result) 9 | @missing << js_result 10 | end 11 | end 12 | actual_ruby_results.each do |ruby_result| 13 | unless expected_js_results.include?(ruby_result) 14 | @extra << ruby_result 15 | end 16 | end 17 | @missing.empty? && @extra.empty? 18 | end 19 | 20 | failure_message do |actual| 21 | "Matches missing from ruby results:\n#{@missing.inspect}\nMatches unique to ruby results:\n#{@extra.inspect}" 22 | end 23 | 24 | def reduce(results) 25 | result = [] 26 | results.each do |hash| 27 | new_hash = {} 28 | (hash.keys - ['sub', 'sub_display']).sort.each do |key| 29 | new_hash[key] = hash[key] 30 | end 31 | result << new_hash 32 | end 33 | result 34 | end 35 | end -------------------------------------------------------------------------------- /spec/tester_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | 4 | require "spec_helper" 5 | 6 | describe Zxcvbn::Tester do 7 | let(:tester) { Zxcvbn::Tester.new } 8 | 9 | TEST_PASSWORDS.each do |password| 10 | it "gives back the same score for #{password}" do 11 | ruby_result = tester.test(password) 12 | js_result = js_zxcvbn(password) 13 | 14 | expect(ruby_result.calc_time).not_to be_nil 15 | expect(ruby_result.password).to eq js_result["password"] 16 | expect(ruby_result.entropy).to eq js_result["entropy"] 17 | expect(ruby_result.crack_time).to eq js_result["crack_time"] 18 | expect(ruby_result.crack_time_display).to eq js_result["crack_time_display"] 19 | expect(ruby_result.score).to eq js_result["score"] 20 | expect(ruby_result.pattern).to eq js_result["pattern"] 21 | expect(ruby_result.match_sequence.count).to eq js_result["match_sequence"].count 22 | 23 | # NOTE: feedback didn't exist in the version of the JS library this gem 24 | # is based on, so instead we just check that it put `Feedback` in 25 | # there. Real tests for its values go in `feedback_giver_spec.rb`. 26 | expect(ruby_result.feedback).to be_a Zxcvbn::Feedback 27 | end 28 | end 29 | 30 | context "with a custom user dictionary" do 31 | it "scores them against the user dictionary" do 32 | result = tester.test("themeforest", ["themeforest"]) 33 | expect(result.entropy).to eq 0 34 | expect(result.score).to eq 0 35 | end 36 | 37 | it "matches l33t substitutions on this dictionary" do 38 | result = tester.test("th3m3for3st", ["themeforest"]) 39 | expect(result.entropy).to eq 1 40 | expect(result.score).to eq 0 41 | end 42 | end 43 | 44 | context "with Unicode entries in the password" do 45 | it "validates the password" do 46 | result = tester.test("✅🐴🔋staple", %w[Theme Forest themeforest]) 47 | expect(result.entropy).to be_positive 48 | expect(result.score).to be_positive 49 | end 50 | end 51 | 52 | context "with Unicode entries in the dictionary" do 53 | it "validates the password" do 54 | result = tester.test("correct horse battery staple", %w[✅ 🐴 🔋]) 55 | expect(result.entropy).to be_positive 56 | expect(result.score).to be_positive 57 | end 58 | end 59 | 60 | context "with Unicode entries in the password and the dictionary" do 61 | it "validates the password" do 62 | result = tester.test("✅🐴🔋staple", %w[✅ 🐴 🔋]) 63 | expect(result.entropy).to be_positive 64 | expect(result.score).to be_zero 65 | end 66 | end 67 | 68 | context "with invalid entries in the dictionary" do 69 | it "ignores those entries" do 70 | result = tester.test("themeforest", [nil, 1, "themeforest"]) 71 | expect(result.entropy).to eq 0 72 | expect(result.score).to eq 0 73 | end 74 | end 75 | 76 | context "with a custom global dictionary" do 77 | before { tester.add_word_lists("envato" => ["envato"]) } 78 | 79 | it "scores them against the dictionary" do 80 | result = tester.test("envato") 81 | expect(result.entropy).to eq 0 82 | expect(result.score).to eq 0 83 | end 84 | 85 | context "with invalid entries in a custom dictionary" do 86 | before { tester.add_word_lists("themeforest" => [nil, 1, "themeforest"]) } 87 | 88 | it "ignores those entries" do 89 | expect(tester.test("themeforest")).to have_attributes(entropy: 0, score: 0, crack_time: 0) 90 | end 91 | end 92 | end 93 | 94 | context "nil password" do 95 | specify do 96 | expect(tester.test(nil)).to have_attributes(entropy: 0, score: 0, crack_time: 0) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/zxcvbn_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Zxcvbn.test' do 4 | context 'with a password' do 5 | it 'returns a result' do 6 | result = Zxcvbn.test('password') 7 | expect(result.entropy).to_not be_nil 8 | end 9 | end 10 | 11 | context 'with a password and user input' do 12 | it 'returns a result' do 13 | result = Zxcvbn.test('password', ['inputs']) 14 | expect(result.entropy).to_not be_nil 15 | end 16 | end 17 | 18 | context 'with a password, user input and custom word lists' do 19 | it 'returns a result' do 20 | result = Zxcvbn.test('password', ['inputs'], {'list' => ['words']}) 21 | expect(result.entropy).to_not be_nil 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /zxcvbn-ruby.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/zxcvbn/version', __FILE__) 3 | 4 | GITHUB_URL = 'https://github.com/envato/zxcvbn-ruby' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.authors = ["Steve Hodgkiss", "Matthieu Aussaguel"] 8 | gem.email = ["steve@hodgkiss.me", "matthieu.aussaguel@gmail.com"] 9 | gem.description = %q{Ruby port of Dropboxs zxcvbn.js} 10 | gem.summary = %q{} 11 | gem.homepage = "http://github.com/envato/zxcvbn-ruby" 12 | 13 | gem.files = `git ls-files`.split($\) 14 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 15 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 16 | gem.name = "zxcvbn-ruby" 17 | gem.require_paths = ["lib"] 18 | gem.version = Zxcvbn::VERSION 19 | gem.license = 'MIT' 20 | 21 | gem.required_ruby_version = '>= 2.5' 22 | 23 | gem.add_development_dependency 'mini_racer' 24 | gem.add_development_dependency 'rspec' 25 | 26 | gem.metadata = { 27 | 'bug_tracker_uri' => "#{GITHUB_URL}/issues", 28 | 'changelog_uri' => "#{GITHUB_URL}/blob/HEAD/CHANGELOG.md", 29 | 'documentation_uri' => "#{GITHUB_URL}/blob/HEAD/README.md", 30 | 'homepage_uri' => GITHUB_URL, 31 | 'source_code_uri' => GITHUB_URL 32 | } 33 | end 34 | --------------------------------------------------------------------------------