├── clippy.toml ├── rustfmt.toml ├── .gitignore ├── .editorconfig ├── src ├── serialization_utils.rs ├── matching │ ├── char_indexing.rs │ ├── patterns.rs │ └── mod.rs ├── time_estimates.rs ├── lib.rs ├── feedback.rs ├── scoring.rs └── adjacency_graphs.rs ├── benches ├── zxcvbn.rs └── zxcvbn_unicode.rs ├── LICENSE ├── Cargo.toml ├── .github └── workflows │ └── zxcvbn.yml ├── README.md └── CHANGELOG.md /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.63" 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | format_strings = false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .idea 4 | *.bk 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.rs] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /src/serialization_utils.rs: -------------------------------------------------------------------------------- 1 | use serde::de::{Deserialize, Deserializer}; 2 | 3 | pub(crate) fn deserialize_f64_null_as_nan<'de, D: Deserializer<'de>>( 4 | des: D, 5 | ) -> Result { 6 | let optional = Option::::deserialize(des)?; 7 | Ok(optional.unwrap_or(f64::NAN)) 8 | } 9 | -------------------------------------------------------------------------------- /benches/zxcvbn.rs: -------------------------------------------------------------------------------- 1 | use criterion::black_box; 2 | use criterion::Criterion; 3 | use criterion::{criterion_group, criterion_main}; 4 | 5 | use zxcvbn::zxcvbn; 6 | 7 | pub fn bench_zxcvbn(c: &mut Criterion) { 8 | c.bench_function("zxcvbn", |b| { 9 | b.iter(|| zxcvbn(black_box("r0sebudmaelstrom11/20/91aaaa"), &[])) 10 | }); 11 | } 12 | 13 | criterion_group!(benches, bench_zxcvbn); 14 | criterion_main!(benches); 15 | -------------------------------------------------------------------------------- /benches/zxcvbn_unicode.rs: -------------------------------------------------------------------------------- 1 | use criterion::black_box; 2 | use criterion::Criterion; 3 | use criterion::{criterion_group, criterion_main}; 4 | 5 | use zxcvbn::zxcvbn; 6 | 7 | pub fn bench_zxcvbn_unicode(c: &mut Criterion) { 8 | c.bench_function("zxcvbn_unicode", |b| { 9 | b.iter(|| zxcvbn(black_box("𐰊𐰂𐰄𐰀𐰁"), &[])) 10 | }); 11 | } 12 | 13 | criterion_group!(benches, bench_zxcvbn_unicode); 14 | criterion_main!(benches); 15 | -------------------------------------------------------------------------------- /src/matching/char_indexing.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | pub(crate) trait CharIndexable<'b> { 4 | fn char_index(&'b self, range: Range) -> &'b str; 5 | } 6 | 7 | pub struct CharIndexableStr<'a> { 8 | s: &'a str, 9 | indices: Vec, 10 | } 11 | 12 | impl CharIndexableStr<'_> { 13 | pub(crate) fn char_count(&self) -> usize { 14 | self.indices.len() 15 | } 16 | } 17 | 18 | impl<'a> From<&'a str> for CharIndexableStr<'a> { 19 | fn from(s: &'a str) -> Self { 20 | CharIndexableStr { 21 | indices: s.char_indices().map(|(i, _c)| i).collect(), 22 | s, 23 | } 24 | } 25 | } 26 | 27 | impl<'a, 'b: 'a> CharIndexable<'b> for CharIndexableStr<'a> { 28 | fn char_index(&'b self, range: Range) -> &'b str { 29 | if range.end >= self.indices.len() { 30 | &self.s[self.indices[range.start]..] 31 | } else { 32 | &self.s[self.indices[range.start]..self.indices[range.end]] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Joshua Holmer 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Josh Holmer "] 3 | description = "An entropy-based password strength estimator, originally for Javascript by Dropbox." 4 | documentation = "https://docs.rs/zxcvbn" 5 | homepage = "https://github.com/shssoichiro/zxcvbn-rs" 6 | license = "MIT" 7 | name = "zxcvbn" 8 | repository = "https://github.com/shssoichiro/zxcvbn-rs" 9 | version = "3.1.0" 10 | edition = "2021" 11 | rust-version = "1.63" 12 | 13 | [badges] 14 | maintenance = { status = "passively-maintained" } 15 | 16 | [dependencies] 17 | derive_builder = { version = "0.20", optional = true } 18 | fancy-regex = "0.13" 19 | itertools = "0.13" 20 | lazy_static = "1.3" 21 | regex = "1" 22 | time = { version = "0.3" } 23 | 24 | [target.'cfg(target_arch = "wasm32")'.dependencies] 25 | chrono = "0.4.38" 26 | wasm-bindgen = "0.2" 27 | web-sys = { version = "0.3", features = ["Performance"] } 28 | 29 | [dependencies.serde] 30 | optional = true 31 | version = "1" 32 | features = ["derive"] 33 | 34 | [dev-dependencies] 35 | quickcheck = "1.0.0" 36 | serde_json = "1" 37 | 38 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 39 | criterion = "0.5" 40 | serde_json = "1" 41 | 42 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 43 | criterion = { version = "0.5", default-features = false } 44 | getrandom = { version = "0.2", features = ["js"] } 45 | wasm-bindgen-test = "0.3" 46 | 47 | [features] 48 | default = ["builder"] 49 | ser = ["serde"] 50 | builder = ["derive_builder"] 51 | custom_wasm_env = [] 52 | 53 | [profile.test] 54 | opt-level = 2 55 | 56 | [[bench]] 57 | name = "zxcvbn" 58 | harness = false 59 | 60 | [[bench]] 61 | name = "zxcvbn_unicode" 62 | harness = false 63 | -------------------------------------------------------------------------------- /.github/workflows/zxcvbn.yml: -------------------------------------------------------------------------------- 1 | name: zxcvbn 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | clippy-rustfmt: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Install stable 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | components: clippy, rustfmt 24 | 25 | - name: Run rustfmt 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: fmt 29 | args: -- --check --verbose 30 | 31 | - name: Lint 32 | uses: actions-rs/clippy-check@v1 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | args: --all-features --tests --benches 36 | name: lint 37 | 38 | build: 39 | strategy: 40 | matrix: 41 | platform: [ubuntu-latest, windows-latest] 42 | 43 | runs-on: ${{ matrix.platform }} 44 | 45 | steps: 46 | - uses: actions/checkout@v2 47 | 48 | - name: Install stable 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | profile: minimal 52 | toolchain: stable 53 | override: true 54 | 55 | - name: Build (default features) 56 | run: cargo build --tests --benches 57 | 58 | - name: Run tests (default features) 59 | run: cargo test 60 | 61 | - name: Build (default features) 62 | run: cargo build --tests --benches 63 | 64 | - name: Run tests (all features) 65 | run: cargo test --all-features 66 | 67 | - name: Generate docs 68 | run: cargo doc --all-features --no-deps 69 | 70 | build-wasm: 71 | runs-on: ubuntu-latest 72 | 73 | steps: 74 | - uses: actions/checkout@v2 75 | 76 | - name: Install stable 77 | uses: actions-rs/toolchain@v1 78 | with: 79 | profile: minimal 80 | toolchain: stable 81 | override: true 82 | target: wasm32-unknown-unknown 83 | 84 | - name: Install wasm-pack 85 | uses: jetli/wasm-pack-action@v0.3.0 86 | with: 87 | version: "latest" 88 | 89 | - name: Build (wasm, default features) 90 | run: cargo build --target wasm32-unknown-unknown --tests --benches 91 | 92 | - name: Run tests (wasm, default features) 93 | run: wasm-pack test --node 94 | 95 | - name: Build (wasm, default features) 96 | run: cargo build --target wasm32-unknown-unknown --tests --benches 97 | 98 | - name: Run tests (wasm, all features) 99 | env: 100 | ALL_WASM_BINDGEN_FEATURES: "default,ser,builder" 101 | run: wasm-pack test --node --features $ALL_WASM_BINDGEN_FEATURES 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zxcvbn 2 | 3 | [![Version](https://img.shields.io/crates/v/zxcvbn.svg)](https://crates.io/crates/zxcvbn) 4 | [![License](https://img.shields.io/crates/l/zxcvbn.svg)](https://github.com/shssoichiro/zxcvbn-rs/blob/master/LICENSE) 5 | 6 | ## Overview 7 | 8 | `zxcvbn` is a password strength estimator based off of Dropbox's zxcvbn library. Through pattern matching and conservative estimation, it recognizes and weighs 30k common passwords, common names and surnames according to US census data, popular English words from Wikipedia and US television and movies, and other common patterns like dates, repeats (`aaa`), sequences (`abcd`), keyboard patterns (`qwertyuiop`), and l33t speak. 9 | 10 | Consider using zxcvbn as an algorithmic alternative to password composition policy — it is more secure, flexible, and usable when sites require a minimal complexity score in place of annoying rules like "passwords must contain three of {lower, upper, numbers, symbols}". 11 | 12 | - **More secure**: policies often fail both ways, allowing weak passwords (`P@ssword1`) and disallowing strong passwords. 13 | - **More flexible**: zxcvbn allows many password styles to flourish so long as it detects sufficient complexity — passphrases are rated highly given enough uncommon words, keyboard patterns are ranked based on length and number of turns, and capitalization adds more complexity when it's unpredictable. 14 | - **More usable**: zxcvbn is designed to power simple, rule-free interfaces that give instant feedback. In addition to strength estimation, zxcvbn includes minimal, targeted verbal feedback that can help guide users towards less guessable passwords. 15 | 16 | ## Installing 17 | 18 | `zxcvbn` can be added to your project's `Cargo.toml` under the `[dependencies]` section, as such: 19 | 20 | ```toml 21 | [dependencies] 22 | zxcvbn = "2" 23 | ``` 24 | 25 | zxcvbn has a "ser" feature flag you can enable if you require serialization/deserialization support via `serde`. 26 | It is disabled by default to reduce bloat. 27 | 28 | zxcvbn follows Semantic Versioning. 29 | 30 | zxcvbn targets the latest stable Rust compiler. 31 | It may compile on earlier versions of the compiler, but is only guaranteed to work on the latest stable. 32 | It should also work on the latest beta and nightly, assuming there are no compiler bugs. 33 | 34 | ## Usage 35 | 36 | Full API documentation can be found [here](https://docs.rs/zxcvbn/*/zxcvbn/). 37 | 38 | `zxcvbn` exposes one function called `zxcvbn` which can be called to calculate a score (0-4) for a password as well as other relevant information. 39 | `zxcvbn` may also take an array of user inputs (e.g. username, email address, city, state) to provide warnings for passwords containing such information. 40 | 41 | Usage example: 42 | 43 | ```rust 44 | extern crate zxcvbn; 45 | 46 | use zxcvbn::zxcvbn; 47 | 48 | fn main() { 49 | let estimate = zxcvbn("correcthorsebatterystaple", &[]); 50 | println!("{}", estimate.score()); // 3 51 | } 52 | ``` 53 | 54 | Other fields available on the returned `Entropy` struct may be viewed in the [full documentation](https://docs.rs/zxcvbn/*/zxcvbn/). 55 | 56 | ## Contributing 57 | 58 | Any contributions are welcome and will be accepted via pull request on GitHub. Bug reports can be 59 | filed via GitHub issues. Please include as many details as possible. If you have the capability 60 | to submit a fix with the bug report, it is preferred that you do so via pull request, 61 | however you do not need to be a Rust developer to contribute. 62 | Other contributions (such as improving documentation or translations) are also welcome via GitHub. 63 | 64 | ## License 65 | 66 | zxcvbn is open-source software, distributed under the MIT license. 67 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | **Version 3.1.0** 2 | 3 | - Add `Display` to `Feedback` 4 | - Add support for WASM targets running in a custom runtime 5 | 6 | **Version 3.0.1** 7 | 8 | - Fix a bug in 3.0.0 where the `Score` enum was private 9 | - Fix a bug where some structs that were intended to implement `Serialize` when the `ser` feature was enabled did not implement it 10 | 11 | **Version 3.0.0** 12 | 13 | - [Breaking] Avoid the possibility for zxcvbn to error 14 | - [Breaking] Refactor the score into an exhaustive enum 15 | - [Breaking] Change `feedback` to return `Option<&Feedback>` 16 | - Bump `itertools` dependency to 0.13 17 | 18 | **Version 2.2.2** 19 | 20 | - Fix a possible panic in spatial pattern checker (https://github.com/shssoichiro/zxcvbn-rs/issues/70)[#70] 21 | - Update several dependencies 22 | - Fix several new clippy lints 23 | - Officially specify minimum Rust version requirement 24 | - The version has not changed, but the requirement has now been added to Cargo.toml 25 | 26 | **Version 2.2.1** 27 | 28 | - Fixes for building on WASM targets 29 | 30 | **Version 2.2.0** 31 | 32 | - Fix an issue where a less specific feedback would be given 33 | when a more specific feedback was available. (https://github.com/shssoichiro/zxcvbn-rs/issues/54)[#54] 34 | - Migrate to Rust edition 2021 35 | - Migrate from `chrono` crate to `time` crate 36 | - Update `fancy-regex` to 0.8 37 | 38 | **Version 2.1.1** 39 | 40 | - Do not download and build wasm dependencies if not building for wasm 41 | 42 | **Version 2.1.0** 43 | 44 | - [Feature] Add support for wasm 45 | - Deprecate the usage of builders (it will still work for now, but will be removed in the next major release) 46 | - Various performance improvements and dependency upgrades 47 | 48 | **Version 2.0.1** 49 | 50 | - Fix overflow bugs that may cause wrong results on very complex passwords 51 | - Fix a panic that could occur on passwords with multibyte unicode characters 52 | - Update `derive_builder` to 0.9 53 | 54 | **Version 2.0.0** 55 | 56 | - [Breaking] Update CrackTimes interface to be more idiomatic to Rust (https://github.com/shssoichiro/zxcvbn-rs/pull/24) 57 | - Upgrade `derive_builder` to 0.8 58 | - Upgrade `fancy_regex` to 0.2 59 | - Move to 2018 edition 60 | - Various internal improvements 61 | 62 | **Version 1.0.2** 63 | 64 | - Fix building on Rust 1.36.0 (https://github.com/shssoichiro/zxcvbn-rs/pull/21) 65 | - Cleanup development profiles which are no longer needed 66 | - Remove built-in clippy and prefer using clippy from rustup 67 | - Upgrade `itertools` to 0.8 68 | - Upgrade `derive_builder` to 0.7 69 | 70 | **Version 1.0.1** 71 | 72 | - Upgrade `regex` to 1.0 73 | 74 | **Version 1.0.0** 75 | 76 | - [SEMVER_MINOR] Add support for UTF-8 strings (https://github.com/shssoichiro/zxcvbn-rs/issues/4) 77 | - [SEMVER_MAJOR] Remove the `ZxcvbnError::NonAsciiPassword` variant, since this error can no longer occur 78 | 79 | **Version 0.7.0** 80 | 81 | - [SEMVER_MAJOR] Refactor `Match` to use an enum internally, to avoid cluttering the struct with several `Option` types (https://github.com/shssoichiro/zxcvbn-rs/issues/19) 82 | - Make `Match` public (https://github.com/shssoichiro/zxcvbn-rs/issues/17) 83 | 84 | **Version 0.6.3** 85 | 86 | - Refactor handling of strings to use streaming of characters. This brings zxcvbn closer to working on UTF-8 inputs. 87 | - Fix an issue that would cause bruteforce scores to be too low (https://github.com/shssoichiro/zxcvbn-rs/issues/15) 88 | 89 | **Version 0.6.2** 90 | 91 | - Upgrade dependencies and fix linter warnings 92 | 93 | **Version 0.6.1** 94 | 95 | - Upgrade `derive_builder` to 0.5.0 96 | - Fix a bug that was causing incorrect scoring for some passwords (https://github.com/shssoichiro/zxcvbn-rs/issues/13) 97 | 98 | **Version 0.6.0** 99 | 100 | - [SEMVER_MAJOR] Change the signature for `zxcvbn` to take `&[]` instead of `Option<&[]>` for `user_inputs` (https://github.com/shssoichiro/zxcvbn-rs/issues/9) 101 | - [SEMVER_MAJOR] Change the signature for `zxcvbn` to return `Result` instead of `Option` (https://github.com/shssoichiro/zxcvbn-rs/issues/11) 102 | 103 | **Version 0.5.0** 104 | 105 | - Fix for a BC-breaking change in nightly Rust (https://github.com/shssoichiro/zxcvbn-rs/pull/8) 106 | - Upgrade `serde` to 1.0 107 | - Silence a warning from `derive_builder` 108 | 109 | **Version 0.4.4** 110 | 111 | - Upgrade `itertools` to 0.6 112 | 113 | **Version 0.4.3** 114 | 115 | - Upgrade to derive_builder 0.4 116 | 117 | **Version 0.4.2** 118 | 119 | - Remove FFI dependency on oniguruma 120 | 121 | **Version 0.4.1** 122 | 123 | - Fix more overflow bugs 124 | - Simplify code for handling overflows 125 | 126 | **Version 0.4.0** 127 | 128 | - Fix bug which caused multiplication overflows on some very strong passwords 129 | - Remove rustc-serialize support (https://github.com/shssoichiro/zxcvbn-rs/issues/5) 130 | 131 | **Version 0.3.0** 132 | 133 | - Make reference year dynamic 134 | - Performance optimizations 135 | - [SEMVER_MAJOR] Rename "serde" feature to "ser" (required by cargo) 136 | - [SEMVER_MAJOR] Bump required serde and serde_derive version to 0.9.x 137 | 138 | **Version 0.2.1** 139 | 140 | - Update regex dependency to 0.2.0 141 | 142 | **Version 0.2.0** 143 | 144 | - [SEMVER_MINOR] Add optional features "rustc-serialize" and "serde" for serialization support. 145 | -------------------------------------------------------------------------------- /src/matching/patterns.rs: -------------------------------------------------------------------------------- 1 | use crate::frequency_lists::DictionaryType; 2 | use crate::matching::Match; 3 | use std::collections::HashMap; 4 | 5 | /// Pattern type used to detect a match 6 | #[derive(Debug, Clone, PartialEq, Default)] 7 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 8 | #[cfg_attr(feature = "ser", serde(tag = "pattern"))] 9 | #[cfg_attr(feature = "ser", serde(rename_all = "lowercase"))] 10 | pub enum MatchPattern { 11 | /// A match based on a word in a dictionary 12 | Dictionary(DictionaryPattern), 13 | /// A match based on keys being close to one another on the keyboard 14 | Spatial(SpatialPattern), 15 | /// A match based on repeating patterns 16 | Repeat(RepeatPattern), 17 | /// A match based on sequences of characters, e.g. "abcd" 18 | Sequence(SequencePattern), 19 | /// A match based on one of the regex patterns used in zxcvbn. 20 | Regex(RegexPattern), 21 | /// A match based on date patterns 22 | Date(DatePattern), 23 | /// A match based on bruteforce attempting to guess a password 24 | #[default] 25 | BruteForce, 26 | } 27 | 28 | impl MatchPattern { 29 | #[cfg(test)] 30 | pub(crate) fn variant(&self) -> &str { 31 | match *self { 32 | MatchPattern::Dictionary(_) => "dictionary", 33 | MatchPattern::Spatial(_) => "spatial", 34 | MatchPattern::Repeat(_) => "repeat", 35 | MatchPattern::Sequence(_) => "sequence", 36 | MatchPattern::Regex(_) => "regex", 37 | MatchPattern::Date(_) => "date", 38 | MatchPattern::BruteForce => "bruteforce", 39 | } 40 | } 41 | } 42 | 43 | /// A match based on a word in a dictionary 44 | #[derive(Debug, Clone, PartialEq, Default)] 45 | #[cfg_attr(feature = "builder", derive(Builder))] 46 | #[cfg_attr(feature = "builder", builder(default))] 47 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 48 | pub struct DictionaryPattern { 49 | /// Word that has been found in a dictionary. 50 | pub matched_word: String, 51 | /// Rank of the the word found in a dictionary. 52 | pub rank: usize, 53 | /// Name of the dictionary in which a word has been found. 54 | pub dictionary_name: DictionaryType, 55 | /// Whether a reversed word has been found in a dictionary. 56 | pub reversed: bool, 57 | /// Whether a l33t-substituted word has been found in a dictionary. 58 | pub l33t: bool, 59 | /// Substitutions used for the match. 60 | pub sub: Option>, 61 | /// String for displaying the substitutions used for the match. 62 | pub sub_display: Option, 63 | /// Number of variations of the matched dictionary word. 64 | pub uppercase_variations: u64, 65 | /// Number of variations of the matched dictionary word. 66 | pub l33t_variations: u64, 67 | /// Estimated number of tries for guessing the dictionary word. 68 | pub base_guesses: u64, 69 | } 70 | 71 | /// A match based on keys being close to one another on the keyboard 72 | #[derive(Debug, Clone, PartialEq, Default)] 73 | #[cfg_attr(feature = "builder", derive(Builder))] 74 | #[cfg_attr(feature = "builder", builder(default))] 75 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 76 | pub struct SpatialPattern { 77 | /// Name of the graph for which a spatial match has been found. 78 | pub graph: String, 79 | /// Number of turns in the matched spatial pattern. 80 | pub turns: usize, 81 | /// Number of shifts in the matched spatial pattern. 82 | pub shifted_count: usize, 83 | } 84 | 85 | /// A match based on repeating patterns 86 | #[derive(Debug, Clone, PartialEq, Default)] 87 | #[cfg_attr(feature = "builder", derive(Builder))] 88 | #[cfg_attr(feature = "builder", builder(default))] 89 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 90 | pub struct RepeatPattern { 91 | /// Base token that repeats in the matched pattern. 92 | pub base_token: String, 93 | /// Matches for the repeating token. 94 | pub base_matches: Vec, 95 | /// Estimated number of tries for guessing the repeating token. 96 | pub base_guesses: u64, 97 | /// Number of repetitions in the matched pattern. 98 | pub repeat_count: usize, 99 | } 100 | 101 | /// A match based on sequences of characters, e.g. "abcd" 102 | #[derive(Debug, Clone, PartialEq, Default)] 103 | #[cfg_attr(feature = "builder", derive(Builder))] 104 | #[cfg_attr(feature = "builder", builder(default))] 105 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 106 | pub struct SequencePattern { 107 | /// Name of the sequence that was matched. 108 | pub sequence_name: String, 109 | /// Size of the sequence that was matched. 110 | pub sequence_space: u8, 111 | /// Whether the matched sequence is ascending. 112 | pub ascending: bool, 113 | } 114 | 115 | /// A match based on one of the regex patterns used in zxcvbn. 116 | #[derive(Debug, Clone, PartialEq, Default)] 117 | #[cfg_attr(feature = "builder", derive(Builder))] 118 | #[cfg_attr(feature = "builder", builder(default))] 119 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 120 | pub struct RegexPattern { 121 | /// Name of the regular expression that was matched. 122 | pub regex_name: String, 123 | /// Matches of the regular expression. 124 | pub regex_match: Vec, 125 | } 126 | 127 | /// A match based on date patterns 128 | #[derive(Debug, Clone, PartialEq, Default)] 129 | #[cfg_attr(feature = "builder", derive(Builder))] 130 | #[cfg_attr(feature = "builder", builder(default))] 131 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 132 | pub struct DatePattern { 133 | /// Separator of a date that was matched. 134 | pub separator: String, 135 | /// Year that was matched. 136 | pub year: i32, 137 | /// Month that was matched. 138 | pub month: i8, 139 | /// Day that was matched. 140 | pub day: i8, 141 | } 142 | -------------------------------------------------------------------------------- /src/time_estimates.rs: -------------------------------------------------------------------------------- 1 | //! Contains structs and methods for calculating estimated time 2 | //! needed to crack a given password. 3 | //! 4 | //! # Example 5 | //! ```rust 6 | //! # use std::error::Error; 7 | //! # 8 | //! # fn main() -> Result<(), Box> { 9 | //! use zxcvbn::zxcvbn; 10 | //! use zxcvbn::time_estimates::CrackTimes; 11 | //! 12 | //! let entropy = zxcvbn("password123", &[]); 13 | //! assert_eq!(entropy.crack_times().guesses(), 596); 14 | //! assert_eq!(entropy.crack_times().online_throttling_100_per_hour().to_string(), "5 hours"); 15 | //! assert_eq!(entropy.crack_times().online_no_throttling_10_per_second().to_string(), "59 seconds"); 16 | //! assert_eq!(entropy.crack_times().offline_slow_hashing_1e4_per_second().to_string(), "less than a second"); 17 | //! assert_eq!(entropy.crack_times().offline_fast_hashing_1e10_per_second().to_string(), "less than a second"); 18 | //! # 19 | //! # Ok(()) 20 | //! # } 21 | //! ``` 22 | 23 | use std::fmt; 24 | 25 | use crate::scoring::Score; 26 | 27 | /// Back-of-the-envelope crack time estimations, in seconds, based on a few scenarios. 28 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] 29 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 30 | pub struct CrackTimes { 31 | guesses: u64, 32 | } 33 | 34 | impl CrackTimes { 35 | /// Get the time needed to crack a password based on the amount of guesses needed. 36 | /// 37 | /// # Arguments 38 | /// * `guesses` - The number of guesses needed to crack a password. 39 | pub fn new(guesses: u64) -> Self { 40 | CrackTimes { guesses } 41 | } 42 | 43 | /// Get the amount of guesses needed to crack the password. 44 | pub fn guesses(self) -> u64 { 45 | self.guesses 46 | } 47 | 48 | /// Online attack on a service that rate-limits password attempts. 49 | pub fn online_throttling_100_per_hour(self) -> CrackTimeSeconds { 50 | CrackTimeSeconds::Integer(self.guesses.saturating_mul(36)) 51 | } 52 | 53 | /// Online attack on a service that doesn't rate-limit, 54 | /// or where an attacker has outsmarted rate-limiting. 55 | pub fn online_no_throttling_10_per_second(self) -> CrackTimeSeconds { 56 | CrackTimeSeconds::Float(self.guesses as f64 / 10.00) 57 | } 58 | 59 | /// Offline attack, assumes multiple attackers. 60 | /// Proper user-unique salting, and a slow hash function 61 | /// such as bcrypt, scrypt, PBKDF2. 62 | pub fn offline_slow_hashing_1e4_per_second(self) -> CrackTimeSeconds { 63 | CrackTimeSeconds::Float(self.guesses as f64 / 10_000.00) 64 | } 65 | 66 | /// Offline attack with user-unique salting but a fast hash function 67 | /// such as SHA-1, SHA-256, or MD5. A wide range of reasonable numbers 68 | /// anywhere from one billion to one trillion guesses per second, 69 | /// depending on number of cores and machines, ballparking at 10 billion per second. 70 | pub fn offline_fast_hashing_1e10_per_second(self) -> CrackTimeSeconds { 71 | CrackTimeSeconds::Float(self.guesses as f64 / 10_000_000_000.00) 72 | } 73 | } 74 | 75 | /// Represents the time to crack a password. 76 | #[derive(Copy, Clone, Debug)] 77 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 78 | #[cfg_attr(feature = "ser", serde(untagged))] 79 | pub enum CrackTimeSeconds { 80 | /// The number of seconds needed to crack a password, expressed as an integer. 81 | Integer(u64), 82 | /// The number of seconds needed to crack a password, expressed as a float. 83 | Float(f64), 84 | } 85 | 86 | impl fmt::Display for CrackTimeSeconds { 87 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 88 | let seconds = match self { 89 | CrackTimeSeconds::Integer(i) => *i, 90 | CrackTimeSeconds::Float(f) => *f as u64, 91 | }; 92 | const MINUTE: u64 = 60; 93 | const HOUR: u64 = MINUTE * 60; 94 | const DAY: u64 = HOUR * 24; 95 | const MONTH: u64 = DAY * 31; 96 | const YEAR: u64 = MONTH * 12; 97 | const CENTURY: u64 = YEAR * 100; 98 | if seconds < 1 { 99 | write!(f, "less than a second") 100 | } else if seconds < MINUTE { 101 | let base = seconds; 102 | write!(f, "{} second{}", base, if base > 1 { "s" } else { "" }) 103 | } else if seconds < HOUR { 104 | let base = seconds / MINUTE; 105 | write!(f, "{} minute{}", base, if base > 1 { "s" } else { "" }) 106 | } else if seconds < DAY { 107 | let base = seconds / HOUR; 108 | write!(f, "{} hour{}", base, if base > 1 { "s" } else { "" }) 109 | } else if seconds < MONTH { 110 | let base = seconds / DAY; 111 | write!(f, "{} day{}", base, if base > 1 { "s" } else { "" }) 112 | } else if seconds < YEAR { 113 | let base = seconds / MONTH; 114 | write!(f, "{} month{}", base, if base > 1 { "s" } else { "" }) 115 | } else if seconds < CENTURY { 116 | let base = seconds / YEAR; 117 | write!(f, "{} year{}", base, if base > 1 { "s" } else { "" }) 118 | } else { 119 | write!(f, "centuries") 120 | } 121 | } 122 | } 123 | 124 | impl From for std::time::Duration { 125 | fn from(s: CrackTimeSeconds) -> std::time::Duration { 126 | match s { 127 | // TODO: Use `from_secs_f64` when it is stable 128 | CrackTimeSeconds::Float(f) => std::time::Duration::from_secs(f as u64), 129 | CrackTimeSeconds::Integer(i) => std::time::Duration::from_secs(i), 130 | } 131 | } 132 | } 133 | 134 | pub(crate) fn estimate_attack_times(guesses: u64) -> (CrackTimes, Score) { 135 | (CrackTimes::new(guesses), calculate_score(guesses)) 136 | } 137 | 138 | fn calculate_score(guesses: u64) -> Score { 139 | const DELTA: u64 = 5; 140 | if guesses < 1_000 + DELTA { 141 | Score::Zero 142 | } else if guesses < 1_000_000 + DELTA { 143 | Score::One 144 | } else if guesses < 100_000_000 + DELTA { 145 | Score::Two 146 | } else if guesses < 10_000_000_000 + DELTA { 147 | Score::Three 148 | } else { 149 | Score::Four 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![recursion_limit = "128"] 3 | #![warn(missing_docs)] 4 | 5 | #[macro_use] 6 | #[cfg(feature = "builder")] 7 | extern crate derive_builder; 8 | 9 | #[macro_use] 10 | extern crate lazy_static; 11 | 12 | use std::time::Duration; 13 | 14 | #[cfg(test)] 15 | #[macro_use] 16 | extern crate quickcheck; 17 | 18 | pub use scoring::Score; 19 | use time_estimates::CrackTimes; 20 | #[cfg(all(target_arch = "wasm32", not(feature = "custom_wasm_env")))] 21 | use wasm_bindgen::prelude::wasm_bindgen; 22 | 23 | pub use crate::matching::Match; 24 | 25 | mod adjacency_graphs; 26 | pub mod feedback; 27 | mod frequency_lists; 28 | /// Defines structures for matches found in a password 29 | pub mod matching; 30 | mod scoring; 31 | pub mod time_estimates; 32 | 33 | #[cfg(feature = "ser")] 34 | mod serialization_utils; 35 | 36 | #[cfg(not(target_arch = "wasm32"))] 37 | fn time_scoped(f: F) -> (R, Duration) 38 | where 39 | F: FnOnce() -> R, 40 | { 41 | let start_time = std::time::Instant::now(); 42 | let result = f(); 43 | let calc_time = std::time::Instant::now().duration_since(start_time); 44 | (result, calc_time) 45 | } 46 | 47 | #[cfg(all(target_arch = "wasm32", not(feature = "custom_wasm_env")))] 48 | #[allow(non_upper_case_globals)] 49 | fn time_scoped(f: F) -> (R, Duration) 50 | where 51 | F: FnOnce() -> R, 52 | { 53 | #[wasm_bindgen] 54 | extern "C" { 55 | #[no_mangle] 56 | #[used] 57 | static performance: web_sys::Performance; 58 | } 59 | 60 | let start_time = performance.now(); 61 | let result = f(); 62 | let calc_time = std::time::Duration::from_secs_f64((performance.now() - start_time) / 1000.0); 63 | (result, calc_time) 64 | } 65 | 66 | #[cfg(all(target_arch = "wasm32", feature = "custom_wasm_env"))] 67 | fn time_scoped(f: F) -> (R, Duration) 68 | where 69 | F: FnOnce() -> R, 70 | { 71 | #[link(wasm_import_module = "zxcvbn")] 72 | extern "C" { 73 | fn unix_time_milliseconds_imported() -> u64; 74 | } 75 | let start_time = unsafe { unix_time_milliseconds_imported() }; 76 | let result = f(); 77 | let end_time = unsafe { unix_time_milliseconds_imported() }; 78 | 79 | let duration = std::time::Duration::from_millis(end_time - start_time); 80 | (result, duration) 81 | } 82 | 83 | /// Contains the results of an entropy calculation 84 | #[derive(Debug, PartialEq, Clone)] 85 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 86 | pub struct Entropy { 87 | /// Estimated guesses needed to crack the password 88 | guesses: u64, 89 | /// Order of magnitude of `guesses` 90 | #[cfg_attr( 91 | feature = "ser", 92 | serde(deserialize_with = "crate::serialization_utils::deserialize_f64_null_as_nan") 93 | )] 94 | guesses_log10: f64, 95 | /// List of back-of-the-envelope crack time estimations based on a few scenarios. 96 | crack_times: time_estimates::CrackTimes, 97 | /// Overall strength score from 0-4. 98 | /// Any score less than 3 should be considered too weak. 99 | score: Score, 100 | /// Verbal feedback to help choose better passwords. Set when `score` <= 2. 101 | feedback: Option, 102 | /// The list of patterns the guess calculation was based on 103 | sequence: Vec, 104 | /// How long it took to calculate the answer. 105 | calc_time: Duration, 106 | } 107 | 108 | impl Entropy { 109 | /// The estimated number of guesses needed to crack the password. 110 | pub fn guesses(&self) -> u64 { 111 | self.guesses 112 | } 113 | 114 | /// The order of magnitude of `guesses`. 115 | pub fn guesses_log10(&self) -> f64 { 116 | self.guesses_log10 117 | } 118 | 119 | /// List of back-of-the-envelope crack time estimations based on a few scenarios. 120 | pub fn crack_times(&self) -> time_estimates::CrackTimes { 121 | self.crack_times 122 | } 123 | 124 | /// Overall strength score from 0-4. 125 | /// Any score less than 3 should be considered too weak. 126 | pub fn score(&self) -> Score { 127 | self.score 128 | } 129 | 130 | /// Feedback to help choose better passwords. Set when `score` <= 2. 131 | pub fn feedback(&self) -> Option<&feedback::Feedback> { 132 | self.feedback.as_ref() 133 | } 134 | 135 | /// The list of patterns the guess calculation was based on 136 | pub fn sequence(&self) -> &[Match] { 137 | &self.sequence 138 | } 139 | 140 | /// How long it took to calculate the answer. 141 | pub fn calculation_time(&self) -> Duration { 142 | self.calc_time 143 | } 144 | } 145 | 146 | /// Takes a password string and optionally a list of user-supplied inputs 147 | /// (e.g. username, email, first name) and calculates the strength of the password 148 | /// based on entropy, using a number of different factors. 149 | pub fn zxcvbn(password: &str, user_inputs: &[&str]) -> Entropy { 150 | if password.is_empty() { 151 | return Entropy { 152 | guesses: 0, 153 | guesses_log10: f64::NEG_INFINITY, 154 | crack_times: CrackTimes::new(0), 155 | score: Score::Zero, 156 | feedback: feedback::get_feedback(Score::Zero, &[]), 157 | sequence: Vec::default(), 158 | calc_time: Duration::from_secs(0), 159 | }; 160 | } 161 | 162 | let (result, calc_time) = time_scoped(|| { 163 | // Only evaluate the first 100 characters of the input. 164 | // This prevents potential DoS attacks from sending extremely long input strings. 165 | let password = password.chars().take(100).collect::(); 166 | 167 | let sanitized_inputs = user_inputs 168 | .iter() 169 | .enumerate() 170 | .map(|(i, x)| (x.to_lowercase(), i + 1)) 171 | .collect(); 172 | 173 | let matches = matching::omnimatch(&password, &sanitized_inputs); 174 | scoring::most_guessable_match_sequence(&password, &matches, false) 175 | }); 176 | let (crack_times, score) = time_estimates::estimate_attack_times(result.guesses); 177 | let feedback = feedback::get_feedback(score, &result.sequence); 178 | 179 | Entropy { 180 | guesses: result.guesses, 181 | guesses_log10: result.guesses_log10, 182 | crack_times, 183 | score, 184 | feedback, 185 | sequence: result.sequence, 186 | calc_time, 187 | } 188 | } 189 | 190 | #[cfg(test)] 191 | mod tests { 192 | use super::*; 193 | 194 | use quickcheck::TestResult; 195 | 196 | #[cfg(target_arch = "wasm32")] 197 | use wasm_bindgen_test::wasm_bindgen_test; 198 | 199 | quickcheck! { 200 | fn test_zxcvbn_doesnt_panic(password: String, user_inputs: Vec) -> TestResult { 201 | let inputs = user_inputs.iter().map(|s| s.as_ref()).collect::>(); 202 | zxcvbn(&password, &inputs); 203 | TestResult::from_bool(true) 204 | } 205 | 206 | #[cfg(feature = "ser")] 207 | fn test_zxcvbn_serialisation_doesnt_panic(password: String, user_inputs: Vec) -> TestResult { 208 | let inputs = user_inputs.iter().map(|s| s.as_ref()).collect::>(); 209 | serde_json::to_string(&zxcvbn(&password, &inputs)).ok(); 210 | TestResult::from_bool(true) 211 | } 212 | 213 | #[cfg(feature = "ser")] 214 | fn test_zxcvbn_serialization_roundtrip(password: String, user_inputs: Vec) -> TestResult { 215 | let inputs = user_inputs.iter().map(|s| s.as_ref()).collect::>(); 216 | let entropy = zxcvbn(&password, &inputs); 217 | // When the entropy is not a finite number (otherwise our equality test fails). We test 218 | // this scenario separately 219 | if !entropy.guesses_log10.is_finite() { 220 | //panic!("infinite guesses_log10: {} => {}", password, entropy.guesses_log10); 221 | return TestResult::discard(); 222 | } 223 | let serialized_entropy = serde_json::to_string(&entropy); 224 | assert!(serialized_entropy.is_ok()); 225 | let serialized_entropy = serialized_entropy.expect("serialized entropy"); 226 | let deserialized_entropy = serde_json::from_str::(&serialized_entropy); 227 | assert!(deserialized_entropy.is_ok()); 228 | let deserialized_entropy = deserialized_entropy.expect("deserialized entropy"); 229 | 230 | // Apply a mask to trim the last bit when comparing guesses_log10, since Serde loses 231 | // precision when deserializing 232 | const MASK: u64 = 0x1111111111111110; 233 | 234 | let original_equal_to_deserialized_version = 235 | (entropy.guesses == deserialized_entropy.guesses) && 236 | (entropy.crack_times == deserialized_entropy.crack_times) && 237 | (entropy.score == deserialized_entropy.score) && 238 | (entropy.feedback == deserialized_entropy.feedback) && 239 | (entropy.sequence == deserialized_entropy.sequence) && 240 | (entropy.calc_time == deserialized_entropy.calc_time) && 241 | (entropy.guesses_log10.to_bits() & MASK == deserialized_entropy.guesses_log10.to_bits() & MASK); 242 | 243 | TestResult::from_bool(original_equal_to_deserialized_version) 244 | } 245 | } 246 | 247 | #[test] 248 | #[cfg(feature = "ser")] 249 | fn test_zxcvbn_serialization_non_finite_guesses_log10() { 250 | let entropy = zxcvbn("", &[]); 251 | assert!(!entropy.guesses_log10.is_finite()); 252 | 253 | let serialized_entropy = serde_json::to_string(&entropy); 254 | assert!(serialized_entropy.is_ok()); 255 | let serialized_entropy = serialized_entropy.expect("serialized entropy"); 256 | let deserialized_entropy = serde_json::from_str::(&serialized_entropy); 257 | assert!(deserialized_entropy.is_ok()); 258 | let deserialized_entropy = deserialized_entropy.expect("deserialized entropy"); 259 | assert!(!deserialized_entropy.guesses_log10.is_finite()); 260 | } 261 | 262 | #[cfg_attr(not(target_arch = "wasm32"), test)] 263 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 264 | fn test_zxcvbn() { 265 | let password = "r0sebudmaelstrom11/20/91aaaa"; 266 | let entropy = zxcvbn(password, &[]); 267 | assert_eq!(entropy.guesses_log10 as u16, 14); 268 | assert_eq!(entropy.score, Score::Four); 269 | assert!(!entropy.sequence.is_empty()); 270 | assert!(entropy.feedback.is_none()); 271 | assert!(entropy.calc_time.as_nanos() > 0); 272 | } 273 | 274 | #[cfg_attr(not(target_arch = "wasm32"), test)] 275 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 276 | fn test_zxcvbn_empty() { 277 | let password = ""; 278 | let entropy = zxcvbn(password, &[]); 279 | assert_eq!(entropy.score, Score::Zero); 280 | assert_eq!(entropy.guesses, 0); 281 | assert_eq!(entropy.guesses_log10, f64::NEG_INFINITY); 282 | assert_eq!(entropy.crack_times, CrackTimes::new(0)); 283 | assert_eq!(entropy.sequence, Vec::default()); 284 | } 285 | 286 | #[cfg_attr(not(target_arch = "wasm32"), test)] 287 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 288 | fn test_zxcvbn_unicode() { 289 | let password = "𐰊𐰂𐰄𐰀𐰁"; 290 | let entropy = zxcvbn(password, &[]); 291 | assert_eq!(entropy.score, Score::One); 292 | } 293 | 294 | #[cfg_attr(not(target_arch = "wasm32"), test)] 295 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 296 | fn test_zxcvbn_unicode_2() { 297 | let password = "r0sebudmaelstrom丂/20/91aaaa"; 298 | let entropy = zxcvbn(password, &[]); 299 | assert_eq!(entropy.score, Score::Four); 300 | } 301 | 302 | #[cfg_attr(not(target_arch = "wasm32"), test)] 303 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 304 | fn test_issue_13() { 305 | let password = "Imaginative-Say-Shoulder-Dish-0"; 306 | let entropy = zxcvbn(password, &[]); 307 | assert_eq!(entropy.score, Score::Four); 308 | } 309 | 310 | #[cfg_attr(not(target_arch = "wasm32"), test)] 311 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 312 | fn test_issue_15_example_1() { 313 | let password = "TestMeNow!"; 314 | let entropy = zxcvbn(password, &[]); 315 | assert_eq!(entropy.guesses, 372_010_000); 316 | assert!((entropy.guesses_log10 - 8.57055461430783).abs() < f64::EPSILON); 317 | assert_eq!(entropy.score, Score::Three); 318 | } 319 | 320 | #[cfg_attr(not(target_arch = "wasm32"), test)] 321 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 322 | fn test_issue_15_example_2() { 323 | let password = "hey<123"; 324 | let entropy = zxcvbn(password, &[]); 325 | assert_eq!(entropy.guesses, 1_010_000); 326 | assert!((entropy.guesses_log10 - 6.004321373782642).abs() < f64::EPSILON); 327 | assert_eq!(entropy.score, Score::Two); 328 | } 329 | 330 | #[cfg_attr(not(target_arch = "wasm32"), test)] 331 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 332 | fn test_overflow_safety() { 333 | let password = "!QASW@#EDFR$%TGHY^&UJKI*(OL"; 334 | let entropy = zxcvbn(password, &[]); 335 | assert_eq!(entropy.guesses, u64::max_value()); 336 | assert_eq!(entropy.score, Score::Four); 337 | } 338 | 339 | #[cfg_attr(not(target_arch = "wasm32"), test)] 340 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 341 | fn test_unicode_mb() { 342 | let password = "08märz2010"; 343 | let entropy = zxcvbn(password, &[]); 344 | assert_eq!(entropy.guesses, 100010000); 345 | assert_eq!(entropy.score, Score::Three); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/feedback.rs: -------------------------------------------------------------------------------- 1 | //! Contains structs and methods related to generating feedback strings 2 | //! for providing help for the user to generate stronger passwords. 3 | 4 | use itertools::Itertools; 5 | 6 | use crate::matching::patterns::*; 7 | use crate::matching::Match; 8 | use crate::{frequency_lists::DictionaryType, scoring::Score}; 9 | use std::fmt; 10 | 11 | /// A warning explains what's wrong with the password. 12 | #[derive(Debug, Copy, Clone, PartialEq)] 13 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 14 | #[allow(missing_docs)] 15 | pub enum Warning { 16 | StraightRowsOfKeysAreEasyToGuess, 17 | ShortKeyboardPatternsAreEasyToGuess, 18 | RepeatsLikeAaaAreEasyToGuess, 19 | RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess, 20 | ThisIsATop10Password, 21 | ThisIsATop100Password, 22 | ThisIsACommonPassword, 23 | ThisIsSimilarToACommonlyUsedPassword, 24 | SequencesLikeAbcAreEasyToGuess, 25 | RecentYearsAreEasyToGuess, 26 | AWordByItselfIsEasyToGuess, 27 | DatesAreOftenEasyToGuess, 28 | NamesAndSurnamesByThemselvesAreEasyToGuess, 29 | CommonNamesAndSurnamesAreEasyToGuess, 30 | } 31 | 32 | impl fmt::Display for Warning { 33 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 34 | match self { 35 | Warning::StraightRowsOfKeysAreEasyToGuess => { 36 | write!(f, "Straight rows of keys are easy to guess.") 37 | } 38 | Warning::ShortKeyboardPatternsAreEasyToGuess => { 39 | write!(f, "Short keyboard patterns are easy to guess.") 40 | } 41 | Warning::RepeatsLikeAaaAreEasyToGuess => { 42 | write!(f, "Repeats like \"aaa\" are easy to guess.") 43 | } 44 | Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess => write!( 45 | f, 46 | "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"." 47 | ), 48 | Warning::ThisIsATop10Password => write!(f, "This is a top-10 common password."), 49 | Warning::ThisIsATop100Password => write!(f, "This is a top-100 common password."), 50 | Warning::ThisIsACommonPassword => write!(f, "This is a very common password."), 51 | Warning::ThisIsSimilarToACommonlyUsedPassword => { 52 | write!(f, "This is similar to a commonly used password.") 53 | } 54 | Warning::SequencesLikeAbcAreEasyToGuess => { 55 | write!(f, "Sequences like abc or 6543 are easy to guess.") 56 | } 57 | Warning::RecentYearsAreEasyToGuess => write!(f, "Recent years are easy to guess."), 58 | Warning::AWordByItselfIsEasyToGuess => write!(f, "A word by itself is easy to guess."), 59 | Warning::DatesAreOftenEasyToGuess => write!(f, "Dates are often easy to guess."), 60 | Warning::NamesAndSurnamesByThemselvesAreEasyToGuess => { 61 | write!(f, "Names and surnames by themselves are easy to guess.") 62 | } 63 | Warning::CommonNamesAndSurnamesAreEasyToGuess => { 64 | write!(f, "Common names and surnames are easy to guess.") 65 | } 66 | } 67 | } 68 | } 69 | 70 | /// A suggestion helps to choose a better password. 71 | #[derive(Debug, Copy, Clone, PartialEq)] 72 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 73 | #[allow(missing_docs)] 74 | pub enum Suggestion { 75 | UseAFewWordsAvoidCommonPhrases, 76 | NoNeedForSymbolsDigitsOrUppercaseLetters, 77 | AddAnotherWordOrTwo, 78 | CapitalizationDoesntHelpVeryMuch, 79 | AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase, 80 | ReversedWordsArentMuchHarderToGuess, 81 | PredictableSubstitutionsDontHelpVeryMuch, 82 | UseALongerKeyboardPatternWithMoreTurns, 83 | AvoidRepeatedWordsAndCharacters, 84 | AvoidSequences, 85 | AvoidRecentYears, 86 | AvoidYearsThatAreAssociatedWithYou, 87 | AvoidDatesAndYearsThatAreAssociatedWithYou, 88 | } 89 | 90 | impl fmt::Display for Suggestion { 91 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 92 | match self { 93 | Suggestion::UseAFewWordsAvoidCommonPhrases => { 94 | write!(f, "Use a few words, avoid common phrases.") 95 | } 96 | Suggestion::NoNeedForSymbolsDigitsOrUppercaseLetters => { 97 | write!(f, "No need for symbols, digits, or uppercase letters.") 98 | } 99 | Suggestion::AddAnotherWordOrTwo => { 100 | write!(f, "Add another word or two. Uncommon words are better.") 101 | } 102 | Suggestion::CapitalizationDoesntHelpVeryMuch => { 103 | write!(f, "Capitalization doesn't help very much.") 104 | } 105 | Suggestion::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => write!( 106 | f, 107 | "All-uppercase is almost as easy to guess as all-lowercase." 108 | ), 109 | Suggestion::ReversedWordsArentMuchHarderToGuess => { 110 | write!(f, "Reversed words aren't much harder to guess.") 111 | } 112 | Suggestion::PredictableSubstitutionsDontHelpVeryMuch => write!( 113 | f, 114 | "Predictable substitutions like '@' instead of 'a' don't help very much." 115 | ), 116 | Suggestion::UseALongerKeyboardPatternWithMoreTurns => { 117 | write!(f, "Use a longer keyboard pattern with more turns.") 118 | } 119 | Suggestion::AvoidRepeatedWordsAndCharacters => { 120 | write!(f, "Avoid repeated words and characters.") 121 | } 122 | Suggestion::AvoidSequences => write!(f, "Avoid sequences."), 123 | Suggestion::AvoidRecentYears => write!(f, "Avoid recent years."), 124 | Suggestion::AvoidYearsThatAreAssociatedWithYou => { 125 | write!(f, "Avoid years that are associated with you.") 126 | } 127 | Suggestion::AvoidDatesAndYearsThatAreAssociatedWithYou => { 128 | write!(f, "Avoid dates and years that are associated with you.") 129 | } 130 | } 131 | } 132 | } 133 | 134 | /// Verbal feedback to help choose better passwords 135 | #[derive(Debug, PartialEq, Clone, Default)] 136 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 137 | pub struct Feedback { 138 | /// Explains what's wrong, e.g. "This is a top-10 common password". Not always set. 139 | warning: Option, 140 | /// A possibly-empty list of suggestions to help choose a less guessable password. 141 | /// E.g. "Add another word or two". 142 | suggestions: Vec, 143 | } 144 | 145 | impl Feedback { 146 | /// Explains what's wrong, e.g. "This is a top-10 common password". Not always set. 147 | pub fn warning(&self) -> Option { 148 | self.warning 149 | } 150 | 151 | /// A possibly-empty list of suggestions to help choose a less guessable password. 152 | /// E.g. "Add another word or two". 153 | pub fn suggestions(&self) -> &[Suggestion] { 154 | &self.suggestions 155 | } 156 | } 157 | 158 | impl fmt::Display for Feedback { 159 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 160 | if let Some(warning) = self.warning { 161 | write!(f, "{} ", warning)?; 162 | } 163 | write!(f, "{}", self.suggestions.iter().join(" "))?; 164 | 165 | Ok(()) 166 | } 167 | } 168 | 169 | pub(crate) fn get_feedback(score: Score, sequence: &[Match]) -> Option { 170 | if sequence.is_empty() { 171 | // default feedback 172 | return Some(Feedback { 173 | warning: None, 174 | suggestions: vec![ 175 | Suggestion::UseAFewWordsAvoidCommonPhrases, 176 | Suggestion::NoNeedForSymbolsDigitsOrUppercaseLetters, 177 | ], 178 | }); 179 | } 180 | if score >= Score::Three { 181 | return None; 182 | } 183 | 184 | let longest_match = sequence 185 | .iter() 186 | .max_by_key(|x| x.token.chars().count()) 187 | .unwrap(); 188 | let mut feedback = get_match_feedback(longest_match, sequence.len() == 1); 189 | let extra_feedback = Suggestion::AddAnotherWordOrTwo; 190 | 191 | feedback.suggestions.insert(0, extra_feedback); 192 | Some(feedback) 193 | } 194 | 195 | fn get_match_feedback(cur_match: &Match, is_sole_match: bool) -> Feedback { 196 | match cur_match.pattern { 197 | MatchPattern::Dictionary(ref pattern) => { 198 | get_dictionary_match_feedback(cur_match, pattern, is_sole_match) 199 | } 200 | MatchPattern::Spatial(ref pattern) => Feedback { 201 | warning: Some(if pattern.turns == 1 { 202 | Warning::StraightRowsOfKeysAreEasyToGuess 203 | } else { 204 | Warning::ShortKeyboardPatternsAreEasyToGuess 205 | }), 206 | suggestions: vec![Suggestion::UseALongerKeyboardPatternWithMoreTurns], 207 | }, 208 | MatchPattern::Repeat(ref pattern) => Feedback { 209 | warning: Some(if pattern.base_token.chars().count() == 1 { 210 | Warning::RepeatsLikeAaaAreEasyToGuess 211 | } else { 212 | Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess 213 | }), 214 | suggestions: vec![Suggestion::AvoidRepeatedWordsAndCharacters], 215 | }, 216 | MatchPattern::Sequence(_) => Feedback { 217 | warning: Some(Warning::SequencesLikeAbcAreEasyToGuess), 218 | suggestions: vec![Suggestion::AvoidSequences], 219 | }, 220 | MatchPattern::Regex(ref pattern) => { 221 | if pattern.regex_name == "recent_year" { 222 | Feedback { 223 | warning: Some(Warning::RecentYearsAreEasyToGuess), 224 | suggestions: vec![ 225 | Suggestion::AvoidRecentYears, 226 | Suggestion::AvoidYearsThatAreAssociatedWithYou, 227 | ], 228 | } 229 | } else { 230 | Feedback::default() 231 | } 232 | } 233 | MatchPattern::Date(_) => Feedback { 234 | warning: Some(Warning::DatesAreOftenEasyToGuess), 235 | suggestions: vec![Suggestion::AvoidDatesAndYearsThatAreAssociatedWithYou], 236 | }, 237 | _ => Feedback { 238 | warning: None, 239 | suggestions: vec![], 240 | }, 241 | } 242 | } 243 | 244 | fn get_dictionary_match_feedback( 245 | cur_match: &Match, 246 | pattern: &DictionaryPattern, 247 | is_sole_match: bool, 248 | ) -> Feedback { 249 | let warning: Option = match pattern.dictionary_name { 250 | DictionaryType::Passwords => Some(if is_sole_match && !pattern.l33t && !pattern.reversed { 251 | let rank = pattern.rank; 252 | if rank <= 10 { 253 | Warning::ThisIsATop10Password 254 | } else if rank <= 100 { 255 | Warning::ThisIsATop100Password 256 | } else { 257 | Warning::ThisIsACommonPassword 258 | } 259 | } else { 260 | Warning::ThisIsSimilarToACommonlyUsedPassword 261 | }), 262 | DictionaryType::English => { 263 | if is_sole_match { 264 | Some(Warning::AWordByItselfIsEasyToGuess) 265 | } else { 266 | None 267 | } 268 | } 269 | DictionaryType::Surnames | DictionaryType::FemaleNames | DictionaryType::MaleNames => { 270 | Some(if is_sole_match { 271 | Warning::NamesAndSurnamesByThemselvesAreEasyToGuess 272 | } else { 273 | Warning::CommonNamesAndSurnamesAreEasyToGuess 274 | }) 275 | } 276 | _ => None, 277 | }; 278 | 279 | let mut suggestions: Vec = Vec::new(); 280 | let word = &cur_match.token; 281 | if word.is_empty() { 282 | return Feedback::default(); 283 | } 284 | 285 | if word.chars().next().unwrap().is_uppercase() { 286 | suggestions.push(Suggestion::CapitalizationDoesntHelpVeryMuch); 287 | } else if word.chars().all(char::is_uppercase) { 288 | suggestions.push(Suggestion::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase); 289 | } 290 | 291 | if pattern.reversed && word.chars().count() >= 4 { 292 | suggestions.push(Suggestion::ReversedWordsArentMuchHarderToGuess); 293 | } 294 | if pattern.l33t { 295 | suggestions.push(Suggestion::PredictableSubstitutionsDontHelpVeryMuch); 296 | } 297 | 298 | Feedback { 299 | warning, 300 | suggestions, 301 | } 302 | } 303 | 304 | #[cfg(test)] 305 | mod tests { 306 | use super::*; 307 | 308 | #[cfg(target_arch = "wasm32")] 309 | use wasm_bindgen_test::wasm_bindgen_test; 310 | 311 | #[cfg_attr(not(target_arch = "wasm32"), test)] 312 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 313 | fn test_top_password_feedback() { 314 | use crate::zxcvbn; 315 | 316 | let password = "password"; 317 | let entropy = zxcvbn(password, &[]); 318 | assert_eq!( 319 | entropy.feedback.unwrap().warning, 320 | Some(Warning::ThisIsATop10Password) 321 | ); 322 | 323 | let password = "test"; 324 | let entropy = zxcvbn(password, &[]); 325 | assert_eq!( 326 | entropy.feedback.unwrap().warning, 327 | Some(Warning::ThisIsATop100Password) 328 | ); 329 | 330 | let password = "p4ssw0rd"; 331 | let entropy = zxcvbn(password, &[]); 332 | assert_eq!( 333 | entropy.feedback.unwrap().warning, 334 | Some(Warning::ThisIsSimilarToACommonlyUsedPassword) 335 | ); 336 | } 337 | 338 | #[test] 339 | fn test_feedback_display() { 340 | let feedback = Feedback { 341 | warning: Some(Warning::ThisIsATop10Password), 342 | suggestions: vec![Suggestion::UseAFewWordsAvoidCommonPhrases], 343 | }; 344 | assert_eq!( 345 | format!("{}", feedback), 346 | "This is a top-10 common password. Use a few words, avoid common phrases." 347 | ); 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/scoring.rs: -------------------------------------------------------------------------------- 1 | use crate::matching::patterns::*; 2 | use crate::matching::Match; 3 | use std::collections::HashMap; 4 | use std::{cmp, fmt::Display}; 5 | 6 | /// Score generated when measuring the entropy of a password. 7 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] 8 | #[non_exhaustive] 9 | #[cfg_attr(feature = "ser", derive(serde::Serialize, serde::Deserialize))] 10 | #[cfg_attr(feature = "ser", serde(try_from = "u8", into = "u8"))] 11 | pub enum Score { 12 | /// Can be cracked with 10^3 guesses or less. 13 | Zero = 0, 14 | /// Can be cracked with 10^6 guesses or less. 15 | One, 16 | /// Can be cracked with 10^8 guesses or less. 17 | Two, 18 | /// Can be cracked with 10^10 guesses or less. 19 | Three, 20 | /// Requires more than 10^10 guesses to crack. 21 | Four, 22 | } 23 | 24 | impl From for u8 { 25 | fn from(score: Score) -> u8 { 26 | score as u8 27 | } 28 | } 29 | 30 | impl TryFrom for Score { 31 | type Error = &'static str; 32 | 33 | fn try_from(value: u8) -> Result { 34 | Ok(match value { 35 | 0 => Self::Zero, 36 | 1 => Self::One, 37 | 2 => Self::Two, 38 | 3 => Self::Three, 39 | 4 => Self::Four, 40 | _ => return Err("zxcvbn entropy score must be in the range 0-4"), 41 | }) 42 | } 43 | } 44 | 45 | impl Display for Score { 46 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 47 | write!(f, "{}", u8::from(*self)) 48 | } 49 | } 50 | 51 | #[derive(Debug, Clone)] 52 | pub struct GuessCalculation { 53 | /// Estimated guesses needed to crack the password 54 | pub guesses: u64, 55 | /// Order of magnitude of `guesses` 56 | pub guesses_log10: f64, 57 | /// The list of patterns the guess calculation was based on 58 | pub sequence: Vec, 59 | } 60 | 61 | #[derive(Debug, Clone)] 62 | struct Optimal { 63 | /// optimal.m[k][l] holds final match in the best length-l match sequence covering the 64 | /// password prefix up to k, inclusive. 65 | /// if there is no length-l sequence that scores better (fewer guesses) than 66 | /// a shorter match sequence spanning the same prefix, optimal.m[k][l] is undefined. 67 | m: Vec>, 68 | /// same structure as optimal.m -- holds the product term Prod(m.guesses for m in sequence). 69 | /// optimal.pi allows for fast (non-looping) updates to the minimization function. 70 | pi: Vec>, 71 | /// same structure as optimal.m -- holds the overall metric. 72 | g: Vec>, 73 | } 74 | 75 | #[cfg(not(target_arch = "wasm32"))] 76 | lazy_static! { 77 | pub(crate) static ref REFERENCE_YEAR: i32 = time::OffsetDateTime::now_utc().year(); 78 | } 79 | 80 | #[cfg(all(target_arch = "wasm32", not(feature = "custom_wasm_env")))] 81 | lazy_static! { 82 | pub(crate) static ref REFERENCE_YEAR: i32 = web_sys::js_sys::Date::new_0() 83 | .get_full_year() 84 | .try_into() 85 | .unwrap(); 86 | } 87 | 88 | #[cfg(all(target_arch = "wasm32", feature = "custom_wasm_env"))] 89 | lazy_static! { 90 | pub(crate) static ref REFERENCE_YEAR: i32 = { 91 | #[link(wasm_import_module = "zxcvbn")] 92 | extern "C" { 93 | fn unix_time_milliseconds_imported() -> u64; 94 | } 95 | let unix_millis = unsafe { unix_time_milliseconds_imported() }; 96 | 97 | use chrono::Datelike; 98 | chrono::DateTime::::from( 99 | std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_millis(unix_millis), 100 | ) 101 | .year() 102 | }; 103 | } 104 | 105 | const MIN_YEAR_SPACE: i32 = 20; 106 | const BRUTEFORCE_CARDINALITY: u64 = 10; 107 | const MIN_GUESSES_BEFORE_GROWING_SEQUENCE: u64 = 10_000; 108 | const MIN_SUBMATCH_GUESSES_SINGLE_CHAR: u64 = 10; 109 | const MIN_SUBMATCH_GUESSES_MULTI_CHAR: u64 = 50; 110 | 111 | pub fn most_guessable_match_sequence( 112 | password: &str, 113 | matches: &[crate::matching::Match], 114 | exclude_additive: bool, 115 | ) -> GuessCalculation { 116 | let n = password.chars().count(); 117 | 118 | // partition matches into sublists according to ending index j 119 | let mut matches_by_j: Vec> = vec![Vec::new(); n]; 120 | for m in matches { 121 | matches_by_j[m.j].push(m.clone()); 122 | } 123 | // small detail: for deterministic output, sort each sublist by i. 124 | for lst in &mut matches_by_j { 125 | lst.sort_by_key(|m| m.i); 126 | } 127 | 128 | let mut optimal = Optimal { 129 | m: vec![HashMap::new(); n], 130 | pi: vec![HashMap::new(); n], 131 | g: vec![HashMap::new(); n], 132 | }; 133 | 134 | /// helper: considers whether a length-l sequence ending at match m is better (fewer guesses) 135 | /// than previously encountered sequences, updating state if so. 136 | fn update( 137 | mut m: Match, 138 | len: usize, 139 | password: &str, 140 | optimal: &mut Optimal, 141 | exclude_additive: bool, 142 | ) { 143 | let k = m.j; 144 | let mut pi = estimate_guesses(&mut m, password); 145 | if len > 1 { 146 | // we're considering a length-l sequence ending with match m: 147 | // obtain the product term in the minimization function by multiplying m's guesses 148 | // by the product of the length-(l-1) sequence ending just before m, at m.i - 1. 149 | pi = pi.saturating_mul(optimal.pi[m.i - 1][&(len - 1)]); 150 | } 151 | // calculate the minimization func 152 | let mut guesses = (factorial(len) as u64).saturating_mul(pi); 153 | if !exclude_additive { 154 | let additive = if len == 1 { 155 | 1 156 | } else { 157 | (2..len).fold(MIN_GUESSES_BEFORE_GROWING_SEQUENCE, |acc, _| { 158 | acc.saturating_mul(MIN_GUESSES_BEFORE_GROWING_SEQUENCE) 159 | }) 160 | }; 161 | guesses = guesses.saturating_add(additive); 162 | } 163 | // update state if new best. 164 | // first see if any competing sequences covering this prefix, with l or fewer matches, 165 | // fare better than this sequence. if so, skip it and return. 166 | for (&competing_l, &competing_guesses) in &optimal.g[k] { 167 | if competing_l > len { 168 | continue; 169 | } 170 | if competing_guesses <= guesses { 171 | return; 172 | } 173 | } 174 | // this sequence might be part of the final optimal sequence. 175 | optimal.g[k].insert(len, guesses); 176 | optimal.m[k].insert(len, m); 177 | optimal.pi[k].insert(len, pi); 178 | } 179 | 180 | /// helper: evaluate bruteforce matches ending at k. 181 | fn bruteforce_update(k: usize, password: &str, optimal: &mut Optimal, exclude_additive: bool) { 182 | // see if a single bruteforce match spanning the k-prefix is optimal. 183 | let m = make_bruteforce_match(0, k, password); 184 | update(m, 1, password, optimal, exclude_additive); 185 | for i in 1..=k { 186 | // generate k bruteforce matches, spanning from (i=1, j=k) up to (i=k, j=k). 187 | // see if adding these new matches to any of the sequences in optimal[i-1] 188 | // leads to new bests. 189 | let m = make_bruteforce_match(i, k, password); 190 | for (l, last_m) in optimal.m[i - 1].clone() { 191 | // corner: an optimal sequence will never have two adjacent bruteforce matches. 192 | // it is strictly better to have a single bruteforce match spanning the same region: 193 | // same contribution to the guess product with a lower length. 194 | // --> safe to skip those cases. 195 | if last_m.pattern == MatchPattern::BruteForce { 196 | continue; 197 | } 198 | // try adding m to this length-l sequence. 199 | update(m.clone(), l + 1, password, optimal, exclude_additive); 200 | } 201 | } 202 | } 203 | 204 | /// helper: make bruteforce match objects spanning i to j, inclusive. 205 | fn make_bruteforce_match(i: usize, j: usize, password: &str) -> Match { 206 | Match { 207 | pattern: MatchPattern::BruteForce, 208 | token: password.chars().take(j + 1).skip(i).collect(), 209 | i, 210 | j, 211 | ..Match::default() 212 | } 213 | } 214 | 215 | /// helper: step backwards through optimal.m starting at the end, 216 | /// constructing the final optimal match sequence. 217 | #[allow(clippy::many_single_char_names)] 218 | fn unwind(n: usize, optimal: &mut Optimal) -> Vec { 219 | let mut optimal_match_sequence = Vec::new(); 220 | let mut k = n - 1; 221 | // find the final best sequence length and score 222 | let mut l = None; 223 | let mut g = None; 224 | for (candidate_l, candidate_g) in &optimal.g[k] { 225 | if g.is_none() || *candidate_g < *g.as_ref().unwrap() { 226 | l = Some(*candidate_l); 227 | g = Some(*candidate_g); 228 | } 229 | } 230 | 231 | loop { 232 | let m = &optimal.m[k][&l.unwrap()]; 233 | optimal_match_sequence.insert(0, m.clone()); 234 | if m.i == 0 { 235 | return optimal_match_sequence; 236 | } 237 | k = m.i - 1; 238 | l = l.map(|x| x - 1); 239 | } 240 | } 241 | 242 | for (k, match_by_j) in matches_by_j.iter().enumerate() { 243 | for m in match_by_j { 244 | if m.i > 0 { 245 | let keys: Vec = optimal.m[m.i - 1].keys().cloned().collect(); 246 | for l in keys { 247 | update(m.clone(), l + 1, password, &mut optimal, exclude_additive); 248 | } 249 | } else { 250 | update(m.clone(), 1, password, &mut optimal, exclude_additive); 251 | } 252 | } 253 | bruteforce_update(k, password, &mut optimal, exclude_additive); 254 | } 255 | let optimal_match_sequence = unwind(n, &mut optimal); 256 | let optimal_l = optimal_match_sequence.len(); 257 | 258 | // corner: empty password 259 | let guesses = if password.is_empty() { 260 | 1 261 | } else { 262 | optimal.g[n - 1][&optimal_l] 263 | }; 264 | 265 | GuessCalculation { 266 | guesses, 267 | guesses_log10: (guesses as f64).log10(), 268 | sequence: optimal_match_sequence, 269 | } 270 | } 271 | 272 | fn factorial(n: usize) -> usize { 273 | (1..=n).product() 274 | } 275 | 276 | fn estimate_guesses(m: &mut Match, password: &str) -> u64 { 277 | if let Some(guesses) = m.guesses { 278 | // a match's guess estimate doesn't change. cache it. 279 | return guesses; 280 | } 281 | let min_guesses = if m.token.chars().count() < password.chars().count() { 282 | if m.token.chars().count() == 1 { 283 | MIN_SUBMATCH_GUESSES_SINGLE_CHAR 284 | } else { 285 | MIN_SUBMATCH_GUESSES_MULTI_CHAR 286 | } 287 | } else { 288 | 1 289 | }; 290 | let guesses = m.pattern.estimate(&m.token); 291 | m.guesses = Some(cmp::max(guesses, min_guesses)); 292 | m.guesses.unwrap() 293 | } 294 | 295 | trait Estimator { 296 | fn estimate(&mut self, token: &str) -> u64; 297 | } 298 | 299 | impl Estimator for MatchPattern { 300 | fn estimate(&mut self, token: &str) -> u64 { 301 | match *self { 302 | MatchPattern::Dictionary(ref mut p) => p.estimate(token), 303 | MatchPattern::Spatial(ref mut p) => p.estimate(token), 304 | MatchPattern::Repeat(ref mut p) => p.estimate(token), 305 | MatchPattern::Sequence(ref mut p) => p.estimate(token), 306 | MatchPattern::Regex(ref mut p) => p.estimate(token), 307 | MatchPattern::Date(ref mut p) => p.estimate(token), 308 | MatchPattern::BruteForce => { 309 | let mut guesses = BRUTEFORCE_CARDINALITY; 310 | let token_len = token.chars().count(); 311 | if token_len >= 2 { 312 | for _ in 2..=token_len { 313 | guesses = guesses.saturating_mul(BRUTEFORCE_CARDINALITY); 314 | } 315 | } 316 | // small detail: make bruteforce matches at minimum one guess bigger than smallest allowed 317 | // submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence. 318 | let min_guesses = if token_len == 1 { 319 | MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1 320 | } else { 321 | MIN_SUBMATCH_GUESSES_MULTI_CHAR + 1 322 | }; 323 | cmp::max(guesses, min_guesses) 324 | } 325 | } 326 | } 327 | } 328 | 329 | impl Estimator for DictionaryPattern { 330 | fn estimate(&mut self, token: &str) -> u64 { 331 | let uppercase_variations = uppercase_variations(token); 332 | let l33t_variations = l33t_variations(self, token); 333 | self.base_guesses = self.rank as u64; 334 | self.uppercase_variations = uppercase_variations; 335 | self.l33t_variations = l33t_variations; 336 | self.base_guesses 337 | * self.uppercase_variations 338 | * self.l33t_variations 339 | * if self.reversed { 2 } else { 1 } 340 | } 341 | } 342 | 343 | fn uppercase_variations(token: &str) -> u64 { 344 | if token.chars().all(char::is_lowercase) || token.to_lowercase().as_str() == token { 345 | return 1; 346 | } 347 | // a capitalized token is the most common capitalization scheme, 348 | // so it only doubles the search space (uncapitalized + capitalized). 349 | // allcaps and end-capitalized are common enough too, underestimate as 2x factor to be safe. 350 | if ((token.chars().next().unwrap().is_uppercase() 351 | || token.chars().last().unwrap().is_uppercase()) 352 | && token.chars().filter(|&c| c.is_uppercase()).count() == 1) 353 | || token.chars().all(char::is_uppercase) 354 | { 355 | return 2; 356 | } 357 | // otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters 358 | // with U uppercase letters or less. or, if there's more uppercase than lower (for eg. PASSwORD), 359 | // the number of ways to lowercase U+L letters with L lowercase letters or less. 360 | let upper = token.chars().filter(|c| c.is_uppercase()).count(); 361 | let lower = token.chars().filter(|c| c.is_lowercase()).count(); 362 | (1..=cmp::min(upper, lower)) 363 | .map(|i| n_ck(upper + lower, i)) 364 | .sum() 365 | } 366 | 367 | fn l33t_variations(pattern: &DictionaryPattern, token: &str) -> u64 { 368 | if !pattern.l33t { 369 | return 1; 370 | } 371 | let mut variations = 1; 372 | for (subbed, unsubbed) in pattern.sub.as_ref().unwrap() { 373 | // lower-case match.token before calculating: capitalization shouldn't affect l33t calc. 374 | let token = token.to_lowercase(); 375 | let subbed = token.chars().filter(|c| c == subbed).count(); 376 | let unsubbed = token.chars().filter(|c| c == unsubbed).count(); 377 | if subbed == 0 || unsubbed == 0 { 378 | // for this sub, password is either fully subbed (444) or fully unsubbed (aaa) 379 | // treat that as doubling the space (attacker needs to try fully subbed chars in addition to 380 | // unsubbed.) 381 | variations *= 2; 382 | } else { 383 | // this case is similar to capitalization: 384 | // with aa44a, U = 3, S = 2, attacker needs to try unsubbed + one sub + two subs 385 | let p = cmp::min(unsubbed, subbed); 386 | let possibilities: u64 = (1..=p).map(|i| n_ck(unsubbed + subbed, i)).sum(); 387 | variations *= possibilities; 388 | } 389 | } 390 | variations 391 | } 392 | 393 | fn n_ck(n: usize, k: usize) -> u64 { 394 | // http://blog.plover.com/math/choose.html 395 | (if k > n { 396 | 0 397 | } else if k == 0 { 398 | 1 399 | } else { 400 | let mut r: usize = 1; 401 | let mut n = n; 402 | for d in 1..=k { 403 | r = r.saturating_mul(n); 404 | r /= d; 405 | n -= 1; 406 | } 407 | r 408 | }) as u64 409 | } 410 | 411 | impl Estimator for SpatialPattern { 412 | fn estimate(&mut self, token: &str) -> u64 { 413 | let (starts, degree) = if ["qwerty", "dvorak"].contains(&self.graph.as_str()) { 414 | (*KEYBOARD_STARTING_POSITIONS, *KEYBOARD_AVERAGE_DEGREE) 415 | } else { 416 | (*KEYPAD_STARTING_POSITIONS, *KEYPAD_AVERAGE_DEGREE) 417 | }; 418 | let mut guesses = 0u64; 419 | let len = token.chars().count(); 420 | // estimate the number of possible patterns w/ length L or less with t turns or less. 421 | for i in 2..=len { 422 | let possible_turns = cmp::min(self.turns, i - 1); 423 | for j in 1..=possible_turns { 424 | guesses = guesses.saturating_add( 425 | n_ck(i - 1, j - 1) 426 | .saturating_mul(starts) 427 | .saturating_mul(degree.pow(j as u32)), 428 | ); 429 | } 430 | } 431 | // add extra guesses for shifted keys. (% instead of 5, A instead of a.) 432 | // math is similar to extra guesses of l33t substitutions in dictionary matches. 433 | let shifted_count = self.shifted_count; 434 | if shifted_count > 0 { 435 | let unshifted_count = len - shifted_count; 436 | if unshifted_count == 0 { 437 | guesses = guesses.saturating_mul(2); 438 | } else { 439 | let shifted_variations: u64 = (1..=cmp::min(shifted_count, unshifted_count)) 440 | .map(|i| n_ck(shifted_count + unshifted_count, i)) 441 | .sum(); 442 | guesses = guesses.saturating_mul(shifted_variations); 443 | } 444 | } 445 | guesses 446 | } 447 | } 448 | 449 | lazy_static! { 450 | static ref KEYBOARD_AVERAGE_DEGREE: u64 = calc_average_degree(&crate::adjacency_graphs::QWERTY); 451 | // slightly different for keypad/mac keypad, but close enough 452 | static ref KEYPAD_AVERAGE_DEGREE: u64 = calc_average_degree(&crate::adjacency_graphs::KEYPAD); 453 | static ref KEYBOARD_STARTING_POSITIONS: u64 = crate::adjacency_graphs::QWERTY.len() as u64; 454 | static ref KEYPAD_STARTING_POSITIONS: u64 = crate::adjacency_graphs::KEYPAD.len() as u64; 455 | } 456 | 457 | fn calc_average_degree(graph: &HashMap>>) -> u64 { 458 | let sum: u64 = graph 459 | .values() 460 | .map(|neighbors| neighbors.iter().filter(|n| n.is_some()).count() as u64) 461 | .sum(); 462 | sum / graph.len() as u64 463 | } 464 | 465 | impl Estimator for RepeatPattern { 466 | fn estimate(&mut self, _: &str) -> u64 { 467 | self.base_guesses.saturating_mul(self.repeat_count as u64) 468 | } 469 | } 470 | 471 | impl Estimator for SequencePattern { 472 | fn estimate(&mut self, token: &str) -> u64 { 473 | let first_chr = token.chars().next().unwrap(); 474 | // lower guesses for obvious starting points 475 | let mut base_guesses = if ['a', 'A', 'z', 'Z', '0', '1', '9'].contains(&first_chr) { 476 | 4 477 | } else if first_chr.is_ascii_digit() { 478 | 10 479 | } else { 480 | // could give a higher base for uppercase, 481 | // assigning 26 to both upper and lower sequences is more conservative. 482 | 26 483 | }; 484 | if !self.ascending { 485 | // need to try a descending sequence in addition to every ascending sequence -> 486 | // 2x guesses 487 | base_guesses *= 2; 488 | } 489 | base_guesses * token.chars().count() as u64 490 | } 491 | } 492 | 493 | impl Estimator for RegexPattern { 494 | fn estimate(&mut self, token: &str) -> u64 { 495 | if CHAR_CLASS_BASES.keys().any(|x| *x == self.regex_name) { 496 | CHAR_CLASS_BASES[&*self.regex_name].pow(token.chars().count() as u32) 497 | } else { 498 | match &*self.regex_name { 499 | "recent_year" => { 500 | let year_space = 501 | (self.regex_match[0].parse::().unwrap() - *REFERENCE_YEAR).abs(); 502 | cmp::max(year_space, MIN_YEAR_SPACE) as u64 503 | } 504 | _ => unreachable!(), 505 | } 506 | } 507 | } 508 | } 509 | 510 | lazy_static! { 511 | static ref CHAR_CLASS_BASES: HashMap<&'static str, u64> = { 512 | let mut table = HashMap::with_capacity(6); 513 | table.insert("alpha_lower", 26); 514 | table.insert("alpha_upper", 26); 515 | table.insert("alpha", 52); 516 | table.insert("alphanumeric", 62); 517 | table.insert("digits", 10); 518 | table.insert("symbols", 33); 519 | table 520 | }; 521 | } 522 | 523 | impl Estimator for DatePattern { 524 | fn estimate(&mut self, _: &str) -> u64 { 525 | // base guesses: (year distance from REFERENCE_YEAR) * num_days * num_years 526 | let year_space = cmp::max((self.year - *REFERENCE_YEAR).abs(), MIN_YEAR_SPACE); 527 | let mut guesses = year_space as u64 * 365; 528 | // add factor of 4 for separator selection (one of ~4 choices) 529 | if !self.separator.is_empty() { 530 | guesses *= 4; 531 | } 532 | guesses 533 | } 534 | } 535 | 536 | #[cfg(test)] 537 | mod tests { 538 | use crate::matching::patterns::*; 539 | use crate::matching::Match; 540 | use crate::scoring; 541 | use crate::scoring::Estimator; 542 | use quickcheck::TestResult; 543 | use std::collections::HashMap; 544 | 545 | #[test] 546 | fn test_n_ck() { 547 | let test_data = [ 548 | (0, 0, 1), 549 | (1, 0, 1), 550 | (5, 0, 1), 551 | (0, 1, 0), 552 | (0, 5, 0), 553 | (2, 1, 2), 554 | (4, 2, 6), 555 | (33, 7, 4_272_048), 556 | ]; 557 | for &(n, k, result) in &test_data { 558 | assert_eq!(scoring::n_ck(n, k), result); 559 | } 560 | } 561 | 562 | quickcheck! { 563 | fn test_n_ck_mul_overflow(n: usize, k: usize) -> TestResult { 564 | if n >= 63 && n <= 100 { 565 | scoring::n_ck(n, k); // Must not panic 566 | TestResult::from_bool(true) 567 | } else { 568 | TestResult::discard() 569 | } 570 | } 571 | 572 | fn test_n_ck_mirror_identity(n: usize, k: usize) -> TestResult { 573 | if k > n || n >= 63 { 574 | return TestResult::discard(); 575 | } 576 | TestResult::from_bool(scoring::n_ck(n, k) == scoring::n_ck(n, n-k)) 577 | } 578 | 579 | fn test_n_ck_pascals_triangle(n: usize, k: usize) -> TestResult { 580 | if n == 0 || k == 0 || n >= 63 { 581 | return TestResult::discard(); 582 | } 583 | TestResult::from_bool(scoring::n_ck(n, k) == scoring::n_ck(n-1, k-1) + scoring::n_ck(n-1, k)) 584 | } 585 | } 586 | 587 | #[test] 588 | fn test_search_returns_one_bruteforce_match_given_empty_match_sequence() { 589 | let password = "0123456789"; 590 | let result = scoring::most_guessable_match_sequence(password, &[], true); 591 | assert_eq!(result.sequence.len(), 1); 592 | let m0 = &result.sequence[0]; 593 | assert_eq!(m0.pattern.variant(), "bruteforce"); 594 | assert_eq!(m0.token, password); 595 | assert_eq!(m0.i, 0); 596 | assert_eq!(m0.j, 9); 597 | } 598 | 599 | #[test] 600 | fn test_search_returns_match_and_bruteforce_when_match_covers_prefix_of_password() { 601 | let password = "0123456789"; 602 | let m = Match { 603 | i: 0, 604 | j: 5, 605 | guesses: Some(1), 606 | pattern: MatchPattern::Dictionary(DictionaryPattern::default()), 607 | ..Match::default() 608 | }; 609 | 610 | let result = scoring::most_guessable_match_sequence(password, &[m.clone()], true); 611 | assert_eq!(result.sequence.len(), 2); 612 | assert_eq!(result.sequence[0], m); 613 | let m1 = &result.sequence[1]; 614 | assert_eq!(m1.pattern.variant(), "bruteforce"); 615 | assert_eq!(m1.i, 6); 616 | assert_eq!(m1.j, 9); 617 | } 618 | 619 | #[test] 620 | fn test_search_returns_bruteforce_and_match_when_match_covers_a_suffix() { 621 | let password = "0123456789"; 622 | let m = Match { 623 | i: 3, 624 | j: 9, 625 | guesses: Some(1), 626 | pattern: MatchPattern::Dictionary(DictionaryPattern::default()), 627 | ..Match::default() 628 | }; 629 | 630 | let result = scoring::most_guessable_match_sequence(password, &[m.clone()], true); 631 | assert_eq!(result.sequence.len(), 2); 632 | let m0 = &result.sequence[0]; 633 | assert_eq!(m0.pattern.variant(), "bruteforce"); 634 | assert_eq!(m0.i, 0); 635 | assert_eq!(m0.j, 2); 636 | assert_eq!(result.sequence[1], m); 637 | } 638 | 639 | #[test] 640 | fn test_search_returns_bruteforce_and_match_when_match_covers_an_infix() { 641 | let password = "0123456789"; 642 | let m = Match { 643 | i: 1, 644 | j: 8, 645 | guesses: Some(1), 646 | pattern: MatchPattern::Dictionary(DictionaryPattern::default()), 647 | ..Match::default() 648 | }; 649 | 650 | let result = scoring::most_guessable_match_sequence(password, &[m.clone()], true); 651 | assert_eq!(result.sequence.len(), 3); 652 | assert_eq!(result.sequence[1], m); 653 | let m0 = &result.sequence[0]; 654 | let m2 = &result.sequence[2]; 655 | assert_eq!(m0.pattern.variant(), "bruteforce"); 656 | assert_eq!(m0.i, 0); 657 | assert_eq!(m0.j, 0); 658 | assert_eq!(m2.pattern.variant(), "bruteforce"); 659 | assert_eq!(m2.i, 9); 660 | assert_eq!(m2.j, 9); 661 | } 662 | 663 | #[test] 664 | fn test_search_chooses_lower_guesses_match_given_two_matches_of_same_span() { 665 | let password = "0123456789"; 666 | let mut m0 = Match { 667 | i: 0, 668 | j: 9, 669 | guesses: Some(1), 670 | pattern: MatchPattern::Dictionary(DictionaryPattern::default()), 671 | ..Match::default() 672 | }; 673 | let m1 = Match { 674 | i: 0, 675 | j: 9, 676 | guesses: Some(2), 677 | pattern: MatchPattern::Dictionary(DictionaryPattern::default()), 678 | ..Match::default() 679 | }; 680 | 681 | let result = 682 | scoring::most_guessable_match_sequence(password, &[m0.clone(), m1.clone()], true); 683 | assert_eq!(result.sequence.len(), 1); 684 | assert_eq!(result.sequence[0], m0); 685 | // make sure ordering doesn't matter 686 | m0.guesses = Some(3); 687 | let result = scoring::most_guessable_match_sequence(password, &[m0, m1.clone()], true); 688 | assert_eq!(result.sequence.len(), 1); 689 | assert_eq!(result.sequence[0], m1); 690 | } 691 | 692 | #[test] 693 | fn test_search_when_m0_covers_m1_and_m2_choose_m0_when_m0_lt_m1_t_m2_t_fact_2() { 694 | let password = "0123456789"; 695 | let m0 = Match { 696 | i: 0, 697 | j: 9, 698 | guesses: Some(3), 699 | pattern: MatchPattern::Dictionary(DictionaryPattern::default()), 700 | ..Match::default() 701 | }; 702 | let m1 = Match { 703 | i: 0, 704 | j: 3, 705 | guesses: Some(2), 706 | pattern: MatchPattern::Dictionary(DictionaryPattern::default()), 707 | ..Match::default() 708 | }; 709 | let m2 = Match { 710 | i: 4, 711 | j: 9, 712 | guesses: Some(1), 713 | pattern: MatchPattern::Dictionary(DictionaryPattern::default()), 714 | ..Match::default() 715 | }; 716 | 717 | let result = scoring::most_guessable_match_sequence(password, &[m0.clone(), m1, m2], true); 718 | assert_eq!(result.guesses, 3); 719 | assert_eq!(result.sequence, vec![m0]); 720 | } 721 | 722 | #[test] 723 | fn test_search_when_m0_covers_m1_and_m2_choose_m1_m2_when_m0_gt_m1_t_m2_t_fact_2() { 724 | let password = "0123456789"; 725 | let m0 = Match { 726 | i: 0, 727 | j: 9, 728 | guesses: Some(5), 729 | pattern: MatchPattern::Dictionary(DictionaryPattern::default()), 730 | ..Match::default() 731 | }; 732 | let m1 = Match { 733 | i: 0, 734 | j: 3, 735 | guesses: Some(2), 736 | pattern: MatchPattern::Dictionary(DictionaryPattern::default()), 737 | ..Match::default() 738 | }; 739 | let m2 = Match { 740 | i: 4, 741 | j: 9, 742 | guesses: Some(1), 743 | pattern: MatchPattern::Dictionary(DictionaryPattern::default()), 744 | ..Match::default() 745 | }; 746 | 747 | let result = 748 | scoring::most_guessable_match_sequence(password, &[m0, m1.clone(), m2.clone()], true); 749 | assert_eq!(result.guesses, 4); 750 | assert_eq!(result.sequence, vec![m1, m2]); 751 | } 752 | 753 | #[test] 754 | fn test_calc_guesses_returns_guesses_when_cached() { 755 | let mut m = Match { 756 | guesses: Some(1), 757 | ..Match::default() 758 | }; 759 | assert_eq!(scoring::estimate_guesses(&mut m, ""), 1); 760 | } 761 | 762 | #[test] 763 | fn test_calc_guesses_delegates_based_on_pattern() { 764 | let mut p = DatePattern { 765 | year: 1977, 766 | month: 7, 767 | day: 14, 768 | ..DatePattern::default() 769 | }; 770 | let token = "1977"; 771 | let mut m = Match { 772 | pattern: MatchPattern::Date(p.clone()), 773 | token: token.to_string(), 774 | ..Match::default() 775 | }; 776 | assert_eq!(scoring::estimate_guesses(&mut m, token), p.estimate(token)); 777 | } 778 | 779 | #[test] 780 | fn test_repeat_guesses() { 781 | let test_data = [ 782 | ("aa", "a", 2), 783 | ("999", "9", 3), 784 | ("$$$$", "$", 4), 785 | ("abab", "ab", 2), 786 | ( 787 | "batterystaplebatterystaplebatterystaple", 788 | "batterystaple", 789 | 3, 790 | ), 791 | ]; 792 | for &(token, base_token, repeat_count) in &test_data { 793 | let base_guesses = scoring::most_guessable_match_sequence( 794 | base_token, 795 | &crate::matching::omnimatch(base_token, &HashMap::new()), 796 | false, 797 | ) 798 | .guesses; 799 | let mut p = RepeatPattern { 800 | base_token: base_token.to_string(), 801 | base_guesses, 802 | repeat_count, 803 | ..RepeatPattern::default() 804 | }; 805 | let expected_guesses = base_guesses * repeat_count as u64; 806 | assert_eq!(p.estimate(token), expected_guesses); 807 | } 808 | } 809 | 810 | #[test] 811 | fn test_sequence_guesses() { 812 | let test_data = [ 813 | ("ab", true, 4 * 2), // obvious start * len-2 814 | ("XYZ", true, 26 * 3), // base26 * len-3 815 | ("4567", true, 10 * 4), // base10 * len-4 816 | ("7654", false, 10 * 4 * 2), // base10 * len 4 * descending 817 | ("ZYX", false, 4 * 3 * 2), /* obvious start * len-3 * descending */ 818 | ]; 819 | for &(token, ascending, guesses) in &test_data { 820 | let mut p = SequencePattern { 821 | ascending, 822 | ..SequencePattern::default() 823 | }; 824 | assert_eq!(p.estimate(token), guesses); 825 | } 826 | } 827 | 828 | #[test] 829 | fn test_regex_guesses_lowercase() { 830 | let token = "aizocdk"; 831 | let mut p = RegexPattern { 832 | regex_name: "alpha_lower".to_owned(), 833 | regex_match: vec![token.to_string()], 834 | }; 835 | assert_eq!(p.estimate(token), 26u64.pow(7)); 836 | } 837 | 838 | #[test] 839 | fn test_regex_guesses_alphanumeric() { 840 | let token = "ag7C8"; 841 | let mut p = RegexPattern { 842 | regex_name: "alphanumeric".to_owned(), 843 | regex_match: vec![token.to_string()], 844 | }; 845 | assert_eq!(p.estimate(token), 62u64.pow(5)); 846 | } 847 | 848 | #[test] 849 | fn test_regex_guesses_distant_year() { 850 | let token = "1972"; 851 | let mut p = RegexPattern { 852 | regex_name: "recent_year".to_owned(), 853 | regex_match: vec![token.to_string()], 854 | }; 855 | assert_eq!( 856 | p.estimate(token), 857 | (*scoring::REFERENCE_YEAR - 1972).abs() as u64 858 | ); 859 | } 860 | 861 | #[test] 862 | fn test_regex_guesses_recent_year() { 863 | let token = "2005"; 864 | let mut p = RegexPattern { 865 | regex_name: "recent_year".to_owned(), 866 | regex_match: vec![token.to_string()], 867 | }; 868 | assert_eq!(p.estimate(token), scoring::MIN_YEAR_SPACE as u64); 869 | } 870 | 871 | #[test] 872 | fn test_regex_guesses_current_year() { 873 | let token = time::OffsetDateTime::now_utc().year().to_string(); 874 | let mut p = RegexPattern { 875 | regex_name: "recent_year".to_owned(), 876 | regex_match: vec![token.to_string()], 877 | }; 878 | assert_eq!(p.estimate(&token), scoring::MIN_YEAR_SPACE as u64); 879 | } 880 | 881 | #[test] 882 | fn test_date_guesses() { 883 | let mut p = DatePattern { 884 | separator: "".to_string(), 885 | year: 1923, 886 | month: 1, 887 | day: 1, 888 | }; 889 | let token = "1123"; 890 | assert_eq!( 891 | p.estimate(token), 892 | 365 * (*scoring::REFERENCE_YEAR - p.year).abs() as u64 893 | ); 894 | } 895 | 896 | #[test] 897 | fn test_date_guesses_recent_years_assume_min_year_space() { 898 | let mut p = DatePattern { 899 | separator: "/".to_string(), 900 | year: 2010, 901 | month: 1, 902 | day: 1, 903 | }; 904 | let token = "1/1/2010"; 905 | assert_eq!(p.estimate(token), 365 * scoring::MIN_YEAR_SPACE as u64 * 4); 906 | } 907 | 908 | #[test] 909 | #[allow(clippy::clone_on_copy)] 910 | fn test_spatial_guesses_no_turns_or_shifts() { 911 | let mut p = SpatialPattern { 912 | graph: "qwerty".to_string(), 913 | turns: 1, 914 | shifted_count: 0, 915 | }; 916 | let token = "zxcvbn"; 917 | let base_guesses = *scoring::KEYBOARD_STARTING_POSITIONS 918 | * *scoring::KEYBOARD_AVERAGE_DEGREE 919 | * (token.len() - 1) as u64; 920 | assert_eq!(p.estimate(token), base_guesses as u64); 921 | } 922 | 923 | #[test] 924 | #[allow(clippy::clone_on_copy)] 925 | fn test_spatial_guesses_adds_for_shifted_keys() { 926 | let mut p = SpatialPattern { 927 | graph: "qwerty".to_string(), 928 | turns: 1, 929 | shifted_count: 2, 930 | }; 931 | let token = "ZxCvbn"; 932 | let base_guesses = *scoring::KEYBOARD_STARTING_POSITIONS 933 | * *scoring::KEYBOARD_AVERAGE_DEGREE 934 | * (token.len() - 1) as u64 935 | * (scoring::n_ck(6, 2) + scoring::n_ck(6, 1)); 936 | assert_eq!(p.estimate(token), base_guesses); 937 | } 938 | 939 | #[test] 940 | #[allow(clippy::clone_on_copy)] 941 | fn test_spatial_guesses_doubles_when_all_shifted() { 942 | let mut p = SpatialPattern { 943 | graph: "qwerty".to_string(), 944 | turns: 1, 945 | shifted_count: 6, 946 | }; 947 | let token = "ZXCVBN"; 948 | let base_guesses = *scoring::KEYBOARD_STARTING_POSITIONS 949 | * *scoring::KEYBOARD_AVERAGE_DEGREE 950 | * (token.len() - 1) as u64 951 | * 2; 952 | assert_eq!(p.estimate(token), base_guesses as u64); 953 | } 954 | 955 | #[test] 956 | #[allow(clippy::clone_on_copy)] 957 | fn test_spatial_guesses_accounts_for_turn_positions_directions_and_start_keys() { 958 | let mut p = SpatialPattern { 959 | graph: "qwerty".to_string(), 960 | turns: 3, 961 | shifted_count: 0, 962 | }; 963 | let token = "zxcft6yh"; 964 | let guesses: u64 = (2..(token.len() + 1)) 965 | .map(|i| { 966 | (1..::std::cmp::min(p.turns + 1, i)) 967 | .map(|j| { 968 | scoring::n_ck(i - 1, j - 1) 969 | * (*scoring::KEYBOARD_STARTING_POSITIONS 970 | * scoring::KEYBOARD_AVERAGE_DEGREE.pow(j as u32)) 971 | as u64 972 | }) 973 | .sum::() 974 | }) 975 | .sum::(); 976 | assert_eq!(p.estimate(token), guesses); 977 | } 978 | 979 | #[test] 980 | fn test_dictionary_base_guesses_equals_rank() { 981 | let mut p = DictionaryPattern { 982 | rank: 32, 983 | ..DictionaryPattern::default() 984 | }; 985 | let token = "aaaaa"; 986 | assert_eq!(p.estimate(token), 32); 987 | } 988 | 989 | #[test] 990 | fn test_dictionary_extra_guesses_added_for_caps() { 991 | let mut p = DictionaryPattern { 992 | rank: 32, 993 | ..DictionaryPattern::default() 994 | }; 995 | let token = "AAAaaa"; 996 | assert_eq!(p.estimate(token), 32 * scoring::uppercase_variations(token)); 997 | } 998 | 999 | #[test] 1000 | fn test_dictionary_guesses_doubled_if_reversed() { 1001 | let mut p = DictionaryPattern { 1002 | rank: 32, 1003 | reversed: true, 1004 | ..DictionaryPattern::default() 1005 | }; 1006 | let token = "aaa"; 1007 | assert_eq!(p.estimate(token), 32 * 2); 1008 | } 1009 | 1010 | #[test] 1011 | fn test_dictionary_guesses_added_for_l33t() { 1012 | let mut subs = HashMap::with_capacity(1); 1013 | subs.insert('@', 'a'); 1014 | let mut p = DictionaryPattern { 1015 | rank: 32, 1016 | l33t: true, 1017 | sub: Some(subs), 1018 | ..DictionaryPattern::default() 1019 | }; 1020 | let token = "aaa@@@"; 1021 | let expected = 32 * scoring::l33t_variations(&p, token); 1022 | assert_eq!(p.estimate(token), expected); 1023 | } 1024 | 1025 | #[test] 1026 | fn test_dictionary_guesses_added_for_caps_and_l33t() { 1027 | let mut subs = HashMap::with_capacity(1); 1028 | subs.insert('@', 'a'); 1029 | let mut p = DictionaryPattern { 1030 | rank: 32, 1031 | l33t: true, 1032 | sub: Some(subs), 1033 | ..DictionaryPattern::default() 1034 | }; 1035 | let token = "AaA@@@"; 1036 | let expected = 1037 | 32 * scoring::l33t_variations(&p, token) * scoring::uppercase_variations(token); 1038 | assert_eq!(p.estimate(token), expected); 1039 | } 1040 | 1041 | #[test] 1042 | fn test_uppercase_variations() { 1043 | let test_data = [ 1044 | ("", 1), 1045 | ("a", 1), 1046 | ("A", 2), 1047 | ("abcdef", 1), 1048 | ("Abcdef", 2), 1049 | ("abcdeF", 2), 1050 | ("ABCDEF", 2), 1051 | ("aBcdef", scoring::n_ck(6, 1)), 1052 | ("aBcDef", scoring::n_ck(6, 1) + scoring::n_ck(6, 2)), 1053 | ("ABCDEf", scoring::n_ck(6, 1)), 1054 | ("aBCDEf", scoring::n_ck(6, 1) + scoring::n_ck(6, 2)), 1055 | ( 1056 | "ABCdef", 1057 | scoring::n_ck(6, 1) + scoring::n_ck(6, 2) + scoring::n_ck(6, 3), 1058 | ), 1059 | ]; 1060 | for &(word, variants) in &test_data { 1061 | assert_eq!(scoring::uppercase_variations(word), variants); 1062 | } 1063 | } 1064 | 1065 | #[test] 1066 | fn test_l33t_variations_for_non_l33t() { 1067 | let p = DictionaryPattern { 1068 | l33t: false, 1069 | ..DictionaryPattern::default() 1070 | }; 1071 | assert_eq!(scoring::l33t_variations(&p, ""), 1); 1072 | } 1073 | 1074 | #[test] 1075 | fn test_l33t_variations() { 1076 | let test_data = [ 1077 | ("", 1, vec![].into_iter().collect::>()), 1078 | ("a", 1, vec![].into_iter().collect::>()), 1079 | ( 1080 | "4", 1081 | 2, 1082 | vec![('4', 'a')] 1083 | .into_iter() 1084 | .collect::>(), 1085 | ), 1086 | ( 1087 | "4pple", 1088 | 2, 1089 | vec![('4', 'a')] 1090 | .into_iter() 1091 | .collect::>(), 1092 | ), 1093 | ( 1094 | "abcet", 1095 | 1, 1096 | vec![].into_iter().collect::>(), 1097 | ), 1098 | ( 1099 | "4bcet", 1100 | 2, 1101 | vec![('4', 'a')] 1102 | .into_iter() 1103 | .collect::>(), 1104 | ), 1105 | ( 1106 | "a8cet", 1107 | 2, 1108 | vec![('8', 'b')] 1109 | .into_iter() 1110 | .collect::>(), 1111 | ), 1112 | ( 1113 | "abce+", 1114 | 2, 1115 | vec![('+', 't')] 1116 | .into_iter() 1117 | .collect::>(), 1118 | ), 1119 | ( 1120 | "48cet", 1121 | 4, 1122 | vec![('4', 'a'), ('8', 'b')] 1123 | .into_iter() 1124 | .collect::>(), 1125 | ), 1126 | ( 1127 | "a4a4aa", 1128 | scoring::n_ck(6, 2) + scoring::n_ck(6, 1), 1129 | vec![('4', 'a')] 1130 | .into_iter() 1131 | .collect::>(), 1132 | ), 1133 | ( 1134 | "4a4a44", 1135 | scoring::n_ck(6, 2) + scoring::n_ck(6, 1), 1136 | vec![('4', 'a')] 1137 | .into_iter() 1138 | .collect::>(), 1139 | ), 1140 | ( 1141 | "Aa44aA", 1142 | scoring::n_ck(6, 2) + scoring::n_ck(6, 1), 1143 | vec![('4', 'a')] 1144 | .into_iter() 1145 | .collect::>(), 1146 | ), 1147 | ( 1148 | "a44att+", 1149 | (scoring::n_ck(4, 2) + scoring::n_ck(4, 1)) * scoring::n_ck(3, 1), 1150 | vec![('4', 'a'), ('+', 't')] 1151 | .into_iter() 1152 | .collect::>(), 1153 | ), 1154 | ]; 1155 | for &(word, variants, ref sub) in &test_data { 1156 | let p = DictionaryPattern { 1157 | sub: Some(sub.clone()), 1158 | l33t: !sub.is_empty(), 1159 | ..DictionaryPattern::default() 1160 | }; 1161 | assert_eq!(scoring::l33t_variations(&p, word), variants); 1162 | } 1163 | } 1164 | 1165 | #[cfg(feature = "ser")] 1166 | #[test] 1167 | fn serde_score() { 1168 | let score = scoring::Score::One; 1169 | let value = serde_json::to_value(&score).unwrap(); 1170 | assert!(matches!(value, serde_json::Value::Number(_))); 1171 | let new_score = serde_json::from_value(value).unwrap(); 1172 | assert_eq!(scoring::Score::One, new_score); 1173 | } 1174 | } 1175 | -------------------------------------------------------------------------------- /src/adjacency_graphs.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | lazy_static! { 4 | pub static ref QWERTY: HashMap>> = { 5 | let mut table = HashMap::with_capacity(94); 6 | table.insert( 7 | '!', 8 | vec![Some("`~"), None, None, Some("2@"), Some("qQ"), None], 9 | ); 10 | table.insert( 11 | '"', 12 | vec![Some(";:"), Some("[{"), Some("]}"), None, None, Some("/?")], 13 | ); 14 | table.insert( 15 | '#', 16 | vec![Some("2@"), None, None, Some("4$"), Some("eE"), Some("wW")], 17 | ); 18 | table.insert( 19 | '$', 20 | vec![Some("3#"), None, None, Some("5%"), Some("rR"), Some("eE")], 21 | ); 22 | table.insert( 23 | '%', 24 | vec![Some("4$"), None, None, Some("6^"), Some("tT"), Some("rR")], 25 | ); 26 | table.insert( 27 | '&', 28 | vec![Some("6^"), None, None, Some("8*"), Some("uU"), Some("yY")], 29 | ); 30 | table.insert( 31 | '\'', 32 | vec![Some(";:"), Some("[{"), Some("]}"), None, None, Some("/?")], 33 | ); 34 | table.insert( 35 | '(', 36 | vec![Some("8*"), None, None, Some("0)"), Some("oO"), Some("iI")], 37 | ); 38 | table.insert( 39 | ')', 40 | vec![Some("9("), None, None, Some("-_"), Some("pP"), Some("oO")], 41 | ); 42 | table.insert( 43 | '*', 44 | vec![Some("7&"), None, None, Some("9("), Some("iI"), Some("uU")], 45 | ); 46 | table.insert( 47 | '+', 48 | vec![Some("-_"), None, None, None, Some("]}"), Some("[{")], 49 | ); 50 | table.insert( 51 | ',', 52 | vec![Some("mM"), Some("kK"), Some("lL"), Some(".>"), None, None], 53 | ); 54 | table.insert( 55 | '-', 56 | vec![Some("0)"), None, None, Some("=+"), Some("[{"), Some("pP")], 57 | ); 58 | table.insert( 59 | '.', 60 | vec![Some(",<"), Some("lL"), Some(";:"), Some("/?"), None, None], 61 | ); 62 | table.insert( 63 | '/', 64 | vec![Some(".>"), Some(";:"), Some("'\""), None, None, None], 65 | ); 66 | table.insert( 67 | '0', 68 | vec![Some("9("), None, None, Some("-_"), Some("pP"), Some("oO")], 69 | ); 70 | table.insert( 71 | '1', 72 | vec![Some("`~"), None, None, Some("2@"), Some("qQ"), None], 73 | ); 74 | table.insert( 75 | '2', 76 | vec![Some("1!"), None, None, Some("3#"), Some("wW"), Some("qQ")], 77 | ); 78 | table.insert( 79 | '3', 80 | vec![Some("2@"), None, None, Some("4$"), Some("eE"), Some("wW")], 81 | ); 82 | table.insert( 83 | '4', 84 | vec![Some("3#"), None, None, Some("5%"), Some("rR"), Some("eE")], 85 | ); 86 | table.insert( 87 | '5', 88 | vec![Some("4$"), None, None, Some("6^"), Some("tT"), Some("rR")], 89 | ); 90 | table.insert( 91 | '6', 92 | vec![Some("5%"), None, None, Some("7&"), Some("yY"), Some("tT")], 93 | ); 94 | table.insert( 95 | '7', 96 | vec![Some("6^"), None, None, Some("8*"), Some("uU"), Some("yY")], 97 | ); 98 | table.insert( 99 | '8', 100 | vec![Some("7&"), None, None, Some("9("), Some("iI"), Some("uU")], 101 | ); 102 | table.insert( 103 | '9', 104 | vec![Some("8*"), None, None, Some("0)"), Some("oO"), Some("iI")], 105 | ); 106 | table.insert( 107 | ':', 108 | vec![ 109 | Some("lL"), 110 | Some("pP"), 111 | Some("[{"), 112 | Some("'\""), 113 | Some("/?"), 114 | Some(".>"), 115 | ], 116 | ); 117 | table.insert( 118 | ';', 119 | vec![ 120 | Some("lL"), 121 | Some("pP"), 122 | Some("[{"), 123 | Some("'\""), 124 | Some("/?"), 125 | Some(".>"), 126 | ], 127 | ); 128 | table.insert( 129 | '<', 130 | vec![Some("mM"), Some("kK"), Some("lL"), Some(".>"), None, None], 131 | ); 132 | table.insert( 133 | '=', 134 | vec![Some("-_"), None, None, None, Some("]}"), Some("[{")], 135 | ); 136 | table.insert( 137 | '>', 138 | vec![Some(",<"), Some("lL"), Some(";:"), Some("/?"), None, None], 139 | ); 140 | table.insert( 141 | '?', 142 | vec![Some(".>"), Some(";:"), Some("'\""), None, None, None], 143 | ); 144 | table.insert( 145 | '@', 146 | vec![Some("1!"), None, None, Some("3#"), Some("wW"), Some("qQ")], 147 | ); 148 | table.insert( 149 | 'A', 150 | vec![None, Some("qQ"), Some("wW"), Some("sS"), Some("zZ"), None], 151 | ); 152 | table.insert( 153 | 'B', 154 | vec![Some("vV"), Some("gG"), Some("hH"), Some("nN"), None, None], 155 | ); 156 | table.insert( 157 | 'C', 158 | vec![Some("xX"), Some("dD"), Some("fF"), Some("vV"), None, None], 159 | ); 160 | table.insert( 161 | 'D', 162 | vec![ 163 | Some("sS"), 164 | Some("eE"), 165 | Some("rR"), 166 | Some("fF"), 167 | Some("cC"), 168 | Some("xX"), 169 | ], 170 | ); 171 | table.insert( 172 | 'E', 173 | vec![ 174 | Some("wW"), 175 | Some("3#"), 176 | Some("4$"), 177 | Some("rR"), 178 | Some("dD"), 179 | Some("sS"), 180 | ], 181 | ); 182 | table.insert( 183 | 'F', 184 | vec![ 185 | Some("dD"), 186 | Some("rR"), 187 | Some("tT"), 188 | Some("gG"), 189 | Some("vV"), 190 | Some("cC"), 191 | ], 192 | ); 193 | table.insert( 194 | 'G', 195 | vec![ 196 | Some("fF"), 197 | Some("tT"), 198 | Some("yY"), 199 | Some("hH"), 200 | Some("bB"), 201 | Some("vV"), 202 | ], 203 | ); 204 | table.insert( 205 | 'H', 206 | vec![ 207 | Some("gG"), 208 | Some("yY"), 209 | Some("uU"), 210 | Some("jJ"), 211 | Some("nN"), 212 | Some("bB"), 213 | ], 214 | ); 215 | table.insert( 216 | 'I', 217 | vec![ 218 | Some("uU"), 219 | Some("8*"), 220 | Some("9("), 221 | Some("oO"), 222 | Some("kK"), 223 | Some("jJ"), 224 | ], 225 | ); 226 | table.insert( 227 | 'J', 228 | vec![ 229 | Some("hH"), 230 | Some("uU"), 231 | Some("iI"), 232 | Some("kK"), 233 | Some("mM"), 234 | Some("nN"), 235 | ], 236 | ); 237 | table.insert( 238 | 'K', 239 | vec![ 240 | Some("jJ"), 241 | Some("iI"), 242 | Some("oO"), 243 | Some("lL"), 244 | Some(",<"), 245 | Some("mM"), 246 | ], 247 | ); 248 | table.insert( 249 | 'L', 250 | vec![ 251 | Some("kK"), 252 | Some("oO"), 253 | Some("pP"), 254 | Some(";:"), 255 | Some(".>"), 256 | Some(",<"), 257 | ], 258 | ); 259 | table.insert( 260 | 'M', 261 | vec![Some("nN"), Some("jJ"), Some("kK"), Some(",<"), None, None], 262 | ); 263 | table.insert( 264 | 'N', 265 | vec![Some("bB"), Some("hH"), Some("jJ"), Some("mM"), None, None], 266 | ); 267 | table.insert( 268 | 'O', 269 | vec![ 270 | Some("iI"), 271 | Some("9("), 272 | Some("0)"), 273 | Some("pP"), 274 | Some("lL"), 275 | Some("kK"), 276 | ], 277 | ); 278 | table.insert( 279 | 'P', 280 | vec![ 281 | Some("oO"), 282 | Some("0)"), 283 | Some("-_"), 284 | Some("[{"), 285 | Some(";:"), 286 | Some("lL"), 287 | ], 288 | ); 289 | table.insert( 290 | 'Q', 291 | vec![None, Some("1!"), Some("2@"), Some("wW"), Some("aA"), None], 292 | ); 293 | table.insert( 294 | 'R', 295 | vec![ 296 | Some("eE"), 297 | Some("4$"), 298 | Some("5%"), 299 | Some("tT"), 300 | Some("fF"), 301 | Some("dD"), 302 | ], 303 | ); 304 | table.insert( 305 | 'S', 306 | vec![ 307 | Some("aA"), 308 | Some("wW"), 309 | Some("eE"), 310 | Some("dD"), 311 | Some("xX"), 312 | Some("zZ"), 313 | ], 314 | ); 315 | table.insert( 316 | 'T', 317 | vec![ 318 | Some("rR"), 319 | Some("5%"), 320 | Some("6^"), 321 | Some("yY"), 322 | Some("gG"), 323 | Some("fF"), 324 | ], 325 | ); 326 | table.insert( 327 | 'U', 328 | vec![ 329 | Some("yY"), 330 | Some("7&"), 331 | Some("8*"), 332 | Some("iI"), 333 | Some("jJ"), 334 | Some("hH"), 335 | ], 336 | ); 337 | table.insert( 338 | 'V', 339 | vec![Some("cC"), Some("fF"), Some("gG"), Some("bB"), None, None], 340 | ); 341 | table.insert( 342 | 'W', 343 | vec![ 344 | Some("qQ"), 345 | Some("2@"), 346 | Some("3#"), 347 | Some("eE"), 348 | Some("sS"), 349 | Some("aA"), 350 | ], 351 | ); 352 | table.insert( 353 | 'X', 354 | vec![Some("zZ"), Some("sS"), Some("dD"), Some("cC"), None, None], 355 | ); 356 | table.insert( 357 | 'Y', 358 | vec![ 359 | Some("tT"), 360 | Some("6^"), 361 | Some("7&"), 362 | Some("uU"), 363 | Some("hH"), 364 | Some("gG"), 365 | ], 366 | ); 367 | table.insert( 368 | 'Z', 369 | vec![None, Some("aA"), Some("sS"), Some("xX"), None, None], 370 | ); 371 | table.insert( 372 | '[', 373 | vec![ 374 | Some("pP"), 375 | Some("-_"), 376 | Some("=+"), 377 | Some("]}"), 378 | Some("'\""), 379 | Some(";:"), 380 | ], 381 | ); 382 | table.insert('\\', vec![Some("]}"), None, None, None, None, None]); 383 | table.insert( 384 | ']', 385 | vec![Some("[{"), Some("=+"), None, Some("\\|"), None, Some("'\"")], 386 | ); 387 | table.insert( 388 | '^', 389 | vec![Some("5%"), None, None, Some("7&"), Some("yY"), Some("tT")], 390 | ); 391 | table.insert( 392 | '_', 393 | vec![Some("0)"), None, None, Some("=+"), Some("[{"), Some("pP")], 394 | ); 395 | table.insert('`', vec![None, None, None, Some("1!"), None, None]); 396 | table.insert( 397 | 'a', 398 | vec![None, Some("qQ"), Some("wW"), Some("sS"), Some("zZ"), None], 399 | ); 400 | table.insert( 401 | 'b', 402 | vec![Some("vV"), Some("gG"), Some("hH"), Some("nN"), None, None], 403 | ); 404 | table.insert( 405 | 'c', 406 | vec![Some("xX"), Some("dD"), Some("fF"), Some("vV"), None, None], 407 | ); 408 | table.insert( 409 | 'd', 410 | vec![ 411 | Some("sS"), 412 | Some("eE"), 413 | Some("rR"), 414 | Some("fF"), 415 | Some("cC"), 416 | Some("xX"), 417 | ], 418 | ); 419 | table.insert( 420 | 'e', 421 | vec![ 422 | Some("wW"), 423 | Some("3#"), 424 | Some("4$"), 425 | Some("rR"), 426 | Some("dD"), 427 | Some("sS"), 428 | ], 429 | ); 430 | table.insert( 431 | 'f', 432 | vec![ 433 | Some("dD"), 434 | Some("rR"), 435 | Some("tT"), 436 | Some("gG"), 437 | Some("vV"), 438 | Some("cC"), 439 | ], 440 | ); 441 | table.insert( 442 | 'g', 443 | vec![ 444 | Some("fF"), 445 | Some("tT"), 446 | Some("yY"), 447 | Some("hH"), 448 | Some("bB"), 449 | Some("vV"), 450 | ], 451 | ); 452 | table.insert( 453 | 'h', 454 | vec![ 455 | Some("gG"), 456 | Some("yY"), 457 | Some("uU"), 458 | Some("jJ"), 459 | Some("nN"), 460 | Some("bB"), 461 | ], 462 | ); 463 | table.insert( 464 | 'i', 465 | vec![ 466 | Some("uU"), 467 | Some("8*"), 468 | Some("9("), 469 | Some("oO"), 470 | Some("kK"), 471 | Some("jJ"), 472 | ], 473 | ); 474 | table.insert( 475 | 'j', 476 | vec![ 477 | Some("hH"), 478 | Some("uU"), 479 | Some("iI"), 480 | Some("kK"), 481 | Some("mM"), 482 | Some("nN"), 483 | ], 484 | ); 485 | table.insert( 486 | 'k', 487 | vec![ 488 | Some("jJ"), 489 | Some("iI"), 490 | Some("oO"), 491 | Some("lL"), 492 | Some(",<"), 493 | Some("mM"), 494 | ], 495 | ); 496 | table.insert( 497 | 'l', 498 | vec![ 499 | Some("kK"), 500 | Some("oO"), 501 | Some("pP"), 502 | Some(";:"), 503 | Some(".>"), 504 | Some(",<"), 505 | ], 506 | ); 507 | table.insert( 508 | 'm', 509 | vec![Some("nN"), Some("jJ"), Some("kK"), Some(",<"), None, None], 510 | ); 511 | table.insert( 512 | 'n', 513 | vec![Some("bB"), Some("hH"), Some("jJ"), Some("mM"), None, None], 514 | ); 515 | table.insert( 516 | 'o', 517 | vec![ 518 | Some("iI"), 519 | Some("9("), 520 | Some("0)"), 521 | Some("pP"), 522 | Some("lL"), 523 | Some("kK"), 524 | ], 525 | ); 526 | table.insert( 527 | 'p', 528 | vec![ 529 | Some("oO"), 530 | Some("0)"), 531 | Some("-_"), 532 | Some("[{"), 533 | Some(";:"), 534 | Some("lL"), 535 | ], 536 | ); 537 | table.insert( 538 | 'q', 539 | vec![None, Some("1!"), Some("2@"), Some("wW"), Some("aA"), None], 540 | ); 541 | table.insert( 542 | 'r', 543 | vec![ 544 | Some("eE"), 545 | Some("4$"), 546 | Some("5%"), 547 | Some("tT"), 548 | Some("fF"), 549 | Some("dD"), 550 | ], 551 | ); 552 | table.insert( 553 | 's', 554 | vec![ 555 | Some("aA"), 556 | Some("wW"), 557 | Some("eE"), 558 | Some("dD"), 559 | Some("xX"), 560 | Some("zZ"), 561 | ], 562 | ); 563 | table.insert( 564 | 't', 565 | vec![ 566 | Some("rR"), 567 | Some("5%"), 568 | Some("6^"), 569 | Some("yY"), 570 | Some("gG"), 571 | Some("fF"), 572 | ], 573 | ); 574 | table.insert( 575 | 'u', 576 | vec![ 577 | Some("yY"), 578 | Some("7&"), 579 | Some("8*"), 580 | Some("iI"), 581 | Some("jJ"), 582 | Some("hH"), 583 | ], 584 | ); 585 | table.insert( 586 | 'v', 587 | vec![Some("cC"), Some("fF"), Some("gG"), Some("bB"), None, None], 588 | ); 589 | table.insert( 590 | 'w', 591 | vec![ 592 | Some("qQ"), 593 | Some("2@"), 594 | Some("3#"), 595 | Some("eE"), 596 | Some("sS"), 597 | Some("aA"), 598 | ], 599 | ); 600 | table.insert( 601 | 'x', 602 | vec![Some("zZ"), Some("sS"), Some("dD"), Some("cC"), None, None], 603 | ); 604 | table.insert( 605 | 'y', 606 | vec![ 607 | Some("tT"), 608 | Some("6^"), 609 | Some("7&"), 610 | Some("uU"), 611 | Some("hH"), 612 | Some("gG"), 613 | ], 614 | ); 615 | table.insert( 616 | 'z', 617 | vec![None, Some("aA"), Some("sS"), Some("xX"), None, None], 618 | ); 619 | table.insert( 620 | '{', 621 | vec![ 622 | Some("pP"), 623 | Some("-_"), 624 | Some("=+"), 625 | Some("]}"), 626 | Some("'\""), 627 | Some(";:"), 628 | ], 629 | ); 630 | table.insert('|', vec![Some("]}"), None, None, None, None, None]); 631 | table.insert( 632 | '}', 633 | vec![Some("[{"), Some("=+"), None, Some("\\|"), None, Some("'\"")], 634 | ); 635 | table.insert('~', vec![None, None, None, Some("1!"), None, None]); 636 | table 637 | }; 638 | pub static ref DVORAK: HashMap>> = { 639 | let mut table = HashMap::with_capacity(94); 640 | table.insert( 641 | '!', 642 | vec![Some("`~"), None, None, Some("2@"), Some("'\""), None], 643 | ); 644 | table.insert( 645 | '"', 646 | vec![None, Some("1!"), Some("2@"), Some(",<"), Some("aA"), None], 647 | ); 648 | table.insert( 649 | '#', 650 | vec![Some("2@"), None, None, Some("4$"), Some(".>"), Some(",<")], 651 | ); 652 | table.insert( 653 | '$', 654 | vec![Some("3#"), None, None, Some("5%"), Some("pP"), Some(".>")], 655 | ); 656 | table.insert( 657 | '%', 658 | vec![Some("4$"), None, None, Some("6^"), Some("yY"), Some("pP")], 659 | ); 660 | table.insert( 661 | '&', 662 | vec![Some("6^"), None, None, Some("8*"), Some("gG"), Some("fF")], 663 | ); 664 | table.insert( 665 | '\'', 666 | vec![None, Some("1!"), Some("2@"), Some(",<"), Some("aA"), None], 667 | ); 668 | table.insert( 669 | '(', 670 | vec![Some("8*"), None, None, Some("0)"), Some("rR"), Some("cC")], 671 | ); 672 | table.insert( 673 | ')', 674 | vec![Some("9("), None, None, Some("[{"), Some("lL"), Some("rR")], 675 | ); 676 | table.insert( 677 | '*', 678 | vec![Some("7&"), None, None, Some("9("), Some("cC"), Some("gG")], 679 | ); 680 | table.insert( 681 | '+', 682 | vec![Some("/?"), Some("]}"), None, Some("\\|"), None, Some("-_")], 683 | ); 684 | table.insert( 685 | ',', 686 | vec![ 687 | Some("'\""), 688 | Some("2@"), 689 | Some("3#"), 690 | Some(".>"), 691 | Some("oO"), 692 | Some("aA"), 693 | ], 694 | ); 695 | table.insert( 696 | '-', 697 | vec![Some("sS"), Some("/?"), Some("=+"), None, None, Some("zZ")], 698 | ); 699 | table.insert( 700 | '.', 701 | vec![ 702 | Some(",<"), 703 | Some("3#"), 704 | Some("4$"), 705 | Some("pP"), 706 | Some("eE"), 707 | Some("oO"), 708 | ], 709 | ); 710 | table.insert( 711 | '/', 712 | vec![ 713 | Some("lL"), 714 | Some("[{"), 715 | Some("]}"), 716 | Some("=+"), 717 | Some("-_"), 718 | Some("sS"), 719 | ], 720 | ); 721 | table.insert( 722 | '0', 723 | vec![Some("9("), None, None, Some("[{"), Some("lL"), Some("rR")], 724 | ); 725 | table.insert( 726 | '1', 727 | vec![Some("`~"), None, None, Some("2@"), Some("'\""), None], 728 | ); 729 | table.insert( 730 | '2', 731 | vec![Some("1!"), None, None, Some("3#"), Some(",<"), Some("'\"")], 732 | ); 733 | table.insert( 734 | '3', 735 | vec![Some("2@"), None, None, Some("4$"), Some(".>"), Some(",<")], 736 | ); 737 | table.insert( 738 | '4', 739 | vec![Some("3#"), None, None, Some("5%"), Some("pP"), Some(".>")], 740 | ); 741 | table.insert( 742 | '5', 743 | vec![Some("4$"), None, None, Some("6^"), Some("yY"), Some("pP")], 744 | ); 745 | table.insert( 746 | '6', 747 | vec![Some("5%"), None, None, Some("7&"), Some("fF"), Some("yY")], 748 | ); 749 | table.insert( 750 | '7', 751 | vec![Some("6^"), None, None, Some("8*"), Some("gG"), Some("fF")], 752 | ); 753 | table.insert( 754 | '8', 755 | vec![Some("7&"), None, None, Some("9("), Some("cC"), Some("gG")], 756 | ); 757 | table.insert( 758 | '9', 759 | vec![Some("8*"), None, None, Some("0)"), Some("rR"), Some("cC")], 760 | ); 761 | table.insert( 762 | ':', 763 | vec![None, Some("aA"), Some("oO"), Some("qQ"), None, None], 764 | ); 765 | table.insert( 766 | ';', 767 | vec![None, Some("aA"), Some("oO"), Some("qQ"), None, None], 768 | ); 769 | table.insert( 770 | '<', 771 | vec![ 772 | Some("'\""), 773 | Some("2@"), 774 | Some("3#"), 775 | Some(".>"), 776 | Some("oO"), 777 | Some("aA"), 778 | ], 779 | ); 780 | table.insert( 781 | '=', 782 | vec![Some("/?"), Some("]}"), None, Some("\\|"), None, Some("-_")], 783 | ); 784 | table.insert( 785 | '>', 786 | vec![ 787 | Some(",<"), 788 | Some("3#"), 789 | Some("4$"), 790 | Some("pP"), 791 | Some("eE"), 792 | Some("oO"), 793 | ], 794 | ); 795 | table.insert( 796 | '?', 797 | vec![ 798 | Some("lL"), 799 | Some("[{"), 800 | Some("]}"), 801 | Some("=+"), 802 | Some("-_"), 803 | Some("sS"), 804 | ], 805 | ); 806 | table.insert( 807 | '@', 808 | vec![Some("1!"), None, None, Some("3#"), Some(",<"), Some("'\"")], 809 | ); 810 | table.insert( 811 | 'A', 812 | vec![None, Some("'\""), Some(",<"), Some("oO"), Some(";:"), None], 813 | ); 814 | table.insert( 815 | 'B', 816 | vec![Some("xX"), Some("dD"), Some("hH"), Some("mM"), None, None], 817 | ); 818 | table.insert( 819 | 'C', 820 | vec![ 821 | Some("gG"), 822 | Some("8*"), 823 | Some("9("), 824 | Some("rR"), 825 | Some("tT"), 826 | Some("hH"), 827 | ], 828 | ); 829 | table.insert( 830 | 'D', 831 | vec![ 832 | Some("iI"), 833 | Some("fF"), 834 | Some("gG"), 835 | Some("hH"), 836 | Some("bB"), 837 | Some("xX"), 838 | ], 839 | ); 840 | table.insert( 841 | 'E', 842 | vec![ 843 | Some("oO"), 844 | Some(".>"), 845 | Some("pP"), 846 | Some("uU"), 847 | Some("jJ"), 848 | Some("qQ"), 849 | ], 850 | ); 851 | table.insert( 852 | 'F', 853 | vec![ 854 | Some("yY"), 855 | Some("6^"), 856 | Some("7&"), 857 | Some("gG"), 858 | Some("dD"), 859 | Some("iI"), 860 | ], 861 | ); 862 | table.insert( 863 | 'G', 864 | vec![ 865 | Some("fF"), 866 | Some("7&"), 867 | Some("8*"), 868 | Some("cC"), 869 | Some("hH"), 870 | Some("dD"), 871 | ], 872 | ); 873 | table.insert( 874 | 'H', 875 | vec![ 876 | Some("dD"), 877 | Some("gG"), 878 | Some("cC"), 879 | Some("tT"), 880 | Some("mM"), 881 | Some("bB"), 882 | ], 883 | ); 884 | table.insert( 885 | 'I', 886 | vec![ 887 | Some("uU"), 888 | Some("yY"), 889 | Some("fF"), 890 | Some("dD"), 891 | Some("xX"), 892 | Some("kK"), 893 | ], 894 | ); 895 | table.insert( 896 | 'J', 897 | vec![Some("qQ"), Some("eE"), Some("uU"), Some("kK"), None, None], 898 | ); 899 | table.insert( 900 | 'K', 901 | vec![Some("jJ"), Some("uU"), Some("iI"), Some("xX"), None, None], 902 | ); 903 | table.insert( 904 | 'L', 905 | vec![ 906 | Some("rR"), 907 | Some("0)"), 908 | Some("[{"), 909 | Some("/?"), 910 | Some("sS"), 911 | Some("nN"), 912 | ], 913 | ); 914 | table.insert( 915 | 'M', 916 | vec![Some("bB"), Some("hH"), Some("tT"), Some("wW"), None, None], 917 | ); 918 | table.insert( 919 | 'N', 920 | vec![ 921 | Some("tT"), 922 | Some("rR"), 923 | Some("lL"), 924 | Some("sS"), 925 | Some("vV"), 926 | Some("wW"), 927 | ], 928 | ); 929 | table.insert( 930 | 'O', 931 | vec![ 932 | Some("aA"), 933 | Some(",<"), 934 | Some(".>"), 935 | Some("eE"), 936 | Some("qQ"), 937 | Some(";:"), 938 | ], 939 | ); 940 | table.insert( 941 | 'P', 942 | vec![ 943 | Some(".>"), 944 | Some("4$"), 945 | Some("5%"), 946 | Some("yY"), 947 | Some("uU"), 948 | Some("eE"), 949 | ], 950 | ); 951 | table.insert( 952 | 'Q', 953 | vec![Some(";:"), Some("oO"), Some("eE"), Some("jJ"), None, None], 954 | ); 955 | table.insert( 956 | 'R', 957 | vec![ 958 | Some("cC"), 959 | Some("9("), 960 | Some("0)"), 961 | Some("lL"), 962 | Some("nN"), 963 | Some("tT"), 964 | ], 965 | ); 966 | table.insert( 967 | 'S', 968 | vec![ 969 | Some("nN"), 970 | Some("lL"), 971 | Some("/?"), 972 | Some("-_"), 973 | Some("zZ"), 974 | Some("vV"), 975 | ], 976 | ); 977 | table.insert( 978 | 'T', 979 | vec![ 980 | Some("hH"), 981 | Some("cC"), 982 | Some("rR"), 983 | Some("nN"), 984 | Some("wW"), 985 | Some("mM"), 986 | ], 987 | ); 988 | table.insert( 989 | 'U', 990 | vec![ 991 | Some("eE"), 992 | Some("pP"), 993 | Some("yY"), 994 | Some("iI"), 995 | Some("kK"), 996 | Some("jJ"), 997 | ], 998 | ); 999 | table.insert( 1000 | 'V', 1001 | vec![Some("wW"), Some("nN"), Some("sS"), Some("zZ"), None, None], 1002 | ); 1003 | table.insert( 1004 | 'W', 1005 | vec![Some("mM"), Some("tT"), Some("nN"), Some("vV"), None, None], 1006 | ); 1007 | table.insert( 1008 | 'X', 1009 | vec![Some("kK"), Some("iI"), Some("dD"), Some("bB"), None, None], 1010 | ); 1011 | table.insert( 1012 | 'Y', 1013 | vec![ 1014 | Some("pP"), 1015 | Some("5%"), 1016 | Some("6^"), 1017 | Some("fF"), 1018 | Some("iI"), 1019 | Some("uU"), 1020 | ], 1021 | ); 1022 | table.insert( 1023 | 'Z', 1024 | vec![Some("vV"), Some("sS"), Some("-_"), None, None, None], 1025 | ); 1026 | table.insert( 1027 | '[', 1028 | vec![Some("0)"), None, None, Some("]}"), Some("/?"), Some("lL")], 1029 | ); 1030 | table.insert('\\', vec![Some("=+"), None, None, None, None, None]); 1031 | table.insert( 1032 | ']', 1033 | vec![Some("[{"), None, None, None, Some("=+"), Some("/?")], 1034 | ); 1035 | table.insert( 1036 | '^', 1037 | vec![Some("5%"), None, None, Some("7&"), Some("fF"), Some("yY")], 1038 | ); 1039 | table.insert( 1040 | '_', 1041 | vec![Some("sS"), Some("/?"), Some("=+"), None, None, Some("zZ")], 1042 | ); 1043 | table.insert('`', vec![None, None, None, Some("1!"), None, None]); 1044 | table.insert( 1045 | 'a', 1046 | vec![None, Some("'\""), Some(",<"), Some("oO"), Some(";:"), None], 1047 | ); 1048 | table.insert( 1049 | 'b', 1050 | vec![Some("xX"), Some("dD"), Some("hH"), Some("mM"), None, None], 1051 | ); 1052 | table.insert( 1053 | 'c', 1054 | vec![ 1055 | Some("gG"), 1056 | Some("8*"), 1057 | Some("9("), 1058 | Some("rR"), 1059 | Some("tT"), 1060 | Some("hH"), 1061 | ], 1062 | ); 1063 | table.insert( 1064 | 'd', 1065 | vec![ 1066 | Some("iI"), 1067 | Some("fF"), 1068 | Some("gG"), 1069 | Some("hH"), 1070 | Some("bB"), 1071 | Some("xX"), 1072 | ], 1073 | ); 1074 | table.insert( 1075 | 'e', 1076 | vec![ 1077 | Some("oO"), 1078 | Some(".>"), 1079 | Some("pP"), 1080 | Some("uU"), 1081 | Some("jJ"), 1082 | Some("qQ"), 1083 | ], 1084 | ); 1085 | table.insert( 1086 | 'f', 1087 | vec![ 1088 | Some("yY"), 1089 | Some("6^"), 1090 | Some("7&"), 1091 | Some("gG"), 1092 | Some("dD"), 1093 | Some("iI"), 1094 | ], 1095 | ); 1096 | table.insert( 1097 | 'g', 1098 | vec![ 1099 | Some("fF"), 1100 | Some("7&"), 1101 | Some("8*"), 1102 | Some("cC"), 1103 | Some("hH"), 1104 | Some("dD"), 1105 | ], 1106 | ); 1107 | table.insert( 1108 | 'h', 1109 | vec![ 1110 | Some("dD"), 1111 | Some("gG"), 1112 | Some("cC"), 1113 | Some("tT"), 1114 | Some("mM"), 1115 | Some("bB"), 1116 | ], 1117 | ); 1118 | table.insert( 1119 | 'i', 1120 | vec![ 1121 | Some("uU"), 1122 | Some("yY"), 1123 | Some("fF"), 1124 | Some("dD"), 1125 | Some("xX"), 1126 | Some("kK"), 1127 | ], 1128 | ); 1129 | table.insert( 1130 | 'j', 1131 | vec![Some("qQ"), Some("eE"), Some("uU"), Some("kK"), None, None], 1132 | ); 1133 | table.insert( 1134 | 'k', 1135 | vec![Some("jJ"), Some("uU"), Some("iI"), Some("xX"), None, None], 1136 | ); 1137 | table.insert( 1138 | 'l', 1139 | vec![ 1140 | Some("rR"), 1141 | Some("0)"), 1142 | Some("[{"), 1143 | Some("/?"), 1144 | Some("sS"), 1145 | Some("nN"), 1146 | ], 1147 | ); 1148 | table.insert( 1149 | 'm', 1150 | vec![Some("bB"), Some("hH"), Some("tT"), Some("wW"), None, None], 1151 | ); 1152 | table.insert( 1153 | 'n', 1154 | vec![ 1155 | Some("tT"), 1156 | Some("rR"), 1157 | Some("lL"), 1158 | Some("sS"), 1159 | Some("vV"), 1160 | Some("wW"), 1161 | ], 1162 | ); 1163 | table.insert( 1164 | 'o', 1165 | vec![ 1166 | Some("aA"), 1167 | Some(",<"), 1168 | Some(".>"), 1169 | Some("eE"), 1170 | Some("qQ"), 1171 | Some(";:"), 1172 | ], 1173 | ); 1174 | table.insert( 1175 | 'p', 1176 | vec![ 1177 | Some(".>"), 1178 | Some("4$"), 1179 | Some("5%"), 1180 | Some("yY"), 1181 | Some("uU"), 1182 | Some("eE"), 1183 | ], 1184 | ); 1185 | table.insert( 1186 | 'q', 1187 | vec![Some(";:"), Some("oO"), Some("eE"), Some("jJ"), None, None], 1188 | ); 1189 | table.insert( 1190 | 'r', 1191 | vec![ 1192 | Some("cC"), 1193 | Some("9("), 1194 | Some("0)"), 1195 | Some("lL"), 1196 | Some("nN"), 1197 | Some("tT"), 1198 | ], 1199 | ); 1200 | table.insert( 1201 | 's', 1202 | vec![ 1203 | Some("nN"), 1204 | Some("lL"), 1205 | Some("/?"), 1206 | Some("-_"), 1207 | Some("zZ"), 1208 | Some("vV"), 1209 | ], 1210 | ); 1211 | table.insert( 1212 | 't', 1213 | vec![ 1214 | Some("hH"), 1215 | Some("cC"), 1216 | Some("rR"), 1217 | Some("nN"), 1218 | Some("wW"), 1219 | Some("mM"), 1220 | ], 1221 | ); 1222 | table.insert( 1223 | 'u', 1224 | vec![ 1225 | Some("eE"), 1226 | Some("pP"), 1227 | Some("yY"), 1228 | Some("iI"), 1229 | Some("kK"), 1230 | Some("jJ"), 1231 | ], 1232 | ); 1233 | table.insert( 1234 | 'v', 1235 | vec![Some("wW"), Some("nN"), Some("sS"), Some("zZ"), None, None], 1236 | ); 1237 | table.insert( 1238 | 'w', 1239 | vec![Some("mM"), Some("tT"), Some("nN"), Some("vV"), None, None], 1240 | ); 1241 | table.insert( 1242 | 'x', 1243 | vec![Some("kK"), Some("iI"), Some("dD"), Some("bB"), None, None], 1244 | ); 1245 | table.insert( 1246 | 'y', 1247 | vec![ 1248 | Some("pP"), 1249 | Some("5%"), 1250 | Some("6^"), 1251 | Some("fF"), 1252 | Some("iI"), 1253 | Some("uU"), 1254 | ], 1255 | ); 1256 | table.insert( 1257 | 'z', 1258 | vec![Some("vV"), Some("sS"), Some("-_"), None, None, None], 1259 | ); 1260 | table.insert( 1261 | '{', 1262 | vec![Some("0)"), None, None, Some("]}"), Some("/?"), Some("lL")], 1263 | ); 1264 | table.insert('|', vec![Some("=+"), None, None, None, None, None]); 1265 | table.insert( 1266 | '}', 1267 | vec![Some("[{"), None, None, None, Some("=+"), Some("/?")], 1268 | ); 1269 | table.insert('~', vec![None, None, None, Some("1!"), None, None]); 1270 | table 1271 | }; 1272 | pub static ref KEYPAD: HashMap>> = { 1273 | let mut table = HashMap::with_capacity(15); 1274 | table.insert( 1275 | '*', 1276 | vec![ 1277 | Some("/"), 1278 | None, 1279 | None, 1280 | None, 1281 | Some("-"), 1282 | Some("+"), 1283 | Some("9"), 1284 | Some("8"), 1285 | ], 1286 | ); 1287 | table.insert( 1288 | '+', 1289 | vec![ 1290 | Some("9"), 1291 | Some("*"), 1292 | Some("-"), 1293 | None, 1294 | None, 1295 | None, 1296 | None, 1297 | Some("6"), 1298 | ], 1299 | ); 1300 | table.insert( 1301 | '-', 1302 | vec![ 1303 | Some("*"), 1304 | None, 1305 | None, 1306 | None, 1307 | None, 1308 | None, 1309 | Some("+"), 1310 | Some("9"), 1311 | ], 1312 | ); 1313 | table.insert( 1314 | '.', 1315 | vec![ 1316 | Some("0"), 1317 | Some("2"), 1318 | Some("3"), 1319 | None, 1320 | None, 1321 | None, 1322 | None, 1323 | None, 1324 | ], 1325 | ); 1326 | table.insert( 1327 | '/', 1328 | vec![ 1329 | None, 1330 | None, 1331 | None, 1332 | None, 1333 | Some("*"), 1334 | Some("9"), 1335 | Some("8"), 1336 | Some("7"), 1337 | ], 1338 | ); 1339 | table.insert( 1340 | '0', 1341 | vec![ 1342 | None, 1343 | Some("1"), 1344 | Some("2"), 1345 | Some("3"), 1346 | Some("."), 1347 | None, 1348 | None, 1349 | None, 1350 | ], 1351 | ); 1352 | table.insert( 1353 | '1', 1354 | vec![ 1355 | None, 1356 | None, 1357 | Some("4"), 1358 | Some("5"), 1359 | Some("2"), 1360 | Some("0"), 1361 | None, 1362 | None, 1363 | ], 1364 | ); 1365 | table.insert( 1366 | '2', 1367 | vec![ 1368 | Some("1"), 1369 | Some("4"), 1370 | Some("5"), 1371 | Some("6"), 1372 | Some("3"), 1373 | Some("."), 1374 | Some("0"), 1375 | None, 1376 | ], 1377 | ); 1378 | table.insert( 1379 | '3', 1380 | vec![ 1381 | Some("2"), 1382 | Some("5"), 1383 | Some("6"), 1384 | None, 1385 | None, 1386 | None, 1387 | Some("."), 1388 | Some("0"), 1389 | ], 1390 | ); 1391 | table.insert( 1392 | '4', 1393 | vec![ 1394 | None, 1395 | None, 1396 | Some("7"), 1397 | Some("8"), 1398 | Some("5"), 1399 | Some("2"), 1400 | Some("1"), 1401 | None, 1402 | ], 1403 | ); 1404 | table.insert( 1405 | '5', 1406 | vec![ 1407 | Some("4"), 1408 | Some("7"), 1409 | Some("8"), 1410 | Some("9"), 1411 | Some("6"), 1412 | Some("3"), 1413 | Some("2"), 1414 | Some("1"), 1415 | ], 1416 | ); 1417 | table.insert( 1418 | '6', 1419 | vec![ 1420 | Some("5"), 1421 | Some("8"), 1422 | Some("9"), 1423 | Some("+"), 1424 | None, 1425 | None, 1426 | Some("3"), 1427 | Some("2"), 1428 | ], 1429 | ); 1430 | table.insert( 1431 | '7', 1432 | vec![ 1433 | None, 1434 | None, 1435 | None, 1436 | Some("/"), 1437 | Some("8"), 1438 | Some("5"), 1439 | Some("4"), 1440 | None, 1441 | ], 1442 | ); 1443 | table.insert( 1444 | '8', 1445 | vec![ 1446 | Some("7"), 1447 | None, 1448 | Some("/"), 1449 | Some("*"), 1450 | Some("9"), 1451 | Some("6"), 1452 | Some("5"), 1453 | Some("4"), 1454 | ], 1455 | ); 1456 | table.insert( 1457 | '9', 1458 | vec![ 1459 | Some("8"), 1460 | Some("/"), 1461 | Some("*"), 1462 | Some("-"), 1463 | Some("+"), 1464 | None, 1465 | Some("6"), 1466 | Some("5"), 1467 | ], 1468 | ); 1469 | table 1470 | }; 1471 | pub static ref MAC_KEYPAD: HashMap>> = { 1472 | let mut table = HashMap::with_capacity(16); 1473 | table.insert( 1474 | '*', 1475 | vec![ 1476 | Some("/"), 1477 | None, 1478 | None, 1479 | None, 1480 | None, 1481 | None, 1482 | Some("-"), 1483 | Some("9"), 1484 | ], 1485 | ); 1486 | table.insert( 1487 | '+', 1488 | vec![ 1489 | Some("6"), 1490 | Some("9"), 1491 | Some("-"), 1492 | None, 1493 | None, 1494 | None, 1495 | None, 1496 | Some("3"), 1497 | ], 1498 | ); 1499 | table.insert( 1500 | '-', 1501 | vec![ 1502 | Some("9"), 1503 | Some("/"), 1504 | Some("*"), 1505 | None, 1506 | None, 1507 | None, 1508 | Some("+"), 1509 | Some("6"), 1510 | ], 1511 | ); 1512 | table.insert( 1513 | '.', 1514 | vec![ 1515 | Some("0"), 1516 | Some("2"), 1517 | Some("3"), 1518 | None, 1519 | None, 1520 | None, 1521 | None, 1522 | None, 1523 | ], 1524 | ); 1525 | table.insert( 1526 | '/', 1527 | vec![ 1528 | Some("="), 1529 | None, 1530 | None, 1531 | None, 1532 | Some("*"), 1533 | Some("-"), 1534 | Some("9"), 1535 | Some("8"), 1536 | ], 1537 | ); 1538 | table.insert( 1539 | '0', 1540 | vec![ 1541 | None, 1542 | Some("1"), 1543 | Some("2"), 1544 | Some("3"), 1545 | Some("."), 1546 | None, 1547 | None, 1548 | None, 1549 | ], 1550 | ); 1551 | table.insert( 1552 | '1', 1553 | vec![ 1554 | None, 1555 | None, 1556 | Some("4"), 1557 | Some("5"), 1558 | Some("2"), 1559 | Some("0"), 1560 | None, 1561 | None, 1562 | ], 1563 | ); 1564 | table.insert( 1565 | '2', 1566 | vec![ 1567 | Some("1"), 1568 | Some("4"), 1569 | Some("5"), 1570 | Some("6"), 1571 | Some("3"), 1572 | Some("."), 1573 | Some("0"), 1574 | None, 1575 | ], 1576 | ); 1577 | table.insert( 1578 | '3', 1579 | vec![ 1580 | Some("2"), 1581 | Some("5"), 1582 | Some("6"), 1583 | Some("+"), 1584 | None, 1585 | None, 1586 | Some("."), 1587 | Some("0"), 1588 | ], 1589 | ); 1590 | table.insert( 1591 | '4', 1592 | vec![ 1593 | None, 1594 | None, 1595 | Some("7"), 1596 | Some("8"), 1597 | Some("5"), 1598 | Some("2"), 1599 | Some("1"), 1600 | None, 1601 | ], 1602 | ); 1603 | table.insert( 1604 | '5', 1605 | vec![ 1606 | Some("4"), 1607 | Some("7"), 1608 | Some("8"), 1609 | Some("9"), 1610 | Some("6"), 1611 | Some("3"), 1612 | Some("2"), 1613 | Some("1"), 1614 | ], 1615 | ); 1616 | table.insert( 1617 | '6', 1618 | vec![ 1619 | Some("5"), 1620 | Some("8"), 1621 | Some("9"), 1622 | Some("-"), 1623 | Some("+"), 1624 | None, 1625 | Some("3"), 1626 | Some("2"), 1627 | ], 1628 | ); 1629 | table.insert( 1630 | '7', 1631 | vec![ 1632 | None, 1633 | None, 1634 | None, 1635 | Some("="), 1636 | Some("8"), 1637 | Some("5"), 1638 | Some("4"), 1639 | None, 1640 | ], 1641 | ); 1642 | table.insert( 1643 | '8', 1644 | vec![ 1645 | Some("7"), 1646 | None, 1647 | Some("="), 1648 | Some("/"), 1649 | Some("9"), 1650 | Some("6"), 1651 | Some("5"), 1652 | Some("4"), 1653 | ], 1654 | ); 1655 | table.insert( 1656 | '9', 1657 | vec![ 1658 | Some("8"), 1659 | Some("="), 1660 | Some("/"), 1661 | Some("*"), 1662 | Some("-"), 1663 | Some("+"), 1664 | Some("6"), 1665 | Some("5"), 1666 | ], 1667 | ); 1668 | table.insert( 1669 | '=', 1670 | vec![ 1671 | None, 1672 | None, 1673 | None, 1674 | None, 1675 | Some("/"), 1676 | Some("9"), 1677 | Some("8"), 1678 | Some("7"), 1679 | ], 1680 | ); 1681 | table 1682 | }; 1683 | } 1684 | -------------------------------------------------------------------------------- /src/matching/mod.rs: -------------------------------------------------------------------------------- 1 | mod char_indexing; 2 | /// Defines potential patterns used to match against a password 3 | pub mod patterns; 4 | 5 | use self::patterns::*; 6 | use crate::frequency_lists::DictionaryType; 7 | use char_indexing::{CharIndexable, CharIndexableStr}; 8 | use fancy_regex::Regex as FancyRegex; 9 | use itertools::Itertools; 10 | use regex::Regex; 11 | use std::collections::HashMap; 12 | 13 | /// A match of a predictable pattern in the password. 14 | #[derive(Debug, Clone, PartialEq, Default)] 15 | #[cfg_attr(feature = "builder", derive(Builder))] 16 | #[cfg_attr(feature = "builder", builder(default))] 17 | #[cfg_attr(feature = "ser", derive(serde::Deserialize, serde::Serialize))] 18 | pub struct Match { 19 | /// Beginning of the match. 20 | pub i: usize, 21 | /// End of the match. 22 | pub j: usize, 23 | /// Token that has been matched. 24 | pub token: String, 25 | /// Pattern type and details used to detect this match. 26 | #[cfg_attr(feature = "ser", serde(flatten))] 27 | pub pattern: MatchPattern, 28 | /// Estimated number of tries for guessing the match. 29 | pub guesses: Option, 30 | } 31 | 32 | impl Match { 33 | /// Get the range of the index of the chars that are included in the match. 34 | pub fn range_inclusive(&self) -> std::ops::RangeInclusive { 35 | self.i..=self.j 36 | } 37 | } 38 | 39 | #[allow(clippy::implicit_hasher)] 40 | pub(crate) fn omnimatch(password: &str, user_inputs: &HashMap) -> Vec { 41 | let mut matches: Vec = MATCHERS 42 | .iter() 43 | .flat_map(|x| x.get_matches(password, user_inputs)) 44 | .collect(); 45 | matches.sort_unstable_by(|a, b| { 46 | let range1 = a.range_inclusive(); 47 | let range2 = b.range_inclusive(); 48 | range1 49 | .start() 50 | .cmp(range2.start()) 51 | .then_with(|| range1.end().cmp(range2.end())) 52 | }); 53 | matches 54 | } 55 | 56 | lazy_static! { 57 | static ref L33T_TABLE: HashMap> = { 58 | let mut table = HashMap::with_capacity(12); 59 | table.insert('a', vec!['4', '@']); 60 | table.insert('b', vec!['8']); 61 | table.insert('c', vec!['(', '{', '[', '<']); 62 | table.insert('e', vec!['3']); 63 | table.insert('g', vec!['6', '9']); 64 | table.insert('i', vec!['1', '!', '|']); 65 | table.insert('l', vec!['1', '|', '7']); 66 | table.insert('o', vec!['0']); 67 | table.insert('s', vec!['$', '5']); 68 | table.insert('t', vec!['+', '7']); 69 | table.insert('x', vec!['%']); 70 | table.insert('z', vec!['2']); 71 | table 72 | }; 73 | static ref GRAPHS: HashMap<&'static str, &'static HashMap>>> = { 74 | let mut table = HashMap::with_capacity(4); 75 | table.insert("qwerty", &*super::adjacency_graphs::QWERTY); 76 | table.insert("dvorak", &*super::adjacency_graphs::DVORAK); 77 | table.insert("keypad", &*super::adjacency_graphs::KEYPAD); 78 | table.insert("mac_keypad", &*super::adjacency_graphs::MAC_KEYPAD); 79 | table 80 | }; 81 | } 82 | 83 | trait Matcher: Send + Sync { 84 | fn get_matches(&self, password: &str, user_inputs: &HashMap) -> Vec; 85 | } 86 | 87 | lazy_static! { 88 | static ref MATCHERS: [Box; 8] = [ 89 | Box::new(DictionaryMatch {}), 90 | Box::new(ReverseDictionaryMatch {}), 91 | Box::new(L33tMatch {}), 92 | Box::new(SpatialMatch {}), 93 | Box::new(RepeatMatch {}), 94 | Box::new(SequenceMatch {}), 95 | Box::new(RegexMatch {}), 96 | Box::new(DateMatch {}), 97 | ]; 98 | } 99 | 100 | struct DictionaryMatch {} 101 | 102 | impl Matcher for DictionaryMatch { 103 | fn get_matches(&self, password: &str, user_inputs: &HashMap) -> Vec { 104 | let password_lower_string = password.to_lowercase(); 105 | let password_lower = CharIndexableStr::from(password_lower_string.as_str()); 106 | 107 | let do_trials = move |matches: &mut Vec, 108 | password: &str, 109 | dictionary_name: DictionaryType, 110 | ranked_dict: &HashMap<&str, usize>| { 111 | let len = password.chars().count(); 112 | for i in 0..len { 113 | for j in i..len { 114 | let word = password_lower.char_index(i..j + 1); 115 | if let Some(rank) = ranked_dict.get(word).cloned() { 116 | let pattern = MatchPattern::Dictionary(DictionaryPattern { 117 | matched_word: word.to_string(), 118 | rank, 119 | dictionary_name, 120 | ..DictionaryPattern::default() 121 | }); 122 | matches.push(Match { 123 | pattern, 124 | i, 125 | j, 126 | token: password.chars().take(j + 1).skip(i).collect(), 127 | ..Match::default() 128 | }); 129 | } 130 | } 131 | } 132 | }; 133 | 134 | let mut matches = Vec::new(); 135 | 136 | for (dictionary_name, ranked_dict) in super::frequency_lists::RANKED_DICTIONARIES.iter() { 137 | do_trials(&mut matches, password, *dictionary_name, ranked_dict); 138 | } 139 | do_trials( 140 | &mut matches, 141 | password, 142 | DictionaryType::UserInputs, 143 | &user_inputs.iter().map(|(x, &i)| (x.as_str(), i)).collect(), 144 | ); 145 | 146 | matches 147 | } 148 | } 149 | 150 | struct ReverseDictionaryMatch {} 151 | 152 | impl Matcher for ReverseDictionaryMatch { 153 | fn get_matches(&self, password: &str, user_inputs: &HashMap) -> Vec { 154 | let reversed_password = password.chars().rev().collect::(); 155 | (DictionaryMatch {}) 156 | .get_matches(&reversed_password, user_inputs) 157 | .into_iter() 158 | .map(|mut m| { 159 | // Reverse token back 160 | m.token = m.token.chars().rev().collect(); 161 | if let MatchPattern::Dictionary(ref mut pattern) = m.pattern { 162 | pattern.reversed = true; 163 | } 164 | let old_i = m.i; 165 | m.i = password.chars().count() - 1 - m.j; 166 | m.j = password.chars().count() - 1 - old_i; 167 | m 168 | }) 169 | .collect() 170 | } 171 | } 172 | 173 | struct L33tMatch {} 174 | 175 | impl Matcher for L33tMatch { 176 | fn get_matches(&self, password: &str, user_inputs: &HashMap) -> Vec { 177 | let mut matches = Vec::new(); 178 | for sub in enumerate_l33t_replacements(&relevant_l33t_subtable(password)) { 179 | if sub.is_empty() { 180 | break; 181 | } 182 | let subbed_password = translate(password, &sub); 183 | for mut m4tch in (DictionaryMatch {}).get_matches(&subbed_password, user_inputs) { 184 | let token = password 185 | .chars() 186 | .take(m4tch.j + 1) 187 | .skip(m4tch.i) 188 | .collect::(); 189 | { 190 | let pattern = if let MatchPattern::Dictionary(ref mut pattern) = m4tch.pattern { 191 | pattern 192 | } else { 193 | unreachable!() 194 | }; 195 | if token.to_lowercase() == pattern.matched_word { 196 | // Only return the matches that contain an actual substitution 197 | continue; 198 | } 199 | let match_sub: HashMap = sub 200 | .clone() 201 | .into_iter() 202 | .filter(|&(subbed_chr, _)| token.contains(subbed_chr)) 203 | .collect(); 204 | m4tch.token = token; 205 | pattern.l33t = true; 206 | pattern.sub_display = Some( 207 | match_sub 208 | .iter() 209 | .map(|(k, v)| format!("{} -> {}", k, v)) 210 | .join(", "), 211 | ); 212 | pattern.sub = Some(match_sub); 213 | } 214 | matches.push(m4tch); 215 | } 216 | } 217 | matches 218 | .into_iter() 219 | .filter(|x| !x.token.is_empty()) 220 | .collect() 221 | } 222 | } 223 | 224 | fn translate(string: &str, chr_map: &HashMap) -> String { 225 | string 226 | .chars() 227 | .map(|c| *chr_map.get(&c).unwrap_or(&c)) 228 | .collect() 229 | } 230 | 231 | fn relevant_l33t_subtable(password: &str) -> HashMap> { 232 | let password_chars: Vec = password.chars().collect(); 233 | let mut subtable: HashMap> = HashMap::new(); 234 | for (letter, subs) in L33T_TABLE.iter() { 235 | let relevant_subs: Vec = subs 236 | .iter() 237 | .filter(|&x| password_chars.contains(x)) 238 | .cloned() 239 | .collect(); 240 | if !relevant_subs.is_empty() { 241 | subtable.insert(*letter, relevant_subs); 242 | } 243 | } 244 | subtable 245 | } 246 | 247 | fn enumerate_l33t_replacements(table: &HashMap>) -> Vec> { 248 | /// Recursive function that does the work 249 | fn helper( 250 | table: &HashMap>, 251 | subs: Vec>, 252 | remaining_keys: &[char], 253 | ) -> Vec> { 254 | if remaining_keys.is_empty() { 255 | return subs; 256 | } 257 | let (first_key, rest_keys) = remaining_keys.split_first().unwrap(); 258 | let mut next_subs: Vec> = Vec::new(); 259 | for l33t_chr in &table[first_key] { 260 | for sub in &subs { 261 | let mut dup_l33t_index = None; 262 | for (i, item) in sub.iter().enumerate() { 263 | if item.0 == *l33t_chr { 264 | dup_l33t_index = Some(i); 265 | break; 266 | } 267 | } 268 | if let Some(idx) = dup_l33t_index { 269 | let mut sub_alternative = sub.clone(); 270 | sub_alternative.remove(idx); 271 | sub_alternative.push((*l33t_chr, *first_key)); 272 | next_subs.push(sub.clone()); 273 | next_subs.push(sub_alternative); 274 | } else { 275 | let mut sub_extension = sub.clone(); 276 | sub_extension.push((*l33t_chr, *first_key)); 277 | next_subs.push(sub_extension); 278 | } 279 | } 280 | } 281 | helper( 282 | table, 283 | next_subs 284 | .into_iter() 285 | .map(|x| x.iter().unique().cloned().collect()) 286 | .collect(), 287 | rest_keys, 288 | ) 289 | } 290 | 291 | helper( 292 | table, 293 | vec![vec![]], 294 | table.keys().cloned().collect::>().as_slice(), 295 | ) 296 | .into_iter() 297 | .map(|sub| sub.into_iter().collect::>()) 298 | .collect() 299 | } 300 | 301 | struct SpatialMatch {} 302 | 303 | impl Matcher for SpatialMatch { 304 | fn get_matches(&self, password: &str, _user_inputs: &HashMap) -> Vec { 305 | GRAPHS 306 | .iter() 307 | .flat_map(|(graph_name, graph)| spatial_match_helper(password, graph, graph_name)) 308 | .collect() 309 | } 310 | } 311 | 312 | const SHIFTED_CHARS: [char; 49] = [ 313 | '[', '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', 'Q', 'W', 'E', 'R', 'T', 314 | 'Y', 'U', 'I', 'O', 'P', '{', '}', '|', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', 315 | 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', ']', 316 | ]; 317 | 318 | fn spatial_match_helper( 319 | password: &str, 320 | graph: &HashMap>>, 321 | graph_name: &str, 322 | ) -> Vec { 323 | let mut matches = Vec::new(); 324 | let password_len = password.chars().count(); 325 | if password_len <= 2 { 326 | return matches; 327 | } 328 | let mut i = 0; 329 | while i < password_len - 1 { 330 | let mut j = i + 1; 331 | let mut last_direction = None; 332 | let mut turns = 0; 333 | let mut shifted_count = if ["qwerty", "dvorak"].contains(&graph_name) 334 | && SHIFTED_CHARS.contains(&password.chars().nth(i).unwrap()) 335 | { 336 | 1 337 | } else { 338 | 0 339 | }; 340 | loop { 341 | let prev_char = password.chars().nth(j - 1).unwrap(); 342 | let mut found = false; 343 | let found_direction; 344 | let mut cur_direction = -1; 345 | let adjacents = graph.get(&prev_char).cloned().unwrap_or_default(); 346 | // consider growing pattern by one character if j hasn't gone over the edge. 347 | if j < password_len { 348 | let cur_char = password.chars().nth(j).unwrap(); 349 | for adj in adjacents { 350 | cur_direction += 1; 351 | if let Some(adj) = adj { 352 | if let Some(adj_position) = adj.find(cur_char) { 353 | found = true; 354 | found_direction = cur_direction; 355 | if adj_position == 1 { 356 | // index 1 in the adjacency means the key is shifted, 357 | // 0 means unshifted: A vs a, % vs 5, etc. 358 | // for example, 'q' is adjacent to the entry '2@'. 359 | // @ is shifted w/ index 1, 2 is unshifted. 360 | shifted_count += 1; 361 | } 362 | if last_direction != Some(found_direction) { 363 | // adding a turn is correct even in the initial case when last_direction is null: 364 | // every spatial pattern starts with a turn. 365 | turns += 1; 366 | last_direction = Some(found_direction); 367 | } 368 | break; 369 | } 370 | } 371 | } 372 | } 373 | if found { 374 | // if the current pattern continued, extend j and try to grow again 375 | j += 1; 376 | } else { 377 | // otherwise push the pattern discovered so far, if any... 378 | if j - i > 2 { 379 | // Don't consider length 1 or 2 chains 380 | let pattern = MatchPattern::Spatial(SpatialPattern { 381 | graph: graph_name.to_string(), 382 | turns, 383 | shifted_count, 384 | }); 385 | matches.push(Match { 386 | pattern, 387 | i, 388 | j: j - 1, 389 | token: password.chars().take(j).skip(i).collect(), 390 | ..Match::default() 391 | }); 392 | } 393 | i = j; 394 | break; 395 | } 396 | } 397 | } 398 | matches 399 | } 400 | 401 | struct RepeatMatch {} 402 | 403 | impl Matcher for RepeatMatch { 404 | fn get_matches(&self, password: &str, user_inputs: &HashMap) -> Vec { 405 | lazy_static! { 406 | static ref GREEDY_REGEX: FancyRegex = FancyRegex::new(r"(.+)\1+").unwrap(); 407 | static ref LAZY_REGEX: FancyRegex = FancyRegex::new(r"(.+?)\1+").unwrap(); 408 | static ref LAZY_ANCHORED_REGEX: FancyRegex = FancyRegex::new(r"^(.+?)\1+$").unwrap(); 409 | } 410 | 411 | let mut matches = Vec::new(); 412 | let mut last_index = 0; 413 | let char_indexable_password = CharIndexableStr::from(password); 414 | let char_count = password.chars().count(); 415 | while last_index < char_count { 416 | let token = char_indexable_password.char_index(last_index..char_count); 417 | let greedy_matches = GREEDY_REGEX.captures(token).unwrap(); 418 | if greedy_matches.is_none() { 419 | break; 420 | } 421 | let lazy_matches = LAZY_REGEX.captures(token).unwrap(); 422 | let greedy_matches = greedy_matches.unwrap(); 423 | let lazy_matches = lazy_matches.unwrap(); 424 | let m4tch; 425 | let base_token = if greedy_matches.get(0).unwrap().as_str().chars().count() 426 | > lazy_matches.get(0).unwrap().as_str().chars().count() 427 | { 428 | // greedy beats lazy for 'aabaab' 429 | // greedy: [aabaab, aab] 430 | // lazy: [aa, a] 431 | m4tch = greedy_matches; 432 | // greedy's repeated string might itself be repeated, eg. 433 | // aabaab in aabaabaabaab. 434 | // run an anchored lazy match on greedy's repeated string 435 | // to find the shortest repeated string 436 | LAZY_ANCHORED_REGEX 437 | .captures(m4tch.get(0).unwrap().as_str()) 438 | .unwrap() 439 | .unwrap() 440 | .get(1) 441 | .unwrap() 442 | .as_str() 443 | .to_string() 444 | } else { 445 | // lazy beats greedy for 'aaaaa' 446 | // greedy: [aaaa, aa] 447 | // lazy: [aaaaa, a] 448 | m4tch = lazy_matches; 449 | m4tch.get(1).unwrap().as_str().to_string() 450 | }; 451 | 452 | let m = m4tch.get(0).unwrap(); 453 | let (i, j) = ( 454 | last_index + token[..m.start()].chars().count(), 455 | last_index + token[..m.end()].chars().count() - 1, 456 | ); 457 | // recursively match and score the base string 458 | let base_analysis = super::scoring::most_guessable_match_sequence( 459 | &base_token, 460 | &omnimatch(&base_token, user_inputs), 461 | false, 462 | ); 463 | let base_matches = base_analysis.sequence; 464 | let base_guesses = base_analysis.guesses; 465 | let pattern = MatchPattern::Repeat(RepeatPattern { 466 | repeat_count: m4tch.get(0).unwrap().as_str().chars().count() 467 | / base_token.chars().count(), 468 | base_token, 469 | base_guesses, 470 | base_matches, 471 | }); 472 | matches.push(Match { 473 | pattern, 474 | i, 475 | j, 476 | token: m4tch.get(0).unwrap().as_str().to_string(), 477 | ..Match::default() 478 | }); 479 | last_index = j + 1; 480 | } 481 | matches 482 | } 483 | } 484 | 485 | const MAX_DELTA: i32 = 5; 486 | 487 | /// Identifies sequences by looking for repeated differences in unicode codepoint. 488 | /// this allows skipping, such as 9753, and also matches some extended unicode sequences 489 | /// such as Greek and Cyrillic alphabets. 490 | /// 491 | /// for example, consider the input 'abcdb975zy' 492 | /// 493 | /// password: a b c d b 9 7 5 z y 494 | /// index: 0 1 2 3 4 5 6 7 8 9 495 | /// delta: 1 1 1 -2 -41 -2 -2 69 1 496 | /// 497 | /// expected result: 498 | /// `[(i, j, delta), ...] = [(0, 3, 1), (5, 7, -2), (8, 9, 1)]` 499 | struct SequenceMatch {} 500 | 501 | impl Matcher for SequenceMatch { 502 | fn get_matches(&self, password: &str, _user_inputs: &HashMap) -> Vec { 503 | fn update(i: usize, j: usize, delta: i32, password: &str, matches: &mut Vec) { 504 | let delta_abs = delta.abs(); 505 | if (j - i > 1 || delta_abs == 1) && (0 < delta_abs && delta_abs <= MAX_DELTA) { 506 | let token = password.chars().take(j + 1).skip(i).collect::(); 507 | let first_chr = token.chars().next().unwrap(); 508 | let (sequence_name, sequence_space) = if first_chr.is_lowercase() { 509 | ("lower", 26) 510 | } else if first_chr.is_uppercase() { 511 | ("upper", 26) 512 | } else if first_chr.is_ascii_digit() { 513 | ("digits", 10) 514 | } else { 515 | // conservatively stick with roman alphabet size. 516 | // (this could be improved) 517 | ("unicode", 26) 518 | }; 519 | let pattern = MatchPattern::Sequence(SequencePattern { 520 | sequence_name: sequence_name.to_owned(), 521 | sequence_space, 522 | ascending: delta > 0, 523 | }); 524 | matches.push(Match { 525 | pattern, 526 | i, 527 | j, 528 | token, 529 | ..Match::default() 530 | }); 531 | } 532 | } 533 | 534 | let mut matches = Vec::new(); 535 | 536 | let password_len = password.chars().count(); 537 | if password_len <= 1 { 538 | return matches; 539 | } 540 | 541 | let mut i = 0; 542 | let mut j; 543 | let mut last_delta = 0; 544 | 545 | for k in 1..password_len { 546 | let delta = password.chars().nth(k).unwrap() as i32 547 | - password.chars().nth(k - 1).unwrap() as i32; 548 | if last_delta == 0 { 549 | last_delta = delta; 550 | } 551 | if last_delta == delta { 552 | continue; 553 | } 554 | j = k - 1; 555 | update(i, j, last_delta, password, &mut matches); 556 | i = j; 557 | last_delta = delta; 558 | } 559 | update(i, password_len - 1, last_delta, password, &mut matches); 560 | matches 561 | } 562 | } 563 | 564 | struct RegexMatch {} 565 | 566 | impl Matcher for RegexMatch { 567 | fn get_matches(&self, password: &str, _user_inputs: &HashMap) -> Vec { 568 | let mut matches = Vec::new(); 569 | for (&name, regex) in REGEXES.iter() { 570 | for capture in regex.captures_iter(password) { 571 | let m = capture.get(0).unwrap(); 572 | let pattern = MatchPattern::Regex(RegexPattern { 573 | regex_name: name.to_owned(), 574 | regex_match: capture 575 | .iter() 576 | .map(|x| x.unwrap().as_str().to_string()) 577 | .collect(), 578 | }); 579 | let (i, j) = ( 580 | password[..m.start()].chars().count(), 581 | password[..m.end()].chars().count() - 1, 582 | ); 583 | matches.push(Match { 584 | pattern, 585 | token: m.as_str().to_string(), 586 | i, 587 | j, 588 | ..Match::default() 589 | }); 590 | } 591 | } 592 | matches 593 | } 594 | } 595 | 596 | lazy_static! { 597 | static ref REGEXES: HashMap<&'static str, Regex> = { 598 | let mut table = HashMap::with_capacity(1); 599 | table.insert("recent_year", Regex::new(r"19[0-9]{2}|20[0-9]{2}").unwrap()); 600 | table 601 | }; 602 | } 603 | 604 | /// a "date" is recognized as: 605 | /// any 3-tuple that starts or ends with a 2- or 4-digit year, 606 | /// with 2 or 0 separator chars (1.1.91 or 1191), 607 | /// maybe zero-padded (01-01-91 vs 1-1-91), 608 | /// a month between 1 and 12, 609 | /// a day between 1 and 31. 610 | /// 611 | /// note: this isn't true date parsing in that "feb 31st" is allowed, 612 | /// this doesn't check for leap years, etc. 613 | /// 614 | /// recipe: 615 | /// start with regex to find maybe-dates, then attempt to map the integers 616 | /// onto month-day-year to filter the maybe-dates into dates. 617 | /// finally, remove matches that are substrings of other matches to reduce noise. 618 | /// 619 | /// note: instead of using a lazy or greedy regex to find many dates over the full string, 620 | /// this uses a ^...$ regex against every substring of the password -- less performant but leads 621 | /// to every possible date match. 622 | struct DateMatch {} 623 | 624 | impl Matcher for DateMatch { 625 | fn get_matches(&self, password: &str, _user_inputs: &HashMap) -> Vec { 626 | let mut matches = Vec::new(); 627 | let char_indexable = CharIndexableStr::from(password); 628 | 629 | let password_len = password.chars().count(); 630 | // dates without separators are between length 4 '1191' and 8 '11111991' 631 | if password_len < 4 { 632 | return matches; 633 | } 634 | for i in 0..(password_len - 3) { 635 | for j in (i + 3)..(i + 8) { 636 | if j >= password_len { 637 | break; 638 | } 639 | let token_str = char_indexable.char_index(i..j + 1); 640 | if !MAYBE_DATE_NO_SEPARATOR_REGEX.is_match(token_str) { 641 | continue; 642 | } 643 | let token = CharIndexableStr::from(token_str); 644 | let mut candidates = Vec::new(); 645 | for &(k, l) in &DATE_SPLITS[&token.char_count()] { 646 | let ymd = map_ints_to_ymd( 647 | token.char_index(0..k).parse().unwrap(), 648 | token.char_index(k..l).parse().unwrap(), 649 | token.char_index(l..j + 1).parse().unwrap(), 650 | ); 651 | if let Some(ymd) = ymd { 652 | candidates.push(ymd); 653 | } 654 | } 655 | if candidates.is_empty() { 656 | continue; 657 | } 658 | // at this point: different possible ymd mappings for the same i,j substring. 659 | // match the candidate date that likely takes the fewest guesses: a year closest to 2000. 660 | // (scoring.REFERENCE_YEAR). 661 | // 662 | // ie, considering '111504', prefer 11-15-04 to 1-1-1504 663 | // (interpreting '04' as 2004) 664 | let metric = |candidate: &(i32, i8, i8)| { 665 | (candidate.0 - *super::scoring::REFERENCE_YEAR).abs() 666 | }; 667 | let best_candidate = candidates.iter().min_by_key(|&c| metric(c)).unwrap(); 668 | let pattern = MatchPattern::Date(DatePattern { 669 | separator: String::new(), 670 | year: best_candidate.0, 671 | month: best_candidate.1, 672 | day: best_candidate.2, 673 | }); 674 | matches.push(Match { 675 | pattern, 676 | token: token_str.to_string(), 677 | i, 678 | j, 679 | ..Match::default() 680 | }); 681 | } 682 | } 683 | 684 | // dates with separators are between length 6 '1/1/91' and 10 '11/11/1991' 685 | if password_len >= 6 { 686 | for i in 0..(password_len - 5) { 687 | for j in (i + 5)..(i + 10) { 688 | if j >= password_len { 689 | break; 690 | } 691 | let token = char_indexable.char_index(i..j + 1); 692 | let (ymd, separator) = { 693 | let captures = MAYBE_DATE_WITH_SEPARATOR_REGEX.captures(token); 694 | if captures.is_none() { 695 | continue; 696 | } 697 | let captures = captures.unwrap(); 698 | if captures[2] != captures[4] { 699 | // Original code uses regex backreferences, Rust doesn't support these. 700 | // Need to manually test that group 2 and 4 are the same 701 | continue; 702 | } 703 | ( 704 | map_ints_to_ymd( 705 | captures[1].parse().unwrap(), 706 | captures[3].parse().unwrap(), 707 | captures[5].parse().unwrap(), 708 | ), 709 | captures[2].to_string(), 710 | ) 711 | }; 712 | if let Some(ymd) = ymd { 713 | let pattern = MatchPattern::Date(DatePattern { 714 | separator, 715 | year: ymd.0, 716 | month: ymd.1, 717 | day: ymd.2, 718 | }); 719 | matches.push(Match { 720 | pattern, 721 | token: token.to_string(), 722 | i, 723 | j, 724 | ..Match::default() 725 | }); 726 | } 727 | } 728 | } 729 | } 730 | 731 | matches 732 | .iter() 733 | .filter(|&x| !matches.iter().any(|y| *x != *y && y.i <= x.i && y.j >= x.j)) 734 | .cloned() 735 | .collect() 736 | } 737 | } 738 | 739 | /// Takes three ints and returns them in a (y, m, d) tuple 740 | fn map_ints_to_ymd(first: u16, second: u16, third: u16) -> Option<(i32, i8, i8)> { 741 | // given a 3-tuple, discard if: 742 | // middle int is over 31 (for all ymd formats, years are never allowed in the middle) 743 | // middle int is zero 744 | // any int is over the max allowable year 745 | // any int is over two digits but under the min allowable year 746 | // 2 ints are over 31, the max allowable day 747 | // 2 ints are zero 748 | // all ints are over 12, the max allowable month 749 | if second > 31 || second == 0 { 750 | return None; 751 | } 752 | let mut over_12 = 0; 753 | let mut over_31 = 0; 754 | let mut zero = 0; 755 | for &i in &[first, second, third] { 756 | if 99 < i && i < DATE_MIN_YEAR || i > DATE_MAX_YEAR { 757 | return None; 758 | } 759 | if i > 31 { 760 | over_31 += 1; 761 | } 762 | if i > 12 { 763 | over_12 += 1; 764 | } 765 | if i == 0 { 766 | zero += 1; 767 | } 768 | } 769 | if over_31 >= 2 || over_12 == 3 || zero >= 2 { 770 | return None; 771 | } 772 | 773 | // first look for a four digit year: yyyy + daymonth or daymonth + yyyy 774 | let possible_year_splits = &[(third, first, second), (first, second, third)]; 775 | for &(year, second, third) in possible_year_splits { 776 | if (DATE_MIN_YEAR..=DATE_MAX_YEAR).contains(&year) { 777 | let dm = map_ints_to_md(second, third); 778 | if let Some(dm) = dm { 779 | return Some((i32::from(year), dm.0, dm.1)); 780 | } else { 781 | // for a candidate that includes a four-digit year, 782 | // when the remaining ints don't match to a day and month, 783 | // it is not a date. 784 | return None; 785 | } 786 | } 787 | } 788 | 789 | // given no four-digit year, two digit years are the most flexible int to match, so 790 | // try to parse a day-month out of (first, second) or (second, first) 791 | for &(year, second, third) in possible_year_splits { 792 | let dm = map_ints_to_md(second, third); 793 | if let Some(dm) = dm { 794 | let year = two_to_four_digit_year(year); 795 | return Some((i32::from(year), dm.0, dm.1)); 796 | } 797 | } 798 | 799 | None 800 | } 801 | 802 | /// Takes two ints and returns them in a (m, d) tuple 803 | fn map_ints_to_md(first: u16, second: u16) -> Option<(i8, i8)> { 804 | for &(d, m) in &[(first, second), (second, first)] { 805 | if (1..=31).contains(&d) && (1..=12).contains(&m) { 806 | return Some((m as i8, d as i8)); 807 | } 808 | } 809 | None 810 | } 811 | 812 | fn two_to_four_digit_year(year: u16) -> u16 { 813 | if year > 99 { 814 | year 815 | } else if year > 50 { 816 | // 87 -> 1987 817 | year + 1900 818 | } else { 819 | // 15 -> 2015 820 | year + 2000 821 | } 822 | } 823 | 824 | const DATE_MIN_YEAR: u16 = 1000; 825 | const DATE_MAX_YEAR: u16 = 2050; 826 | lazy_static! { 827 | static ref DATE_SPLITS: HashMap> = { 828 | let mut table = HashMap::with_capacity(5); 829 | // for length-4 strings, eg 1191 or 9111, two ways to split: 830 | // 1 1 91 (2nd split starts at index 1, 3rd at index 2) 831 | // 91 1 1 832 | table.insert(4, vec![(1, 2), (2, 3)]); 833 | // 1 11 91 834 | // 11 1 91 835 | table.insert(5, vec![(1, 3), (2, 3)]); 836 | // 1 1 1991 837 | // 11 11 91 838 | // 1991 1 1 839 | table.insert(6, vec![(1, 2), (2, 4), (4, 5)]); 840 | // 1 11 1991 841 | // 11 1 1991 842 | // 1991 1 11 843 | // 1991 11 1 844 | table.insert(7, vec![(1, 3), (2, 3), (4, 5), (4, 6)]); 845 | // 11 11 1991 846 | // 1991 11 11 847 | table.insert(8, vec![(2, 4), (4, 6)]); 848 | table 849 | }; 850 | static ref MAYBE_DATE_NO_SEPARATOR_REGEX: Regex = Regex::new(r"^[0-9]{4,8}$").unwrap(); 851 | static ref MAYBE_DATE_WITH_SEPARATOR_REGEX: Regex = Regex::new(r"^([0-9]{1,4})([\s/\\_.-])([0-9]{1,2})([\s/\\_.-])([0-9]{1,4})$").unwrap(); 852 | } 853 | 854 | #[cfg(test)] 855 | mod tests { 856 | use crate::matching; 857 | use crate::matching::patterns::*; 858 | use crate::matching::Matcher; 859 | use std::collections::HashMap; 860 | 861 | #[test] 862 | fn test_translate() { 863 | let chr_map = vec![('a', 'A'), ('b', 'B')] 864 | .into_iter() 865 | .collect::>(); 866 | let test_data = [ 867 | ("a", chr_map.clone(), "A"), 868 | ("c", chr_map.clone(), "c"), 869 | ("ab", chr_map.clone(), "AB"), 870 | ("abc", chr_map.clone(), "ABc"), 871 | ("aa", chr_map.clone(), "AA"), 872 | ("abab", chr_map.clone(), "ABAB"), 873 | ("", chr_map, ""), 874 | ("", HashMap::new(), ""), 875 | ("abc", HashMap::new(), "abc"), 876 | ]; 877 | for &(string, ref map, result) in &test_data { 878 | assert_eq!(matching::translate(string, map), result); 879 | } 880 | } 881 | 882 | #[test] 883 | fn test_dictionary_matches_words_that_contain_other_words() { 884 | let matches = (matching::DictionaryMatch {}).get_matches("motherboard", &HashMap::new()); 885 | let patterns = ["mother", "motherboard", "board"]; 886 | let ijs = [(0, 5), (0, 10), (6, 10)]; 887 | for (k, &pattern) in patterns.iter().enumerate() { 888 | let m = matches.iter().find(|m| m.token == *pattern).unwrap(); 889 | let (i, j) = ijs[k]; 890 | assert_eq!(m.i, i); 891 | assert_eq!(m.j, j); 892 | if let MatchPattern::Dictionary(ref p) = m.pattern { 893 | p 894 | } else { 895 | panic!("Wrong match pattern") 896 | }; 897 | } 898 | } 899 | 900 | #[test] 901 | fn test_dictionary_matches_multiple_words_when_they_overlap() { 902 | let matches = (matching::DictionaryMatch {}).get_matches("1abcdef12", &HashMap::new()); 903 | let patterns = ["1abcdef", "abcdef12"]; 904 | let ijs = [(0, 6), (1, 8)]; 905 | for (k, &pattern) in patterns.iter().enumerate() { 906 | let m = matches.iter().find(|m| m.token == *pattern).unwrap(); 907 | let (i, j) = ijs[k]; 908 | assert_eq!(m.i, i); 909 | assert_eq!(m.j, j); 910 | if let MatchPattern::Dictionary(ref p) = m.pattern { 911 | p 912 | } else { 913 | panic!("Wrong match pattern") 914 | }; 915 | } 916 | } 917 | 918 | #[test] 919 | fn test_dictionary_ignores_uppercasing() { 920 | let matches = (matching::DictionaryMatch {}).get_matches("BoaRdZ", &HashMap::new()); 921 | let patterns = ["BoaRd"]; 922 | let ijs = [(0, 4)]; 923 | for (k, &pattern) in patterns.iter().enumerate() { 924 | let m = matches.iter().find(|m| m.token == *pattern).unwrap(); 925 | let (i, j) = ijs[k]; 926 | assert_eq!(m.i, i); 927 | assert_eq!(m.j, j); 928 | if let MatchPattern::Dictionary(ref p) = m.pattern { 929 | p 930 | } else { 931 | panic!("Wrong match pattern") 932 | }; 933 | } 934 | } 935 | 936 | #[test] 937 | fn test_dictionary_identifies_words_surrounded_by_non_words() { 938 | let matches = (matching::DictionaryMatch {}).get_matches("asdf1234&*", &HashMap::new()); 939 | let patterns = ["asdf", "asdf1234"]; 940 | let ijs = [(0, 3), (0, 7)]; 941 | for (k, &pattern) in patterns.iter().enumerate() { 942 | let m = matches.iter().find(|m| m.token == *pattern).unwrap(); 943 | let (i, j) = ijs[k]; 944 | assert_eq!(m.i, i); 945 | assert_eq!(m.j, j); 946 | if let MatchPattern::Dictionary(ref p) = m.pattern { 947 | p 948 | } else { 949 | panic!("Wrong match pattern") 950 | }; 951 | } 952 | } 953 | 954 | #[test] 955 | fn test_dictionary_matches_user_inputs() { 956 | use crate::frequency_lists::DictionaryType; 957 | let user_inputs = [("bejeebus".to_string(), 1)] 958 | .iter() 959 | .cloned() 960 | .collect::>(); 961 | let matches = (matching::DictionaryMatch {}).get_matches("bejeebus", &user_inputs); 962 | let patterns = ["bejeebus"]; 963 | let ijs = [(0, 7)]; 964 | for (k, &pattern) in patterns.iter().enumerate() { 965 | let m = matches.iter().find(|m| m.token == *pattern).unwrap(); 966 | let (i, j) = ijs[k]; 967 | assert_eq!(m.i, i); 968 | assert_eq!(m.j, j); 969 | let p = if let MatchPattern::Dictionary(ref p) = m.pattern { 970 | p 971 | } else { 972 | panic!("Wrong match pattern") 973 | }; 974 | assert_eq!(p.dictionary_name, DictionaryType::UserInputs); 975 | } 976 | } 977 | 978 | #[test] 979 | fn test_dictionary_matches_against_reversed_words() { 980 | let matches = (matching::ReverseDictionaryMatch {}).get_matches("rehtom", &HashMap::new()); 981 | let patterns = ["rehtom"]; 982 | let ijs = [(0, 5)]; 983 | for (k, &pattern) in patterns.iter().enumerate() { 984 | let m = matches.iter().find(|m| m.token == *pattern).unwrap(); 985 | let (i, j) = ijs[k]; 986 | assert_eq!(m.i, i); 987 | assert_eq!(m.j, j); 988 | let p = if let MatchPattern::Dictionary(ref p) = m.pattern { 989 | p 990 | } else { 991 | panic!("Wrong match pattern") 992 | }; 993 | assert_eq!(p.reversed, true); 994 | } 995 | } 996 | 997 | #[test] 998 | fn test_reduces_l33t_table_to_only_relevant_substitutions() { 999 | let test_data = vec![ 1000 | ("", HashMap::new()), 1001 | ("a", HashMap::new()), 1002 | ("4", vec![('a', vec!['4'])].into_iter().collect()), 1003 | ("4@", vec![('a', vec!['4', '@'])].into_iter().collect()), 1004 | ( 1005 | "4({60", 1006 | vec![ 1007 | ('a', vec!['4']), 1008 | ('c', vec!['(', '{']), 1009 | ('g', vec!['6']), 1010 | ('o', vec!['0']), 1011 | ] 1012 | .into_iter() 1013 | .collect(), 1014 | ), 1015 | ]; 1016 | for (pw, expected) in test_data { 1017 | assert_eq!(matching::relevant_l33t_subtable(pw), expected); 1018 | } 1019 | } 1020 | 1021 | #[test] 1022 | fn test_enumerates_sets_of_l33t_subs_a_password_might_be_using() { 1023 | let test_data = vec![ 1024 | (HashMap::new(), vec![HashMap::new()]), 1025 | ( 1026 | vec![('a', vec!['@'])].into_iter().collect(), 1027 | vec![vec![('@', 'a')].into_iter().collect()], 1028 | ), 1029 | ( 1030 | vec![('a', vec!['@', '4'])].into_iter().collect(), 1031 | vec![ 1032 | vec![('@', 'a')].into_iter().collect(), 1033 | vec![('4', 'a')].into_iter().collect(), 1034 | ], 1035 | ), 1036 | ( 1037 | vec![('a', vec!['@', '4']), ('c', vec!['('])] 1038 | .into_iter() 1039 | .collect(), 1040 | vec![ 1041 | vec![('@', 'a'), ('(', 'c')].into_iter().collect(), 1042 | vec![('4', 'a'), ('(', 'c')].into_iter().collect(), 1043 | ], 1044 | ), 1045 | ]; 1046 | for (table, subs) in test_data { 1047 | assert_eq!(matching::enumerate_l33t_replacements(&table), subs); 1048 | } 1049 | } 1050 | 1051 | #[test] 1052 | fn test_dictionary_matches_against_l33t_words() { 1053 | let matches = (matching::L33tMatch {}).get_matches("m0th3r", &HashMap::new()); 1054 | let patterns = ["m0th3r"]; 1055 | let ijs = [(0, 5)]; 1056 | for (k, &pattern) in patterns.iter().enumerate() { 1057 | let m = matches.iter().find(|m| m.token == *pattern).unwrap(); 1058 | let (i, j) = ijs[k]; 1059 | assert_eq!(m.i, i); 1060 | assert_eq!(m.j, j); 1061 | let p = if let MatchPattern::Dictionary(ref p) = m.pattern { 1062 | p 1063 | } else { 1064 | panic!("Wrong match pattern") 1065 | }; 1066 | assert_eq!(p.l33t, true); 1067 | } 1068 | } 1069 | 1070 | #[test] 1071 | fn test_dictionary_matches_overlapping_l33ted_words() { 1072 | let matches = (matching::L33tMatch {}).get_matches("p@ssw0rd", &HashMap::new()); 1073 | let patterns = ["p@ss", "@ssw0rd"]; 1074 | let ijs = [(0, 3), (1, 7)]; 1075 | for (k, &pattern) in patterns.iter().enumerate() { 1076 | let m = matches.iter().find(|m| m.token == *pattern).unwrap(); 1077 | let (i, j) = ijs[k]; 1078 | assert_eq!(m.i, i); 1079 | assert_eq!(m.j, j); 1080 | let p = if let MatchPattern::Dictionary(ref p) = m.pattern { 1081 | p 1082 | } else { 1083 | panic!("Wrong match pattern") 1084 | }; 1085 | assert_eq!(p.l33t, true); 1086 | } 1087 | } 1088 | 1089 | #[test] 1090 | fn test_doesnt_match_when_multiple_l33t_subs_needed_for_same_letter() { 1091 | let matches = (matching::L33tMatch {}).get_matches("p4@ssword", &HashMap::new()); 1092 | assert!(!matches.iter().any(|m| &m.token == "p4@ssword")); 1093 | } 1094 | 1095 | #[test] 1096 | fn test_doesnt_match_single_character_l33ted_words() { 1097 | let matches = (matching::L33tMatch {}).get_matches("4 ( @", &HashMap::new()); 1098 | assert!(matches.is_empty()); 1099 | } 1100 | 1101 | #[test] 1102 | fn test_doesnt_match_1_and_2_char_spatial_patterns() { 1103 | for password in &["", "/", "qw", "*/"] { 1104 | let result = (matching::SpatialMatch {}).get_matches(password, &HashMap::new()); 1105 | assert!(!result.into_iter().any(|m| m.token == *password)); 1106 | } 1107 | } 1108 | 1109 | #[test] 1110 | fn test_matches_spatial_patterns_surrounded_by_non_spatial_patterns() { 1111 | let password = "6tfGHJ"; 1112 | let m = (matching::SpatialMatch {}) 1113 | .get_matches(password, &HashMap::new()) 1114 | .into_iter() 1115 | .find(|m| m.token == *password) 1116 | .unwrap(); 1117 | let p = if let MatchPattern::Spatial(ref p) = m.pattern { 1118 | p 1119 | } else { 1120 | panic!("Wrong match pattern") 1121 | }; 1122 | assert_eq!(p.graph, "qwerty".to_string()); 1123 | assert_eq!(p.turns, 2); 1124 | assert_eq!(p.shifted_count, 3); 1125 | } 1126 | 1127 | #[test] 1128 | fn test_matches_pattern_as_a_keyboard_pattern() { 1129 | let test_data = vec![ 1130 | ("12345", "qwerty", 1, 0), 1131 | ("@WSX", "qwerty", 1, 4), 1132 | ("6tfGHJ", "qwerty", 2, 3), 1133 | ("hGFd", "qwerty", 1, 2), 1134 | ("/;p09876yhn", "qwerty", 3, 0), 1135 | ("Xdr%", "qwerty", 1, 2), 1136 | ("159-", "keypad", 1, 0), 1137 | ("*84", "keypad", 1, 0), 1138 | ("/8520", "keypad", 1, 0), 1139 | ("369", "keypad", 1, 0), 1140 | ("/963.", "mac_keypad", 1, 0), 1141 | ("*-632.0214", "mac_keypad", 9, 0), 1142 | ("aoEP%yIxkjq:", "dvorak", 4, 5), 1143 | (";qoaOQ:Aoq;a", "dvorak", 11, 4), 1144 | ]; 1145 | for (password, keyboard, turns, shifts) in test_data { 1146 | let matches = (matching::SpatialMatch {}).get_matches(password, &HashMap::new()); 1147 | let m = matches 1148 | .into_iter() 1149 | .find(|m| { 1150 | if let MatchPattern::Spatial(ref p) = m.pattern { 1151 | if m.token == *password && p.graph == keyboard { 1152 | return true; 1153 | } 1154 | }; 1155 | false 1156 | }) 1157 | .unwrap(); 1158 | let p = if let MatchPattern::Spatial(ref p) = m.pattern { 1159 | p 1160 | } else { 1161 | panic!("Wrong match pattern") 1162 | }; 1163 | assert_eq!(p.turns, turns); 1164 | assert_eq!(p.shifted_count, shifts); 1165 | } 1166 | } 1167 | 1168 | #[test] 1169 | fn test_doesnt_match_len_1_sequences() { 1170 | for &password in &["", "a", "1"] { 1171 | assert_eq!( 1172 | (matching::SequenceMatch {}).get_matches(password, &HashMap::new()), 1173 | Vec::new() 1174 | ); 1175 | } 1176 | } 1177 | 1178 | #[test] 1179 | fn test_matches_overlapping_sequences() { 1180 | let password = "abcbabc"; 1181 | let matches = (matching::SequenceMatch {}).get_matches(password, &HashMap::new()); 1182 | for &(pattern, i, j, ascending) in &[ 1183 | ("abc", 0, 2, true), 1184 | ("cba", 2, 4, false), 1185 | ("abc", 4, 6, true), 1186 | ] { 1187 | let m = matches 1188 | .iter() 1189 | .find(|m| m.token == *pattern && m.i == i && m.j == j) 1190 | .unwrap(); 1191 | let p = if let MatchPattern::Sequence(ref p) = m.pattern { 1192 | p 1193 | } else { 1194 | panic!("Wrong match pattern") 1195 | }; 1196 | assert_eq!(p.ascending, ascending); 1197 | } 1198 | } 1199 | 1200 | #[test] 1201 | fn test_matches_embedded_sequence_patterns() { 1202 | let password = "!jihg22"; 1203 | let matches = (matching::SequenceMatch {}).get_matches(password, &HashMap::new()); 1204 | let m = matches.iter().find(|m| &m.token == "jihg").unwrap(); 1205 | let p = if let MatchPattern::Sequence(ref p) = m.pattern { 1206 | p 1207 | } else { 1208 | panic!("Wrong match pattern") 1209 | }; 1210 | assert_eq!(p.sequence_name, "lower"); 1211 | assert_eq!(p.ascending, false); 1212 | } 1213 | 1214 | #[test] 1215 | fn test_matches_pattern_as_sequence() { 1216 | let test_data = [ 1217 | ("ABC", "upper", true), 1218 | ("CBA", "upper", false), 1219 | ("PQR", "upper", true), 1220 | ("RQP", "upper", false), 1221 | ("XYZ", "upper", true), 1222 | ("ZYX", "upper", false), 1223 | ("abcd", "lower", true), 1224 | ("dcba", "lower", false), 1225 | ("jihg", "lower", false), 1226 | ("wxyz", "lower", true), 1227 | ("zxvt", "lower", false), 1228 | ("0369", "digits", true), 1229 | ("97531", "digits", false), 1230 | ]; 1231 | for &(pattern, name, is_ascending) in &test_data { 1232 | let matches = (matching::SequenceMatch {}).get_matches(pattern, &HashMap::new()); 1233 | let m = matches.iter().find(|m| m.token == *pattern).unwrap(); 1234 | assert_eq!(m.i, 0); 1235 | assert_eq!(m.j, pattern.len() - 1); 1236 | let p = if let MatchPattern::Sequence(ref p) = m.pattern { 1237 | p 1238 | } else { 1239 | panic!("Wrong match pattern") 1240 | }; 1241 | assert_eq!(p.sequence_name, name); 1242 | assert_eq!(p.ascending, is_ascending); 1243 | } 1244 | } 1245 | 1246 | #[test] 1247 | fn test_doesnt_match_len_1_repeat_patterns() { 1248 | for &password in &["", "#"] { 1249 | assert_eq!( 1250 | (matching::RepeatMatch {}).get_matches(password, &HashMap::new()), 1251 | Vec::new() 1252 | ); 1253 | } 1254 | } 1255 | 1256 | #[test] 1257 | fn test_matches_embedded_repeat_patterns() { 1258 | let password = "y4@&&&&&u%7"; 1259 | let (i, j) = (3, 7); 1260 | let matches = (matching::RepeatMatch {}).get_matches(password, &HashMap::new()); 1261 | let m = matches.iter().find(|m| &m.token == "&&&&&").unwrap(); 1262 | assert_eq!(m.i, i); 1263 | assert_eq!(m.j, j); 1264 | let p = if let MatchPattern::Repeat(ref p) = m.pattern { 1265 | p 1266 | } else { 1267 | panic!("Wrong match pattern") 1268 | }; 1269 | assert_eq!(p.base_token, "&".to_string()); 1270 | } 1271 | 1272 | #[test] 1273 | fn test_repeats_with_base_character() { 1274 | for len in 3..13 { 1275 | for &chr in &['a', 'Z', '4', '&'] { 1276 | let password = (0..len).map(|_| chr).collect::(); 1277 | let matches = (matching::RepeatMatch {}).get_matches(&password, &HashMap::new()); 1278 | let m = matches 1279 | .iter() 1280 | .find(|m| { 1281 | if let MatchPattern::Repeat(ref p) = m.pattern { 1282 | if p.base_token == format!("{}", chr) { 1283 | return true; 1284 | } 1285 | }; 1286 | false 1287 | }) 1288 | .unwrap(); 1289 | assert_eq!(m.i, 0); 1290 | assert_eq!(m.j, len - 1); 1291 | } 1292 | } 1293 | } 1294 | 1295 | #[test] 1296 | fn test_multiple_adjacent_repeats() { 1297 | let password = "BBB1111aaaaa@@@@@@"; 1298 | let matches = (matching::RepeatMatch {}).get_matches(password, &HashMap::new()); 1299 | let test_data = [ 1300 | ("BBB", 0, 2), 1301 | ("1111", 3, 6), 1302 | ("aaaaa", 7, 11), 1303 | ("@@@@@@", 12, 17), 1304 | ]; 1305 | for &(pattern, i, j) in &test_data { 1306 | let m = matches.iter().find(|m| m.token == pattern).unwrap(); 1307 | assert_eq!(m.i, i); 1308 | assert_eq!(m.j, j); 1309 | let p = if let MatchPattern::Repeat(ref p) = m.pattern { 1310 | p 1311 | } else { 1312 | panic!("Wrong match pattern") 1313 | }; 1314 | assert_eq!(p.base_token, pattern[0..1].to_string()); 1315 | } 1316 | } 1317 | 1318 | #[test] 1319 | fn test_multiple_non_adjacent_repeats() { 1320 | let password = "2818BBBbzsdf1111@*&@!aaaaaEUDA@@@@@@1729"; 1321 | let matches = (matching::RepeatMatch {}).get_matches(password, &HashMap::new()); 1322 | let test_data = [ 1323 | ("BBB", 4, 6), 1324 | ("1111", 12, 15), 1325 | ("aaaaa", 21, 25), 1326 | ("@@@@@@", 30, 35), 1327 | ]; 1328 | for &(pattern, i, j) in &test_data { 1329 | let m = matches.iter().find(|m| m.token == pattern).unwrap(); 1330 | assert_eq!(m.i, i); 1331 | assert_eq!(m.j, j); 1332 | let p = if let MatchPattern::Repeat(ref p) = m.pattern { 1333 | p 1334 | } else { 1335 | panic!("Wrong match pattern") 1336 | }; 1337 | assert_eq!(p.base_token, pattern[0..1].to_string()); 1338 | } 1339 | } 1340 | 1341 | #[test] 1342 | fn test_multiple_character_repeats() { 1343 | let password = "abab"; 1344 | let (i, j) = (0, 3); 1345 | let matches = (matching::RepeatMatch {}).get_matches(password, &HashMap::new()); 1346 | let m = matches.iter().find(|m| m.token == *password).unwrap(); 1347 | assert_eq!(m.i, i); 1348 | assert_eq!(m.j, j); 1349 | let p = if let MatchPattern::Repeat(ref p) = m.pattern { 1350 | p 1351 | } else { 1352 | panic!("Wrong match pattern") 1353 | }; 1354 | assert_eq!(p.base_token, "ab".to_string()); 1355 | } 1356 | 1357 | #[test] 1358 | fn test_matches_longest_repeat() { 1359 | let password = "aabaab"; 1360 | let (i, j) = (0, 5); 1361 | let matches = (matching::RepeatMatch {}).get_matches(password, &HashMap::new()); 1362 | let m = matches.iter().find(|m| m.token == *password).unwrap(); 1363 | assert_eq!(m.i, i); 1364 | assert_eq!(m.j, j); 1365 | let p = if let MatchPattern::Repeat(ref p) = m.pattern { 1366 | p 1367 | } else { 1368 | panic!("Wrong match pattern") 1369 | }; 1370 | assert_eq!(p.base_token, "aab".to_string()); 1371 | } 1372 | 1373 | #[test] 1374 | fn test_identifies_simplest_repeat() { 1375 | let password = "abababab"; 1376 | let (i, j) = (0, 7); 1377 | let matches = (matching::RepeatMatch {}).get_matches(password, &HashMap::new()); 1378 | let m = matches.iter().find(|m| m.token == *password).unwrap(); 1379 | assert_eq!(m.i, i); 1380 | assert_eq!(m.j, j); 1381 | let p = if let MatchPattern::Repeat(ref p) = m.pattern { 1382 | p 1383 | } else { 1384 | panic!("Wrong match pattern") 1385 | }; 1386 | assert_eq!(p.base_token, "ab".to_string()); 1387 | } 1388 | 1389 | #[test] 1390 | fn test_identifies_repeat_with_multibyte_utf8() { 1391 | let password = "x\u{1F431}\u{1F436}\u{1F431}\u{1F436}"; 1392 | let (i, j) = (1, 4); 1393 | let matches = (matching::RepeatMatch {}).get_matches(password, &HashMap::new()); 1394 | let m = matches.iter().find(|m| m.token == password[1..]).unwrap(); 1395 | assert_eq!(m.i, i); 1396 | assert_eq!(m.j, j); 1397 | let p = if let MatchPattern::Repeat(ref p) = m.pattern { 1398 | p 1399 | } else { 1400 | panic!("Wrong match pattern") 1401 | }; 1402 | assert_eq!(p.base_token, "\u{1F431}\u{1F436}".to_string()); 1403 | } 1404 | 1405 | #[test] 1406 | fn test_regex_matching() { 1407 | let test_data = [("1922", "recent_year"), ("2017", "recent_year")]; 1408 | for &(pattern, name) in &test_data { 1409 | let matches = (matching::RegexMatch {}).get_matches(pattern, &HashMap::new()); 1410 | let m = matches.iter().find(|m| m.token == *pattern).unwrap(); 1411 | assert_eq!(m.i, 0); 1412 | assert_eq!(m.j, pattern.len() - 1); 1413 | let p = if let MatchPattern::Regex(ref p) = m.pattern { 1414 | p 1415 | } else { 1416 | panic!("Wrong match pattern") 1417 | }; 1418 | assert_eq!(p.regex_name, name); 1419 | } 1420 | } 1421 | 1422 | #[test] 1423 | fn test_date_matching_with_various_separators() { 1424 | let separators = ["", " ", "-", "/", "\\", "_", "."]; 1425 | for sep in &separators { 1426 | let password = format!("13{}2{}1921", sep, sep); 1427 | let matches = (matching::DateMatch {}).get_matches(&password, &HashMap::new()); 1428 | let m = matches.iter().find(|m| m.token == password).unwrap(); 1429 | assert_eq!(m.i, 0); 1430 | assert_eq!(m.j, password.len() - 1); 1431 | let p = if let MatchPattern::Date(ref p) = m.pattern { 1432 | p 1433 | } else { 1434 | panic!("Wrong match pattern") 1435 | }; 1436 | assert_eq!(p.year, 1921); 1437 | assert_eq!(p.month, 2); 1438 | assert_eq!(p.day, 13); 1439 | assert_eq!(p.separator, sep.to_string()); 1440 | } 1441 | } 1442 | 1443 | #[test] 1444 | fn test_date_matches_year_closest_to_reference_year() { 1445 | let now = time::OffsetDateTime::now_utc(); 1446 | let password = format!("1115{}", now.year() % 100); 1447 | let matches = (matching::DateMatch {}).get_matches(&password, &HashMap::new()); 1448 | let m = matches.iter().find(|m| m.token == password).unwrap(); 1449 | assert_eq!(m.i, 0); 1450 | assert_eq!(m.j, password.len() - 1); 1451 | let p = if let MatchPattern::Date(ref p) = m.pattern { 1452 | p 1453 | } else { 1454 | panic!("Wrong match pattern") 1455 | }; 1456 | assert_eq!(p.year, now.year()); 1457 | assert_eq!(p.month, 11); 1458 | assert_eq!(p.day, 15); 1459 | assert_eq!(p.separator, "".to_string()); 1460 | } 1461 | 1462 | #[test] 1463 | fn test_date_matches() { 1464 | let test_data = [(1, 1, 1999), (11, 8, 2000), (9, 12, 2005), (22, 11, 1551)]; 1465 | for &(day, month, year) in &test_data { 1466 | let password = format!("{}{}{}", year, month, day); 1467 | let matches = (matching::DateMatch {}).get_matches(&password, &HashMap::new()); 1468 | let m = matches.iter().find(|m| m.token == password).unwrap(); 1469 | assert_eq!(m.i, 0); 1470 | assert_eq!(m.j, password.len() - 1); 1471 | let p = if let MatchPattern::Date(ref p) = m.pattern { 1472 | p 1473 | } else { 1474 | panic!("Wrong match pattern") 1475 | }; 1476 | assert_eq!(p.year, year); 1477 | assert_eq!(p.separator, "".to_string()); 1478 | } 1479 | for &(day, month, year) in &test_data { 1480 | let password = format!("{}.{}.{}", year, month, day); 1481 | let matches = (matching::DateMatch {}).get_matches(&password, &HashMap::new()); 1482 | let m = matches.iter().find(|m| m.token == password).unwrap(); 1483 | assert_eq!(m.i, 0); 1484 | assert_eq!(m.j, password.len() - 1); 1485 | let p = if let MatchPattern::Date(ref p) = m.pattern { 1486 | p 1487 | } else { 1488 | panic!("Wrong match pattern") 1489 | }; 1490 | assert_eq!(p.year, year); 1491 | assert_eq!(p.separator, ".".to_string()); 1492 | } 1493 | } 1494 | 1495 | #[test] 1496 | fn test_matching_zero_padded_dates() { 1497 | let password = "02/02/02"; 1498 | let matches = (matching::DateMatch {}).get_matches(password, &HashMap::new()); 1499 | let m = matches.iter().find(|m| m.token == password).unwrap(); 1500 | assert_eq!(m.i, 0); 1501 | assert_eq!(m.j, password.len() - 1); 1502 | let p = if let MatchPattern::Date(ref p) = m.pattern { 1503 | p 1504 | } else { 1505 | panic!("Wrong match pattern") 1506 | }; 1507 | assert_eq!(p.year, 2002); 1508 | assert_eq!(p.month, 2); 1509 | assert_eq!(p.day, 2); 1510 | assert_eq!(p.separator, "/".to_string()); 1511 | } 1512 | 1513 | #[test] 1514 | fn test_matching_embedded_dates() { 1515 | let password = "a1/1/91!"; 1516 | let matches = (matching::DateMatch {}).get_matches(password, &HashMap::new()); 1517 | let m = matches.iter().find(|m| &m.token == "1/1/91").unwrap(); 1518 | assert_eq!(m.i, 1); 1519 | assert_eq!(m.j, password.len() - 2); 1520 | let p = if let MatchPattern::Date(ref p) = m.pattern { 1521 | p 1522 | } else { 1523 | panic!("Wrong match pattern") 1524 | }; 1525 | assert_eq!(p.year, 1991); 1526 | assert_eq!(p.month, 1); 1527 | assert_eq!(p.day, 1); 1528 | assert_eq!(p.separator, "/".to_string()); 1529 | } 1530 | 1531 | #[test] 1532 | fn test_matching_overlapping_dates() { 1533 | let password = "12/20/1991.12.20"; 1534 | let matches = (matching::DateMatch {}).get_matches(password, &HashMap::new()); 1535 | let m = matches.iter().find(|m| &m.token == "12/20/1991").unwrap(); 1536 | assert_eq!(m.i, 0); 1537 | assert_eq!(m.j, 9); 1538 | let p = if let MatchPattern::Date(ref p) = m.pattern { 1539 | p 1540 | } else { 1541 | panic!("Wrong match pattern") 1542 | }; 1543 | assert_eq!(p.year, 1991); 1544 | assert_eq!(p.month, 12); 1545 | assert_eq!(p.day, 20); 1546 | assert_eq!(p.separator, "/".to_string()); 1547 | let m = matches.iter().find(|m| &m.token == "1991.12.20").unwrap(); 1548 | assert_eq!(m.i, 6); 1549 | assert_eq!(m.j, password.len() - 1); 1550 | let p = if let MatchPattern::Date(ref p) = m.pattern { 1551 | p 1552 | } else { 1553 | panic!("Wrong match pattern") 1554 | }; 1555 | assert_eq!(p.year, 1991); 1556 | assert_eq!(p.month, 12); 1557 | assert_eq!(p.day, 20); 1558 | assert_eq!(p.separator, ".".to_string()); 1559 | } 1560 | 1561 | #[test] 1562 | fn test_matches_dates_padded_by_non_ambiguous_digits() { 1563 | let password = "912/20/919"; 1564 | let matches = (matching::DateMatch {}).get_matches(password, &HashMap::new()); 1565 | let m = matches.iter().find(|m| &m.token == "12/20/91").unwrap(); 1566 | assert_eq!(m.i, 1); 1567 | assert_eq!(m.j, password.len() - 2); 1568 | let p = if let MatchPattern::Date(ref p) = m.pattern { 1569 | p 1570 | } else { 1571 | panic!("Wrong match pattern") 1572 | }; 1573 | assert_eq!(p.year, 1991); 1574 | assert_eq!(p.month, 12); 1575 | assert_eq!(p.day, 20); 1576 | assert_eq!(p.separator, "/".to_string()); 1577 | } 1578 | 1579 | #[test] 1580 | fn test_omnimatch() { 1581 | assert_eq!(matching::omnimatch("", &HashMap::new()), Vec::new()); 1582 | let password = "r0sebudmaelstrom11/20/91aaaa"; 1583 | let expected = [ 1584 | ("dictionary", 0, 6), 1585 | ("dictionary", 7, 15), 1586 | ("date", 16, 23), 1587 | ("repeat", 24, 27), 1588 | ]; 1589 | let matches = matching::omnimatch(password, &HashMap::new()); 1590 | for &(pattern_name, i, j) in &expected { 1591 | assert!(matches 1592 | .iter() 1593 | .any(|m| m.pattern.variant() == pattern_name && m.i == i && m.j == j)); 1594 | } 1595 | } 1596 | } 1597 | --------------------------------------------------------------------------------