├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── resources │ │ └── com │ │ │ └── nulabinc │ │ │ └── zxcvbn │ │ │ ├── matchers │ │ │ └── keyboards │ │ │ │ ├── keypad.txt │ │ │ │ ├── mac_keypad.txt │ │ │ │ ├── jis.txt │ │ │ │ ├── dvorak.txt │ │ │ │ └── qwerty.txt │ │ │ ├── messages.properties │ │ │ ├── messages_it.properties │ │ │ ├── messages_nl.properties │ │ │ ├── messages_de.properties │ │ │ ├── messages_es.properties │ │ │ ├── messages_pt.properties │ │ │ ├── messages_fr.properties │ │ │ └── messages_ja.properties │ ├── java9 │ │ └── module-info.java │ └── java │ │ └── com │ │ └── nulabinc │ │ └── zxcvbn │ │ ├── io │ │ ├── Resource.java │ │ └── ClasspathResource.java │ │ ├── guesses │ │ ├── RepeatGuess.java │ │ ├── BaseGuess.java │ │ ├── DateGuess.java │ │ ├── BruteforceGuess.java │ │ ├── SequenceGuess.java │ │ ├── Guess.java │ │ ├── RegexGuess.java │ │ ├── EstimateGuess.java │ │ ├── SpatialGuess.java │ │ └── DictionaryGuess.java │ │ ├── Pattern.java │ │ ├── matchers │ │ ├── AlignedKeyboardLoader.java │ │ ├── SlantedKeyboardLoader.java │ │ ├── Matcher.java │ │ ├── BaseMatcher.java │ │ ├── Dictionary.java │ │ ├── AlignedAdjacentGraphBuilder.java │ │ ├── RegexMatcher.java │ │ ├── OmnibusMatcher.java │ │ ├── KeyboardLoader.java │ │ ├── SlantedAdjacentGraphBuilder.java │ │ ├── DictionaryLoader.java │ │ ├── ReverseDictionaryMatcher.java │ │ ├── DictionaryMatcher.java │ │ ├── L33tSubDict.java │ │ ├── MatchFactory.java │ │ ├── SpatialMatcher.java │ │ ├── RepeatMatcher.java │ │ ├── SequenceMatcher.java │ │ ├── L33tMatcher.java │ │ ├── Keyboard.java │ │ └── Match.java │ │ ├── MatchSequence.java │ │ ├── Matcher.java │ │ ├── Context.java │ │ ├── StandardContext.java │ │ ├── ZxcvbnBuilder.java │ │ ├── Matching.java │ │ ├── Guess.java │ │ ├── Zxcvbn.java │ │ ├── Optimal.java │ │ ├── StandardKeyboards.java │ │ ├── StandardDictionaries.java │ │ ├── TimeEstimates.java │ │ ├── FeedbackFactory.java │ │ ├── Strength.java │ │ ├── Feedback.java │ │ ├── WipeableString.java │ │ ├── AttackTimes.java │ │ └── Scoring.java ├── test │ ├── resources │ │ └── passwords.txt │ └── java │ │ └── com │ │ └── nulabinc │ │ └── zxcvbn │ │ ├── EdgeCaseTest.java │ │ ├── JSScriptEngineBuilder.java │ │ ├── ZxcvbnBuilderTest.java │ │ ├── MeasureTest.java │ │ ├── JavaPortTest.java │ │ ├── WipeableStringTest.java │ │ └── ApproachComparisonTest.java └── jmh │ └── java │ └── com │ └── nulabinc │ └── zxcvbn │ └── RandomPasswordMeasureBenchmark.java ├── .gitignore ├── config └── checkstyle │ └── suppressions-xpath.xml ├── LICENSE.txt ├── .github └── workflows │ └── build.yml ├── gradlew.bat ├── dictionary.gradle └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'zxcvbn4j' 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nulab/zxcvbn4j/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/matchers/keyboards/keypad.txt: -------------------------------------------------------------------------------- 1 | / * - 2 | 7 8 9 + 3 | 4 5 6 4 | 1 2 3 5 | 0 . 6 | -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/matchers/keyboards/mac_keypad.txt: -------------------------------------------------------------------------------- 1 | = / * 2 | 7 8 9 - 3 | 4 5 6 + 4 | 1 2 3 5 | 0 . 6 | -------------------------------------------------------------------------------- /src/main/java9/module-info.java: -------------------------------------------------------------------------------- 1 | module com.nulabinc.zxcvbn { 2 | exports com.nulabinc.zxcvbn; 3 | exports com.nulabinc.zxcvbn.io; 4 | exports com.nulabinc.zxcvbn.matchers; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/matchers/keyboards/jis.txt: -------------------------------------------------------------------------------- 1 | 1! 2" 3# 4$ 5% 6& 7' 8( 9) 00 -= ^~ ¥| 2 | qQ wW eE rR tT yY uU iI oO pP @` [{ 3 | aA sS dD fF gG hH jJ kK lL ;+ :* ]} 4 | zZ xX cC vV bB nN mM ,< .> /? 5 | -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/matchers/keyboards/dvorak.txt: -------------------------------------------------------------------------------- 1 | `~ 1! 2@ 3# 4$ 5% 6^ 7& 8* 9( 0) [{ ]} 2 | '" ,< .> pP yY fF gG cC rR lL /? =+ \| 3 | aA oO eE uU iI dD hH tT nN sS -_ 4 | ;: qQ jJ kK xX bB mM wW vV zZ 5 | -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/matchers/keyboards/qwerty.txt: -------------------------------------------------------------------------------- 1 | `~ 1! 2@ 3# 4$ 5% 6^ 7& 8* 9( 0) -_ =+ 2 | qQ wW eE rR tT yY uU iI oO pP [{ ]} \| 3 | aA sS dD fF gG hH jJ kK lL ;: '" 4 | zZ xX cC vV bB nN mM ,< .> /? 5 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/io/Resource.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.io; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | public interface Resource { 7 | 8 | InputStream getInputStream() throws IOException; 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | !/.gitignore 3 | 4 | /.idea/ 5 | 6 | /bin/ 7 | /build/ 8 | /target/ 9 | /out 10 | 11 | /buildSrc/bin/ 12 | /buildSrc/build/ 13 | /buildSrc/target/ 14 | 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | *.class 20 | 21 | # Package Files # 22 | *.jar 23 | *.war 24 | *.ear 25 | 26 | !gradle-wrapper.jar 27 | .gradle -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/guesses/RepeatGuess.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.guesses; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.matchers.Match; 5 | 6 | public class RepeatGuess extends BaseGuess { 7 | 8 | public RepeatGuess(final Context context) { 9 | super(context); 10 | } 11 | 12 | @Override 13 | public double exec(Match match) { 14 | return match.baseGuesses * match.repeatCount; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/Pattern.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | @SuppressWarnings("java:S115") 4 | public enum Pattern { 5 | Bruteforce("bruteforce"), 6 | Dictionary("dictionary"), 7 | Spatial("spatial"), 8 | Repeat("repeat"), 9 | Sequence("sequence"), 10 | Regex("regex"), 11 | Date("date"); 12 | 13 | private final String value; 14 | 15 | Pattern(final String value) { 16 | this.value = value; 17 | } 18 | 19 | public String value() { 20 | return value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/AlignedKeyboardLoader.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.io.Resource; 4 | 5 | public class AlignedKeyboardLoader extends KeyboardLoader { 6 | 7 | public AlignedKeyboardLoader(final String name, final Resource inputStreamSource) { 8 | super(name, inputStreamSource); 9 | } 10 | 11 | @Override 12 | protected Keyboard.AdjacentGraphBuilder buildAdjacentGraphBuilder(final String layout) { 13 | return new AlignedAdjacentGraphBuilder(layout); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/SlantedKeyboardLoader.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.io.Resource; 4 | 5 | public class SlantedKeyboardLoader extends KeyboardLoader { 6 | 7 | public SlantedKeyboardLoader(final String name, final Resource inputStreamSource) { 8 | super(name, inputStreamSource); 9 | } 10 | 11 | @Override 12 | protected Keyboard.AdjacentGraphBuilder buildAdjacentGraphBuilder(final String layout) { 13 | return new SlantedAdjacentGraphBuilder(layout); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/MatchSequence.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Match; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | public class MatchSequence { 8 | 9 | private final List sequence; 10 | private final double guesses; 11 | 12 | public MatchSequence(List sequence, double guesses) { 13 | this.sequence = Collections.unmodifiableList(sequence); 14 | this.guesses = guesses; 15 | } 16 | 17 | public List getSequence() { 18 | return sequence; 19 | } 20 | 21 | public double getGuesses() { 22 | return guesses; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/Matcher.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Match; 4 | import java.util.List; 5 | 6 | /** 7 | * Represents a matcher responsible for identifying patterns within passwords. 8 | * 9 | *

Implementations of this interface provide specific matching strategies to detect various 10 | * patterns such as dictionary words, sequences, and spatial patterns. 11 | * 12 | * @deprecated This interface is deprecated. Use {@link com.nulabinc.zxcvbn.matchers.Matcher} 13 | * instead. 14 | */ 15 | @Deprecated 16 | public interface Matcher { 17 | List execute(CharSequence password); 18 | } 19 | -------------------------------------------------------------------------------- /config/checkstyle/suppressions-xpath.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/Matcher.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Represents a matcher responsible for identifying patterns within passwords. 7 | * 8 | *

Implementations of this interface provide specific matching strategies to detect various 9 | * patterns such as dictionary words, sequences, and spatial patterns. 10 | * 11 | * @see Match 12 | */ 13 | public interface Matcher { 14 | /** 15 | * Analyzes the given password and returns a list of detected patterns as {@link Match} objects. 16 | * 17 | * @param password the password to analyze for patterns. 18 | * @return a list of matches identifying patterns found within the password. 19 | */ 20 | List execute(CharSequence password); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/guesses/BaseGuess.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.guesses; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | 5 | public abstract class BaseGuess implements Guess { 6 | 7 | private final Context context; 8 | 9 | protected BaseGuess(Context context) { 10 | this.context = context; 11 | } 12 | 13 | protected Context getContext() { 14 | return context; 15 | } 16 | 17 | protected static int calculateBinomialCoefficient(int n, int k) { 18 | // http://blog.plover.com/math/choose.html 19 | if (k > n) { 20 | return 0; 21 | } 22 | if (k == 0) { 23 | return 1; 24 | } 25 | int r = 1; 26 | for (int d = 1; d <= k; d++) { 27 | r *= n; 28 | r /= d; 29 | n -= 1; 30 | } 31 | return r; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/guesses/DateGuess.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.guesses; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.matchers.Match; 5 | 6 | public class DateGuess extends BaseGuess { 7 | 8 | public DateGuess(final Context context) { 9 | super(context); 10 | } 11 | 12 | @Override 13 | public double exec(Match match) { 14 | double yearSpace = calculateYearSpace(match.year); 15 | double guesses = yearSpace * 365; 16 | if (hasSeparator(match.separator)) { 17 | guesses *= 4; 18 | } 19 | return guesses; 20 | } 21 | 22 | private double calculateYearSpace(int year) { 23 | return Math.max(Math.abs(year - REFERENCE_YEAR), MIN_YEAR_SPACE); 24 | } 25 | 26 | private boolean hasSeparator(String separator) { 27 | return separator != null && !separator.isEmpty(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/Context.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Dictionary; 4 | import com.nulabinc.zxcvbn.matchers.Keyboard; 5 | import java.util.Collections; 6 | import java.util.Map; 7 | 8 | public class Context { 9 | 10 | private final Map dictionaryMap; 11 | 12 | private final Map keyboardMap; 13 | 14 | public Context( 15 | final Map dictionaryMap, final Map keyboardMap) { 16 | this.dictionaryMap = dictionaryMap; 17 | this.keyboardMap = keyboardMap; 18 | } 19 | 20 | public Map getDictionaryMap() { 21 | return Collections.unmodifiableMap(this.dictionaryMap); 22 | } 23 | 24 | public Map getKeyboardMap() { 25 | return Collections.unmodifiableMap(this.keyboardMap); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/resources/passwords.txt: -------------------------------------------------------------------------------- 1 | q 2 | 5 3 | & 4 | qwER43@! 5 | Tr0ub4dour&3 6 | correcthorsebatterystaple 7 | password 8 | drowssap 9 | passwordp 10 | passwordadmin 11 | p@$$word@dmin 12 | 19700101 13 | 20300101 14 | 1970/01/01 15 | 1970-01-01 16 | aaaaaaaaa 17 | 123456789 18 | abcdefghijklmnopqrstuvwxyz 19 | qwertyuiop@[ 20 | zxcvbnm,./_ 21 | asdfghjkl;:] 22 | pandapandapandapandapandapandapandapandapandaa 23 | appleappleappleappleappleappleappleappleapplea 24 | dncrbliehbvkehr734yf;ewhihwfph@houaegfueqpg30^r0urfvhej¥]e;l,ckvniwbgoidnci@oewhfoobojabouhqwou12482386fhoiwehe@o 25 | apple orenge aabb 26 | eTq($%u-44c_j9NJB45a#2#JP7sH 27 | IB7~EOw!51gug+7s#+%A9P1O/w8f 28 | 1v_f%7JvS8w!_t398+ON-CObI#v0 29 | 8lFmfc0!w)&iU9DM6~4_w)D)Y44J 30 | &BZ09gjG!iKG&#M09s_1Gr41&o%i 31 | T9Y-!ciS%XW9U5l/~aw9+4!5u8Ti 32 | QMji&0uze5O#%+%2e_Y08E(R6L8p 33 | 6EG4y1nJASd!1~!//#6+Yhb1vW3d 34 | 8$q_5f2U3s6~W(S7iv)_8N%lJkOE 35 | %nbd~$)2y/6hV6)2R9vYPpA49A~C 36 | xsw234rfvb 37 | yaq123edc 38 | cde345tgbn 39 | yaqwedcvb -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/StandardContext.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Dictionary; 4 | import com.nulabinc.zxcvbn.matchers.Keyboard; 5 | import java.io.IOException; 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | 9 | class StandardContext { 10 | 11 | private StandardContext() { 12 | throw new IllegalStateException("StandardContext should not be instantiated"); 13 | } 14 | 15 | static Context build() throws IOException { 16 | Map dictionaryMap = new LinkedHashMap<>(); 17 | for (Dictionary dictionary : StandardDictionaries.loadAllDictionaries()) { 18 | dictionaryMap.put(dictionary.getName(), dictionary); 19 | } 20 | 21 | Map keyboardMap = new LinkedHashMap<>(); 22 | for (Keyboard keyboard : StandardKeyboards.loadAllKeyboards()) { 23 | keyboardMap.put(keyboard.getName(), keyboard); 24 | } 25 | 26 | return new Context(dictionaryMap, keyboardMap); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/guesses/BruteforceGuess.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.guesses; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.matchers.Match; 5 | 6 | public class BruteforceGuess extends BaseGuess { 7 | 8 | protected BruteforceGuess(final Context context) { 9 | super(context); 10 | } 11 | 12 | @Override 13 | public double exec(Match match) { 14 | double guesses = calculateBruteforceGuesses(match.tokenLength()); 15 | double minGuesses = calculateMinGuesses(match.tokenLength()); 16 | return Math.max(guesses, minGuesses); 17 | } 18 | 19 | private double calculateBruteforceGuesses(int tokenLength) { 20 | double guesses = Math.pow(BRUTEFORCE_CARDINALITY, tokenLength); 21 | return Double.isInfinite(guesses) ? Double.MAX_VALUE : guesses; 22 | } 23 | 24 | private double calculateMinGuesses(int tokenLength) { 25 | return tokenLength == 1 26 | ? MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1 27 | : MIN_SUBMATCH_GUESSES_MULTI_CHAR + 1; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/BaseMatcher.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import java.io.Serializable; 5 | import java.util.Collections; 6 | import java.util.Comparator; 7 | import java.util.List; 8 | 9 | public abstract class BaseMatcher implements Matcher { 10 | 11 | private final Context context; 12 | 13 | protected BaseMatcher(Context context) { 14 | this.context = context; 15 | } 16 | 17 | protected Context getContext() { 18 | return context; 19 | } 20 | 21 | protected List sorted(List matches) { 22 | Collections.sort(matches, new MatchComparator()); 23 | return matches; 24 | } 25 | 26 | private static class MatchComparator implements Comparator, Serializable { 27 | private static final long serialVersionUID = 1L; 28 | 29 | @Override 30 | public int compare(Match o1, Match o2) { 31 | int c = o1.i - o2.i; 32 | if (c != 0) { 33 | return c; 34 | } else { 35 | return (o1.j - o2.j); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nulab Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/Dictionary.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | public class Dictionary { 8 | 9 | private final String name; 10 | 11 | private final List frequencies; 12 | 13 | private final Map rankedDictionary; 14 | 15 | public Dictionary(String name, List frequencies) { 16 | this.name = name; 17 | this.frequencies = frequencies; 18 | this.rankedDictionary = toRankedDictionary(frequencies); 19 | } 20 | 21 | private Map toRankedDictionary(final List frequencies) { 22 | Map result = new HashMap<>(); 23 | int i = 1; // rank starts at 1, not 0 24 | for (String word : frequencies) { 25 | result.put(word, i); 26 | i++; 27 | } 28 | return result; 29 | } 30 | 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | public List getFrequencies() { 36 | return frequencies; 37 | } 38 | 39 | public Map getRankedDictionary() { 40 | return rankedDictionary; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/nulabinc/zxcvbn/EdgeCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import static org.junit.Assert.assertNotNull; 4 | 5 | import org.junit.Test; 6 | 7 | public class EdgeCaseTest { 8 | 9 | /** 10 | * Reproduce Issue #49 from GitHub. 11 | * 12 | *

This should fail if the trimTrailingWhitespace call is removed from 13 | * WipeableString.parseInt(s,radix). 14 | */ 15 | @Test 16 | public void testWindowsNewlineInDate() { 17 | StringBuilder buf = new StringBuilder("PW2001"); 18 | buf.append((char) 10); 19 | buf.append((char) 13); 20 | buf.append("0101"); 21 | assertNotNull(new Zxcvbn().measure(buf.toString())); 22 | } 23 | 24 | @Test 25 | public void testUnixNewlineInDate() { 26 | StringBuilder buf = new StringBuilder("PW2001"); 27 | buf.append((char) 13); 28 | buf.append("0101"); 29 | assertNotNull(new Zxcvbn().measure(buf.toString())); 30 | } 31 | 32 | @Test 33 | public void testSpaceAfterDate() { 34 | assertNotNull(new Zxcvbn().measure("PW2009 ")); 35 | } 36 | 37 | /** Try to reproduce GitHub issue #34 */ 38 | @Test 39 | public void testJustFourDigitNumber() { 40 | assertNotNull(new Zxcvbn().measure("8604 ")); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/guesses/SequenceGuess.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.guesses; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.matchers.Match; 5 | import java.util.Arrays; 6 | import java.util.HashSet; 7 | import java.util.Set; 8 | import java.util.regex.Pattern; 9 | 10 | public class SequenceGuess extends BaseGuess { 11 | 12 | private static final Set START_POINTS = 13 | new HashSet<>(Arrays.asList('a', 'A', 'z', 'Z', '0', '1', '9')); 14 | private static final Pattern DIGIT_PATTERN = Pattern.compile("\\d"); 15 | 16 | public SequenceGuess(final Context context) { 17 | super(context); 18 | } 19 | 20 | @Override 21 | public double exec(Match match) { 22 | final char firstChar = match.token.charAt(0); 23 | double baseGuesses = determineBaseGuesses(firstChar); 24 | if (!match.ascending) { 25 | baseGuesses *= 2; 26 | } 27 | return baseGuesses * match.tokenLength(); 28 | } 29 | 30 | private double determineBaseGuesses(char firstChar) { 31 | if (START_POINTS.contains(firstChar)) { 32 | return 4; 33 | } else if (DIGIT_PATTERN.matcher(String.valueOf(firstChar)).find()) { 34 | return 10; 35 | } else { 36 | return 26; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | java: [ 15 | # '8', Refs: https://github.com/nulab/zxcvbn4j/pull/104 16 | '11', 17 | # '17' Refs: https://github.com/nulab/zxcvbn4j/issues/118 18 | ] 19 | fail-fast: false 20 | name: Build and test on JDK ${{ matrix.Java }} 21 | if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" 22 | steps: 23 | - name: Checkout project sources 24 | uses: actions/checkout@v3 25 | - name: Validate gradle wrapper 26 | uses: gradle/wrapper-validation-action@v1 27 | - name: Setup java 28 | uses: actions/setup-java@v3 29 | with: 30 | java-version: ${{ matrix.java }} 31 | distribution: temurin 32 | - name: Run gradle 33 | uses: gradle/gradle-build-action@v2 34 | with: 35 | arguments: jacocoTestReport coveralls verGJF checkstyleMain spotbugsMain publishToMavenLocal 36 | env: 37 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} -------------------------------------------------------------------------------- /src/test/java/com/nulabinc/zxcvbn/JSScriptEngineBuilder.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine; 4 | import java.io.File; 5 | import java.io.FileNotFoundException; 6 | import java.io.FileReader; 7 | import java.net.URISyntaxException; 8 | import java.net.URL; 9 | import javax.script.ScriptEngine; 10 | import javax.script.ScriptException; 11 | import org.graalvm.polyglot.Context; 12 | import org.graalvm.polyglot.Engine; 13 | 14 | public class JSScriptEngineBuilder { 15 | 16 | public ScriptEngine build() { 17 | final GraalJSScriptEngine engine = 18 | GraalJSScriptEngine.create( 19 | Engine.newBuilder().option("engine.WarnInterpreterOnly", "false").build(), 20 | Context.newBuilder("js")); 21 | loadZxcvbnJs(engine); 22 | return engine; 23 | } 24 | 25 | private void loadZxcvbnJs(ScriptEngine engine) { 26 | try { 27 | // using the 4.4.1 release 28 | URL script = JSScriptEngineBuilder.class.getClassLoader().getResource("zxcvbn.js"); 29 | engine.eval(new FileReader(new File(script.toURI()))); 30 | } catch (URISyntaxException | FileNotFoundException | ScriptException e) { 31 | throw new RuntimeException("Cannot instantiate Javascript Engine", e); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/AlignedAdjacentGraphBuilder.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | public class AlignedAdjacentGraphBuilder extends Keyboard.AdjacentGraphBuilder { 7 | 8 | public AlignedAdjacentGraphBuilder(final String layout) { 9 | super(layout); 10 | } 11 | 12 | @Override 13 | public boolean isSlanted() { 14 | return false; 15 | } 16 | 17 | @Override 18 | protected int calcSlant(int y) { 19 | return 0; 20 | } 21 | 22 | /** 23 | * returns the nine clockwise adjacent coordinates on a keypad, where each row is vert aligned. 24 | */ 25 | @Override 26 | protected List getAdjacentCoords(final Position position) { 27 | return Arrays.asList( 28 | Position.of(position.getX() - 1, position.getY()), 29 | Position.of(position.getX() - 1, position.getY() - 1), 30 | Position.of(position.getX(), position.getY() - 1), 31 | Position.of(position.getX() + 1, position.getY() - 1), 32 | Position.of(position.getX() + 1, position.getY()), 33 | Position.of(position.getX() + 1, position.getY() + 1), 34 | Position.of(position.getX(), position.getY() + 1), 35 | Position.of(position.getX() - 1, position.getY() + 1)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/ZxcvbnBuilder.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Dictionary; 4 | import com.nulabinc.zxcvbn.matchers.Keyboard; 5 | import java.util.LinkedHashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public class ZxcvbnBuilder { 10 | 11 | private final Map dictionaryMap = new LinkedHashMap<>(); 12 | 13 | private final Map keyboardMap = new LinkedHashMap<>(); 14 | 15 | public Zxcvbn build() { 16 | return new Zxcvbn(new Context(dictionaryMap, keyboardMap)); 17 | } 18 | 19 | public ZxcvbnBuilder dictionary(final Dictionary dictionary) { 20 | this.dictionaryMap.put(dictionary.getName(), dictionary); 21 | return this; 22 | } 23 | 24 | public ZxcvbnBuilder dictionaries(final List dictionaries) { 25 | for (Dictionary dictionary : dictionaries) { 26 | this.dictionary(dictionary); 27 | } 28 | return this; 29 | } 30 | 31 | public ZxcvbnBuilder keyboard(final Keyboard keyboard) { 32 | this.keyboardMap.put(keyboard.getName(), keyboard); 33 | return this; 34 | } 35 | 36 | public ZxcvbnBuilder keyboards(final List keyboards) { 37 | for (Keyboard keyboard : keyboards) { 38 | this.keyboard(keyboard); 39 | } 40 | return this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/RegexMatcher.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.WipeableString; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.regex.Pattern; 10 | 11 | public class RegexMatcher extends BaseMatcher { 12 | 13 | private static final Map PATTERNS = new HashMap<>(); 14 | 15 | static { 16 | PATTERNS.put("recent_year", Pattern.compile("19\\d\\d|200\\d|201\\d|202\\d")); 17 | } 18 | 19 | public RegexMatcher(final Context context) { 20 | super(context); 21 | } 22 | 23 | @Override 24 | public List execute(CharSequence password) { 25 | List matches = new ArrayList<>(); 26 | for (Map.Entry patternRef : PATTERNS.entrySet()) { 27 | String name = patternRef.getKey(); 28 | java.util.regex.Matcher matcher = patternRef.getValue().matcher(password); 29 | while (matcher.find()) { 30 | CharSequence token = new WipeableString(matcher.group()); 31 | matches.add( 32 | MatchFactory.createRegexMatch( 33 | matcher.start(), matcher.start() + token.length() - 1, token, name, matcher)); 34 | } 35 | } 36 | return this.sorted(matches); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/OmnibusMatcher.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public class OmnibusMatcher extends BaseMatcher { 10 | 11 | private final List matchers = new ArrayList<>(); 12 | 13 | public OmnibusMatcher(Context context, Map> dictionaries) { 14 | super(context); 15 | if (dictionaries == null) { 16 | dictionaries = new HashMap<>(); 17 | } 18 | matchers.add(new DictionaryMatcher(getContext(), dictionaries)); 19 | matchers.add(new ReverseDictionaryMatcher(getContext(), dictionaries)); 20 | matchers.add(new L33tMatcher(getContext(), dictionaries)); 21 | matchers.add(new SpatialMatcher(getContext())); 22 | matchers.add(new RepeatMatcher(getContext())); 23 | matchers.add(new SequenceMatcher(getContext())); 24 | matchers.add(new RegexMatcher(getContext())); 25 | matchers.add(new DateMatcher(getContext())); 26 | } 27 | 28 | @Override 29 | public List execute(CharSequence password) { 30 | List matches = new ArrayList<>(); 31 | for (Matcher matcher : matchers) { 32 | matches.addAll(matcher.execute(password)); 33 | } 34 | return sorted(matches); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/KeyboardLoader.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.io.Resource; 4 | import java.io.BufferedReader; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | 9 | public abstract class KeyboardLoader { 10 | 11 | private final String name; 12 | 13 | private final Resource resource; 14 | 15 | protected KeyboardLoader(final String name, final Resource resource) { 16 | this.name = name; 17 | this.resource = resource; 18 | } 19 | 20 | public Keyboard load() throws IOException { 21 | InputStream inputStream = resource.getInputStream(); 22 | String layout = loadAsString(inputStream); 23 | return new Keyboard(name, buildAdjacentGraphBuilder(layout)); 24 | } 25 | 26 | protected abstract Keyboard.AdjacentGraphBuilder buildAdjacentGraphBuilder(final String layout); 27 | 28 | private static String loadAsString(final InputStream input) { 29 | try (final BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8"))) { 30 | final StringBuilder sb = new StringBuilder(1024 * 4); 31 | String str; 32 | while ((str = reader.readLine()) != null) { 33 | sb.append(str); 34 | sb.append('\n'); 35 | } 36 | return sb.toString(); 37 | } catch (final IOException e) { 38 | throw new IllegalArgumentException(e); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/Matching.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Dictionary; 4 | import com.nulabinc.zxcvbn.matchers.Match; 5 | import com.nulabinc.zxcvbn.matchers.OmnibusMatcher; 6 | import java.util.HashMap; 7 | import java.util.LinkedHashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class Matching { 12 | 13 | private final Context context; 14 | 15 | protected final Map> rankedDictionaries; 16 | 17 | public Matching(Context context, List orderedList) { 18 | this.context = context; 19 | 20 | final Map dictionaryMap = new LinkedHashMap<>(context.getDictionaryMap()); 21 | dictionaryMap.put("user_inputs", new Dictionary("user_inputs", orderedList)); 22 | this.rankedDictionaries = buildRankedDictionaryMap(dictionaryMap); 23 | } 24 | 25 | public List omnimatch(CharSequence password) { 26 | return new OmnibusMatcher(context, rankedDictionaries).execute(password); 27 | } 28 | 29 | private static Map> buildRankedDictionaryMap( 30 | Map dictionaryMap) { 31 | Map> rankedDictionaries = new HashMap<>(); 32 | for (Dictionary dictionary : dictionaryMap.values()) { 33 | rankedDictionaries.put(dictionary.getName(), dictionary.getRankedDictionary()); 34 | } 35 | return rankedDictionaries; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/SlantedAdjacentGraphBuilder.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | public class SlantedAdjacentGraphBuilder extends Keyboard.AdjacentGraphBuilder { 7 | 8 | public SlantedAdjacentGraphBuilder(final String layout) { 9 | super(layout); 10 | } 11 | 12 | /** 13 | * returns the six adjacent coordinates on a standard keyboard, where each row is slanted to the 14 | * right from the last. adjacencies are clockwise, starting with key to the left, then two keys 15 | * above, then right key, then two keys below. (that is, only near-diagonal keys are adjacent, so 16 | * g's coordinate is adjacent to those of t,y,b,v, but not those of r,u,n,c.) 17 | */ 18 | @Override 19 | protected List getAdjacentCoords(final Position position) { 20 | return Arrays.asList( 21 | Position.of(position.getX() - 1, position.getY()), 22 | Position.of(position.getX(), position.getY() - 1), 23 | Position.of(position.getX() + 1, position.getY() - 1), 24 | Position.of(position.getX() + 1, position.getY()), 25 | Position.of(position.getX(), position.getY() + 1), 26 | Position.of(position.getX() - 1, position.getY() + 1)); 27 | } 28 | 29 | @Override 30 | public boolean isSlanted() { 31 | return true; 32 | } 33 | 34 | @Override 35 | protected int calcSlant(int y) { 36 | return y - 1; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/guesses/Guess.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.guesses; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Match; 4 | import java.util.Calendar; 5 | 6 | /** 7 | * Represents a strategy for estimating the number of guesses required to crack a given {@link 8 | * Match}. 9 | * 10 | *

Implementations of this interface are expected to evaluate the strength or weakness of a 11 | * matched pattern within a password and return an estimated guess number. 12 | * 13 | * @see Match 14 | */ 15 | public interface Guess { 16 | 17 | /** Cardinality used in brute force attacks. */ 18 | int BRUTEFORCE_CARDINALITY = 10; 19 | 20 | /** Minimum number of guesses when the sub-match contains a single character. */ 21 | int MIN_SUBMATCH_GUESSES_SINGLE_CHAR = 10; 22 | 23 | /** Minimum number of guesses when the sub-match contains multiple characters. */ 24 | int MIN_SUBMATCH_GUESSES_MULTI_CHAR = 50; 25 | 26 | /** The minimum range of years to be considered when evaluating date-based patterns. */ 27 | int MIN_YEAR_SPACE = 20; 28 | 29 | /** Reference year used for date-based pattern evaluations. */ 30 | int REFERENCE_YEAR = Calendar.getInstance().get(Calendar.YEAR); 31 | 32 | /** 33 | * Evaluates the given {@link Match} and estimates the number of guesses required to crack it. 34 | * 35 | * @param match the matched pattern to evaluate. 36 | * @return the estimated number of guesses required to crack the given match. 37 | */ 38 | double exec(Match match); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/DictionaryLoader.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.io.Resource; 4 | import java.io.BufferedReader; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class DictionaryLoader { 12 | 13 | private final String name; 14 | 15 | private final Resource resource; 16 | 17 | public DictionaryLoader(final String name, final Resource resource) { 18 | this.name = name; 19 | this.resource = resource; 20 | } 21 | 22 | public Dictionary load() throws IOException { 23 | List words = new ArrayList<>(); 24 | // Reasons for not using StandardCharsets 25 | // refs: https://github.com/nulab/zxcvbn4j/issues/62 26 | try (final InputStream inputStream = resource.getInputStream(); 27 | final InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8"); 28 | final BufferedReader br = new BufferedReader(inputStreamReader)) { 29 | String line; 30 | while ((line = br.readLine()) != null) { 31 | words.add(line); 32 | } 33 | } catch (IOException e) { 34 | throw new DictionaryLoadException("Error while reading " + name, e); 35 | } 36 | return new Dictionary(name, words); 37 | } 38 | 39 | static class DictionaryLoadException extends IOException { 40 | 41 | DictionaryLoadException(String message, Throwable cause) { 42 | super(message, cause); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/Guess.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Match; 4 | import java.util.Calendar; 5 | 6 | /** 7 | * Represents a strategy for estimating the number of guesses required to crack a given {@link 8 | * Match}. 9 | * 10 | *

Implementations of this interface are expected to evaluate the strength or weakness of a 11 | * matched pattern within a password and return an estimated guess number. 12 | * 13 | * @deprecated This interface is deprecated. Use {@link com.nulabinc.zxcvbn.guesses.Guess} instead. 14 | * @see Match 15 | */ 16 | public interface Guess { 17 | 18 | /** Cardinality used in brute force attacks. */ 19 | int BRUTEFORCE_CARDINALITY = 10; 20 | 21 | /** Minimum number of guesses when the sub-match contains a single character. */ 22 | int MIN_SUBMATCH_GUESSES_SINGLE_CHAR = 10; 23 | 24 | /** Minimum number of guesses when the sub-match contains multiple characters. */ 25 | int MIN_SUBMATCH_GUESSES_MULTI_CHAR = 50; 26 | 27 | /** The minimum range of years to be considered when evaluating date-based patterns. */ 28 | int MIN_YEAR_SPACE = 20; 29 | 30 | /** Reference year used for date-based pattern evaluations. */ 31 | int REFERENCE_YEAR = Calendar.getInstance().get(Calendar.YEAR); 32 | 33 | /** 34 | * Evaluates the given {@link Match} and estimates the number of guesses required to crack it. 35 | * 36 | * @param match the matched pattern to evaluate. 37 | * @return the estimated number of guesses required to crack the given match. 38 | */ 39 | double exec(Match match); 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/ReverseDictionaryMatcher.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.WipeableString; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public class ReverseDictionaryMatcher extends BaseMatcher { 11 | 12 | private final Map> rankedDictionaries; 13 | 14 | public ReverseDictionaryMatcher( 15 | Context context, Map> rankedDictionaries) { 16 | super(context); 17 | if (rankedDictionaries == null) { 18 | this.rankedDictionaries = new HashMap<>(); 19 | } else { 20 | this.rankedDictionaries = rankedDictionaries; 21 | } 22 | } 23 | 24 | @Override 25 | public List execute(CharSequence password) { 26 | CharSequence reversedPassword = WipeableString.reversed(password); 27 | List matches = new ArrayList<>(); 28 | DictionaryMatcher dictionaryMatcher = new DictionaryMatcher(getContext(), rankedDictionaries); 29 | for (Match match : dictionaryMatcher.execute(reversedPassword)) { 30 | int reversedStartIndex = password.length() - 1 - match.j; 31 | int reversedEndIndex = password.length() - 1 - match.i; 32 | matches.add( 33 | MatchFactory.createReversedDictionaryMatch( 34 | reversedStartIndex, 35 | reversedEndIndex, 36 | WipeableString.reversed(match.token), 37 | match.matchedWord, 38 | match.rank, 39 | match.dictionaryName)); 40 | } 41 | return this.sorted(matches); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/guesses/RegexGuess.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.guesses; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.WipeableString; 5 | import com.nulabinc.zxcvbn.matchers.Match; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | public class RegexGuess extends BaseGuess { 10 | 11 | private static final Map CHAR_CLASS_BASES = new HashMap<>(); 12 | 13 | static { 14 | CHAR_CLASS_BASES.put("alpha_lower", 26); 15 | CHAR_CLASS_BASES.put("alpha_upper", 26); 16 | CHAR_CLASS_BASES.put("alpha", 52); 17 | CHAR_CLASS_BASES.put("alphanumeric", 62); 18 | CHAR_CLASS_BASES.put("digits", 10); 19 | CHAR_CLASS_BASES.put("symbols", 33); 20 | } 21 | 22 | private static final String RECENT_YEAR = "recent_year"; 23 | 24 | protected RegexGuess(final Context context) { 25 | super(context); 26 | } 27 | 28 | @Override 29 | public double exec(Match match) { 30 | if (CHAR_CLASS_BASES.containsKey(match.regexName)) { 31 | return calculateCharClassGuesses(match); 32 | } 33 | if (RECENT_YEAR.equals(match.regexName)) { 34 | return calculateYearSpace(match.token); 35 | } 36 | return 0; 37 | } 38 | 39 | private double calculateCharClassGuesses(Match match) { 40 | return Math.pow(CHAR_CLASS_BASES.get(match.regexName), match.tokenLength()); 41 | } 42 | 43 | private double calculateYearSpace(CharSequence token) { 44 | double yearSpace = Math.abs(parseInt(token) - REFERENCE_YEAR); 45 | return Math.max(yearSpace, MIN_YEAR_SPACE); 46 | } 47 | 48 | private static int parseInt(CharSequence s) { 49 | try { 50 | return WipeableString.parseInt(s); 51 | } catch (NumberFormatException e) { 52 | return 0; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/jmh/java/com/nulabinc/zxcvbn/RandomPasswordMeasureBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import java.io.IOException; 4 | import java.util.Random; 5 | import java.util.concurrent.TimeUnit; 6 | import org.openjdk.jmh.annotations.Benchmark; 7 | import org.openjdk.jmh.annotations.BenchmarkMode; 8 | import org.openjdk.jmh.annotations.Fork; 9 | import org.openjdk.jmh.annotations.Measurement; 10 | import org.openjdk.jmh.annotations.Mode; 11 | import org.openjdk.jmh.annotations.OutputTimeUnit; 12 | import org.openjdk.jmh.annotations.Param; 13 | import org.openjdk.jmh.annotations.Scope; 14 | import org.openjdk.jmh.annotations.Setup; 15 | import org.openjdk.jmh.annotations.State; 16 | import org.openjdk.jmh.annotations.Warmup; 17 | 18 | @BenchmarkMode(Mode.AverageTime) 19 | @OutputTimeUnit(TimeUnit.MILLISECONDS) 20 | @State(Scope.Thread) 21 | @Warmup(iterations = 2) 22 | @Measurement(iterations = 3) 23 | @Fork(1) 24 | public class RandomPasswordMeasureBenchmark { 25 | 26 | @Param({"8", "32", "128", "512", "1024"}) 27 | private int passwordLength; 28 | 29 | private String password; 30 | Zxcvbn zxcvbn; 31 | 32 | @Setup 33 | public void setup() throws IOException { 34 | zxcvbn = 35 | new ZxcvbnBuilder() 36 | .dictionaries(StandardDictionaries.loadAllDictionaries()) 37 | .keyboards(StandardKeyboards.loadAllKeyboards()) 38 | .build(); 39 | 40 | Random random = new Random(42); 41 | StringBuilder sb = new StringBuilder(passwordLength); 42 | for (int i = 0; i < passwordLength; i++) { 43 | char c = (char) (random.nextInt() % Character.MAX_VALUE); 44 | sb.append(c); 45 | } 46 | password = sb.toString(); 47 | } 48 | 49 | @Benchmark 50 | public Strength measure() { 51 | return zxcvbn.measure(password); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/guesses/EstimateGuess.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.guesses; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.Pattern; 5 | import com.nulabinc.zxcvbn.Scoring; 6 | import com.nulabinc.zxcvbn.matchers.Match; 7 | import java.util.EnumMap; 8 | import java.util.Map; 9 | 10 | public class EstimateGuess extends BaseGuess { 11 | 12 | private final CharSequence password; 13 | private final Map patternGuessMap = new EnumMap<>(Pattern.class); 14 | 15 | public EstimateGuess(Context context, CharSequence password) { 16 | super(context); 17 | this.password = password; 18 | patternGuessMap.put(Pattern.Bruteforce, new BruteforceGuess(context)); 19 | patternGuessMap.put(Pattern.Dictionary, new DictionaryGuess(context)); 20 | patternGuessMap.put(Pattern.Spatial, new SpatialGuess(context)); 21 | patternGuessMap.put(Pattern.Repeat, new RepeatGuess(context)); 22 | patternGuessMap.put(Pattern.Sequence, new SequenceGuess(context)); 23 | patternGuessMap.put(Pattern.Regex, new RegexGuess(context)); 24 | patternGuessMap.put(Pattern.Date, new DateGuess(context)); 25 | } 26 | 27 | @Override 28 | public double exec(Match match) { 29 | if (match.guesses != null) { 30 | return match.guesses; 31 | } 32 | 33 | int minGuesses = 1; 34 | if (match.tokenLength() < password.length()) { 35 | minGuesses = 36 | match.tokenLength() == 1 37 | ? MIN_SUBMATCH_GUESSES_SINGLE_CHAR 38 | : MIN_SUBMATCH_GUESSES_MULTI_CHAR; 39 | } 40 | 41 | Guess guess = patternGuessMap.get(match.pattern); 42 | double guesses = guess != null ? guess.exec(match) : 0; 43 | 44 | match.guesses = Math.max(guesses, minGuesses); 45 | match.guessesLog10 = Scoring.log10(match.guesses); 46 | return match.guesses; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/Zxcvbn.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Match; 4 | import java.io.IOException; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Locale; 9 | 10 | public class Zxcvbn { 11 | 12 | private final Context context; 13 | 14 | public Zxcvbn() { 15 | try { 16 | context = StandardContext.build(); 17 | } catch (IOException e) { 18 | throw new IllegalStateException(e); 19 | } 20 | } 21 | 22 | Zxcvbn(Context context) { 23 | this.context = context; 24 | } 25 | 26 | public Strength measure(CharSequence password) { 27 | return measure(password, null); 28 | } 29 | 30 | public Strength measure(CharSequence password, List sanitizedInputs) { 31 | if (password == null) { 32 | throw new IllegalArgumentException("Password is null."); 33 | } 34 | List lowerSanitizedInputs; 35 | if (sanitizedInputs != null && !sanitizedInputs.isEmpty()) { 36 | lowerSanitizedInputs = new ArrayList<>(sanitizedInputs.size()); 37 | for (String sanitizedInput : sanitizedInputs) { 38 | lowerSanitizedInputs.add(sanitizedInput.toLowerCase(Locale.getDefault())); 39 | } 40 | } else { 41 | lowerSanitizedInputs = Collections.emptyList(); 42 | } 43 | long start = time(); 44 | Matching matching = new Matching(context, lowerSanitizedInputs); 45 | List matches = matching.omnimatch(password); 46 | Scoring scoring = new Scoring(context); 47 | MatchSequence matchSequence = scoring.calculateMostGuessableMatchSequence(password, matches); 48 | long end = time() - start; 49 | return new Strength(password, matchSequence.getGuesses(), matchSequence.getSequence(), end); 50 | } 51 | 52 | private long time() { 53 | return System.nanoTime(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/Optimal.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Match; 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | class Optimal { 10 | 11 | private final List> bestMatches = new ArrayList<>(); 12 | 13 | private final List> totalGuesses = new ArrayList<>(); 14 | 15 | private final List> overallMetrics = new ArrayList<>(); 16 | 17 | Optimal(int length) { 18 | for (int i = 0; i < length; i++) { 19 | bestMatches.add(new HashMap()); 20 | totalGuesses.add(new HashMap()); 21 | overallMetrics.add(new HashMap()); 22 | } 23 | } 24 | 25 | Match putToBestMatches(int index, Integer key, Match value) { 26 | return bestMatches.get(index).put(key, value); 27 | } 28 | 29 | Double putToTotalGuesses(int index, Integer key, Double value) { 30 | return totalGuesses.get(index).put(key, value); 31 | } 32 | 33 | Double putToOverallMetrics(int index, Integer key, Double value) { 34 | return overallMetrics.get(index).put(key, value); 35 | } 36 | 37 | Map getBestMatchesAt(int index) { 38 | return bestMatches.get(index); 39 | } 40 | 41 | Map getTotalGuessAt(int index) { 42 | return totalGuesses.get(index); 43 | } 44 | 45 | Map getOverallMetricsAt(int index) { 46 | return overallMetrics.get(index); 47 | } 48 | 49 | Match getBestMatch(int index, int key) { 50 | return getBestMatchesAt(index).get(key); 51 | } 52 | 53 | Double getTotalGuess(int index, int key) { 54 | return getTotalGuessAt(index).get(key); 55 | } 56 | 57 | Double getOverallMetric(int index, int key) { 58 | return getOverallMetricsAt(index).get(key); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/guesses/SpatialGuess.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.guesses; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.matchers.Keyboard; 5 | import com.nulabinc.zxcvbn.matchers.Match; 6 | 7 | public class SpatialGuess extends BaseGuess { 8 | 9 | public SpatialGuess(final Context context) { 10 | super(context); 11 | } 12 | 13 | @Override 14 | public double exec(Match match) { 15 | Keyboard keyboard = this.getContext().getKeyboardMap().get(match.graph); 16 | int startingPositions = keyboard.getStartingPositions(); 17 | double averageDegree = keyboard.getAverageDegree(); 18 | 19 | double totalGuesses = calculateBaseGuesses(match, startingPositions, averageDegree); 20 | totalGuesses *= calculateShiftedVariations(match); 21 | 22 | return totalGuesses; 23 | } 24 | 25 | private double calculateBaseGuesses(Match match, int startingPositions, double averageDegree) { 26 | double guesses = 0; 27 | int tokenLength = match.tokenLength(); 28 | int turns = match.turns; 29 | 30 | for (int i = 2; i <= tokenLength; i++) { 31 | int possibleTurns = Math.min(turns, i - 1); 32 | for (int j = 1; j <= possibleTurns; j++) { 33 | guesses += 34 | calculateBinomialCoefficient(i - 1, j - 1) 35 | * startingPositions 36 | * Math.pow(averageDegree, j); 37 | } 38 | } 39 | return guesses; 40 | } 41 | 42 | private double calculateShiftedVariations(Match match) { 43 | int shiftedCount = match.shiftedCount; 44 | if (shiftedCount == 0) { 45 | return 1; 46 | } 47 | 48 | int unshiftedCount = match.tokenLength() - shiftedCount; 49 | if (unshiftedCount == 0) { 50 | return 2; 51 | } 52 | 53 | int shiftedVariations = 0; 54 | int minCount = Math.min(shiftedCount, unshiftedCount); 55 | for (int i = 1; i <= minCount; i++) { 56 | shiftedVariations += calculateBinomialCoefficient(shiftedCount + unshiftedCount, i); 57 | } 58 | return shiftedVariations; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/DictionaryMatcher.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.WipeableString; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public class DictionaryMatcher extends BaseMatcher { 11 | 12 | private final Map> rankedDictionaries; 13 | 14 | public DictionaryMatcher(Context context, Map> rankedDictionaries) { 15 | super(context); 16 | if (rankedDictionaries == null) { 17 | this.rankedDictionaries = new HashMap<>(); 18 | } else { 19 | this.rankedDictionaries = rankedDictionaries; 20 | } 21 | } 22 | 23 | @Override 24 | public List execute(CharSequence password) { 25 | List matches = new ArrayList<>(); 26 | WipeableString passwordLower = WipeableString.lowerCase(password); 27 | for (Map.Entry> rankedDictionaryRef : 28 | this.rankedDictionaries.entrySet()) { 29 | String dictionaryName = rankedDictionaryRef.getKey(); 30 | Map rankedDict = rankedDictionaryRef.getValue(); 31 | matches.addAll(findMatchesInDictionary(password, passwordLower, dictionaryName, rankedDict)); 32 | } 33 | passwordLower.wipe(); 34 | return this.sorted(matches); 35 | } 36 | 37 | private List findMatchesInDictionary( 38 | CharSequence password, 39 | WipeableString passwordLower, 40 | String dictionaryName, 41 | Map rankedDict) { 42 | List matches = new ArrayList<>(); 43 | int len = password.length(); 44 | for (int startIndex = 0; startIndex < len; startIndex++) { 45 | for (int endIndex = startIndex; endIndex < len; endIndex++) { 46 | CharSequence word = passwordLower.subSequence(startIndex, endIndex + 1).toString(); 47 | Integer rank = rankedDict.get(word); // Try to get the rank directly. 48 | if (rank != null) { 49 | WipeableString token = WipeableString.copy(password, startIndex, endIndex + 1); 50 | matches.add( 51 | MatchFactory.createDictionaryMatch( 52 | startIndex, endIndex, token, word, rank, dictionaryName)); 53 | } 54 | } 55 | } 56 | return matches; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/StandardKeyboards.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.io.ClasspathResource; 4 | import com.nulabinc.zxcvbn.matchers.AlignedKeyboardLoader; 5 | import com.nulabinc.zxcvbn.matchers.Keyboard; 6 | import com.nulabinc.zxcvbn.matchers.KeyboardLoader; 7 | import com.nulabinc.zxcvbn.matchers.SlantedKeyboardLoader; 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class StandardKeyboards { 13 | 14 | @SuppressWarnings("java:S1075") 15 | private static final String RESOURCES_PACKAGE_PATH = "/com/nulabinc/zxcvbn/matchers/keyboards/"; 16 | 17 | public static final String QWERTY = "qwerty"; 18 | 19 | public static final String DVORAK = "dvorak"; 20 | 21 | public static final String JIS = "jis"; 22 | 23 | public static final String KEYPAD = "keypad"; 24 | 25 | public static final String MAC_KEYPAD = "mac_keypad"; 26 | 27 | private StandardKeyboards() { 28 | throw new IllegalStateException("StandardKeyboards should not be instantiated"); 29 | } 30 | 31 | public static final KeyboardLoader QWERTY_LOADER = 32 | new SlantedKeyboardLoader( 33 | QWERTY, new ClasspathResource(RESOURCES_PACKAGE_PATH + "qwerty.txt")); 34 | 35 | public static final KeyboardLoader DVORAK_LOADER = 36 | new SlantedKeyboardLoader( 37 | DVORAK, new ClasspathResource(RESOURCES_PACKAGE_PATH + "dvorak.txt")); 38 | 39 | public static final KeyboardLoader JIS_LOADER = 40 | new SlantedKeyboardLoader(JIS, new ClasspathResource(RESOURCES_PACKAGE_PATH + "jis.txt")); 41 | 42 | public static final KeyboardLoader KEYPAD_LOADER = 43 | new AlignedKeyboardLoader( 44 | KEYPAD, new ClasspathResource(RESOURCES_PACKAGE_PATH + "keypad.txt")); 45 | 46 | public static final KeyboardLoader MAC_KEYPAD_LOADER = 47 | new AlignedKeyboardLoader( 48 | MAC_KEYPAD, new ClasspathResource(RESOURCES_PACKAGE_PATH + "mac_keypad.txt")); 49 | 50 | private static final KeyboardLoader[] ALL_LOADERS = 51 | new KeyboardLoader[] { 52 | QWERTY_LOADER, DVORAK_LOADER, JIS_LOADER, KEYPAD_LOADER, MAC_KEYPAD_LOADER 53 | }; 54 | 55 | public static List loadAllKeyboards() throws IOException { 56 | List keyboards = new ArrayList<>(); 57 | for (KeyboardLoader keyboardLoader : ALL_LOADERS) { 58 | keyboards.add(keyboardLoader.load()); 59 | } 60 | return keyboards; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/nulabinc/zxcvbn/ZxcvbnBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import static org.junit.Assert.assertNotNull; 4 | 5 | import com.nulabinc.zxcvbn.io.ClasspathResource; 6 | import com.nulabinc.zxcvbn.matchers.AlignedKeyboardLoader; 7 | import com.nulabinc.zxcvbn.matchers.DictionaryLoader; 8 | import com.nulabinc.zxcvbn.matchers.SlantedKeyboardLoader; 9 | import java.io.IOException; 10 | import org.junit.*; 11 | 12 | public class ZxcvbnBuilderTest { 13 | 14 | @Test 15 | public void testBuild1() { 16 | Zxcvbn zxcvbn = new ZxcvbnBuilder().build(); 17 | assertNotNull(zxcvbn); 18 | } 19 | 20 | @Test 21 | public void testBuild2() throws IOException { 22 | // This way is same as "new Zxcvbn();" 23 | Zxcvbn zxcvbn = 24 | new ZxcvbnBuilder() 25 | .dictionaries(StandardDictionaries.loadAllDictionaries()) 26 | .keyboards(StandardKeyboards.loadAllKeyboards()) 27 | .build(); 28 | assertNotNull(zxcvbn); 29 | } 30 | 31 | @Test 32 | public void testBuild3() throws IOException { 33 | Zxcvbn zxcvbn = 34 | new ZxcvbnBuilder() 35 | .dictionary(StandardDictionaries.ENGLISH_WIKIPEDIA_LOADER.load()) 36 | .dictionary(StandardDictionaries.PASSWORDS_LOADER.load()) 37 | .keyboard(StandardKeyboards.QWERTY_LOADER.load()) 38 | .keyboard(StandardKeyboards.DVORAK_LOADER.load()) 39 | .build(); 40 | assertNotNull(zxcvbn); 41 | } 42 | 43 | @Test 44 | public void testBuild4() throws IOException { 45 | Zxcvbn zxcvbn = 46 | new ZxcvbnBuilder() 47 | .dictionary( 48 | new DictionaryLoader( 49 | "us_tv_and_film", 50 | new ClasspathResource( 51 | "/com/nulabinc/zxcvbn/matchers/dictionaries/us_tv_and_film.txt")) 52 | .load()) 53 | .keyboard( 54 | new SlantedKeyboardLoader( 55 | "qwerty", 56 | new ClasspathResource("/com/nulabinc/zxcvbn/matchers/keyboards/qwerty.txt")) 57 | .load()) 58 | .keyboard( 59 | new AlignedKeyboardLoader( 60 | "keypad", 61 | new ClasspathResource("/com/nulabinc/zxcvbn/matchers/keyboards/keypad.txt")) 62 | .load()) 63 | .build(); 64 | assertNotNull(zxcvbn); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/messages.properties: -------------------------------------------------------------------------------- 1 | # Feedback 2 | ## Extra 3 | feedback.extra.suggestions.addAnotherWord=Add another word or two. Uncommon words are better. 4 | 5 | ## Default 6 | feedback.default.suggestions.useFewWords=Use a few words, avoid common phrases. 7 | feedback.default.suggestions.noNeedSymbols=No need for symbols, digits, or uppercase letters. 8 | 9 | ## Dictionary 10 | feedback.dictionary.warning.passwords.top10=This is a top-10 common password. 11 | feedback.dictionary.warning.passwords.top100=This is a top-100 common password. 12 | feedback.dictionary.warning.passwords.veryCommon=This is a very common password. 13 | feedback.dictionary.warning.passwords.similar=This is similar to a commonly used password. 14 | feedback.dictionary.warning.englishWikipedia.itself=A word by itself is easy to guess. 15 | feedback.dictionary.warning.etc.namesThemselves=Names and surnames by themselves are easy to guess. 16 | feedback.dictionary.warning.etc.namesCommon=Common names and surnames are easy to guess. 17 | feedback.dictionary.suggestions.capitalization=Capitalization doesn't help very much. 18 | feedback.dictionary.suggestions.allUppercase=All-uppercase is almost as easy to guess as all-lowercase. 19 | feedback.dictionary.suggestions.reversed=Reversed words aren't much harder to guess. 20 | feedback.dictionary.suggestions.l33t=Predictable substitutions like '@' instead of 'a' don't help very much. 21 | 22 | ## Spatial 23 | feedback.spatial.warning.straightRowsOfKeys=Straight rows of keys are easy to guess. 24 | feedback.spatial.warning.shortKeyboardPatterns=Short keyboard patterns are easy to guess. 25 | feedback.spatial.suggestions.UseLongerKeyboardPattern=Use a longer keyboard pattern with more turns. 26 | 27 | ## Repeat 28 | feedback.repeat.warning.likeAAA=Repeats like "aaa" are easy to guess. 29 | feedback.repeat.warning.likeABCABCABC=Repeats like "abcabcabc" are only slightly harder to guess than "abc". 30 | feedback.repeat.suggestions.avoidRepeatedWords=Avoid repeated words and characters. 31 | 32 | ## Sequence 33 | feedback.sequence.warning.likeABCor6543=Sequences like abc or 6543 are easy to guess. 34 | feedback.sequence.suggestions.avoidSequences=Avoid sequences. 35 | 36 | ## Regex 37 | feedback.regex.warning.recentYears=Recent years are easy to guess. 38 | feedback.regex.suggestions.avoidRecentYears=Avoid recent years, avoid years that are associated with you. 39 | 40 | ## Date 41 | feedback.date.warning.dates=Dates are often easy to guess. 42 | feedback.date.suggestions.avoidDates=Avoid dates and years that are associated with you. -------------------------------------------------------------------------------- /src/test/java/com/nulabinc/zxcvbn/MeasureTest.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.Arrays; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.junit.runners.Parameterized; 9 | 10 | @RunWith(Parameterized.class) 11 | public class MeasureTest { 12 | private String password; 13 | 14 | public MeasureTest(String password) { 15 | this.password = password; 16 | } 17 | 18 | @Test 19 | public void testMeasure() throws Exception { 20 | Zxcvbn zxcvbn = new Zxcvbn(); 21 | Strength strength = zxcvbn.measure(password); 22 | assertEquals( 23 | "Unexpected error. Password is " + password, password, strength.getPassword().toString()); 24 | } 25 | 26 | @Parameterized.Parameters(name = "{0}") 27 | public static Iterable data() { 28 | return Arrays.asList( 29 | new Object[][] { 30 | {"qwER43@!"}, 31 | {"Tr0ub4dour&3"}, 32 | {"correcthorsebatterystaple"}, 33 | {"password"}, 34 | {"drowssap"}, 35 | {"passwordp"}, 36 | {"passwordadmin"}, 37 | {"p@$$word@dmin"}, 38 | {"19700101"}, 39 | {"20300101"}, 40 | {"aaaaaaaaa"}, 41 | {"123456789"}, 42 | {"abcdefghijklmnopqrstuvwxyz"}, 43 | {"qwertyuiop@["}, 44 | {"zxcvbnm,./_"}, 45 | {"asdfghjkl;:]"}, 46 | {"pandapandapandapandapandapandapandapandapandaa"}, 47 | {"appleappleappleappleappleappleappleappleapplea"}, 48 | { 49 | "dncrbliehbvkehr734yf;ewhihwfph@houaegfueqpg30^r0urfvhej¥]e;l,ckvniwbgoidnci@oewhfoobojabouhqwou12482386fhoiwehe@o" 50 | }, 51 | {"apple orenge aabb "}, 52 | {"eTq($%u-44c_j9NJB45a#2#JP7sH"}, 53 | {"IB7~EOw!51gug+7s#+%A9P1O/w8f"}, 54 | {"1v_f%7JvS8w!_t398+ON-CObI#v0"}, 55 | {"8lFmfc0!w)&iU9DM6~4_w)D)Y44J"}, 56 | {"&BZ09gjG!iKG&#M09s_1Gr41&o%i"}, 57 | {"T9Y-!ciS%XW9U5l/~aw9+4!5u8Ti"}, 58 | {"QMji&0uze5O#%+%2e_Y08E(R6L8p"}, 59 | {"6EG4y1nJASd!1~!//#6+Yhb1vW3d"}, 60 | {"8$q_5f2U3s6~W(S7iv)_8N%lJkOE"}, 61 | {"%nbd~$)2y/6hV6)2R9vYPpA49A~C"}, 62 | { 63 | "Rh&pW%EXT=/Z1lzouG.wU_+2MT+FG4sm+&jqN?L25jDtjW3EQuppfvD_30Vo3K=SX4=z3-U2gVf7A0oSM5oWegRa_sV$-GLI3LzCo&@!h@$v#OkoN#@-eS8Y&W$pGmmVXc#XHAv?n$M+_wQx1FAB_*iaZE1_9ZV.cwn-d@+90B8z0bVOKc63lV9QntW0kryN7Y#rjv@0+Bd8hc-3WW_Yn%z5/DE?R*UeiKgR#$/F8kA9I!Ib*GDa.x0T7UWCCxDV&ithebyz$=7vW6TdmlmL%WZxmA7K%*Rg1035UO%WOTIgiMs4AjpmL1" 64 | } 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/StandardDictionaries.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.io.ClasspathResource; 4 | import com.nulabinc.zxcvbn.matchers.Dictionary; 5 | import com.nulabinc.zxcvbn.matchers.DictionaryLoader; 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class StandardDictionaries { 11 | 12 | @SuppressWarnings("java:S1075") 13 | private static final String BASE_PATH = "/com/nulabinc/zxcvbn/matchers/dictionaries/"; 14 | 15 | public static final String US_TV_AND_FILM = "us_tv_and_film"; 16 | 17 | public static final String ENGLISH_WIKIPEDIA = "english_wikipedia"; 18 | 19 | public static final String PASSWORDS = "passwords"; 20 | 21 | public static final String SURNAMES = "surnames"; 22 | 23 | public static final String MALE_NAMES = "male_names"; 24 | 25 | public static final String FEMALE_NAMES = "female_names"; 26 | 27 | public static final DictionaryLoader US_TV_AND_FILM_LOADER = 28 | new DictionaryLoader(US_TV_AND_FILM, new ClasspathResource(BASE_PATH + "us_tv_and_film.txt")); 29 | 30 | public static final DictionaryLoader ENGLISH_WIKIPEDIA_LOADER = 31 | new DictionaryLoader( 32 | ENGLISH_WIKIPEDIA, new ClasspathResource(BASE_PATH + "english_wikipedia.txt")); 33 | 34 | public static final DictionaryLoader PASSWORDS_LOADER = 35 | new DictionaryLoader(PASSWORDS, new ClasspathResource(BASE_PATH + "passwords.txt")); 36 | 37 | public static final DictionaryLoader SURNAMES_LOADER = 38 | new DictionaryLoader(SURNAMES, new ClasspathResource(BASE_PATH + "surnames.txt")); 39 | 40 | public static final DictionaryLoader MALE_NAMES_LOADER = 41 | new DictionaryLoader(MALE_NAMES, new ClasspathResource(BASE_PATH + "male_names.txt")); 42 | 43 | public static final DictionaryLoader FEMALE_NAMES_LOADER = 44 | new DictionaryLoader(FEMALE_NAMES, new ClasspathResource(BASE_PATH + "female_names.txt")); 45 | 46 | private StandardDictionaries() { 47 | throw new IllegalStateException("StandardDictionaries should not be instantiated"); 48 | } 49 | 50 | private static final DictionaryLoader[] ALL_LOADERS = { 51 | US_TV_AND_FILM_LOADER, 52 | ENGLISH_WIKIPEDIA_LOADER, 53 | PASSWORDS_LOADER, 54 | SURNAMES_LOADER, 55 | MALE_NAMES_LOADER, 56 | FEMALE_NAMES_LOADER 57 | }; 58 | 59 | public static List loadAllDictionaries() throws IOException { 60 | List dictionaries = new ArrayList<>(); 61 | for (DictionaryLoader dictionaryLoader : ALL_LOADERS) { 62 | dictionaries.add(dictionaryLoader.load()); 63 | } 64 | return dictionaries; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/messages_it.properties: -------------------------------------------------------------------------------- 1 | # Feedback 2 | ## Extra 3 | feedback.extra.suggestions.addAnotherWord=Aggiungi un'altra parola o due. Le parole non comuni sono migliori. 4 | 5 | ## Default 6 | feedback.default.suggestions.useFewWords=Usa alcune parole, evita le frasi comuni. 7 | feedback.default.suggestions.noNeedSymbols=Non c'\u00E8 bisogno di simboli, cifre o lettere maiuscole. 8 | 9 | ## Dictionary 10 | feedback.dictionary.warning.passwords.top10=Questa \u00E8 una delle 10 password pi\u00F9 comuni. 11 | feedback.dictionary.warning.passwords.top100=Questa \u00E8 una delle 100 password pi\u00F9 comuni. 12 | feedback.dictionary.warning.passwords.veryCommon=Questa \u00E8 una password molto comune. 13 | feedback.dictionary.warning.passwords.similar=Questa \u00E8 simile a una password comunemente usata. 14 | feedback.dictionary.warning.englishWikipedia.itself=Una parola da sola \u00E8 facile da indovinare. 15 | feedback.dictionary.warning.etc.namesThemselves=I nomi e i cognomi da soli sono facili da indovinare. 16 | feedback.dictionary.warning.etc.namesCommon=I nomi e i cognomi comuni sono facili da indovinare. 17 | feedback.dictionary.suggestions.capitalization=L'uso delle maiuscole non aiuta molto. 18 | feedback.dictionary.suggestions.allUppercase=Tutto in maiuscolo \u00E8 facile da indovinare quasi come tutto in minuscolo. 19 | feedback.dictionary.suggestions.reversed=Le parole al contrario non sono molto pi\u00F9 difficili da indovinare. 20 | feedback.dictionary.suggestions.l33t=Sostituzioni prevedibili come '@' al posto di 'a' non aiutano molto. 21 | 22 | ## Spatial 23 | feedback.spatial.warning.straightRowsOfKeys=Le righe di tasti sono facili da indovinare. 24 | feedback.spatial.warning.shortKeyboardPatterns=Combinazioni di lettere vicine sulla tastiera sono facili da indovinare. 25 | feedback.spatial.suggestions.UseLongerKeyboardPattern=Usa una combinazione sulla tastiera pi\u00F9 lungha e con pi\u00F9 giri. 26 | 27 | ## Repeat 28 | feedback.repeat.warning.likeAAA=Ripetizioni come "aaa" sono facili da indovinare. 29 | feedback.repeat.warning.likeABCABCABC=Ripetizioni come "abcabcabc" sono solo leggermente pi\u00F9 difficili da indovinare rispetto a "abc". 30 | feedback.repeat.suggestions.avoidRepeatedWords=Evita parole e caratteri ripetuti. 31 | 32 | ## Sequence 33 | feedback.sequence.warning.likeABCor6543=Sequenze come abc o 6543 sono facili da indovinare. 34 | feedback.sequence.suggestions.avoidSequences=Evita le sequenze. 35 | 36 | ## Regex 37 | feedback.regex.warning.recentYears=Anni recenti sono facili da indovinare. 38 | feedback.regex.suggestions.avoidRecentYears=Evita anni recenti, evita anni collegati a te. 39 | 40 | ## Date 41 | feedback.date.warning.dates=Le date sono spesso facili da indovinare. 42 | feedback.date.suggestions.avoidDates=Evita le date e gli anni collegati a te. -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/messages_nl.properties: -------------------------------------------------------------------------------- 1 | # Feedback 2 | ## Extra 3 | feedback.extra.suggestions.addAnotherWord=Voeg een of meerdere woorden toe. Ongebruikelijke woorden zijn beter. 4 | 5 | ## Default 6 | feedback.default.suggestions.useFewWords=Gebruik een aantal woorden, maar vermijd veelgebruikte zinnen. 7 | feedback.default.suggestions.noNeedSymbols=Symbolen, getallen en hoofdletters zijn niet per se noodzakelijk. 8 | 9 | ## Dictionary 10 | feedback.dictionary.warning.passwords.top10=Dit woord staat in de top 10 meest gebruikte woorden. 11 | feedback.dictionary.warning.passwords.top100=Dit woord staat in de top 100 meest gebruikte woorden. 12 | feedback.dictionary.warning.passwords.veryCommon=Dit is een veelgebruikt wachtwoord. 13 | feedback.dictionary.warning.passwords.similar=Dit lijkt op een veelgebruikt wachtwoord. 14 | feedback.dictionary.warning.englishWikipedia.itself=Een los Engels woord is gemakkelijk te raden. 15 | feedback.dictionary.warning.etc.namesThemselves=Voor- en achternamen zijn als zodanig gemakkelijk te raden. 16 | feedback.dictionary.warning.etc.namesCommon=Voor- en achternamen zijn gemakkelijk te raden. 17 | feedback.dictionary.suggestions.capitalization=Hoofdlettergebruik draagt niet veel bij. 18 | feedback.dictionary.suggestions.allUppercase=Enkel hoofdlettergebruik is net zo gemakkelijk te raden als enkel kleine letters. 19 | feedback.dictionary.suggestions.reversed=Woorden achterstevoren gespeld zijn niet veel moeilijker te raden. 20 | feedback.dictionary.suggestions.l33t=Voorspelbare vervangingen zoals '@' in plaats van 'a' dragen niet veel bij. 21 | 22 | ## Spatial 23 | feedback.spatial.warning.straightRowsOfKeys=Opeenvolgende toetsen zijn gemakkelijk te raden. 24 | feedback.spatial.warning.shortKeyboardPatterns=Korte toetsenbordpatronen zijn gemakkelijk te raden. 25 | feedback.spatial.suggestions.UseLongerKeyboardPattern=Gebruik een langer toetsenbordpatroon met afwijkende richtingen. 26 | 27 | ## Repeat 28 | feedback.repeat.warning.likeAAA=Herhalingen zoals "aaa" zijn gemakkelijk te raden. 29 | feedback.repeat.warning.likeABCABCABC=Herhalingen zoals "abcabcabc" zijn slechts marginaal moeilijker te raden dan "abc". 30 | feedback.repeat.suggestions.avoidRepeatedWords=Voorkom herhaling van zowel woorden als karakters. 31 | 32 | ## Sequence 33 | feedback.sequence.warning.likeABCor6543=Opeenvolgende reeksen zoals "abc" of "6543" zijn gemakkelijk te raden. 34 | feedback.sequence.suggestions.avoidSequences=Vermijd gebruik van voorspelbare reeksen. 35 | 36 | ## Regex 37 | feedback.regex.warning.recentYears=Recente jaartallen zijn gemakkelijk te raden. 38 | feedback.regex.suggestions.avoidRecentYears=Vermijd recente en persoonlijke jaartallen, zoals geboortejaren. 39 | 40 | ## Date 41 | feedback.date.warning.dates=Data zijn gemakkelijk te raden. 42 | feedback.date.suggestions.avoidDates=Vermijd het gebruik van recente of persoonlijke data. -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/L33tSubDict.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.Iterator; 6 | import java.util.LinkedHashSet; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | public class L33tSubDict implements Iterable> { 12 | 13 | private final List> l33tSubDictionaries; 14 | 15 | L33tSubDict(Map> table) { 16 | this.l33tSubDictionaries = buildSubDictionaries(table); 17 | } 18 | 19 | private List> buildSubDictionaries( 20 | final Map> table) { 21 | final Set> initialSubs = new LinkedHashSet<>(); 22 | initialSubs.add(new ArrayList()); 23 | final Set> allCombinations = 24 | generateCombinationsRecursively(table, table.keySet().iterator(), initialSubs); 25 | 26 | List> subDictionaries = new ArrayList<>(); 27 | for (List combination : allCombinations) { 28 | Map subDictionary = new HashMap<>(); 29 | for (CharSequence pair : combination) { 30 | subDictionary.put(pair.charAt(0), pair.charAt(1)); 31 | } 32 | subDictionaries.add(subDictionary); 33 | } 34 | return subDictionaries; 35 | } 36 | 37 | private Set> generateCombinationsRecursively( 38 | final Map> table, 39 | final Iterator keysIterator, 40 | final Set> subs) { 41 | if (!keysIterator.hasNext()) { 42 | return subs; 43 | } 44 | 45 | Character key = keysIterator.next(); 46 | Set> nextSubs = new LinkedHashSet<>(); 47 | for (Character l33tChr : table.get(key)) { 48 | for (List sub : subs) { 49 | boolean found = false; 50 | for (int i = 0; i < sub.size(); i++) { 51 | if (sub.get(i).charAt(0) == l33tChr) { 52 | List subAlternative = new ArrayList<>(sub); 53 | subAlternative.remove(i); 54 | subAlternative.add(String.valueOf(new char[] {l33tChr, key})); 55 | nextSubs.add(sub); 56 | nextSubs.add(subAlternative); 57 | found = true; 58 | break; 59 | } 60 | } 61 | if (!found) { 62 | List subExtension = new ArrayList<>(sub); 63 | subExtension.add(String.valueOf(new char[] {l33tChr, key})); 64 | nextSubs.add(subExtension); 65 | } 66 | } 67 | } 68 | 69 | return generateCombinationsRecursively(table, keysIterator, nextSubs); 70 | } 71 | 72 | @Override 73 | public Iterator> iterator() { 74 | return l33tSubDictionaries.iterator(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/messages_de.properties: -------------------------------------------------------------------------------- 1 | # Feedback 2 | ## Extra 3 | feedback.extra.suggestions.addAnotherWord=Bitte ein oder zwei andere W\u00F6rter hinzuf\u00FCgen. Je ungew\u00F6hnlicher, desto besser. 4 | 5 | ## Default 6 | feedback.default.suggestions.useFewWords=M\u00F6glichst wenige Worte und keine Redewendungen verwenden. 7 | feedback.default.suggestions.noNeedSymbols=Symbole, Zahlen oder Gro\u00DFbuchstaben sind nicht notwendig. 8 | 9 | ## Dictionary 10 | feedback.dictionary.warning.passwords.top10=Das ist eines der 10 h\u00E4ufigsten Passw\u00F6rter. 11 | feedback.dictionary.warning.passwords.top100=Das ist eines der 100 h\u00E4ufigsten Passw\u00F6rter. 12 | feedback.dictionary.warning.passwords.veryCommon=Das ist ein sehr h\u00E4ufig verwendetes Passwort. 13 | feedback.dictionary.warning.passwords.similar=Das ist sehr \u00E4hnlich zu einem h\u00E4ufig verwendeten Passwort. 14 | feedback.dictionary.warning.englishWikipedia.itself=Ein einzelnes Wort ist leicht zu erraten. 15 | feedback.dictionary.warning.etc.namesThemselves=Namen sind einfach zu erraten. 16 | feedback.dictionary.warning.etc.namesCommon=Bekannte Namen sind einfach zu erraten. 17 | feedback.dictionary.suggestions.capitalization=Anfangsbuchstaben gro\u00DFzuschreiben tr\u00E4gt nicht zur Sicherheit bei. 18 | feedback.dictionary.suggestions.allUppercase=Nur Gro\u00DFbuchstaben ist genauso leicht zu erraten wie nur Kleinbuchstaben. 19 | feedback.dictionary.suggestions.reversed=Umgedrehte Worte sind nicht schwer zu erraten. 20 | feedback.dictionary.suggestions.l33t=Die Verwendung eines '@' anstatt von 'a' tr\u00E4gt nicht zur Sicherheit bei. 21 | 22 | ## Spatial 23 | feedback.spatial.warning.straightRowsOfKeys=Buchstaben in einer Reihe auf der Tastatur sind einfach zu erraten. 24 | feedback.spatial.warning.shortKeyboardPatterns=Kurze Muster auf der Tastatur sind leicht zu erraten. 25 | feedback.spatial.suggestions.UseLongerKeyboardPattern=Komplizierteres Muster auf der Tastatur verwenden. 26 | 27 | ## Repeat 28 | feedback.repeat.warning.likeAAA=Wiederholungen wie "aaa" sind einfach zu erraten. 29 | feedback.repeat.warning.likeABCABCABC=Wiederholungen wie "abcabcabc" sind lediglich ein wenig schwerer zu erraten als "abc". 30 | feedback.repeat.suggestions.avoidRepeatedWords=Wiederholungen von W\u00F6rtern und Buchstaben vermeiden. 31 | 32 | ## Sequence 33 | feedback.sequence.warning.likeABCor6543=Zeichenfolgen wie "abc" oder "6543" sind leicht zu erraten. 34 | feedback.sequence.suggestions.avoidSequences=Aufeinanderfolgende Zeichen oder Buchstaben bitte vermeiden. 35 | 36 | ## Regex 37 | feedback.regex.warning.recentYears=Die letzten Jahreszahlen sind einfach zu erraten. 38 | feedback.regex.suggestions.avoidRecentYears=Die letzten Jahreszahlen oder Jahre mit pers\u00F6nlicher Bedeutung bitte vermeiden. 39 | 40 | ## Date 41 | feedback.date.warning.dates=Datumsangaben sind leicht zu erraten. 42 | feedback.date.suggestions.avoidDates=Jahreszahlen und Daten mit pers\u00F6nlicher Bedeutung bitte vermeiden. 43 | -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/messages_es.properties: -------------------------------------------------------------------------------- 1 | # Feedback 2 | ## Extra 3 | feedback.extra.suggestions.addAnotherWord=A\u00F1ade otra palabra. Palabras no comunes son mejores. 4 | 5 | ## Default 6 | feedback.default.suggestions.useFewWords=Usa varias palabras, evita frases comunes. 7 | feedback.default.suggestions.noNeedSymbols=No necesita s\u00EDmbolos, d\u00EDgitos o letras may\u00FAsculas. 8 | 9 | ## Dictionary 10 | feedback.dictionary.warning.passwords.top10=Esta contrase\u00F1a se encuentra entre las 10 m\u00E1s comunes. 11 | feedback.dictionary.warning.passwords.top100=Esta contrase\u00F1a se encuentra entre las 100 m\u00E1s comunes. 12 | feedback.dictionary.warning.passwords.veryCommon=Esto es una contrase\u00F1a muy com\u00FAn. 13 | feedback.dictionary.warning.passwords.similar=Esto es similar a una contrase\u00F1a com\u00FAnmente utilizada. 14 | feedback.dictionary.warning.englishWikipedia.itself=Una palabra por s\u00ED sola es f\u00E1cil de adivinar. 15 | feedback.dictionary.warning.etc.namesThemselves=Los nombres y apellidos por s\u00ED solos son f\u00E1ciles de adivinar. 16 | feedback.dictionary.warning.etc.namesCommon=Los nombres y apellidos comunes son f\u00E1ciles de adivinar. 17 | feedback.dictionary.suggestions.capitalization=La capitalizaci\u00F3n no ayuda mucho. 18 | feedback.dictionary.suggestions.allUppercase=Escribir todo en may\u00FAsculas es casi tan f\u00E1cil de adivinar como escribir todo en min\u00FAsculas. 19 | feedback.dictionary.suggestions.reversed=Las palabras al rev\u00E9s no son mucho m\u00E1s dif\u00EDciles de adivinar. 20 | feedback.dictionary.suggestions.l33t=Las sustituciones predecibles como '@' en lugar de 'a' no ayudan mucho. 21 | 22 | ## Spatial 23 | feedback.spatial.warning.straightRowsOfKeys=Las filas de teclas rectas son f\u00E1ciles de adivinar. 24 | feedback.spatial.warning.shortKeyboardPatterns=Los patrones de teclado cortos son f\u00E1ciles de adivinar. 25 | feedback.spatial.suggestions.UseLongerKeyboardPattern=Utiliza un patrón de teclado m\u00E1s largo con m\u00E1s vueltas. 26 | 27 | ## Repeat 28 | feedback.repeat.warning.likeAAA=Repeticiones como "aaa" son f\u00E1ciles de adivinar. 29 | feedback.repeat.warning.likeABCABCABC=Repeticiones como "abcabcabc" son solo ligeramente m\u00E1s dif\u00EDciles de adivinar que "abc". 30 | feedback.repeat.suggestions.avoidRepeatedWords=Evita palabras y caracteres repetidos. 31 | 32 | ## Sequence 33 | feedback.sequence.warning.likeABCor6543=Secuencias como "abc" o "6543" son f\u00E1ciles de adivinar. 34 | feedback.sequence.suggestions.avoidSequences=Evita las secuencias. 35 | 36 | ## Regex 37 | feedback.regex.warning.recentYears=Los a\u00F1os recientes son f\u00E1ciles de adivinar. 38 | feedback.regex.suggestions.avoidRecentYears=Evita a\u00F1os recientes y aquellos a\u00F1os que est\u00E9n relacionados contigo. 39 | 40 | ## Date 41 | feedback.date.warning.dates=Las fechas son a menudo f\u00E1ciles de adivinar. 42 | feedback.date.suggestions.avoidDates=Evita fechas y a\u00F1os que est\u00E9n relacionados contigo. -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/messages_pt.properties: -------------------------------------------------------------------------------- 1 | # Feedback 2 | ## Extra 3 | feedback.extra.suggestions.addAnotherWord=Adicione outra palavra. Palavras incomuns s\u00E3o melhores. 4 | 5 | ## Default 6 | feedback.default.suggestions.useFewWords=Use v\u00E1rias palavras, evite frases comuns. 7 | feedback.default.suggestions.noNeedSymbols=Voc\u00EA n\u00E3o precisa de s\u00EDmbolos, d\u00EDgitos ou letras mai\u00FAsculas. 8 | 9 | ## Dictionary 10 | feedback.dictionary.warning.passwords.top10=Essa senha est\u00E1 entre as 10 mais comuns. 11 | feedback.dictionary.warning.passwords.top100=Essa senha est\u00E1 entre as 100 mais comuns. 12 | feedback.dictionary.warning.passwords.veryCommon=Esta \u00E9 uma senha muito comum. 13 | feedback.dictionary.warning.passwords.similar=Isso \u00E9 semelhante a uma senha comumente usada. 14 | feedback.dictionary.warning.englishWikipedia.itself=Uma palavra por si s\u00F3 \u00E9 f\u00E1cil de adivinhar. 15 | feedback.dictionary.warning.etc.namesThemselves=Somente o nome e o sobrenome s\u00E3o f\u00E1ceis de adivinhar. 16 | feedback.dictionary.warning.etc.namesCommon=Nomes e sobrenomes comuns s\u00E3o f\u00E1ceis de adivinhar. 17 | feedback.dictionary.suggestions.capitalization=A capitaliza\u00E7\u00E3o n\u00E3o ajuda muito. 18 | feedback.dictionary.suggestions.allUppercase=Escrever tudo em letras mai\u00FAsculas \u00E9 quase t\u00E3o f\u00E1cil de adivinhar quanto escrever tudo em letras min\u00FAsculas. 19 | feedback.dictionary.suggestions.reversed=As palavras ao contr\u00E1rio n\u00E3o s\u00E3o muito mais dif\u00EDceis de adivinhar. 20 | feedback.dictionary.suggestions.l33t=Substitui\u00E7\u00F5es previs\u00EDveis como '@' em vez de 'a' n\u00E3o ajudam muito. 21 | 22 | ## Spatial 23 | feedback.spatial.warning.straightRowsOfKeys=As linhas de teclas retas s\u00E3o f\u00E1ceis de adivinhar. 24 | feedback.spatial.warning.shortKeyboardPatterns=Padr\u00F5es curtos de teclado s\u00E3o f\u00E1ceis de adivinhar. 25 | feedback.spatial.suggestions.UseLongerKeyboardPattern=Use um padr\u00E3o de teclado mais longo com mais voltas. 26 | 27 | ## Repeat 28 | feedback.repeat.warning.likeAAA=Repeti\u00E7\u00F5es como "aaa" s\u00E3o f\u00E1ceis de adivinhar. 29 | feedback.repeat.warning.likeABCABCABC=Repeti\u00E7\u00F5es como "abcabcabc" s\u00E3o apenas um pouco mais dif\u00EDceis de adivinhar do que "abc". 30 | feedback.repeat.suggestions.avoidRepeatedWords=Evite palavras e caracteres repetidos. 31 | 32 | ## Sequence 33 | feedback.sequence.warning.likeABCor6543=Sequ\u00EAncias como "abc" ou "6543" s\u00E3o f\u00E1ceis de adivinhar. 34 | feedback.sequence.suggestions.avoidSequences=Evite sequ\u00EAncias. 35 | 36 | ## Regex 37 | feedback.regex.warning.recentYears=Os \u00FAltimos anos s\u00E3o f\u00E1ceis de adivinhar. 38 | feedback.regex.suggestions.avoidRecentYears=Evite os \u00FAltimos anos e os anos que est\u00E3o relacionados a voc\u00EA. 39 | 40 | ## Date 41 | feedback.date.warning.dates=As datas costumam ser f\u00E1ceis de adivinhar. 42 | feedback.date.suggestions.avoidDates=Evite datas e anos relacionados a voc\u00EA. 43 | -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/messages_fr.properties: -------------------------------------------------------------------------------- 1 | # Feedback 2 | ## Extra 3 | feedback.extra.suggestions.addAnotherWord=Ajoutez un ou deux autres mots. Les mots inhabituels sont meilleurs. 4 | 5 | ## Default 6 | feedback.default.suggestions.useFewWords=Utilisez plusieurs mots, \u00E9vitez les phrases courantes. 7 | feedback.default.suggestions.noNeedSymbols=Les caract\u00E8res sp\u00E9ciaux, les chiffres ou les lettres majuscules ne sont pas n\u00E9cessaires. 8 | 9 | ## Dictionary 10 | feedback.dictionary.warning.passwords.top10=C'est l'un des 10 mots de passe les plus courants. 11 | feedback.dictionary.warning.passwords.top100=C'est l'un des 100 mots de passe les plus couramment utilis\u00E9s. 12 | feedback.dictionary.warning.passwords.veryCommon=Il s'agit d'un mot de passe tr\u00E8s courant. 13 | feedback.dictionary.warning.passwords.similar=C'est tr\u00E8s similaire \u00E0 un mot de passe fr\u00E9quemment utilis\u00E9. 14 | feedback.dictionary.warning.englishWikipedia.itself=Un seul mot est facile \u00E0 deviner. 15 | feedback.dictionary.warning.etc.namesThemselves=Les noms et noms de familles individuels sont faciles \u00E0 deviner. 16 | feedback.dictionary.warning.etc.namesCommon=Les noms communs et les noms de famille sont faciles \u00E0 deviner. 17 | feedback.dictionary.suggestions.capitalization=Les majuscules ne sont pas d'une grande aide. 18 | feedback.dictionary.suggestions.allUppercase=Tout en majuscules est presque aussi facile \u00E0 deviner que tout en minuscules. 19 | feedback.dictionary.suggestions.reversed=Les mots invers\u00E9s ne sont pas beaucoup plus difficiles \u00E0 deviner. 20 | feedback.dictionary.suggestions.l33t=Les substitutions pr\u00E9visibles comme'@' au lieu de'a' ne sont pas d'une grande aide. 21 | 22 | ## Spatial 23 | feedback.spatial.warning.straightRowsOfKeys=Les suites de touches sont faciles \u00E0 deviner. 24 | feedback.spatial.warning.shortKeyboardPatterns=De courtes s\u00E9ries de touches sont faciles \u00E0 deviner. 25 | feedback.spatial.suggestions.UseLongerKeyboardPattern=Utilisez une s\u00E9rie plus longue de touches avec des changements de ligne plus fr\u00E9quents. 26 | 27 | ## Repeat 28 | feedback.repeat.warning.likeAAA=Les r\u00E9p\u00E9titions comme'aaaa' sont faciles \u00E0 deviner. 29 | feedback.repeat.warning.likeABCABCABC=Les r\u00E9p\u00E9titions comme'abcabcabcabcabc' sont juste un peu plus difficiles \u00E0 deviner que'abc'. 30 | feedback.repeat.suggestions.avoidRepeatedWords=\u00C9vitez de r\u00E9p\u00E9ter des mots et des lettres. 31 | 32 | ## Sequence 33 | feedback.sequence.warning.likeABCor6543=Les s\u00E9quences comme abc ou 6543 sont faciles \u00E0 deviner. 34 | feedback.sequence.suggestions.avoidSequences=\u00C9vitez les s\u00E9quences. 35 | 36 | ## Regex 37 | feedback.regex.warning.recentYears=Les ann\u00E9es r\u00E9centes sont faciles \u00E0 deviner. 38 | feedback.regex.suggestions.avoidRecentYears=\u00C9vitez les ann\u00E9es r\u00E9centes, \u00E9vitez les ann\u00E9es qui vous sont associ\u00E9es. 39 | 40 | ## Date 41 | feedback.date.warning.dates=Les dates sont souvent faciles \u00E0 deviner. 42 | feedback.date.suggestions.avoidDates=\u00C9vitez les dates et les ann\u00E9es qui vous sont associ\u00E9es. 43 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/io/ClasspathResource.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.io; 2 | 3 | import java.io.FileNotFoundException; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | 7 | public final class ClasspathResource implements Resource { 8 | 9 | private final String path; 10 | 11 | public ClasspathResource(final String path) { 12 | this.path = path; 13 | } 14 | 15 | @Override 16 | public InputStream getInputStream() throws IOException { 17 | InputStream in = this.getResourceAsStreamWithFallback(path); 18 | if (in == null) { 19 | throw new FileNotFoundException("Could not get resource as stream"); 20 | } 21 | return in; 22 | } 23 | 24 | /** 25 | * This code base is spring-framework's ClassUtils#getDefaultClassLoader(). 26 | * https://github.com/spring-projects/spring-framework/blob/dfb7ca733ad309b35040e0027fb7a2f10f3a196a/spring-core/src/main/java/org/springframework/util/ClassUtils.java#L173-L210 27 | * 28 | *

First, return the InputStream to use: typically the thread context ClassLoader, if 29 | * available; Next, the ClassLoader that loaded the ResourceLoader class will be used as fallback. 30 | * Finally, if even the system ClassLoader could not access resource as stream, return null. 31 | */ 32 | @SuppressWarnings("java:S1181") 33 | private InputStream getResourceAsStreamWithFallback(String path) { 34 | // Try loading the resource from the same artifact as this class 35 | InputStream in = getClass().getResourceAsStream(path); 36 | if (in != null) { 37 | return in; 38 | } 39 | 40 | // 1. try to get resource with thread context ClassLoader 41 | try { 42 | ClassLoader cl = Thread.currentThread().getContextClassLoader(); 43 | in = this.getResourceAsStream(cl, path); 44 | if (in != null) { 45 | return in; 46 | } 47 | } catch (Throwable ex) { 48 | // Cannot access thread context ClassLoader - falling back... 49 | } 50 | 51 | // 2. try to get resource with this class context ClassLoader 52 | try { 53 | ClassLoader cl = this.getClass().getClassLoader(); 54 | in = this.getResourceAsStream(cl, path); 55 | if (in != null) { 56 | return in; 57 | } 58 | } catch (Throwable ex) { 59 | // Cannot access this class context ClassLoader - falling back... 60 | } 61 | 62 | // 3. try to get resource with this class context ClassLoader 63 | try { 64 | ClassLoader cl = ClassLoader.getSystemClassLoader(); 65 | in = this.getResourceAsStream(cl, path); 66 | if (in != null) { 67 | return in; 68 | } 69 | } catch (Throwable ex) { 70 | // Cannot access system ClassLoader - oh well, maybe the caller can live with null... 71 | } 72 | 73 | return null; 74 | } 75 | 76 | @SuppressWarnings("java:S1181") 77 | private InputStream getResourceAsStream(ClassLoader cl, String path) { 78 | try { 79 | if (cl != null) { 80 | InputStream in = cl.getResourceAsStream(path); 81 | if (in != null) { 82 | return in; 83 | } 84 | } 85 | } catch (Throwable ex) { 86 | // Cannot access resource as stream 87 | } 88 | return null; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/TimeEstimates.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.RoundingMode; 5 | 6 | public class TimeEstimates { 7 | 8 | private static final double GUESSES_PER_HOUR = 100.0 / 3600.0; 9 | private static final double GUESSES_ONLINE_NO_THROTTLING = 10; 10 | private static final double GUESSES_OFFLINE_SLOW_HASHING = 1e4; 11 | private static final double GUESSES_OFFLINE_FAST_HASHING = 1e10; 12 | private static final int DELTA = 5; 13 | private static final double MINUTE = 60.0; 14 | private static final double HOUR = MINUTE * 60; 15 | private static final double DAY = HOUR * 24; 16 | private static final double MONTH = DAY * 31; 17 | private static final double YEAR = MONTH * 12; 18 | private static final double CENTURY = YEAR * 100; 19 | 20 | private TimeEstimates() { 21 | throw new IllegalStateException("TimeEstimates should not be instantiated"); 22 | } 23 | 24 | public static AttackTimes estimateAttackTimes(double guesses) { 25 | AttackTimes.CrackTimeSeconds crackTimeSeconds = 26 | new AttackTimes.CrackTimeSeconds( 27 | divide(guesses, GUESSES_PER_HOUR), 28 | guesses / GUESSES_ONLINE_NO_THROTTLING, 29 | guesses / GUESSES_OFFLINE_SLOW_HASHING, 30 | guesses / GUESSES_OFFLINE_FAST_HASHING); 31 | AttackTimes.CrackTimesDisplay crackTimesDisplay = 32 | new AttackTimes.CrackTimesDisplay( 33 | displayTime(crackTimeSeconds.getOnlineThrottling100perHour()), 34 | displayTime(crackTimeSeconds.getOnlineNoThrottling10perSecond()), 35 | displayTime(crackTimeSeconds.getOfflineSlowHashing1e4perSecond()), 36 | displayTime(crackTimeSeconds.getOfflineFastHashing1e10PerSecond())); 37 | return new AttackTimes(crackTimeSeconds, crackTimesDisplay, guessesToScore(guesses)); 38 | } 39 | 40 | public static int guessesToScore(double guesses) { 41 | if (guesses < 1e3 + DELTA) { 42 | return 0; 43 | } 44 | if (guesses < 1e6 + DELTA) { 45 | return 1; 46 | } 47 | if (guesses < 1e8 + DELTA) { 48 | return 2; 49 | } 50 | if (guesses < 1e10 + DELTA) { 51 | return 3; 52 | } 53 | return 4; 54 | } 55 | 56 | public static String displayTime(final double seconds) { 57 | if (seconds < 1) { 58 | return "less than a second"; 59 | } 60 | if (seconds < MINUTE) { 61 | return format(seconds, "%s second"); 62 | } 63 | if (seconds < HOUR) { 64 | return format(divide(seconds, MINUTE), "%s minute"); 65 | } 66 | if (seconds < DAY) { 67 | return format(divide(seconds, HOUR), "%s hour"); 68 | } 69 | if (seconds < MONTH) { 70 | return format(divide(seconds, DAY), "%s day"); 71 | } 72 | if (seconds < YEAR) { 73 | return format(divide(seconds, MONTH), "%s month"); 74 | } 75 | if (seconds < CENTURY) { 76 | return format(divide(seconds, YEAR), "%s year"); 77 | } 78 | return "centuries"; 79 | } 80 | 81 | private static String format(double number, String text) { 82 | return String.format(text, Math.round(number)) + (number != 1 ? "s" : ""); 83 | } 84 | 85 | private static double divide(double dividend, double divisor) { 86 | BigDecimal dividendDecimal = BigDecimal.valueOf(dividend); 87 | BigDecimal divisorDecimal = BigDecimal.valueOf(divisor); 88 | return dividendDecimal.divide(divisorDecimal, RoundingMode.HALF_DOWN).doubleValue(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/MatchFactory.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.Pattern; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | public class MatchFactory { 8 | 9 | private MatchFactory() {} 10 | 11 | public static Match createBruteforceMatch(int i, int j, CharSequence token) { 12 | return new Match.Builder(Pattern.Bruteforce, i, j, token).build(); 13 | } 14 | 15 | public static Match createDictionaryMatch( 16 | int i, int j, CharSequence token, CharSequence matchedWord, int rank, String dictionaryName) { 17 | return new Match.Builder(Pattern.Dictionary, i, j, token) 18 | .matchedWord(matchedWord) 19 | .rank(rank) 20 | .dictionaryName(dictionaryName) 21 | .reversed(false) 22 | .l33t(false) 23 | .build(); 24 | } 25 | 26 | public static Match createReversedDictionaryMatch( 27 | int i, int j, CharSequence token, CharSequence matchedWord, int rank, String dictionaryName) { 28 | return new Match.Builder(Pattern.Dictionary, i, j, token) 29 | .matchedWord(matchedWord) 30 | .rank(rank) 31 | .dictionaryName(dictionaryName) 32 | .reversed(true) 33 | .l33t(false) 34 | .build(); 35 | } 36 | 37 | @SuppressWarnings("java:S107") 38 | public static Match createDictionaryL33tMatch( 39 | int i, 40 | int j, 41 | CharSequence token, 42 | CharSequence matchedWord, 43 | int rank, 44 | String dictionaryName, 45 | boolean reversed, 46 | Map sub, 47 | String subDisplay) { 48 | return new Match.Builder(Pattern.Dictionary, i, j, token) 49 | .matchedWord(matchedWord) 50 | .rank(rank) 51 | .dictionaryName(dictionaryName) 52 | .reversed(reversed) 53 | .sub(sub) 54 | .subDisplay(subDisplay) 55 | .l33t(true) 56 | .build(); 57 | } 58 | 59 | public static Match createSpatialMatch( 60 | int i, int j, CharSequence token, String graph, int turns, int shiftedCount) { 61 | return new Match.Builder(Pattern.Spatial, i, j, token) 62 | .graph(graph) 63 | .turns(turns) 64 | .shiftedCount(shiftedCount) 65 | .build(); 66 | } 67 | 68 | public static Match createRepeatMatch( 69 | int i, 70 | int j, 71 | CharSequence token, 72 | CharSequence baseToken, 73 | double baseGuesses, 74 | List baseMatches, 75 | int repeatCount) { 76 | return new Match.Builder(Pattern.Repeat, i, j, token) 77 | .baseToken(baseToken) 78 | .baseGuesses(baseGuesses) 79 | .baseMatches(baseMatches) 80 | .repeatCount(repeatCount) 81 | .build(); 82 | } 83 | 84 | public static Match createSequenceMatch( 85 | int i, int j, CharSequence token, String sequenceName, int sequenceSpace, boolean ascending) { 86 | return new Match.Builder(Pattern.Sequence, i, j, token) 87 | .sequenceName(sequenceName) 88 | .sequenceSpace(sequenceSpace) 89 | .ascending(ascending) 90 | .build(); 91 | } 92 | 93 | public static Match createRegexMatch( 94 | int i, int j, CharSequence token, String regexName, java.util.regex.Matcher regexMatch) { 95 | return new Match.Builder(Pattern.Regex, i, j, token) 96 | .regexName(regexName) 97 | .regexMatch(regexMatch) 98 | .build(); 99 | } 100 | 101 | public static Match createDateMatch( 102 | int i, int j, CharSequence token, String separator, int year, int month, int day) { 103 | return new Match.Builder(Pattern.Date, i, j, token) 104 | .separator(separator) 105 | .year(year) 106 | .month(month) 107 | .day(day) 108 | .build(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/com/nulabinc/zxcvbn/JavaPortTest.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import java.util.Arrays; 4 | import java.util.Map; 5 | import javax.script.ScriptEngine; 6 | import org.junit.Assert; 7 | import org.junit.BeforeClass; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.junit.runners.Parameterized; 11 | 12 | @RunWith(Parameterized.class) 13 | public class JavaPortTest { 14 | private static ScriptEngine engine; 15 | private final String password; 16 | private final Zxcvbn zxcvbn; 17 | 18 | public JavaPortTest(String password) { 19 | this.password = password; 20 | this.zxcvbn = new Zxcvbn(); 21 | } 22 | 23 | @BeforeClass 24 | public static void initEngine() { 25 | engine = new JSScriptEngineBuilder().build(); 26 | } 27 | 28 | @Test 29 | public void testMeasure() throws Exception { 30 | // add password to the engine scope 31 | engine.put("pwd", password); 32 | @SuppressWarnings("unchecked") 33 | Map result = (Map) engine.eval("zxcvbn(pwd);"); 34 | Object score = result.get("score"); 35 | int jsScore; 36 | // nashorn returns int, rhino returns double 37 | if (score instanceof Double) { 38 | jsScore = ((Double) score).intValue(); 39 | } else { 40 | jsScore = (int) score; 41 | } 42 | Strength strength = zxcvbn.measure(password); 43 | int javaScore = strength.getScore(); 44 | Assert.assertEquals("Password score difference for " + password, jsScore, javaScore); 45 | } 46 | 47 | @Parameterized.Parameters(name = "{0}") 48 | public static Iterable data() { 49 | return Arrays.asList( 50 | new Object[][] { 51 | {"qwER43@!"}, 52 | {"Tr0ub4dour&3"}, 53 | {"correcthorsebatterystaple"}, 54 | {"password"}, 55 | {"drowssap"}, 56 | {"passwordp"}, 57 | {"passwordadmin"}, 58 | {"p@$$word@dmin"}, 59 | {"19700101"}, 60 | {"20300101"}, 61 | {"aaaaaaaaa"}, 62 | {"123456789"}, 63 | {"abcdefghijklmnopqrstuvwxyz"}, 64 | {"qwertyuiop@["}, 65 | {"zxcvbnm,./_"}, 66 | {"asdfghjkl;:]"}, 67 | {"pandapandapandapandapandapandapandapandapandaa"}, 68 | {"appleappleappleappleappleappleappleappleapplea"}, 69 | { 70 | "dncrbliehbvkehr734yf;ewhihwfph@houaegfueqpg30^r0urfvhej¥]e;l,ckvniwbgoidnci@oewhfoobojabouhqwou12482386fhoiwehe@o" 71 | }, 72 | {"apple orenge aabb "}, 73 | {"eTq($%u-44c_j9NJB45a#2#JP7sH"}, 74 | {"IB7~EOw!51gug+7s#+%A9P1O/w8f"}, 75 | {"1v_f%7JvS8w!_t398+ON-CObI#v0"}, 76 | {"8lFmfc0!w)&iU9DM6~4_w)D)Y44J"}, 77 | {"&BZ09gjG!iKG&#M09s_1Gr41&o%i"}, 78 | {"T9Y-!ciS%XW9U5l/~aw9+4!5u8Ti"}, 79 | {"QMji&0uze5O#%+%2e_Y08E(R6L8p"}, 80 | {"6EG4y1nJASd!1~!//#6+Yhb1vW3d"}, 81 | {"8$q_5f2U3s6~W(S7iv)_8N%lJkOE"}, 82 | {"%nbd~$)2y/6hV6)2R9vYPpA49A~C"}, 83 | {"xsw234rfvb"}, 84 | {"yaq123edc"}, 85 | {"cde345tgbn"}, 86 | {"yaqwedcvb"}, 87 | {"5621127"}, 88 | {"61526611441"}, 89 | {"0078690420729"}, 90 | {"zhang198822"}, 91 | {"Sigma@123"}, 92 | {"password@123"}, 93 | {"lkjhgfdsa"}, 94 | {"hGFd"}, 95 | {"2352523452bd dhf"}, 96 | {"23525"}, 97 | // the following password fails in version 4.4.1 98 | // https://github.com/dropbox/zxcvbn/issues/174 99 | // 100 | // {"Rh&pW%EXT=/Z1lzouG.wU_+2MT+FG4sm+&jqN?L25jDtjW3EQuppfvD_30Vo3K=SX4=z3-U2gVf7A0oSM5oWegRa_sV$-GLI3LzCo&@!h@$v#OkoN#@-eS8Y&W$pGmmVXc#XHAv?n$M+_wQx1FAB_*iaZE1_9ZV.cwn-d@+90B8z0bVOKc63lV9QntW0kryN7Y#rjv@0+Bd8hc-3WW_Yn%z5/DE?R*UeiKgR#$/F8kA9I!Ib*GDa.x0T7UWCCxDV&ithebyz$=7vW6TdmlmL%WZxmA7K%*Rg1035UO%WOTIgiMs4AjpmL1"} 101 | }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/nulabinc/zxcvbn/WipeableStringTest.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import static junit.framework.TestCase.*; 4 | import static org.junit.Assert.assertNotEquals; 5 | 6 | import java.nio.CharBuffer; 7 | import org.junit.Test; 8 | 9 | public class WipeableStringTest { 10 | 11 | @Test 12 | public void testHashCode() { 13 | assertEquals("testing".hashCode(), new WipeableString("testing").hashCode()); 14 | } 15 | 16 | @Test 17 | public void testEquals() { 18 | assertEquals("hello", new WipeableString("hello").toString()); 19 | assertNotEquals("hello", new WipeableString("goodbye").toString()); 20 | } 21 | 22 | @Test 23 | public void testLowerCase() { 24 | assertEquals("abc", WipeableString.lowerCase("ABC").toString()); 25 | assertEquals("abc", WipeableString.lowerCase("abc").toString()); 26 | assertEquals("abcxyz", WipeableString.lowerCase("abcXYZ").toString()); 27 | assertEquals("", WipeableString.lowerCase("").toString()); 28 | } 29 | 30 | @Test 31 | public void testReversed() { 32 | assertEquals("CBA", WipeableString.reversed("ABC").toString()); 33 | assertEquals("", WipeableString.reversed("").toString()); 34 | assertEquals("X", WipeableString.reversed("X").toString()); 35 | } 36 | 37 | @Test 38 | public void testWipeIfPossible() { 39 | testWipeIfPossible(new StringBuffer("password"), "wiping StringBuffer"); 40 | testWipeIfPossible(new StringBuilder("password"), "wiping StringBuilder"); 41 | testWipeIfPossible(CharBuffer.wrap("password".toCharArray()), "wiping CharBuffer"); 42 | } 43 | 44 | private void testWipeIfPossible(CharSequence text, String message) { 45 | assertEquals(message + " (before)", "password", text.toString()); 46 | WipeableString.wipeIfPossible(text); 47 | assertEquals(message, "", text.toString().trim()); 48 | } 49 | 50 | @Test 51 | public void testWipeStrength() { 52 | Strength strength = new Zxcvbn().measure(new WipeableString("pa55w0rd")); 53 | 54 | assertEquals("pa55w0rd", strength.getPassword().toString()); 55 | assertEquals("pa55w0rd", strength.getSequence().get(0).token.toString()); 56 | 57 | strength.wipe(); 58 | 59 | assertEquals("", strength.getPassword().toString()); 60 | assertEquals("", strength.getSequence().get(0).token.toString()); 61 | } 62 | 63 | @Test 64 | public void testParseInt() { 65 | assertEquals(1, WipeableString.parseInt("1")); 66 | assertEquals(1, WipeableString.parseInt("+1")); 67 | assertEquals(1, WipeableString.parseInt("+01")); 68 | assertEquals(1928, WipeableString.parseInt("1928")); 69 | assertEquals(19369, WipeableString.parseInt("00019369")); 70 | assertEquals(-101, WipeableString.parseInt("-101")); 71 | assertEquals(5, WipeableString.parseInt("101", 2)); 72 | } 73 | 74 | @Test 75 | public void testWipeStrengthWithStringPassword() { 76 | Strength strength = new Zxcvbn().measure("pa55w0rd"); 77 | 78 | assertEquals("pa55w0rd", strength.getPassword().toString()); 79 | 80 | strength.wipe(); 81 | 82 | assertEquals("string passwords cannot be wiped", "pa55w0rd", strength.getPassword().toString()); 83 | } 84 | 85 | @Test 86 | public void testParseIntWithTrailingSpaces() { 87 | assertEquals(2001, WipeableString.parseInt("2001 ")); 88 | assertEquals(1, WipeableString.parseInt("1 ")); 89 | assertEquals(2001, WipeableString.parseInt("2001 ")); 90 | } 91 | 92 | @Test 93 | public void testParseIntWithTrailingCRLF() { 94 | assertEquals( 95 | 2001, 96 | WipeableString.parseInt( 97 | CharBuffer.wrap(new char[] {'2', '0', '0', '1', (char) 13, (char) 10}))); 98 | } 99 | 100 | @Test 101 | public void testParseIntWithTrailingCR() { 102 | assertEquals( 103 | 2001, WipeableString.parseInt(CharBuffer.wrap(new char[] {'2', '0', '0', '1', (char) 13}))); 104 | } 105 | 106 | @Test 107 | public void testParseIntWithTrailingLF() { 108 | assertEquals( 109 | 2001, WipeableString.parseInt(CharBuffer.wrap(new char[] {'2', '0', '0', '1', (char) 10}))); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/guesses/DictionaryGuess.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.guesses; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.WipeableString; 5 | import com.nulabinc.zxcvbn.matchers.Match; 6 | import java.util.AbstractMap; 7 | import java.util.Map; 8 | import java.util.regex.Pattern; 9 | 10 | public class DictionaryGuess extends BaseGuess { 11 | 12 | public static final Pattern START_UPPER = Pattern.compile("^[A-Z][^A-Z]+$"); 13 | private static final Pattern END_UPPER = Pattern.compile("^[^A-Z]+[A-Z]$"); 14 | public static final Pattern ALL_UPPER = Pattern.compile("^[^a-z]+$"); 15 | private static final Pattern ALL_LOWER = Pattern.compile("^[^A-Z]+$"); 16 | 17 | public DictionaryGuess(final Context context) { 18 | super(context); 19 | } 20 | 21 | @Override 22 | public double exec(Match match) { 23 | match.baseGuesses = (double) match.rank; 24 | int uppercaseVariations = uppercaseVariations(match); 25 | int l33tVariations = l33tVariations(match); 26 | int reversedVariations = match.reversed ? 2 : 1; 27 | return (double) match.rank * uppercaseVariations * l33tVariations * reversedVariations; 28 | } 29 | 30 | public int uppercaseVariations(Match match) { 31 | CharSequence token = match.token; 32 | WipeableString lowercaseToken = WipeableString.lowerCase(token); 33 | if (ALL_LOWER.matcher(token).find(0) || lowercaseToken.equals(token)) { 34 | return 1; 35 | } 36 | if (START_UPPER.matcher(token).find() 37 | || END_UPPER.matcher(token).find() 38 | || ALL_UPPER.matcher(token).find()) { 39 | return 2; 40 | } 41 | 42 | int upperCount = 0; 43 | int lowerCount = 0; 44 | for (int i = 0; i < token.length(); i++) { 45 | lowerCount += Character.isLowerCase(token.charAt(i)) ? 1 : 0; 46 | upperCount += Character.isUpperCase(token.charAt(i)) ? 1 : 0; 47 | } 48 | int variations = 0; 49 | for (int i = 1; i <= Math.min(upperCount, lowerCount); i++) { 50 | variations += calculateBinomialCoefficient(upperCount + lowerCount, i); 51 | } 52 | lowercaseToken.wipe(); 53 | return variations; 54 | } 55 | 56 | public int l33tVariations(Match match) { 57 | if (!match.l33t) { 58 | return 1; 59 | } 60 | int totalVariations = 1; 61 | WipeableString lowercaseToken = WipeableString.lowerCase(match.token); 62 | for (Map.Entry substitution : match.sub.entrySet()) { 63 | totalVariations *= calculateSubstitutionVariation(substitution, lowercaseToken); 64 | } 65 | return totalVariations; 66 | } 67 | 68 | private static int calculateSubstitutionVariation( 69 | Map.Entry substitution, WipeableString token) { 70 | Character substitutedChar = substitution.getKey(); 71 | Character originalChar = substitution.getValue(); 72 | AbstractMap.SimpleImmutableEntry counts = 73 | countCharOccurrences(token, substitutedChar, originalChar); 74 | int substitutedCount = counts.getKey(); 75 | int originalCount = counts.getValue(); 76 | if (substitutedCount == 0 || originalCount == 0) { 77 | return 2; 78 | } 79 | return calculatePossibleCombinations(originalCount, substitutedCount); 80 | } 81 | 82 | private static AbstractMap.SimpleImmutableEntry countCharOccurrences( 83 | WipeableString str, char char1, char char2) { 84 | int count1 = 0; 85 | int count2 = 0; 86 | for (char currentChar : str.charArray()) { 87 | if (currentChar == char1) { 88 | count1++; 89 | } 90 | if (currentChar == char2) { 91 | count2++; 92 | } 93 | } 94 | return new AbstractMap.SimpleImmutableEntry<>(count1, count2); 95 | } 96 | 97 | private static int calculatePossibleCombinations(int originalCount, int substitutedCount) { 98 | int minCount = Math.min(originalCount, substitutedCount); 99 | int possibleCombinations = 0; 100 | for (int i = 1; i <= minCount; i++) { 101 | possibleCombinations += calculateBinomialCoefficient(originalCount + substitutedCount, i); 102 | } 103 | return possibleCombinations; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/SpatialMatcher.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.WipeableString; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.LinkedHashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.regex.Pattern; 11 | 12 | public class SpatialMatcher extends BaseMatcher { 13 | 14 | private static final Pattern SHIFTED_RX = 15 | Pattern.compile("[~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:\"ZXCVBNM<>?]"); 16 | 17 | private static final List EMPTY_ADJACENTS = Collections.emptyList(); 18 | 19 | private final Map keyboards; 20 | 21 | public SpatialMatcher(Context context, Map keyboardMap) { 22 | super(context); 23 | this.keyboards = new LinkedHashMap<>(keyboardMap); 24 | } 25 | 26 | public SpatialMatcher(Context context) { 27 | this(context, context.getKeyboardMap()); 28 | } 29 | 30 | @Override 31 | public List execute(CharSequence password) { 32 | List matches = new ArrayList<>(); 33 | for (Keyboard keyboard : this.keyboards.values()) { 34 | matches.addAll(findSpatialMatchesInKeyboard(password, keyboard)); 35 | } 36 | return this.sorted(matches); 37 | } 38 | 39 | private List findSpatialMatchesInKeyboard(CharSequence password, Keyboard keyboard) { 40 | List matches = new ArrayList<>(); 41 | int curCharIndex = 0; 42 | while (curCharIndex < password.length() - 1) { 43 | curCharIndex = processSpatialMatch(password, keyboard, matches, curCharIndex); 44 | } 45 | return matches; 46 | } 47 | 48 | private int processSpatialMatch( 49 | CharSequence password, Keyboard keyboard, List matches, int curCharIndex) { 50 | int nextCharIndex = curCharIndex + 1; 51 | Integer lastDirection = null; 52 | int turns = 0; 53 | int shiftedCount = calculateShiftedCount(keyboard, password.charAt(curCharIndex)); 54 | final Map> graph = keyboard.getAdjacencyGraph(); 55 | while (true) { 56 | char prevChar = password.charAt(nextCharIndex - 1); 57 | List adjacents = graph.containsKey(prevChar) ? graph.get(prevChar) : EMPTY_ADJACENTS; 58 | AdjacentSearchResult result = findAdjacent(password, nextCharIndex, adjacents); 59 | if (result.found) { 60 | nextCharIndex++; 61 | shiftedCount += result.shiftedCount; 62 | if (lastDirection == null || lastDirection != result.foundDirection) { 63 | // adding a turn is correct even in the initial case when last_direction is null: 64 | // every spatial pattern starts with a turn. 65 | turns++; 66 | lastDirection = result.foundDirection; 67 | } 68 | } else { 69 | if (nextCharIndex - curCharIndex > 2) { 70 | matches.add( 71 | MatchFactory.createSpatialMatch( 72 | curCharIndex, 73 | nextCharIndex - 1, 74 | WipeableString.copy(password, curCharIndex, nextCharIndex), 75 | keyboard.getName(), 76 | turns, 77 | shiftedCount)); 78 | } 79 | return nextCharIndex; 80 | } 81 | } 82 | } 83 | 84 | private int calculateShiftedCount(Keyboard keyboard, char charAt) { 85 | return (keyboard.isSlanted() && SHIFTED_RX.matcher(String.valueOf(charAt)).find()) ? 1 : 0; 86 | } 87 | 88 | private static class AdjacentSearchResult { 89 | boolean found; 90 | int foundDirection; 91 | int shiftedCount; 92 | 93 | AdjacentSearchResult(boolean found, int foundDirection, int shiftedCount) { 94 | this.found = found; 95 | this.foundDirection = foundDirection; 96 | this.shiftedCount = shiftedCount; 97 | } 98 | } 99 | 100 | private AdjacentSearchResult findAdjacent( 101 | CharSequence password, int curCharIndex, List adjacents) { 102 | int curDirection = -1; 103 | if (curCharIndex < password.length()) { 104 | char curChar = password.charAt(curCharIndex); 105 | String curString = String.valueOf(curChar); 106 | for (String adj : adjacents) { 107 | curDirection++; 108 | int foundAdjacentIndex = adj != null ? adj.indexOf(curString) : -1; 109 | if (foundAdjacentIndex != -1) { 110 | return new AdjacentSearchResult(true, curDirection, foundAdjacentIndex == 1 ? 1 : 0); 111 | } 112 | } 113 | } 114 | return new AdjacentSearchResult(false, 0, 0); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/RepeatMatcher.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.MatchSequence; 5 | import com.nulabinc.zxcvbn.Matching; 6 | import com.nulabinc.zxcvbn.Scoring; 7 | import com.nulabinc.zxcvbn.WipeableString; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.regex.Pattern; 11 | 12 | public class RepeatMatcher extends BaseMatcher { 13 | 14 | private static final Pattern GREEDY_PATTERN = Pattern.compile("(.+)\\1+"); 15 | private static final Pattern LAZY_PATTERN = Pattern.compile("(.+?)\\1+"); 16 | private static final Pattern LAZY_ANCHORED_PATTERN = Pattern.compile("^(.+?)\\1+$"); 17 | 18 | private final Scoring scoring; 19 | private final Matching matching; 20 | 21 | public RepeatMatcher(final Context context) { 22 | super(context); 23 | this.scoring = new Scoring(context); 24 | this.matching = new Matching(context, new ArrayList()); 25 | } 26 | 27 | @Override 28 | public List execute(CharSequence password) { 29 | List matches = new ArrayList<>(); 30 | int passwordLength = password.length(); 31 | int lastIndex = 0; 32 | 33 | while (lastIndex < passwordLength) { 34 | java.util.regex.Matcher greedyMatcher = 35 | createRegionMatcher(GREEDY_PATTERN, password, lastIndex, passwordLength); 36 | java.util.regex.Matcher lazyMatcher = 37 | createRegionMatcher(LAZY_PATTERN, password, lastIndex, passwordLength); 38 | 39 | if (!greedyMatcher.find()) { 40 | break; 41 | } 42 | 43 | ChosenMatch chosenMatch = chooseMatch(greedyMatcher, lazyMatcher); 44 | Match repeatMatch = 45 | createRepeatMatch( 46 | chosenMatch.baseToken, chosenMatch.matchResult, chosenMatch.start, chosenMatch.end); 47 | matches.add(repeatMatch); 48 | lastIndex = chosenMatch.end + 1; 49 | } 50 | 51 | return matches; 52 | } 53 | 54 | private java.util.regex.Matcher createRegionMatcher( 55 | Pattern pattern, CharSequence password, int start, int end) { 56 | java.util.regex.Matcher matcher = pattern.matcher(password); 57 | matcher.region(start, end); 58 | return matcher; 59 | } 60 | 61 | private ChosenMatch chooseMatch( 62 | java.util.regex.Matcher greedyMatcher, java.util.regex.Matcher lazyMatcher) { 63 | 64 | String greedyMatchResult = greedyMatcher.group(0); 65 | String lazyMatchResult = lazyMatcher.find() ? lazyMatcher.group(0) : ""; 66 | boolean isGreedyLonger = greedyMatchResult.length() > lazyMatchResult.length(); 67 | 68 | String matchResult; 69 | CharSequence baseToken; 70 | int start; 71 | int end; 72 | 73 | if (isGreedyLonger) { 74 | matchResult = greedyMatchResult; 75 | baseToken = deriveBaseTokenFromGreedyMatchResult(greedyMatchResult); 76 | start = greedyMatcher.start(0); 77 | end = start + greedyMatchResult.length() - 1; 78 | } else { 79 | matchResult = lazyMatchResult; 80 | baseToken = lazyMatcher.group(1); 81 | start = lazyMatcher.start(0); 82 | end = start + lazyMatchResult.length() - 1; 83 | } 84 | return new ChosenMatch(matchResult, baseToken, start, end); 85 | } 86 | 87 | private CharSequence deriveBaseTokenFromGreedyMatchResult(String greedyMatchResult) { 88 | java.util.regex.Matcher lazyAnchoredMatcher = LAZY_ANCHORED_PATTERN.matcher(greedyMatchResult); 89 | return lazyAnchoredMatcher.find() ? lazyAnchoredMatcher.group(1) : greedyMatchResult; 90 | } 91 | 92 | private Match createRepeatMatch(CharSequence baseToken, String matchResult, int start, int end) { 93 | List omnimatch = matching.omnimatch(baseToken); 94 | MatchSequence baseAnalysis = scoring.calculateMostGuessableMatchSequence(baseToken, omnimatch); 95 | CharSequence wipeableBaseToken = new WipeableString(baseToken); 96 | int repeatCount = matchResult.length() / wipeableBaseToken.length(); 97 | return MatchFactory.createRepeatMatch( 98 | start, 99 | end, 100 | matchResult, 101 | wipeableBaseToken, 102 | baseAnalysis.getGuesses(), 103 | baseAnalysis.getSequence(), 104 | repeatCount); 105 | } 106 | 107 | private static class ChosenMatch { 108 | final String matchResult; 109 | final CharSequence baseToken; 110 | final int start; 111 | final int end; 112 | 113 | public ChosenMatch(String matchResult, CharSequence baseToken, int start, int end) { 114 | this.matchResult = matchResult; 115 | this.baseToken = baseToken; 116 | this.start = start; 117 | this.end = end; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/resources/com/nulabinc/zxcvbn/messages_ja.properties: -------------------------------------------------------------------------------- 1 | # Feedback 2 | ## Extra 3 | feedback.extra.suggestions.addAnotherWord=1\u301C2\u306E\u5358\u8A9E\u3092\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4E00\u822C\u7684\u3067\u306F\u306A\u3044\u5358\u8A9E\u306E\u307B\u3046\u304C\u9069\u3057\u3066\u3044\u307E\u3059\u3002 4 | 5 | ## Default 6 | feedback.default.suggestions.useFewWords=\u4E00\u822C\u7684\u306A\u30D5\u30EC\u30FC\u30BA\u3092\u907F\u3051\u308B\u305F\u3081\u3001\u3044\u304F\u3064\u304B\u306E\u5358\u8A9E\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002 7 | feedback.default.suggestions.noNeedSymbols=\u8A18\u53F7\u3001\u6570\u5B57\u3001\u307E\u305F\u306F\u5927\u6587\u5B57\u3067\u3042\u308B\u5FC5\u8981\u306F\u3042\u308A\u307E\u305B\u3093\u3002 8 | 9 | ## Dictionary 10 | feedback.dictionary.warning.passwords.top10=\u3053\u308C\u306F\u4F7F\u308F\u308C\u3084\u3059\u3044\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u30C8\u30C3\u30D710\u3067\u3059\u3002 11 | feedback.dictionary.warning.passwords.top100=\u3053\u308C\u306F\u4F7F\u308F\u308C\u3084\u3059\u3044\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u30C8\u30C3\u30D7100\u3067\u3059\u3002 12 | feedback.dictionary.warning.passwords.veryCommon=\u3053\u308C\u306F\u4E00\u822C\u7684\u306B\u3088\u304F\u4F7F\u308F\u308C\u308B\u30D1\u30B9\u30EF\u30FC\u30C9\u3067\u3059\u3002 13 | feedback.dictionary.warning.passwords.similar=\u3053\u308C\u306F\u4E00\u822C\u7684\u306B\u3088\u304F\u4F7F\u308F\u308C\u308B\u30D1\u30B9\u30EF\u30FC\u30C9\u306B\u4F3C\u3066\u3044\u307E\u3059\u3002 14 | feedback.dictionary.warning.englishWikipedia.itself=\u63A8\u6E2C\u3057\u3084\u3059\u3044\u5358\u8A9E\u3067\u3059\u3002 15 | feedback.dictionary.warning.etc.namesThemselves=\u81EA\u5206\u81EA\u8EAB\u306E\u59D3\u540D\u306F\u7C21\u5358\u306B\u63A8\u6E2C\u3067\u304D\u3066\u3057\u307E\u3044\u307E\u3059\u3002 16 | feedback.dictionary.warning.etc.namesCommon=\u4E00\u822C\u7684\u306A\u59D3\u540D\u306F\u7C21\u5358\u306B\u63A8\u6E2C\u3067\u304D\u3066\u3057\u307E\u3044\u307E\u3059\u3002 17 | feedback.dictionary.suggestions.capitalization=\u5927\u6587\u5B57\u306B\u3059\u308B\u3053\u3068\u306F\u3042\u307E\u308A\u52B9\u679C\u304C\u3042\u308A\u307E\u305B\u3093\u3002 18 | feedback.dictionary.suggestions.allUppercase=\u3059\u3079\u3066\u5927\u6587\u5B57\u306B\u3057\u305F\u3068\u3057\u3066\u3082\u3001\u3059\u3079\u3066\u5C0F\u6587\u5B57\u306B\u3057\u305F\u3068\u304D\u3068\u540C\u3058\u304F\u3089\u3044\u7C21\u5358\u306B\u63A8\u6E2C\u3067\u304D\u3066\u3057\u307E\u3044\u307E\u3059\u3002 19 | feedback.dictionary.suggestions.reversed=\u53CD\u8EE2\u3057\u305F\u5358\u8A9E\u3092\u63A8\u6E2C\u3059\u308B\u3053\u3068\u306F\u3042\u307E\u308A\u96E3\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002 20 | feedback.dictionary.suggestions.l33t=\u300Ca\u300D\u306E\u4EE3\u308F\u308A\u306B\u300C@\u300D\u3092\u4F7F\u3046\u3088\u3046\u306A\u3001\u4E88\u6E2C\u3057\u3084\u3059\u3044\u7F6E\u304D\u63DB\u3048\u306F\u3042\u307E\u308A\u52B9\u679C\u304C\u3042\u308A\u307E\u305B\u3093\u3002 21 | 22 | ## Spatial 23 | feedback.spatial.warning.straightRowsOfKeys=\u30AD\u30FC\u30DC\u30FC\u30C9\u306E1\u884C\u306F\u7C21\u5358\u306B\u63A8\u6E2C\u3067\u304D\u3066\u3057\u307E\u3044\u307E\u3059\u3002 24 | feedback.spatial.warning.shortKeyboardPatterns=\u77ED\u3044\u30AD\u30FC\u30DC\u30FC\u30C9\u306E\u30D1\u30BF\u30FC\u30F3\u306F\u7C21\u5358\u306B\u63A8\u6E2C\u3067\u304D\u3066\u3057\u307E\u3044\u307E\u3059\u3002 25 | feedback.spatial.suggestions.UseLongerKeyboardPattern=\u3082\u3063\u3068\u9577\u3044\u30AD\u30FC\u30DC\u30FC\u30C9\u306E\u30D1\u30BF\u30FC\u30F3\u3092\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044\u3002 26 | 27 | ## Repeat 28 | feedback.repeat.warning.likeAAA=\u300Caaa\u300D\u306E\u3088\u3046\u306A\u7E70\u308A\u8FD4\u3057\u306F\u7C21\u5358\u306B\u63A8\u6E2C\u3067\u304D\u3066\u3057\u307E\u3044\u307E\u3059\u3002 29 | feedback.repeat.warning.likeABCABCABC=\u300Cabcabcabc\u300D\u306E\u3088\u3046\u306A\u7E70\u308A\u8FD4\u3057\u306F\u300Caaa\u300D\u306E\u3088\u3046\u306A\u7E70\u308A\u8FD4\u3057\u3088\u308A\u3082\u5C11\u3057\u96E3\u3057\u3044\u3060\u3051\u3067\u3059\u3002 30 | feedback.repeat.suggestions.avoidRepeatedWords=\u5358\u8A9E\u3084\u6587\u5B57\u306E\u7E70\u308A\u8FD4\u3057\u306F\u907F\u3051\u3066\u304F\u3060\u3055\u3044\u3002 31 | 32 | ## Sequence 33 | feedback.sequence.warning.likeABCor6543=\u300Cabc\u300D\u3084\u300C6543\u300D\u306E\u3088\u3046\u306A\u4E26\u3073\u306F\u7C21\u5358\u306B\u63A8\u6E2C\u3067\u304D\u3066\u3057\u307E\u3044\u307E\u3059\u3002 34 | feedback.sequence.suggestions.avoidSequences=\u30B7\u30FC\u30B1\u30F3\u30B9\u3092\u907F\u3051\u308B\u3088\u3046\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002 35 | 36 | ## Regex 37 | feedback.regex.warning.recentYears=\u6700\u8FD1\u306E\u5E74\u306F\u7C21\u5358\u306B\u63A8\u6E2C\u3067\u304D\u3066\u3057\u307E\u3044\u307E\u3059\u3002 38 | feedback.regex.suggestions.avoidRecentYears=\u6700\u8FD1\u306E\u5E74\u3084\u3042\u306A\u305F\u306B\u95A2\u9023\u3059\u308B\u5E74\u3092\u907F\u3051\u308B\u3088\u3046\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002 39 | 40 | ## Date 41 | feedback.date.warning.dates=\u65E5\u4ED8\u306F\u3057\u3070\u3057\u3070\u7C21\u5358\u306B\u63A8\u6E2C\u3067\u304D\u3066\u3057\u307E\u3044\u307E\u3059\u3002 42 | feedback.date.suggestions.avoidDates=\u3042\u306A\u305F\u306B\u95A2\u9023\u3059\u308B\u5E74\u3084\u65E5\u4ED8\u3092\u907F\u3051\u308B\u3088\u3046\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002 -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/SequenceMatcher.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.WipeableString; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.regex.Pattern; 8 | 9 | public class SequenceMatcher extends BaseMatcher { 10 | 11 | private static final int MAX_CODE_POINT_DIFF = 5; 12 | private static final int MIN_VALID_SEQUENCE_LENGTH = 1; 13 | private static final Pattern LOWERCASE_PATTERN = Pattern.compile("^[a-z]+$"); 14 | private static final Pattern UPPERCASE_PATTERN = Pattern.compile("^[A-Z]+$"); 15 | private static final Pattern DIGIT_PATTERN = Pattern.compile("^\\d+$"); 16 | 17 | public SequenceMatcher(final Context context) { 18 | super(context); 19 | } 20 | 21 | /** 22 | * Identifies sequence by looking for repeated differences in unicode codepoint. this allows 23 | * skipping, such as 9753, and also matches some extended unicode sequences such as Greek and 24 | * Cyrillic alphabets. 25 | * 26 | *

for example, consider the input 'abcdb975zy' 27 | * 28 | *

password: a b c d b 9 7 5 z y index: 0 1 2 3 4 5 6 7 8 9 delta: 1 1 1 -2 -41 -2 -2 69 1 29 | * 30 | *

expected result: [(i, j, delta), ...] = [(0, 3, 1), (5, 7, -2), (8, 9, 1)] 31 | */ 32 | @Override 33 | public List execute(CharSequence password) { 34 | List matches = new ArrayList<>(); 35 | if (password == null || password.length() == 1) { 36 | return matches; 37 | } 38 | // Initial value. This value itself is not used in actual data processing. 39 | int lastCodePointDiff = 0; 40 | WipeableString wipeable = new WipeableString(password); 41 | int startIndex = 0; 42 | 43 | for (int curIndex = 1; curIndex < password.length(); curIndex++) { 44 | int codePointDiff = wipeable.codePointAt(curIndex) - wipeable.codePointAt(curIndex - 1); 45 | if (curIndex == 1) { // is first iteration 46 | lastCodePointDiff = codePointDiff; 47 | } 48 | if (codePointDiff == lastCodePointDiff) { 49 | continue; 50 | } 51 | 52 | int endIndex = curIndex - 1; 53 | addMatchIfPresent(password, matches, startIndex, endIndex, lastCodePointDiff); 54 | startIndex = endIndex; 55 | lastCodePointDiff = codePointDiff; 56 | } 57 | 58 | wipeable.wipe(); 59 | addMatchIfPresent(password, matches, startIndex, password.length() - 1, lastCodePointDiff); 60 | 61 | return matches; 62 | } 63 | 64 | private Match createSequenceMatch( 65 | CharSequence password, int startIndex, int endIndex, int codePointDiff) { 66 | if (!isValidSequenceLength(endIndex, startIndex, codePointDiff) 67 | || !isValidCodePointDiffValue(codePointDiff)) { 68 | return null; 69 | } 70 | 71 | CharSequence token = WipeableString.copy(password, startIndex, endIndex + 1); 72 | SequenceType sequenceType = determineSequenceType(token); 73 | return MatchFactory.createSequenceMatch( 74 | startIndex, 75 | endIndex, 76 | token, 77 | sequenceType.getName(), 78 | sequenceType.getSpace(), 79 | codePointDiff > 0); 80 | } 81 | 82 | private boolean isValidSequenceLength(int endIndex, int startIndex, int codePointDiff) { 83 | return (endIndex - startIndex) > MIN_VALID_SEQUENCE_LENGTH 84 | || Math.abs(codePointDiff) == MIN_VALID_SEQUENCE_LENGTH; 85 | } 86 | 87 | private boolean isValidCodePointDiffValue(int codePointDiff) { 88 | return Math.abs(codePointDiff) <= MAX_CODE_POINT_DIFF; 89 | } 90 | 91 | private SequenceType determineSequenceType(CharSequence token) { 92 | if (isLowercase(token)) { 93 | return SequenceType.LOWER; 94 | } else if (isUppercase(token)) { 95 | return SequenceType.UPPER; 96 | } else if (isDigits(token)) { 97 | return SequenceType.DIGITS; 98 | } else { 99 | return SequenceType.UNICODE; 100 | } 101 | } 102 | 103 | private boolean isLowercase(CharSequence token) { 104 | return LOWERCASE_PATTERN.matcher(token).matches(); 105 | } 106 | 107 | private boolean isUppercase(CharSequence token) { 108 | return UPPERCASE_PATTERN.matcher(token).matches(); 109 | } 110 | 111 | private boolean isDigits(CharSequence token) { 112 | return DIGIT_PATTERN.matcher(token).matches(); 113 | } 114 | 115 | private void addMatchIfPresent( 116 | CharSequence password, List matches, int startIndex, int endIndex, int codePointDiff) { 117 | Match match = createSequenceMatch(password, startIndex, endIndex, codePointDiff); 118 | if (match != null) { 119 | matches.add(match); 120 | } 121 | } 122 | 123 | private enum SequenceType { 124 | LOWER("lower", 26), 125 | UPPER("upper", 26), 126 | DIGITS("digits", 10), 127 | // conservatively stick with roman alphabet size. (this could be improved) 128 | UNICODE("unicode", 26); 129 | 130 | private final String name; 131 | private final int space; 132 | 133 | SequenceType(String name, int space) { 134 | this.name = name; 135 | this.space = space; 136 | } 137 | 138 | public String getName() { 139 | return name; 140 | } 141 | 142 | public int getSpace() { 143 | return space; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/FeedbackFactory.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.guesses.DictionaryGuess; 4 | import com.nulabinc.zxcvbn.matchers.Match; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | class FeedbackFactory { 10 | 11 | private static final List NAME_DICTIONARIES = 12 | Arrays.asList("surnames", "male_names", "female_names"); 13 | 14 | private FeedbackFactory() { 15 | throw new IllegalStateException("FeedbackFactory should not be instantiated"); 16 | } 17 | 18 | static Feedback getFeedbackWithoutWarnings(String... suggestions) { 19 | return new Feedback(null, suggestions); 20 | } 21 | 22 | static Feedback getEmptyFeedback() { 23 | return new Feedback(null); 24 | } 25 | 26 | static Feedback createMatchFeedback(Match match, boolean isSoleMatch) { 27 | switch (match.pattern) { 28 | case Dictionary: 29 | return createDictionaryMatchFeedback(match, isSoleMatch); 30 | case Spatial: 31 | return createSpatialMatchFeedback(match); 32 | case Repeat: 33 | return createRepeatMatchFeedback(match); 34 | case Sequence: 35 | return createSequenceMatchFeedback(); 36 | case Regex: 37 | return createRegexMatchFeedback(match); 38 | case Date: 39 | return createDateMatchFeedback(); 40 | default: 41 | return getFeedbackWithoutWarnings(Feedback.EXTRA_SUGGESTIONS_ADD_ANOTHER_WORD); 42 | } 43 | } 44 | 45 | private static Feedback createSpatialMatchFeedback(Match match) { 46 | String warning = 47 | match.turns == 1 48 | ? Feedback.SPATIAL_WARNING_STRAIGHT_ROWS_OF_KEYS 49 | : Feedback.SPATIAL_WARNING_SHORT_KEYBOARD_PATTERNS; 50 | return new Feedback( 51 | warning, 52 | Feedback.EXTRA_SUGGESTIONS_ADD_ANOTHER_WORD, 53 | Feedback.SPATIAL_SUGGESTIONS_USE_LONGER_KEYBOARD_PATTERN); 54 | } 55 | 56 | private static Feedback createRepeatMatchFeedback(Match match) { 57 | String warning = 58 | match.baseToken.length() == 1 59 | ? Feedback.REPEAT_WARNING_LIKE_AAA 60 | : Feedback.REPEAT_WARNING_LIKE_ABCABCABC; 61 | return new Feedback( 62 | warning, 63 | Feedback.EXTRA_SUGGESTIONS_ADD_ANOTHER_WORD, 64 | Feedback.REPEAT_SUGGESTIONS_AVOID_REPEATED_WORDS); 65 | } 66 | 67 | private static Feedback createSequenceMatchFeedback() { 68 | return new Feedback( 69 | Feedback.SEQUENCE_WARNING_LIKE_ABCOR6543, 70 | Feedback.EXTRA_SUGGESTIONS_ADD_ANOTHER_WORD, 71 | Feedback.SEQUENCE_SUGGESTIONS_AVOID_SEQUENCES); 72 | } 73 | 74 | private static Feedback createRegexMatchFeedback(Match match) { 75 | String warning = 76 | "recent_year".equals(match.regexName) ? Feedback.REGEX_WARNING_RECENT_YEARS : null; 77 | return new Feedback( 78 | warning, 79 | Feedback.EXTRA_SUGGESTIONS_ADD_ANOTHER_WORD, 80 | Feedback.REGEX_SUGGESTIONS_AVOID_RECENT_YEARS); 81 | } 82 | 83 | private static Feedback createDateMatchFeedback() { 84 | return new Feedback( 85 | Feedback.DATE_WARNING_DATES, 86 | Feedback.EXTRA_SUGGESTIONS_ADD_ANOTHER_WORD, 87 | Feedback.DATE_SUGGESTIONS_AVOID_DATES); 88 | } 89 | 90 | private static Feedback createDictionaryMatchFeedback(Match match, boolean isSoleMatch) { 91 | String warning = getWarningBasedOnMatch(match, isSoleMatch); 92 | List suggestions = generateSuggestions(match); 93 | return new Feedback(warning, suggestions.toArray(new String[0])); 94 | } 95 | 96 | private static String getWarningBasedOnMatch(Match match, boolean isSoleMatch) { 97 | if ("passwords".equals(match.dictionaryName)) { 98 | return getPasswordWarning(match, isSoleMatch); 99 | } 100 | 101 | if ("english_wikipedia".equals(match.dictionaryName) && isSoleMatch) { 102 | return Feedback.DICTIONARY_WARNING_ENGLISH_WIKIPEDIA_ITSELF; 103 | } 104 | 105 | if (NAME_DICTIONARIES.contains(match.dictionaryName)) { 106 | return getNameDictionaryWarning(isSoleMatch); 107 | } 108 | 109 | return null; 110 | } 111 | 112 | private static String getPasswordWarning(Match match, boolean isSoleMatch) { 113 | if (isSoleMatch && !match.l33t && !match.reversed) { 114 | if (match.rank <= 10) { 115 | return Feedback.DICTIONARY_WARNING_PASSWORDS_TOP10; 116 | } 117 | if (match.rank <= 100) { 118 | return Feedback.DICTIONARY_WARNING_PASSWORDS_TOP100; 119 | } 120 | return Feedback.DICTIONARY_WARNING_PASSWORDS_VERY_COMMON; 121 | } 122 | if (match.guessesLog10 <= 4) { 123 | return Feedback.DICTIONARY_WARNING_PASSWORDS_SIMILAR; 124 | } 125 | return null; 126 | } 127 | 128 | private static String getNameDictionaryWarning(boolean isSoleMatch) { 129 | if (isSoleMatch) { 130 | return Feedback.DICTIONARY_WARNING_ETC_NAMES_THEMSELVES; 131 | } 132 | return Feedback.DICTIONARY_WARNING_ETC_NAMES_COMMON; 133 | } 134 | 135 | private static List generateSuggestions(Match match) { 136 | List suggestions = new ArrayList<>(); 137 | suggestions.add(Feedback.EXTRA_SUGGESTIONS_ADD_ANOTHER_WORD); 138 | 139 | CharSequence word = match.token; 140 | WipeableString lower = WipeableString.lowerCase(word); 141 | 142 | if (DictionaryGuess.START_UPPER.matcher(word).find()) { 143 | suggestions.add(Feedback.DICTIONARY_SUGGESTIONS_CAPITALIZATION); 144 | } 145 | 146 | if (DictionaryGuess.ALL_UPPER.matcher(word).find() && !lower.equals(word)) { 147 | suggestions.add(Feedback.DICTIONARY_SUGGESTIONS_ALL_UPPERCASE); 148 | } 149 | 150 | if (match.reversed && match.tokenLength() >= 4) { 151 | suggestions.add(Feedback.DICTIONARY_SUGGESTIONS_REVERSED); 152 | } 153 | 154 | if (match.l33t) { 155 | suggestions.add(Feedback.DICTIONARY_SUGGESTIONS_L33T); 156 | } 157 | 158 | lower.wipe(); 159 | return suggestions; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/L33tMatcher.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.Context; 4 | import com.nulabinc.zxcvbn.WipeableString; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | public class L33tMatcher extends BaseMatcher { 14 | 15 | private final Map> rankedDictionaries; 16 | private static final Map> L33T_TABLE; 17 | 18 | static { 19 | Map> table = new HashMap<>(); 20 | table.put('a', Arrays.asList('4', '@')); 21 | table.put('b', Collections.singletonList('8')); 22 | table.put('c', Arrays.asList('(', '{', '[', '<')); 23 | table.put('e', Collections.singletonList('3')); 24 | table.put('g', Arrays.asList('6', '9')); 25 | table.put('i', Arrays.asList('1', '!', '|')); 26 | table.put('l', Arrays.asList('1', '|', '7')); 27 | table.put('o', Collections.singletonList('0')); 28 | table.put('s', Arrays.asList('$', '5')); 29 | table.put('t', Arrays.asList('+', '7')); 30 | table.put('x', Collections.singletonList('%')); 31 | table.put('z', Collections.singletonList('2')); 32 | L33T_TABLE = Collections.unmodifiableMap(table); 33 | } 34 | 35 | public L33tMatcher(Context context, Map> rankedDictionaries) { 36 | super(context); 37 | this.rankedDictionaries = rankedDictionaries; 38 | } 39 | 40 | public Map> relevantL33tSubTable(CharSequence password) { 41 | return relevantL33tSubTable(password, L33T_TABLE); 42 | } 43 | 44 | public Map> relevantL33tSubTable( 45 | CharSequence password, Map> table) { 46 | HashSet passwordChars = new HashSet<>(); 47 | for (int n = 0; n < password.length(); n++) { 48 | passwordChars.add(password.charAt(n)); 49 | } 50 | Map> subTable = new HashMap<>(); 51 | for (Map.Entry> l33tRowRef : table.entrySet()) { 52 | Character letter = l33tRowRef.getKey(); 53 | List subs = l33tRowRef.getValue(); 54 | List relevantSubs = new ArrayList<>(); 55 | for (Character sub : subs) { 56 | if (passwordChars.contains(sub)) { 57 | relevantSubs.add(sub); 58 | } 59 | } 60 | if (!relevantSubs.isEmpty()) { 61 | subTable.put(letter, relevantSubs); 62 | } 63 | } 64 | return subTable; 65 | } 66 | 67 | @Override 68 | public List execute(CharSequence password) { 69 | List matches = new ArrayList<>(); 70 | Map> subTable = relevantL33tSubTable(password); 71 | L33tSubDict l33tSubs = new L33tSubDict(subTable); 72 | DictionaryMatcher dictionaryMatcher = 73 | new DictionaryMatcher(this.getContext(), rankedDictionaries); 74 | 75 | for (Map sub : l33tSubs) { 76 | if (sub.isEmpty()) { 77 | break; // corner case: password has no relevant subs. 78 | } 79 | CharSequence subbedPassword = decodeL33tSpeak(password, sub); 80 | 81 | for (Match match : dictionaryMatcher.execute(subbedPassword)) { 82 | 83 | WipeableString token = WipeableString.copy(password, match.i, match.j + 1); 84 | WipeableString lower = WipeableString.lowerCase(token); 85 | if (lower.equals(match.matchedWord)) { 86 | token.wipe(); 87 | lower.wipe(); 88 | continue; 89 | } 90 | 91 | Map matchSub = extractMatchSub(token, sub); 92 | String subDisplay = generateSubDisplay(matchSub); 93 | 94 | matches.add( 95 | MatchFactory.createDictionaryL33tMatch( 96 | match.i, 97 | match.j, 98 | token, 99 | match.matchedWord, 100 | match.rank, 101 | match.dictionaryName, 102 | match.reversed, 103 | matchSub, 104 | subDisplay)); 105 | // Don't wipe token as the Match needs it 106 | lower.wipe(); 107 | } 108 | } 109 | return filterMatches(matches); 110 | } 111 | 112 | private Map extractMatchSub( 113 | WipeableString token, Map sub) { 114 | Map matchSub = new HashMap<>(); 115 | for (Map.Entry subRef : sub.entrySet()) { 116 | Character subbedChr = subRef.getKey(); 117 | Character chr = subRef.getValue(); 118 | if (token.indexOf(subbedChr) != -1) { 119 | matchSub.put(subbedChr, chr); 120 | } 121 | } 122 | return matchSub; 123 | } 124 | 125 | private String generateSubDisplay(Map matchSub) { 126 | List subDisplays = new ArrayList<>(); 127 | for (Map.Entry matchSubRef : matchSub.entrySet()) { 128 | Character k = matchSubRef.getKey(); 129 | Character v = matchSubRef.getValue(); 130 | subDisplays.add(String.format("%s -> %s", k, v)); 131 | } 132 | return Arrays.toString(subDisplays.toArray(new String[0])); 133 | } 134 | 135 | private List filterMatches(List matches) { 136 | List filteredMatches = new ArrayList<>(); 137 | for (Match match : matches) { 138 | if (match.tokenLength() > 1) { 139 | filteredMatches.add(match); 140 | } 141 | } 142 | return this.sorted(filteredMatches); 143 | } 144 | 145 | private CharSequence decodeL33tSpeak( 146 | CharSequence password, Map l33tToRegularMapping) { 147 | StringBuilder sb = new StringBuilder(password.length()); 148 | for (int charIndex = 0; charIndex < password.length(); charIndex++) { 149 | char curChar = password.charAt(charIndex); 150 | Character replacement = l33tToRegularMapping.get(curChar); 151 | sb.append(replacement != null ? replacement : curChar); 152 | } 153 | WipeableString result = new WipeableString(sb); 154 | WipeableString.wipeIfPossible(sb); 155 | return result; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/Strength.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Match; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class Strength { 8 | 9 | private CharSequence password; 10 | private double guesses; 11 | private double guessesLog10; 12 | private AttackTimes.CrackTimeSeconds crackTimeSeconds; 13 | private AttackTimes.CrackTimesDisplay crackTimesDisplay; 14 | private int score; 15 | private Feedback feedback; 16 | private List sequence; 17 | private long calcTime; 18 | 19 | /** 20 | * Default constructor. 21 | * 22 | * @deprecated This constructor is discouraged from use as it does not ensure all fields are 23 | * initialized properly. Instead, use the {@link #Strength(CharSequence, double, List, long)} 24 | * constructor to provide all necessary data. 25 | */ 26 | @Deprecated 27 | public Strength() { 28 | this.sequence = new ArrayList<>(); 29 | } 30 | 31 | /** 32 | * Constructs a Strength object with the given parameters. 33 | * 34 | * @param password The password for which strength is calculated. 35 | * @param guesses Estimated number of guesses needed to crack the password. 36 | * @param sequence A list of matching patterns found in the password. 37 | * @param calcTime Time taken to calculate the password's strength. 38 | */ 39 | public Strength(CharSequence password, double guesses, List sequence, long calcTime) { 40 | this.password = password; 41 | this.guesses = guesses; 42 | this.guessesLog10 = Scoring.log10(guesses); 43 | 44 | if (sequence == null) { 45 | sequence = new ArrayList<>(); 46 | } 47 | this.sequence = sequence; 48 | 49 | AttackTimes attackTimes = TimeEstimates.estimateAttackTimes(guesses); 50 | this.crackTimeSeconds = attackTimes.getCrackTimeSeconds(); 51 | this.crackTimesDisplay = attackTimes.getCrackTimesDisplay(); 52 | this.score = attackTimes.getScore(); 53 | this.feedback = Feedback.getFeedback(attackTimes.getScore(), sequence); 54 | 55 | this.calcTime = calcTime; 56 | } 57 | 58 | public CharSequence getPassword() { 59 | return password; 60 | } 61 | 62 | /** 63 | * Sets the password. 64 | * 65 | * @deprecated Use constructor for initialization. Modifying after instantiation is not 66 | * recommended. 67 | */ 68 | @Deprecated 69 | public void setPassword(CharSequence password) { 70 | this.password = password; 71 | } 72 | 73 | public double getGuesses() { 74 | return guesses; 75 | } 76 | 77 | /** 78 | * Sets the estimated number of guesses. 79 | * 80 | * @deprecated Use constructor for initialization. Modifying after instantiation is not 81 | * recommended. 82 | */ 83 | @Deprecated 84 | public void setGuesses(double guesses) { 85 | this.guesses = guesses; 86 | } 87 | 88 | public double getGuessesLog10() { 89 | return guessesLog10; 90 | } 91 | 92 | /** 93 | * Sets the logarithm (base 10) of the estimated number of guesses. 94 | * 95 | * @deprecated Use constructor for initialization. Modifying after instantiation is not 96 | * recommended. 97 | */ 98 | @Deprecated 99 | public void setGuessesLog10(double guessesLog10) { 100 | this.guessesLog10 = guessesLog10; 101 | } 102 | 103 | public AttackTimes.CrackTimeSeconds getCrackTimeSeconds() { 104 | return crackTimeSeconds; 105 | } 106 | 107 | /** 108 | * Sets the crack time in seconds. 109 | * 110 | * @deprecated Use constructor for initialization. Modifying after instantiation is not 111 | * recommended. 112 | */ 113 | @Deprecated 114 | public void setCrackTimeSeconds(AttackTimes.CrackTimeSeconds crackTimeSeconds) { 115 | this.crackTimeSeconds = crackTimeSeconds; 116 | } 117 | 118 | public AttackTimes.CrackTimesDisplay getCrackTimesDisplay() { 119 | return crackTimesDisplay; 120 | } 121 | 122 | /** 123 | * Sets the display times for crack attempts. 124 | * 125 | * @deprecated Use constructor for initialization. Modifying after instantiation is not 126 | * recommended. 127 | */ 128 | @Deprecated 129 | public void setCrackTimesDisplay(AttackTimes.CrackTimesDisplay crackTimesDisplay) { 130 | this.crackTimesDisplay = crackTimesDisplay; 131 | } 132 | 133 | public int getScore() { 134 | return score; 135 | } 136 | 137 | /** 138 | * Sets the score. 139 | * 140 | * @deprecated Use constructor for initialization. Modifying after instantiation is not 141 | * recommended. 142 | */ 143 | @Deprecated 144 | public void setScore(int score) { 145 | this.score = score; 146 | } 147 | 148 | public Feedback getFeedback() { 149 | return feedback; 150 | } 151 | 152 | /** 153 | * Sets the feedback. 154 | * 155 | * @deprecated Use constructor for initialization. Modifying after instantiation is not 156 | * recommended. 157 | */ 158 | @Deprecated 159 | public void setFeedback(Feedback feedback) { 160 | this.feedback = feedback; 161 | } 162 | 163 | public List getSequence() { 164 | return sequence; 165 | } 166 | 167 | /** 168 | * Sets the sequence of matches. 169 | * 170 | * @deprecated Use constructor for initialization. Modifying after instantiation is not 171 | * recommended. 172 | */ 173 | @Deprecated 174 | public void setSequence(List sequence) { 175 | if (sequence == null) { 176 | sequence = new ArrayList<>(); 177 | } 178 | this.sequence = sequence; 179 | } 180 | 181 | public long getCalcTime() { 182 | return calcTime; 183 | } 184 | 185 | /** 186 | * Sets the calculation time. 187 | * 188 | * @deprecated Use constructor for initialization. Modifying after instantiation is not 189 | * recommended. 190 | */ 191 | @Deprecated 192 | public void setCalcTime(long calcTime) { 193 | this.calcTime = calcTime; 194 | } 195 | 196 | /** Attempts to wipe any sensitive content from the object. */ 197 | public void wipe() { 198 | WipeableString.wipeIfPossible(password); 199 | for (Match match : sequence) { 200 | WipeableString.wipeIfPossible(match.token); 201 | WipeableString.wipeIfPossible(match.baseToken); 202 | WipeableString.wipeIfPossible(match.matchedWord); 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /dictionary.gradle: -------------------------------------------------------------------------------- 1 | 2 | 3 | task createDictionary(type: DictionaryTask) 4 | 5 | class DictionaryTask extends DefaultTask { 6 | 7 | String src = "./data/" 8 | 9 | String dest = "./src/main/resources/com/nulabinc/zxcvbn/matchers/dictionaries/" 10 | 11 | String ext = ".txt" 12 | 13 | Map params = [ "us_tv_and_film": 30000, 14 | "english_wikipedia": 30000, 15 | "passwords": 30000, 16 | "surnames": 10000, 17 | "male_names": null, 18 | "female_names": null ] 19 | @TaskAction 20 | def generate() { 21 | Map> unfilteredFreqLists = parseFrequencyLists(); 22 | def frequencyLists = filterFrequencyLists(unfilteredFreqLists); 23 | for (Map.Entry ref: frequencyLists.entrySet()) { 24 | FileWriter fw = new FileWriter(dest + ref.key + ext, false); 25 | PrintWriter pw = new PrintWriter(new BufferedWriter(fw)); 26 | for (String word: ref.value) { 27 | pw.println(word); 28 | } 29 | pw.close(); 30 | } 31 | } 32 | 33 | def buildResourcePath(filename) { 34 | src + filename + ext; 35 | } 36 | 37 | def Map> parseFrequencyLists() { 38 | Map> freqLists = new HashMap<>(); 39 | for (String filename: params.keySet()) { 40 | String freqListName = filename; 41 | if (!params.containsKey(freqListName)) { 42 | String msg = "Warning: %s appears in directory but not in DICTIONARY settings. Excluding."; 43 | System.out.println(String.format(msg, freqListName)); 44 | continue; 45 | } 46 | Map tokenToRank = new HashMap<>(); 47 | File file = new File(buildResourcePath(filename)); 48 | BufferedReader br = new BufferedReader(new FileReader(file)); 49 | String line; 50 | long rank = 0; 51 | while ((line = br.readLine()) != null) { 52 | rank++; 53 | String token = line.split(" ")[0]; 54 | tokenToRank.put(token, rank); 55 | } 56 | freqLists.put(freqListName, tokenToRank); 57 | } 58 | return freqLists; 59 | } 60 | 61 | def boolean isRareAndShort(String token, long rank) { 62 | return rank >= Math.pow(10, token.length()); 63 | } 64 | 65 | def boolean hasCommaOrDoubleQuote(String token) { 66 | if (token.indexOf(",") != -1 || token.indexOf("\"") != -1) { 67 | return true; 68 | } 69 | return false; 70 | } 71 | 72 | def Map filterFrequencyLists(Map> freqLists) { 73 | Map> filteredTokenAndRank = new HashMap<>(); 74 | Map tokenCount = new HashMap<>(); 75 | for(String name: freqLists.keySet()) { 76 | filteredTokenAndRank.put(name, new HashMap()); 77 | tokenCount.put(name, Long.valueOf(0)); 78 | } 79 | Map minimumRank = new HashMap<>(); 80 | Map minimumName = new HashMap<>(); 81 | for (Map.Entry> freqRef: freqLists.entrySet()) { 82 | String name = freqRef.getKey(); 83 | Map tokenToRank = freqRef.getValue(); 84 | for (Map.Entry tokenToRankRef: tokenToRank.entrySet()) { 85 | String token = tokenToRankRef.getKey(); 86 | long rank = tokenToRankRef.getValue(); 87 | 88 | if (!minimumRank.containsKey(token)) { 89 | minimumRank.put(token, rank); 90 | minimumName.put(token, name); 91 | } else { 92 | long minRank = minimumRank.get(token); 93 | if (rank < minRank) { 94 | minimumRank.put(token, rank); 95 | minimumName.put(token, name); 96 | } 97 | } 98 | } 99 | } 100 | for (Map.Entry> freqRef: freqLists.entrySet()) { 101 | String name = freqRef.getKey(); 102 | Map tokenToRank = freqRef.getValue(); 103 | for (Map.Entry tokenToRankRef : tokenToRank.entrySet()) { 104 | String token = tokenToRankRef.getKey(); 105 | long rank = tokenToRankRef.getValue(); 106 | 107 | if (!minimumName.get(token).equals(name)) { 108 | continue; 109 | } 110 | if (isRareAndShort(token, rank) 111 | || hasCommaOrDoubleQuote(token)) { 112 | continue; 113 | } 114 | filteredTokenAndRank.get(name).put(token, rank); 115 | tokenCount.put(name, (tokenCount.get(name) + 1)); 116 | } 117 | } 118 | Map result = new HashMap<>(); 119 | for (Map.Entry> filteredTokenAndRankRef: 120 | filteredTokenAndRank.entrySet()) { 121 | String name = filteredTokenAndRankRef.getKey(); 122 | Map tokenRankPairs = filteredTokenAndRankRef.getValue(); 123 | List> entries = new ArrayList<>(tokenRankPairs.entrySet()); 124 | Collections.sort(entries, new Comparator>() { 125 | @Override 126 | public int compare(Map.Entry o1, Map.Entry o2) { 127 | return o1.getValue().compareTo(o2.getValue()); 128 | } 129 | }); 130 | Integer cutoffLimit = params.get(name); 131 | if (cutoffLimit != null && tokenRankPairs.size() > cutoffLimit) { 132 | entries = entries.subList(0, cutoffLimit); 133 | } 134 | List tr = new ArrayList<>(); 135 | for (Map.Entry tokenRankPairRef: entries) { 136 | tr.add(tokenRankPairRef.getKey()); 137 | } 138 | result.put(name, tr.toArray()); 139 | } 140 | return result; 141 | } 142 | } -------------------------------------------------------------------------------- /src/test/java/com/nulabinc/zxcvbn/ApproachComparisonTest.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import static java.nio.CharBuffer.wrap; 4 | import static junit.framework.TestCase.assertEquals; 5 | import static junit.framework.TestCase.assertNotNull; 6 | import static junit.framework.TestCase.fail; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.util.Collections; 13 | import java.util.LinkedList; 14 | import java.util.List; 15 | import java.util.Map; 16 | import javax.script.ScriptEngine; 17 | import javax.script.ScriptException; 18 | import org.junit.*; 19 | import org.junit.runner.RunWith; 20 | import org.junit.runners.Parameterized; 21 | 22 | /** 23 | * These tests compare the output from different approaches for calculating password strength. 24 | * 25 | *

The approaches include measuring String passwords with Java, measuring CharSequence passwords 26 | * in Java, and using the JavaScript version. The measure method with an empty list as the second 27 | * parameter is also compared. All versions should produce the same results. 28 | * 29 | *

The list of password to do the comparisons with is loaded from passwords.txt in the 30 | * test/resources folder. 31 | */ 32 | @RunWith(Parameterized.class) 33 | public class ApproachComparisonTest { 34 | 35 | private static ScriptEngine engine; 36 | 37 | private final CharSequence password; 38 | 39 | private Strength charSequenceStrength; 40 | private Strength stringStrength; 41 | private Strength stringInputsStrength; 42 | private JavaScriptStrength jsStrength; 43 | 44 | public ApproachComparisonTest(CharSequence password) { 45 | this.password = wrap(password); 46 | 47 | Zxcvbn zxcvbn = new Zxcvbn(); 48 | 49 | calculateAndRecordStrengthUsingAllMethods(password, zxcvbn); 50 | } 51 | 52 | private void calculateAndRecordStrengthUsingAllMethods(CharSequence password, Zxcvbn zxcvbn) { 53 | charSequenceStrength = zxcvbn.measure(new WipeableString(password)); 54 | 55 | stringStrength = zxcvbn.measure(password.toString()); 56 | 57 | stringInputsStrength = zxcvbn.measure(password, Collections.emptyList()); 58 | 59 | jsStrength = invokeJsVersion(password); 60 | } 61 | 62 | // =================================================================================// 63 | 64 | @Test 65 | public void keyValuesAreNotNull() { 66 | assertNotNull(password); 67 | assertNotNull(stringStrength); 68 | assertNotNull(charSequenceStrength); 69 | assertNotNull(stringInputsStrength); 70 | assertNotNull(jsStrength); 71 | } 72 | 73 | @Test 74 | public void passwordStrengthMatchesStringStrength() { 75 | assertEquals(stringStrength.getScore(), charSequenceStrength.getScore()); 76 | } 77 | 78 | @Test 79 | public void passwordStrengthMatchesStringInputsStrength() { 80 | assertEquals(stringStrength.getScore(), stringInputsStrength.getScore()); 81 | } 82 | 83 | @Test 84 | public void charsequenceAttackTimeMatchesStringAttackTime() { 85 | assertEquals( 86 | stringStrength.getCrackTimesDisplay().getOfflineFastHashing1e10PerSecond(), 87 | charSequenceStrength.getCrackTimesDisplay().getOfflineFastHashing1e10PerSecond()); 88 | } 89 | 90 | @Test 91 | public void charsequenceStrengthPasswordMatchesStringStrengthPassword() { 92 | assertEquals( 93 | stringStrength.getPassword().toString(), charSequenceStrength.getPassword().toString()); 94 | } 95 | 96 | @Test 97 | public void strengthPasswordMatchesInput() { 98 | assertEquals(password.toString(), charSequenceStrength.getPassword().toString()); 99 | } 100 | 101 | @Test 102 | public void strengthScoreMatchesJavascript() { 103 | assertEquals(jsStrength.getScore(), charSequenceStrength.getScore()); 104 | } 105 | 106 | @Test 107 | public void strengthPasswordMatchesJavascript() { 108 | assertEquals(jsStrength.getPassword(), charSequenceStrength.getPassword().toString()); 109 | } 110 | 111 | @Test 112 | public void charsequenceSuggestionsMatchStringSuggestions() { 113 | assertStringListsAreEqual( 114 | stringStrength.getFeedback().getSuggestions(), 115 | charSequenceStrength.getFeedback().getSuggestions()); 116 | } 117 | 118 | @Test 119 | public void charsequenceGuessesMatchesStringGuesses() { 120 | assertEquals(stringStrength.getGuessesLog10(), charSequenceStrength.getGuessesLog10(), 0.1); 121 | } 122 | 123 | // =================================================================================// 124 | 125 | @Parameterized.Parameters(name = "{0}") 126 | public static Iterable data() throws IOException { 127 | List passwords = new LinkedList<>(); 128 | passwords.add(new Object[] {""}); 129 | try (InputStream data = ApproachComparisonTest.class.getResourceAsStream("/passwords.txt")) { 130 | BufferedReader in = new BufferedReader(new InputStreamReader(data)); 131 | String line; 132 | while ((line = in.readLine()) != null) { 133 | if (line.trim().length() > 0) { 134 | passwords.add(new Object[] {line}); 135 | } 136 | } 137 | } 138 | return passwords; 139 | } 140 | 141 | public void assertStringListsAreEqual(List expectedStrings, List actualStrings) { 142 | Collections.sort(expectedStrings); 143 | Collections.sort(actualStrings); 144 | 145 | assertEquals(expectedStrings.size(), actualStrings.size()); 146 | 147 | for (int n = 0; n < expectedStrings.size(); n++) { 148 | assertEquals(expectedStrings.get(n), actualStrings.get(n)); 149 | } 150 | } 151 | 152 | static class JavaScriptStrength { 153 | private final Map values; 154 | 155 | public JavaScriptStrength(Map values) { 156 | this.values = values; 157 | } 158 | 159 | public int getScore() { 160 | Object score = values.get("score"); 161 | // nashorn returns int, rhino returns double 162 | if (score instanceof Double) { 163 | return ((Double) score).intValue(); 164 | } else { 165 | return (int) score; 166 | } 167 | } 168 | 169 | public String getPassword() { 170 | return (String) values.get("password"); 171 | } 172 | } 173 | 174 | @SuppressWarnings("unchecked") 175 | public JavaScriptStrength invokeJsVersion(CharSequence password) { 176 | engine.put("pwd", password.toString()); 177 | try { 178 | return new JavaScriptStrength((Map) engine.eval("zxcvbn(pwd);")); 179 | } catch (ScriptException e) { 180 | fail("Error invoking JavaScript version for password " + password); 181 | return null; 182 | } 183 | } 184 | 185 | @BeforeClass 186 | public static void beforeClass() { 187 | engine = new JSScriptEngineBuilder().build(); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/Keyboard.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class Keyboard { 9 | 10 | private final String name; 11 | 12 | private final Map> adjacencyGraph; 13 | 14 | private final boolean slanted; 15 | 16 | private final int startingPositions; 17 | 18 | private final double averageDegree; 19 | 20 | Keyboard(final String name, final AdjacentGraphBuilder adjacentGraphBuilder) { 21 | this.name = name; 22 | this.adjacencyGraph = adjacentGraphBuilder.build(); 23 | this.slanted = adjacentGraphBuilder.isSlanted(); 24 | this.startingPositions = adjacencyGraph.size(); 25 | this.averageDegree = calcAverageDegree(adjacencyGraph); 26 | } 27 | 28 | private static double calcAverageDegree(final Map> adjacencyGraph) { 29 | double average = 0; 30 | for (Map.Entry> graphRef : adjacencyGraph.entrySet()) { 31 | List neighbors = graphRef.getValue(); 32 | List results = new ArrayList<>(); 33 | for (String neighbor : neighbors) { 34 | if (neighbor != null) { 35 | results.add(neighbor); 36 | } 37 | } 38 | average += results.size(); 39 | } 40 | List keys = new ArrayList<>(); 41 | for (Map.Entry> graphRef : adjacencyGraph.entrySet()) { 42 | keys.add(graphRef.getKey()); 43 | } 44 | average /= keys.size(); 45 | return average; 46 | } 47 | 48 | public String getName() { 49 | return name; 50 | } 51 | 52 | public Map> getAdjacencyGraph() { 53 | return adjacencyGraph; 54 | } 55 | 56 | public boolean isSlanted() { 57 | return slanted; 58 | } 59 | 60 | public int getStartingPositions() { 61 | return startingPositions; 62 | } 63 | 64 | public double getAverageDegree() { 65 | return averageDegree; 66 | } 67 | 68 | public abstract static class AdjacentGraphBuilder { 69 | 70 | private static final SplitMatcher WHITESPACE_SPLIT_MATCHER = 71 | new SplitMatcher() { 72 | @Override 73 | public boolean match(final char c) { 74 | return Character.isWhitespace(c); 75 | } 76 | }; 77 | 78 | private static final SplitMatcher NEW_LINE_SPLIT_MATCHER = 79 | new SplitMatcher() { 80 | @Override 81 | public boolean match(final char c) { 82 | return c == '\n'; 83 | } 84 | }; 85 | 86 | private final String layout; 87 | 88 | protected AdjacentGraphBuilder(final String layout) { 89 | this.layout = layout; 90 | } 91 | 92 | /** 93 | * builds an adjacency graph as a dictionary: {character: [adjacent_characters]}. adjacent 94 | * characters occur in a clockwise order. for example: on qwerty layout, 'g' maps to ['fF', 95 | * 'tT', 'yY', 'hH', 'bB', 'vV'] on keypad layout, '7' maps to [None, None, None, '=', '8', '5', 96 | * '4', None] * 97 | */ 98 | public Map> build() { 99 | final Map positionTable = buildPositionTable(layout); 100 | 101 | final Map> adjacencyGraph = new HashMap<>(); 102 | for (Map.Entry entry : positionTable.entrySet()) { 103 | for (final char key : entry.getValue().toCharArray()) { 104 | final List adjacencies = new ArrayList<>(); 105 | final Position position = entry.getKey(); 106 | for (final Position coord : getAdjacentCoords(position)) { 107 | adjacencies.add(positionTable.get(coord)); 108 | } 109 | adjacencyGraph.put(key, adjacencies); 110 | } 111 | } 112 | 113 | return adjacencyGraph; 114 | } 115 | 116 | private Map buildPositionTable(final String layout) { 117 | final Map positionTable = new HashMap<>(); 118 | 119 | final List tokens = split(layout, WHITESPACE_SPLIT_MATCHER); 120 | final int tokenSize = tokens.get(0).length(); 121 | final int xUnit = tokenSize + 1; 122 | 123 | for (String token : tokens) { 124 | assert token.length() == tokenSize 125 | : String.format("token [%s] length mismatch:%n%s", token, layout); 126 | } 127 | 128 | int y = 1; 129 | for (final String line : split(layout, NEW_LINE_SPLIT_MATCHER)) { 130 | // the way I illustrated keys above, each qwerty row is indented one space in from the last 131 | int slant = calcSlant(y); 132 | for (final String token : split(line, WHITESPACE_SPLIT_MATCHER)) { 133 | int index = line.indexOf(token) - slant; 134 | int x = index / xUnit; 135 | final int remainder = index % xUnit; 136 | assert remainder == 0 137 | : String.format("unexpected x offset [%d] for %s in:%n%s", x, token, layout); 138 | positionTable.put(Position.of(x, y), token); 139 | } 140 | 141 | y++; 142 | } 143 | return positionTable; 144 | } 145 | 146 | protected abstract List getAdjacentCoords(final Position position); 147 | 148 | private static List split(final String str, final SplitMatcher splitMatcher) { 149 | final int len = str.length(); 150 | final List list = new ArrayList<>(); 151 | int i = 0; 152 | int start = 0; 153 | boolean match = false; 154 | while (i < len) { 155 | if (splitMatcher.match(str.charAt(i))) { 156 | if (match) { 157 | list.add(str.substring(start, i)); 158 | match = false; 159 | } 160 | start = ++i; 161 | continue; 162 | } 163 | match = true; 164 | i++; 165 | } 166 | if (match) { 167 | list.add(str.substring(start, i)); 168 | } 169 | return list; 170 | } 171 | 172 | protected abstract int calcSlant(int y); 173 | 174 | public abstract boolean isSlanted(); 175 | 176 | private interface SplitMatcher { 177 | boolean match(char c); 178 | } 179 | 180 | static class Position { 181 | 182 | private final int x; 183 | 184 | private final int y; 185 | 186 | private Position(int x, int y) { 187 | this.x = x; 188 | this.y = y; 189 | } 190 | 191 | public static Position of(int x, int y) { 192 | return new Position(x, y); 193 | } 194 | 195 | public int getX() { 196 | return x; 197 | } 198 | 199 | public int getY() { 200 | return y; 201 | } 202 | 203 | @Override 204 | public int hashCode() { 205 | int result = x; 206 | result = 31 * result + y; 207 | return result; 208 | } 209 | 210 | @Override 211 | public boolean equals(Object o) { 212 | if (this == o) { 213 | return true; 214 | } 215 | if (!(o instanceof Position)) { 216 | return false; 217 | } 218 | 219 | final Position position = (Position) o; 220 | 221 | return x == position.x && y == position.y; 222 | } 223 | 224 | @Override 225 | public String toString() { 226 | return "[" + x + "," + y + ']'; 227 | } 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/matchers/Match.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn.matchers; 2 | 3 | import com.nulabinc.zxcvbn.Pattern; 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public class Match { 10 | 11 | public final Pattern pattern; 12 | public final int i; 13 | public final int j; 14 | public final CharSequence token; 15 | public final CharSequence matchedWord; 16 | public final int rank; 17 | public final String dictionaryName; 18 | public final boolean reversed; 19 | public final boolean l33t; 20 | public final Map sub; 21 | public final String subDisplay; 22 | public final String sequenceName; 23 | public final int sequenceSpace; 24 | public final boolean ascending; 25 | public final String regexName; 26 | public final java.util.regex.Matcher regexMatch; 27 | public final CharSequence baseToken; 28 | public final List baseMatches; 29 | public final int repeatCount; 30 | public final String graph; 31 | public final int turns; 32 | public final Integer shiftedCount; 33 | public final String separator; 34 | public final int year; 35 | public final int month; 36 | public final int day; 37 | 38 | @SuppressWarnings("java:S1104") 39 | public Double baseGuesses; 40 | 41 | @SuppressWarnings("java:S1104") 42 | public Double guesses; 43 | 44 | @SuppressWarnings("java:S1104") 45 | public Double guessesLog10; 46 | 47 | private Match(Builder builder) { 48 | this.pattern = builder.pattern; 49 | this.i = builder.i; 50 | this.j = builder.j; 51 | this.token = builder.token; 52 | this.matchedWord = builder.matchedWord; 53 | this.rank = builder.rank; 54 | this.dictionaryName = builder.dictionaryName; 55 | this.reversed = builder.reversed; 56 | this.l33t = builder.l33t; 57 | this.sub = builder.sub; 58 | this.subDisplay = builder.subDisplay; 59 | this.sequenceName = builder.sequenceName; 60 | this.sequenceSpace = builder.sequenceSpace; 61 | this.ascending = builder.ascending; 62 | this.regexName = builder.regexName; 63 | this.regexMatch = builder.regexMatch; 64 | this.baseToken = builder.baseToken; 65 | this.baseGuesses = builder.baseGuesses; 66 | this.baseMatches = builder.baseMatches; 67 | this.repeatCount = builder.repeatCount; 68 | this.graph = builder.graph; 69 | this.turns = builder.turns; 70 | this.shiftedCount = builder.shiftedCount; 71 | this.separator = builder.separator; 72 | this.year = builder.year; 73 | this.month = builder.month; 74 | this.day = builder.day; 75 | this.guesses = builder.guesses; 76 | this.guessesLog10 = builder.guessesLog10; 77 | } 78 | 79 | public int tokenLength() { 80 | return token == null ? 0 : token.length(); 81 | } 82 | 83 | public static class Builder { 84 | 85 | private final Pattern pattern; 86 | private final int i; 87 | private final int j; 88 | private final CharSequence token; 89 | 90 | private CharSequence matchedWord; 91 | private int rank; 92 | private String dictionaryName; 93 | private boolean reversed; 94 | private boolean l33t; 95 | private Map sub; 96 | private String subDisplay; 97 | private String sequenceName; 98 | private int sequenceSpace; 99 | private boolean ascending; 100 | private String regexName; 101 | private java.util.regex.Matcher regexMatch; 102 | private CharSequence baseToken; 103 | private double baseGuesses; 104 | private List baseMatches; 105 | private int repeatCount; 106 | private String graph; 107 | private int turns; 108 | private int shiftedCount; 109 | private String separator; 110 | private int year; 111 | private int month; 112 | private int day; 113 | 114 | private Double guesses; 115 | private Double guessesLog10; 116 | 117 | public Builder(Pattern pattern, int i, int j, CharSequence token) { 118 | this.pattern = pattern; 119 | this.i = i; 120 | this.j = j; 121 | this.token = token; 122 | } 123 | 124 | public Builder matchedWord(CharSequence matchedWord) { 125 | this.matchedWord = matchedWord; 126 | return this; 127 | } 128 | 129 | public Builder rank(int rank) { 130 | this.rank = rank; 131 | return this; 132 | } 133 | 134 | public Builder dictionaryName(String dictionaryName) { 135 | this.dictionaryName = dictionaryName; 136 | return this; 137 | } 138 | 139 | public Builder reversed(boolean reversed) { 140 | this.reversed = reversed; 141 | return this; 142 | } 143 | 144 | public Builder l33t(boolean l33t) { 145 | this.l33t = l33t; 146 | return this; 147 | } 148 | 149 | public Builder sub(Map sub) { 150 | this.sub = sub == null ? new HashMap() : sub; 151 | return this; 152 | } 153 | 154 | public Builder subDisplay(String subDisplay) { 155 | this.subDisplay = subDisplay; 156 | return this; 157 | } 158 | 159 | public Builder sequenceName(String sequenceName) { 160 | this.sequenceName = sequenceName; 161 | return this; 162 | } 163 | 164 | public Builder sequenceSpace(int sequenceSpace) { 165 | this.sequenceSpace = sequenceSpace; 166 | return this; 167 | } 168 | 169 | public Builder ascending(boolean ascending) { 170 | this.ascending = ascending; 171 | return this; 172 | } 173 | 174 | public Builder regexName(String regexName) { 175 | this.regexName = regexName; 176 | return this; 177 | } 178 | 179 | public Builder regexMatch(java.util.regex.Matcher regexMatch) { 180 | this.regexMatch = regexMatch; 181 | return this; 182 | } 183 | 184 | public Builder baseToken(CharSequence baseToken) { 185 | this.baseToken = baseToken; 186 | return this; 187 | } 188 | 189 | public Builder baseGuesses(double baseGuesses) { 190 | this.baseGuesses = baseGuesses; 191 | return this; 192 | } 193 | 194 | public Builder baseMatches(List baseMatches) { 195 | this.baseMatches = baseMatches == null ? new ArrayList() : baseMatches; 196 | return this; 197 | } 198 | 199 | public Builder repeatCount(int repeatCount) { 200 | this.repeatCount = repeatCount; 201 | return this; 202 | } 203 | 204 | public Builder graph(String graph) { 205 | this.graph = graph; 206 | return this; 207 | } 208 | 209 | public Builder turns(int turns) { 210 | this.turns = turns; 211 | return this; 212 | } 213 | 214 | public Builder shiftedCount(int shiftedCount) { 215 | this.shiftedCount = shiftedCount; 216 | return this; 217 | } 218 | 219 | public Builder separator(String separator) { 220 | this.separator = separator; 221 | return this; 222 | } 223 | 224 | public Builder year(int year) { 225 | this.year = year; 226 | return this; 227 | } 228 | 229 | public Builder month(int month) { 230 | this.month = month; 231 | return this; 232 | } 233 | 234 | public Builder day(int day) { 235 | this.day = day; 236 | return this; 237 | } 238 | 239 | public Builder guesses(Double guesses) { 240 | this.guesses = guesses; 241 | return this; 242 | } 243 | 244 | public Builder guessesLog10(Double guessesLog10) { 245 | this.guessesLog10 = guessesLog10; 246 | return this; 247 | } 248 | 249 | public Match build() { 250 | return new Match(this); 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/Feedback.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.matchers.Match; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Locale; 7 | import java.util.Map; 8 | import java.util.MissingResourceException; 9 | import java.util.ResourceBundle; 10 | 11 | public class Feedback { 12 | 13 | private static final String DEFAULT_BUNDLE_NAME = "com/nulabinc/zxcvbn/messages"; 14 | 15 | private static final ResourceBundle.Control CONTROL = 16 | ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT); 17 | 18 | public static final String DEFAULT_SUGGESTIONS_USE_FEW_WORDS = 19 | "feedback.default.suggestions.useFewWords"; 20 | public static final String DEFAULT_SUGGESTIONS_NO_NEED_SYMBOLS = 21 | "feedback.default.suggestions.noNeedSymbols"; 22 | public static final String EXTRA_SUGGESTIONS_ADD_ANOTHER_WORD = 23 | "feedback.extra.suggestions.addAnotherWord"; 24 | public static final String SPATIAL_WARNING_STRAIGHT_ROWS_OF_KEYS = 25 | "feedback.spatial.warning.straightRowsOfKeys"; 26 | public static final String SPATIAL_WARNING_SHORT_KEYBOARD_PATTERNS = 27 | "feedback.spatial.warning.shortKeyboardPatterns"; 28 | public static final String SPATIAL_SUGGESTIONS_USE_LONGER_KEYBOARD_PATTERN = 29 | "feedback.spatial.suggestions.UseLongerKeyboardPattern"; 30 | public static final String REPEAT_WARNING_LIKE_AAA = "feedback.repeat.warning.likeAAA"; 31 | public static final String REPEAT_WARNING_LIKE_ABCABCABC = 32 | "feedback.repeat.warning.likeABCABCABC"; 33 | public static final String REPEAT_SUGGESTIONS_AVOID_REPEATED_WORDS = 34 | "feedback.repeat.suggestions.avoidRepeatedWords"; 35 | public static final String SEQUENCE_WARNING_LIKE_ABCOR6543 = 36 | "feedback.sequence.warning.likeABCor6543"; 37 | public static final String SEQUENCE_SUGGESTIONS_AVOID_SEQUENCES = 38 | "feedback.sequence.suggestions.avoidSequences"; 39 | public static final String REGEX_WARNING_RECENT_YEARS = "feedback.regex.warning.recentYears"; 40 | public static final String REGEX_SUGGESTIONS_AVOID_RECENT_YEARS = 41 | "feedback.regex.suggestions.avoidRecentYears"; 42 | public static final String DATE_WARNING_DATES = "feedback.date.warning.dates"; 43 | public static final String DATE_SUGGESTIONS_AVOID_DATES = "feedback.date.suggestions.avoidDates"; 44 | public static final String DICTIONARY_WARNING_PASSWORDS_TOP10 = 45 | "feedback.dictionary.warning.passwords.top10"; 46 | public static final String DICTIONARY_WARNING_PASSWORDS_TOP100 = 47 | "feedback.dictionary.warning.passwords.top100"; 48 | public static final String DICTIONARY_WARNING_PASSWORDS_VERY_COMMON = 49 | "feedback.dictionary.warning.passwords.veryCommon"; 50 | public static final String DICTIONARY_WARNING_PASSWORDS_SIMILAR = 51 | "feedback.dictionary.warning.passwords.similar"; 52 | public static final String DICTIONARY_WARNING_ENGLISH_WIKIPEDIA_ITSELF = 53 | "feedback.dictionary.warning.englishWikipedia.itself"; 54 | public static final String DICTIONARY_WARNING_ETC_NAMES_THEMSELVES = 55 | "feedback.dictionary.warning.etc.namesThemselves"; 56 | public static final String DICTIONARY_WARNING_ETC_NAMES_COMMON = 57 | "feedback.dictionary.warning.etc.namesCommon"; 58 | public static final String DICTIONARY_SUGGESTIONS_CAPITALIZATION = 59 | "feedback.dictionary.suggestions.capitalization"; 60 | public static final String DICTIONARY_SUGGESTIONS_ALL_UPPERCASE = 61 | "feedback.dictionary.suggestions.allUppercase"; 62 | public static final String DICTIONARY_SUGGESTIONS_REVERSED = 63 | "feedback.dictionary.suggestions.reversed"; 64 | public static final String DICTIONARY_SUGGESTIONS_L33T = "feedback.dictionary.suggestions.l33t"; 65 | 66 | private final String warning; 67 | private final String[] suggestions; 68 | 69 | Feedback(String warning, String... suggestions) { 70 | this.warning = warning; 71 | this.suggestions = suggestions; 72 | } 73 | 74 | public String getWarning() { 75 | return getWarning(Locale.getDefault()); 76 | } 77 | 78 | public String getWarning(Locale locale) { 79 | if (this.warning == null) { 80 | return ""; 81 | } 82 | ResourceBundle messages = resolveResourceBundle(locale); 83 | return l10n(messages, this.warning); 84 | } 85 | 86 | public List getSuggestions() { 87 | return getSuggestions(Locale.getDefault()); 88 | } 89 | 90 | public List getSuggestions(Locale locale) { 91 | List suggestionTexts = new ArrayList<>(this.suggestions.length); 92 | ResourceBundle messages = resolveResourceBundle(locale); 93 | for (String suggestion : this.suggestions) { 94 | suggestionTexts.add(l10n(messages, suggestion)); 95 | } 96 | return suggestionTexts; 97 | } 98 | 99 | protected ResourceBundle resolveResourceBundle(Locale locale) { 100 | try { 101 | return ResourceBundle.getBundle(DEFAULT_BUNDLE_NAME, locale, CONTROL); 102 | } catch (MissingResourceException | UnsupportedOperationException e) { 103 | // MissingResourceException: 104 | // Fix for issue of Android refs: https://github.com/nulab/zxcvbn4j/issues/21 105 | // 106 | // UnsupportedOperationException: 107 | // Fix for issue of JDK 9 refs: https://github.com/nulab/zxcvbn4j/issues/45 108 | // ResourceBundle.Control is not supported in named modules. 109 | // See https://docs.oracle.com/javase/9/docs/api/java/util/ResourceBundle.html#bundleprovider 110 | // for more details 111 | return ResourceBundle.getBundle(DEFAULT_BUNDLE_NAME, locale); 112 | } 113 | } 114 | 115 | public Feedback withResourceBundle(ResourceBundle messages) { 116 | return new ResourceBundleFeedback(messages, warning, suggestions); 117 | } 118 | 119 | public Feedback replaceResourceBundle(Map messages) { 120 | return new ReplacedMessagesFeedback(messages, warning, suggestions); 121 | } 122 | 123 | private String l10n(ResourceBundle messages, String messageId) { 124 | return messages != null ? messages.getString(messageId) : messageId; 125 | } 126 | 127 | static Feedback getFeedback(int score, List sequence) { 128 | if (sequence.isEmpty()) { 129 | return FeedbackFactory.getFeedbackWithoutWarnings( 130 | DEFAULT_SUGGESTIONS_USE_FEW_WORDS, DEFAULT_SUGGESTIONS_NO_NEED_SYMBOLS); 131 | } 132 | if (score > 2) { 133 | return FeedbackFactory.getEmptyFeedback(); 134 | } 135 | Match longestMatch = sequence.get(0); 136 | if (sequence.size() > 1) { 137 | for (Match match : sequence.subList(1, sequence.size())) { 138 | if (match.tokenLength() > longestMatch.tokenLength()) { 139 | longestMatch = match; 140 | } 141 | } 142 | } 143 | boolean isSoleMatch = sequence.size() == 1; 144 | return FeedbackFactory.createMatchFeedback(longestMatch, isSoleMatch); 145 | } 146 | 147 | private static class ResourceBundleFeedback extends Feedback { 148 | private final ResourceBundle messages; 149 | 150 | private ResourceBundleFeedback(ResourceBundle messages, String warning, String... suggestions) { 151 | super(warning, suggestions); 152 | this.messages = messages; 153 | } 154 | 155 | @Override 156 | protected ResourceBundle resolveResourceBundle(Locale locale) { 157 | return messages; 158 | } 159 | } 160 | 161 | private static class ReplacedMessagesFeedback extends Feedback { 162 | private final Map messages; 163 | 164 | private ReplacedMessagesFeedback( 165 | Map messages, String warning, String... suggestions) { 166 | super(warning, suggestions); 167 | this.messages = messages; 168 | } 169 | 170 | @Override 171 | protected ResourceBundle resolveResourceBundle(Locale locale) { 172 | try { 173 | ResourceBundle resource = messages.get(locale); 174 | if (resource != null) { 175 | return resource; 176 | } 177 | return ResourceBundle.getBundle(DEFAULT_BUNDLE_NAME, locale, CONTROL); 178 | } catch (MissingResourceException | UnsupportedOperationException e) { 179 | return ResourceBundle.getBundle(DEFAULT_BUNDLE_NAME, locale); 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/WipeableString.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import java.nio.CharBuffer; 4 | import java.util.Arrays; 5 | 6 | /** A character sequence with many attributes of Strings, but that can have its content wiped. */ 7 | public class WipeableString implements CharSequence { 8 | 9 | private char[] content; 10 | private int hash = 0; 11 | private boolean wiped = false; 12 | 13 | /** Creates a new wipeable string, copying the content from the specified source. */ 14 | public WipeableString(CharSequence source) { 15 | this.content = new char[source.length()]; 16 | for (int n = 0; n < content.length; n++) { 17 | content[n] = source.charAt(n); 18 | } 19 | } 20 | 21 | /** Creates a new wipeable string, copying the content from the specified source. */ 22 | public WipeableString(char[] source) { 23 | this.content = Arrays.copyOf(source, source.length); 24 | } 25 | 26 | @Override 27 | public int length() { 28 | return content == null ? 0 : content.length; 29 | } 30 | 31 | @Override 32 | public char charAt(int index) { 33 | return content[index]; 34 | } 35 | 36 | @Override 37 | public WipeableString subSequence(int start, int end) { 38 | return new WipeableString(Arrays.copyOfRange(content, start, end)); 39 | } 40 | 41 | /** 42 | * Wipe the content of the wipeable string. 43 | * 44 | *

Overwrites the content buffer with spaces, then replaces the buffer with an empty one. 45 | */ 46 | public void wipe() { 47 | Arrays.fill(content, ' '); 48 | hash = 0; 49 | content = new char[0]; 50 | wiped = true; 51 | } 52 | 53 | /** Returns a new wipeable string with the specified content forced into lower case. */ 54 | public static WipeableString lowerCase(CharSequence source) { 55 | if (source == null) { 56 | throw new IllegalArgumentException("source is null"); 57 | } 58 | 59 | char[] chars = new char[source.length()]; 60 | for (int n = 0; n < source.length(); n++) { 61 | chars[n] = Character.toLowerCase(source.charAt(n)); 62 | } 63 | return new WipeableString(chars); 64 | } 65 | 66 | /** 67 | * Returns a new wipeable string with the specified content but with the order of the characters 68 | * reversed. 69 | */ 70 | public static WipeableString reversed(CharSequence source) { 71 | if (source == null) { 72 | throw new IllegalArgumentException("source is null"); 73 | } 74 | int length = source.length(); 75 | char[] chars = new char[length]; 76 | for (int n = 0; n < source.length(); n++) { 77 | chars[n] = source.charAt(length - n - 1); 78 | } 79 | return new WipeableString(chars); 80 | } 81 | 82 | /** Returns a copy of a portion of a character sequence as a wipeable string. */ 83 | public static WipeableString copy(CharSequence source, int start, int end) { 84 | return new WipeableString(source.subSequence(start, end)); 85 | } 86 | 87 | /** Returns the position of the first match of the specified character (indexed from 0). */ 88 | public int indexOf(char character) { 89 | for (int n = 0; n < content.length; n++) { 90 | if (content[n] == character) { 91 | return n; 92 | } 93 | } 94 | return -1; 95 | } 96 | 97 | /** Returns the nth Unicode code point. */ 98 | public int codePointAt(int index) { 99 | // Copy the implementation from String 100 | if ((index < 0) || (index >= content.length)) { 101 | throw new StringIndexOutOfBoundsException(index); 102 | } 103 | return Character.codePointAt(content, index, content.length); 104 | } 105 | 106 | /** Returns true if the wipeable string has been wiped. */ 107 | public boolean isWiped() { 108 | return this.wiped; 109 | } 110 | 111 | /** Returns a copy of the content as a char array. */ 112 | public char[] charArray() { 113 | return Arrays.copyOf(content, content.length); 114 | } 115 | 116 | /** 117 | * Trims whitespace from a CharSequence. 118 | * 119 | *

If there is no trailing whitespace then the original value is returned. If there is trailing 120 | * whitespace then the content (without that trailing whitespace) is copied into a new 121 | * WipeableString. 122 | */ 123 | static CharSequence trimTrailingWhitespace(CharSequence s) { 124 | if (!Character.isWhitespace(s.charAt(s.length() - 1))) { 125 | return s; 126 | } 127 | 128 | int length = s.length(); 129 | 130 | while (length > 0 && Character.isWhitespace(s.charAt(length - 1))) { 131 | length--; 132 | } 133 | 134 | return WipeableString.copy(s, 0, length); 135 | } 136 | 137 | /** A version of Integer.parse(String) that accepts CharSequence as parameter. */ 138 | public static int parseInt(CharSequence s) throws NumberFormatException { 139 | return parseInt(s, 10); 140 | } 141 | 142 | /** A version of Integer.parse(String) that accepts CharSequence as parameter. */ 143 | @SuppressWarnings("java:S3776") 144 | public static int parseInt(CharSequence s, int radix) throws NumberFormatException { 145 | if (s == null) { 146 | throw new NumberFormatException("null"); 147 | } 148 | 149 | s = trimTrailingWhitespace(s); 150 | 151 | if (radix < Character.MIN_RADIX) { 152 | throw new NumberFormatException("radix " + radix + " less than Character.MIN_RADIX"); 153 | } 154 | 155 | if (radix > Character.MAX_RADIX) { 156 | throw new NumberFormatException("radix " + radix + " greater than Character.MAX_RADIX"); 157 | } 158 | 159 | int result = 0; 160 | boolean negative = false; 161 | int i = 0; 162 | int len = s.length(); 163 | int limit = -Integer.MAX_VALUE; 164 | int multmin; 165 | int digit; 166 | 167 | if (len > 0) { 168 | char firstChar = s.charAt(0); 169 | if (firstChar < '0') { // Possible leading "+" or "-" 170 | if (firstChar == '-') { 171 | negative = true; 172 | limit = Integer.MIN_VALUE; 173 | } else if (firstChar != '+') { 174 | throw numberFormatException(s); 175 | } 176 | if (len == 1) { // Cannot have lone "+" or "-" 177 | throw numberFormatException(s); 178 | } 179 | i++; 180 | } 181 | multmin = limit / radix; 182 | while (i < len) { 183 | // Accumulating negatively avoids surprises near MAX_VALUE 184 | digit = Character.digit(s.charAt(i++), radix); 185 | if (digit < 0) { 186 | throw numberFormatException(s); 187 | } 188 | if (result < multmin) { 189 | throw numberFormatException(s); 190 | } 191 | result *= radix; 192 | if (result < limit + digit) { 193 | throw numberFormatException(s); 194 | } 195 | result -= digit; 196 | } 197 | } else { 198 | throw numberFormatException(s); 199 | } 200 | return negative ? result : -result; 201 | } 202 | 203 | private static NumberFormatException numberFormatException(CharSequence s) { 204 | return new NumberFormatException("For input string: \"" + s + "\""); 205 | } 206 | 207 | @Override 208 | public String toString() { 209 | return new String(content); 210 | } 211 | 212 | @Override 213 | public int hashCode() { 214 | // Reproduce the same hash as String 215 | int h = hash; 216 | if (h == 0 && content.length > 0) { 217 | char[] val = content; 218 | 219 | for (int i = 0; i < content.length; i++) { 220 | h = 31 * h + val[i]; 221 | } 222 | hash = h; 223 | } 224 | return h; 225 | } 226 | 227 | @Override 228 | public boolean equals(Object obj) { 229 | // Use an algorithm that matches any CharSequence (including Strings) with identical content. 230 | if (obj == null) { 231 | return false; 232 | } 233 | if (obj == this) { 234 | return true; 235 | } 236 | if (obj instanceof CharSequence) { 237 | CharSequence other = (CharSequence) obj; 238 | if (other.length() != length()) { 239 | return false; 240 | } 241 | for (int n = 0; n < length(); n++) { 242 | if (charAt(n) != other.charAt(n)) { 243 | return false; 244 | } 245 | } 246 | return true; 247 | } 248 | return false; 249 | } 250 | 251 | /** 252 | * Wipes the content of the specified character sequence if possible. 253 | * 254 | *

The following types can be wiped... WipeableString StringBuilder StringBuffer CharBuffer (if 255 | * not readOnly) 256 | */ 257 | public static void wipeIfPossible(CharSequence text) { 258 | if (text == null) { 259 | return; 260 | } 261 | if (text instanceof WipeableString) { 262 | ((WipeableString) text).wipe(); 263 | } else if (text instanceof StringBuilder) { 264 | for (int n = 0; n < text.length(); n++) { 265 | ((StringBuilder) text).setCharAt(n, ' '); 266 | } 267 | ((StringBuilder) text).setLength(0); 268 | } else if (text instanceof StringBuffer) { 269 | for (int n = 0; n < text.length(); n++) { 270 | ((StringBuffer) text).setCharAt(n, ' '); 271 | } 272 | ((StringBuffer) text).setLength(0); 273 | } else if (text instanceof CharBuffer && !((CharBuffer) text).isReadOnly()) { 274 | for (int n = 0; n < text.length(); n++) { 275 | ((CharBuffer) text).put(n, ' '); 276 | } 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/AttackTimes.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | public class AttackTimes { 4 | 5 | private CrackTimeSeconds crackTimeSeconds; 6 | private CrackTimesDisplay crackTimesDisplay; 7 | private int score; 8 | 9 | public AttackTimes( 10 | CrackTimeSeconds crackTimeSeconds, CrackTimesDisplay crackTimesDisplay, int score) { 11 | this.crackTimeSeconds = crackTimeSeconds; 12 | this.crackTimesDisplay = crackTimesDisplay; 13 | this.score = score; 14 | } 15 | 16 | public CrackTimeSeconds getCrackTimeSeconds() { 17 | return crackTimeSeconds; 18 | } 19 | 20 | /** 21 | * Sets the crack time in seconds. 22 | * 23 | * @param crackTimeSeconds The crack time in seconds. 24 | * @deprecated It is recommended to initialize using the constructor. 25 | */ 26 | @Deprecated 27 | public void setCrackTimeSeconds(CrackTimeSeconds crackTimeSeconds) { 28 | this.crackTimeSeconds = crackTimeSeconds; 29 | } 30 | 31 | public CrackTimesDisplay getCrackTimesDisplay() { 32 | return crackTimesDisplay; 33 | } 34 | 35 | /** 36 | * Sets the display representation for the crack times. 37 | * 38 | * @param crackTimesDisplay The display values for crack times. 39 | * @deprecated It is recommended to initialize using the constructor. 40 | */ 41 | @Deprecated 42 | public void setCrackTimesDisplay(CrackTimesDisplay crackTimesDisplay) { 43 | this.crackTimesDisplay = crackTimesDisplay; 44 | } 45 | 46 | public int getScore() { 47 | return score; 48 | } 49 | 50 | /** 51 | * Sets the score value. 52 | * 53 | * @param score The score value. 54 | * @deprecated It is recommended to initialize using the constructor. 55 | */ 56 | @Deprecated 57 | public void setScore(int score) { 58 | this.score = score; 59 | } 60 | 61 | public static class CrackTimeSeconds { 62 | private double onlineThrottling100perHour; 63 | private double onlineNoThrottling10perSecond; 64 | private double offlineSlowHashing1e4perSecond; 65 | private double offlineFastHashing1e10PerSecond; 66 | 67 | public CrackTimeSeconds( 68 | double onlineThrottling100perHour, 69 | double onlineNoThrottling10perSecond, 70 | double offlineSlowHashing1e4perSecond, 71 | double offlineFastHashing1e10PerSecond) { 72 | this.onlineThrottling100perHour = onlineThrottling100perHour; 73 | this.onlineNoThrottling10perSecond = onlineNoThrottling10perSecond; 74 | this.offlineSlowHashing1e4perSecond = offlineSlowHashing1e4perSecond; 75 | this.offlineFastHashing1e10PerSecond = offlineFastHashing1e10PerSecond; 76 | } 77 | 78 | public double getOnlineThrottling100perHour() { 79 | return onlineThrottling100perHour; 80 | } 81 | 82 | /** 83 | * Sets the time required to crack a password with online throttling at 100 attempts per hour. 84 | * 85 | * @param onlineThrottling100perHour Time in seconds for online throttling at 100 attempts per 86 | * hour. 87 | * @deprecated This method is deprecated. It is recommended to initialize using the constructor. 88 | */ 89 | @Deprecated 90 | public void setOnlineThrottling100perHour(double onlineThrottling100perHour) { 91 | this.onlineThrottling100perHour = onlineThrottling100perHour; 92 | } 93 | 94 | public double getOnlineNoThrottling10perSecond() { 95 | return onlineNoThrottling10perSecond; 96 | } 97 | 98 | /** 99 | * Sets the time required to crack a password with online attacks without throttling at 10 100 | * attempts per second. 101 | * 102 | * @param onlineNoThrottling10perSecond Time in seconds for online attacks without throttling at 103 | * 10 attempts per second. 104 | * @deprecated This method is deprecated. It is recommended to initialize using the constructor. 105 | */ 106 | @Deprecated 107 | public void setOnlineNoThrottling10perSecond(double onlineNoThrottling10perSecond) { 108 | this.onlineNoThrottling10perSecond = onlineNoThrottling10perSecond; 109 | } 110 | 111 | public double getOfflineSlowHashing1e4perSecond() { 112 | return offlineSlowHashing1e4perSecond; 113 | } 114 | 115 | /** 116 | * Sets the time required to crack a password with offline slow hashing at 1e4 attempts per 117 | * second. 118 | * 119 | * @param offlineSlowHashing1e4perSecond Time in seconds for offline slow hashing at 1e4 120 | * attempts per second. 121 | * @deprecated This method is deprecated. It is recommended to initialize using the constructor. 122 | */ 123 | @Deprecated 124 | public void setOfflineSlowHashing1e4perSecond(double offlineSlowHashing1e4perSecond) { 125 | this.offlineSlowHashing1e4perSecond = offlineSlowHashing1e4perSecond; 126 | } 127 | 128 | public double getOfflineFastHashing1e10PerSecond() { 129 | return offlineFastHashing1e10PerSecond; 130 | } 131 | 132 | /** 133 | * Sets the time required to crack a password with offline fast hashing at 1e10 attempts per 134 | * second. 135 | * 136 | * @param offlineFastHashing1e10PerSecond Time in seconds for offline fast hashing at 1e10 137 | * attempts per second. 138 | * @deprecated This method is deprecated. It is recommended to initialize using the constructor. 139 | */ 140 | @Deprecated 141 | public void setOfflineFastHashing1e10PerSecond(double offlineFastHashing1e10PerSecond) { 142 | this.offlineFastHashing1e10PerSecond = offlineFastHashing1e10PerSecond; 143 | } 144 | } 145 | 146 | public static class CrackTimesDisplay { 147 | private String onlineThrottling100perHour; 148 | private String onlineNoThrottling10perSecond; 149 | private String offlineSlowHashing1e4perSecond; 150 | private String offlineFastHashing1e10PerSecond; 151 | 152 | public CrackTimesDisplay( 153 | String onlineThrottling100perHour, 154 | String onlineNoThrottling10perSecond, 155 | String offlineSlowHashing1e4perSecond, 156 | String offlineFastHashing1e10PerSecond) { 157 | this.onlineThrottling100perHour = onlineThrottling100perHour; 158 | this.onlineNoThrottling10perSecond = onlineNoThrottling10perSecond; 159 | this.offlineSlowHashing1e4perSecond = offlineSlowHashing1e4perSecond; 160 | this.offlineFastHashing1e10PerSecond = offlineFastHashing1e10PerSecond; 161 | } 162 | 163 | public String getOnlineThrottling100perHour() { 164 | return onlineThrottling100perHour; 165 | } 166 | 167 | /** 168 | * Sets the display representation for the time required to crack a password with online 169 | * throttling at 100 attempts per hour. 170 | * 171 | * @param onlineThrottling100perHour Display representation for online throttling at 100 172 | * attempts per hour. 173 | * @deprecated This method is deprecated. It is recommended to initialize using the constructor. 174 | */ 175 | @Deprecated 176 | public void setOnlineThrottling100perHour(String onlineThrottling100perHour) { 177 | this.onlineThrottling100perHour = onlineThrottling100perHour; 178 | } 179 | 180 | public String getOnlineNoThrottling10perSecond() { 181 | return onlineNoThrottling10perSecond; 182 | } 183 | 184 | /** 185 | * Sets the display representation for the time required to crack a password with online attacks 186 | * without throttling at 10 attempts per second. 187 | * 188 | * @param onlineNoThrottling10perSecond Display representation for online attacks without 189 | * throttling at 10 attempts per second. 190 | * @deprecated This method is deprecated. It is recommended to initialize using the constructor. 191 | */ 192 | @Deprecated 193 | public void setOnlineNoThrottling10perSecond(String onlineNoThrottling10perSecond) { 194 | this.onlineNoThrottling10perSecond = onlineNoThrottling10perSecond; 195 | } 196 | 197 | public String getOfflineSlowHashing1e4perSecond() { 198 | return offlineSlowHashing1e4perSecond; 199 | } 200 | 201 | /** 202 | * Sets the display representation for the time required to crack a password with offline slow 203 | * hashing at 1e4 attempts per second. 204 | * 205 | * @param offlineSlowHashing1e4perSecond Display representation for offline slow hashing at 1e4 206 | * attempts per second. 207 | * @deprecated This method is deprecated. It is recommended to initialize using the constructor. 208 | */ 209 | @Deprecated 210 | public void setOfflineSlowHashing1e4perSecond(String offlineSlowHashing1e4perSecond) { 211 | this.offlineSlowHashing1e4perSecond = offlineSlowHashing1e4perSecond; 212 | } 213 | 214 | public String getOfflineFastHashing1e10PerSecond() { 215 | return offlineFastHashing1e10PerSecond; 216 | } 217 | 218 | /** 219 | * Sets the display representation for the time required to crack a password with offline fast 220 | * hashing at 1e10 attempts per second. 221 | * 222 | * @param offlineFastHashing1e10PerSecond Display representation for offline fast hashing at 223 | * 1e10 attempts per second. 224 | * @deprecated This method is deprecated. It is recommended to initialize using the constructor. 225 | */ 226 | @Deprecated 227 | public void setOfflineFastHashing1e10PerSecond(String offlineFastHashing1e10PerSecond) { 228 | this.offlineFastHashing1e10PerSecond = offlineFastHashing1e10PerSecond; 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command; 206 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 207 | # shell script including quotes and variable substitutions, so put them in 208 | # double quotes to make sure that they get re-expanded; and 209 | # * put everything else in single quotes, so that it's not re-expanded. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /src/main/java/com/nulabinc/zxcvbn/Scoring.java: -------------------------------------------------------------------------------- 1 | package com.nulabinc.zxcvbn; 2 | 3 | import com.nulabinc.zxcvbn.guesses.EstimateGuess; 4 | import com.nulabinc.zxcvbn.matchers.Match; 5 | import com.nulabinc.zxcvbn.matchers.MatchFactory; 6 | import java.io.Serializable; 7 | import java.util.ArrayList; 8 | import java.util.Calendar; 9 | import java.util.Collections; 10 | import java.util.Comparator; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | public class Scoring { 15 | 16 | public static final int REFERENCE_YEAR = Calendar.getInstance().get(Calendar.YEAR); 17 | 18 | public static final int MIN_GUESSES_BEFORE_GROWING_SEQUENCE = 10000; 19 | 20 | public static final long JS_NUMBER_MAX = 9007199254740991L; 21 | 22 | private final Context context; 23 | 24 | public static double log10(double n) { 25 | return Math.log(n) / Math.log(10); 26 | } 27 | 28 | public Scoring(Context context) { 29 | this.context = context; 30 | } 31 | 32 | /** 33 | * Calculates the most guessable match sequence for a password. 34 | * 35 | * @deprecated Use {@link #calculateMostGuessableMatchSequence} instead for better clarity and 36 | * maintainability. 37 | */ 38 | @Deprecated 39 | public Strength mostGuessableMatchSequence(CharSequence password, List matches) { 40 | return mostGuessableMatchSequence(password, matches, false); 41 | } 42 | 43 | /** 44 | * Calculates the most guessable match sequence for a password with an option to exclude additive. 45 | * 46 | * @deprecated Use {@link #calculateMostGuessableMatchSequence} instead for better clarity and 47 | * maintainability. 48 | */ 49 | @Deprecated 50 | public Strength mostGuessableMatchSequence( 51 | CharSequence password, List matches, boolean excludeAdditive) { 52 | MatchSequence matchSequence = 53 | calculateMostGuessableMatchSequence(password, matches, excludeAdditive); 54 | return new Strength(password, matchSequence.getGuesses(), matchSequence.getSequence(), 0); 55 | } 56 | 57 | /** 58 | * Calculates the most guessable match sequence for a password. 59 | * 60 | * @param password The password to evaluate. 61 | * @param matches A list of matches detected in the password. 62 | * @return A MatchSequence containing the most guessable sequence and associated guesses. 63 | */ 64 | public MatchSequence calculateMostGuessableMatchSequence( 65 | CharSequence password, List matches) { 66 | return calculateMostGuessableMatchSequence(password, matches, false); 67 | } 68 | 69 | /** 70 | * Calculates the most guessable match sequence for a password with an option to exclude additive. 71 | * 72 | * @param password The password to evaluate. 73 | * @param matches A list of matches detected in the password. 74 | * @param excludeAdditive If true, excludes additive computations from the guess estimation. 75 | * @return A MatchSequence containing the most guessable sequence and associated guesses. 76 | */ 77 | public MatchSequence calculateMostGuessableMatchSequence( 78 | CharSequence password, List matches, boolean excludeAdditive) { 79 | List> matchesByEndPosition = groupMatchesByEndPosition(password.length(), matches); 80 | Optimal optimal = computeOptimal(context, password, matchesByEndPosition, excludeAdditive); 81 | List optimalMatchSequence = unwindOptimal(password.length(), optimal); 82 | double guesses = 0; 83 | if (password.length() == 0) { 84 | guesses = 1; 85 | } else { 86 | guesses = optimal.getOverallMetric(password.length() - 1, optimalMatchSequence.size()); 87 | } 88 | return new MatchSequence(optimalMatchSequence, guesses); 89 | } 90 | 91 | private static List> groupMatchesByEndPosition(int length, List matches) { 92 | final List> matchesByEndPosition = new ArrayList<>(); 93 | for (int i = 0; i < length; i++) { 94 | matchesByEndPosition.add(new ArrayList()); 95 | } 96 | for (Match match : matches) { 97 | matchesByEndPosition.get(match.j).add(match); 98 | } 99 | for (List lst : matchesByEndPosition) { 100 | Collections.sort(lst, new MatchStartPositionComparator()); 101 | } 102 | return matchesByEndPosition; 103 | } 104 | 105 | private static Optimal computeOptimal( 106 | Context context, 107 | CharSequence password, 108 | List> matchesByEndPosition, 109 | boolean excludeAdditive) { 110 | int length = password.length(); 111 | Optimal optimal = new Optimal(length); 112 | for (int k = 0; k < length; k++) { 113 | for (Match m : matchesByEndPosition.get(k)) { 114 | if (m.i > 0) { 115 | for (Map.Entry entry : optimal.getBestMatchesAt(m.i - 1).entrySet()) { 116 | int l = entry.getKey(); 117 | updateOptimal(context, password, m, l + 1, optimal, excludeAdditive); 118 | } 119 | } else { 120 | updateOptimal(context, password, m, 1, optimal, excludeAdditive); 121 | } 122 | } 123 | updateBruteforceMatches(context, password, k, optimal, excludeAdditive); 124 | } 125 | return optimal; 126 | } 127 | 128 | private static void updateOptimal( 129 | Context context, 130 | CharSequence password, 131 | Match match, 132 | int l, 133 | Optimal optimal, 134 | boolean excludeAdditive) { 135 | 136 | double guesses = calculateGuesses(context, password, match, l, optimal); 137 | double metrics = calculateMetrics(l, guesses, excludeAdditive); 138 | 139 | if (shouldUpdateMetrics(match, l, metrics, optimal)) { 140 | optimal.putToBestMatches(match.j, l, match); 141 | optimal.putToTotalGuesses(match.j, l, guesses); 142 | optimal.putToOverallMetrics(match.j, l, metrics); 143 | } 144 | } 145 | 146 | private static double calculateGuesses( 147 | Context context, CharSequence password, Match match, int l, Optimal optimal) { 148 | double guesses = new EstimateGuess(context, password).exec(match); 149 | if (l > 1) { 150 | guesses *= optimal.getTotalGuess(match.i - 1, l - 1); 151 | } 152 | return handleInfinity(guesses); 153 | } 154 | 155 | private static double calculateMetrics(int l, double guesses, boolean excludeAdditive) { 156 | double metrics = factorial(l) * guesses; 157 | metrics = handleInfinity(metrics); 158 | 159 | if (!excludeAdditive) { 160 | metrics += Math.pow(MIN_GUESSES_BEFORE_GROWING_SEQUENCE, (double) l - 1); 161 | metrics = handleInfinity(metrics); 162 | } 163 | 164 | return metrics; 165 | } 166 | 167 | private static double handleInfinity(double value) { 168 | return Double.isInfinite(value) ? Double.MAX_VALUE : value; 169 | } 170 | 171 | private static boolean shouldUpdateMetrics(Match match, int l, double metrics, Optimal optimal) { 172 | Map overallMetrics = optimal.getOverallMetricsAt(match.j); 173 | for (Map.Entry competing : overallMetrics.entrySet()) { 174 | if (competing.getKey() > l) { 175 | continue; 176 | } 177 | if (competing.getValue() <= metrics) { 178 | return false; 179 | } 180 | } 181 | return true; 182 | } 183 | 184 | private static void updateBruteforceMatches( 185 | Context context, 186 | CharSequence password, 187 | int endIndex, 188 | Optimal optimal, 189 | boolean excludeAdditive) { 190 | 191 | Match match = makeBruteforceMatch(password, 0, endIndex); 192 | updateOptimal(context, password, match, 1, optimal, excludeAdditive); 193 | 194 | for (int startIndex = 1; startIndex <= endIndex; startIndex++) { 195 | match = makeBruteforceMatch(password, startIndex, endIndex); 196 | Map previousBestMatches = optimal.getBestMatchesAt(startIndex - 1); 197 | 198 | for (Map.Entry entry : previousBestMatches.entrySet()) { 199 | int matchLength = entry.getKey(); 200 | Match lastMatch = entry.getValue(); 201 | if (lastMatch.pattern != Pattern.Bruteforce) { 202 | updateOptimal(context, password, match, matchLength + 1, optimal, excludeAdditive); 203 | } 204 | } 205 | } 206 | } 207 | 208 | private static List unwindOptimal(int passwordLength, Optimal optimal) { 209 | List optimalMatchSequence = new ArrayList<>(); 210 | if (passwordLength <= 0) { 211 | return optimalMatchSequence; 212 | } 213 | 214 | int lastIndex = passwordLength - 1; 215 | Map metricsForLastIndex = optimal.getOverallMetricsAt(lastIndex); 216 | 217 | int optimalLength = getOptimalMatchLength(metricsForLastIndex); 218 | 219 | while (lastIndex >= 0) { 220 | Match currentMatch = optimal.getBestMatch(lastIndex, optimalLength); 221 | optimalMatchSequence.add(0, currentMatch); 222 | lastIndex = currentMatch.i - 1; 223 | optimalLength--; 224 | } 225 | return optimalMatchSequence; 226 | } 227 | 228 | private static int getOptimalMatchLength(Map metrics) { 229 | int optimalLength = 0; 230 | Double minMetric = Double.POSITIVE_INFINITY; 231 | 232 | for (Map.Entry candidate : metrics.entrySet()) { 233 | Double currentMetric = candidate.getValue(); 234 | if (currentMetric < minMetric) { 235 | optimalLength = candidate.getKey(); 236 | minMetric = currentMetric; 237 | } 238 | } 239 | 240 | return optimalLength; 241 | } 242 | 243 | private static Match makeBruteforceMatch(CharSequence password, int i, int j) { 244 | return MatchFactory.createBruteforceMatch(i, j, password.subSequence(i, j + 1)); 245 | } 246 | 247 | private static long factorial(int n) { 248 | if (n < 2) { 249 | return 1; 250 | } 251 | if (n > 19) { 252 | return JS_NUMBER_MAX; 253 | } 254 | long f = 1; 255 | for (int i = 2; i <= n; i++) { 256 | f *= i; 257 | } 258 | return f; 259 | } 260 | 261 | private static class MatchStartPositionComparator implements Comparator, Serializable { 262 | private static final long serialVersionUID = 1L; 263 | 264 | @Override 265 | public int compare(Match m1, Match m2) { 266 | return m1.i - m2.i; 267 | } 268 | } 269 | } 270 | --------------------------------------------------------------------------------