~jorgecastro/password-critic

5a45e314db74d199cd101e97d2075c3e86d8de40 — Jorge Castro 2 years ago
Initial commit
A  => .gitattributes +9 -0
@@ 1,9 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew        text eol=lf

# These are Windows script files and should use crlf
*.bat           text eol=crlf


A  => .gitignore +170 -0
@@ 1,170 @@
# Created by https://www.toptal.com/developers/gitignore/api/java,gradle,intellij
# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,intellij

### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# AWS User-specific
.idea/**/aws.xml

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn.  Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# SonarLint plugin
.idea/sonarlint/

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr

# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/

# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml

# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/

# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$

# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml

# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml

### Java ###
# Compiled class file
*.class

# Log file
*.log

# BlueJ files
*.ctxt

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*

### Gradle ###
.gradle
**/build/
!src/**/build/

# Ignore Gradle GUI config
gradle-app.setting

# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar

# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties

# Cache of project
.gradletasknamecache

# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath

### Gradle Patch ###
# Java heap dump
*.hprof

# End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij

A  => .idea/.gitignore +3 -0
@@ 1,3 @@
# Default ignored files
/shelf/
/workspace.xml

A  => .idea/compiler.xml +6 -0
@@ 1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="CompilerConfiguration">
    <bytecodeTargetLevel target="17" />
  </component>
</project>
\ No newline at end of file

A  => .idea/jarRepositories.xml +20 -0
@@ 1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="RemoteRepositoriesConfiguration">
    <remote-repository>
      <option name="id" value="central" />
      <option name="name" value="Maven Central repository" />
      <option name="url" value="https://repo1.maven.org/maven2" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="jboss.community" />
      <option name="name" value="JBoss Community repository" />
      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="MavenRepo" />
      <option name="name" value="MavenRepo" />
      <option name="url" value="https://repo.maven.apache.org/maven2/" />
    </remote-repository>
  </component>
</project>
\ No newline at end of file

A  => .idea/markdown.xml +6 -0
@@ 1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="MarkdownSettings">
    <option name="showProblemsInCodeBlocks" value="false" />
  </component>
</project>
\ No newline at end of file

A  => .idea/misc.xml +5 -0
@@ 1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ExternalStorageConfigurationManager" enabled="true" />
  <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
</project>
\ No newline at end of file

A  => .idea/vcs.xml +6 -0
@@ 1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
  </component>
</project>
\ No newline at end of file

A  => LICENSE.md +21 -0
@@ 1,21 @@
MIT License

Copyright (c) 2022 Jorge Castro

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

A  => README.md +9 -0
@@ 1,9 @@
## Password Critic

Password Critic is a small Java library providing functionality to assess password strength and present the results in a user-friendly format.

It is a useful tool for testing current or potential passwords and evaluating their viability.

### Usage

See the [examples](examples) for help with using the library.

A  => examples/DemoCLI.java +101 -0
@@ 1,101 @@
import xyz.jorgecastro.passwordcritic.PasswordCritic;

import java.io.Console;

public class DemoCLI {
    public static void main(String[] args) {
        Console console = System.console();

        System.out.println("Hello! This is a demo of the Password Critic.");
        System.out.println("Please type in your password to assess its strength:");

        /*
         Instantiate the Password Critic; the char[] passed to the constructor is automatically zeroed out for
         security
        */
        PasswordCritic passwordCritic = new PasswordCritic(console.readPassword());

        /* Get an estimate on how long it'd take to crack this password in different scenarios */
        // System.out.println(passwordCritic.getCrackTimesEstimate());
        /*
         Example output:

         Online attack on a rate-limited service (~100 guesses/hour): centuries
         Online attack on a service with absent or bypassed rate limiting (~10 guesses/second): centuries
         Offline attack using a slow hash function and many cores (~10K guesses/second): 3 months
         Offline attack using a fast hash function and many cores (~10B guesses/second): 8 seconds
         Get feedback on the password
        */

        /* Get feedback on the password */
        // System.out.println(passwordCritic.getFeedback());
        /*
         Example output:

         Warnings: This is a top-100 common password.

         Suggestions: Add another word or two. Uncommon words are better.
        */

        /*
         Get the above and more information, i.e. the estimated number of guesses it'd take to crack this password,
         the password's score in a 0-4 scale and the time it took to compute it all
        */
        // System.out.println(passwordCritic.getStrengthSummary());
        /*
         Example output:
         Estimated number of guesses it would take to crack this password: 25.0000

         Here's how long it would take to crack your password in different scenarios:

         Online attack on a rate-limited service (~100 guesses/hour): 15 minutes
         Online attack on a service with absent or bypassed rate limiting (~10 guesses/second): 3 seconds
         Offline attack using a slow hash function and many cores (~10K guesses/second): less than a second
         Offline attack using a fast hash function and many cores (~10B guesses/second): less than a second

         Score on a 0-4 scale; 0 (Weak), 1 (Fair), 2 (Good), 3 (Strong), 4 (Very strong): 0

         Warnings: Sequences like abc or 6543 are easy to guess.

         Suggestions: Add another word or two. Uncommon words are better. Avoid sequences.

         Time it took to calculate the above (in seconds): 0.0333254
        */

        /* Returns the number of times the given password has appeared in data breach reports */
        // System.out.println(passwordCritic.search()); // ==> 747

        /* Provide a detailed assessment containing all the information appearing above */
        System.out.println(passwordCritic.critique());
        /*
         Example output:

         Estimated number of guesses it would take to crack this password: 1220.00

         Here's how long it would take to crack your password in different scenarios:

         Online attack on a rate-limited service (~100 guesses/hour): 12 hours
         Online attack on a service with absent or bypassed rate limiting (~10 guesses/second): 2 minutes
         Offline attack using a slow hash function and many cores (~10K guesses/second): less than a second
         Offline attack using a fast hash function and many cores (~10B guesses/second): less than a second

         Score on a 0-4 scale; 0 (Weak), 1 (Fair), 2 (Good), 3 (Strong), 4 (Very strong): 1

         Warnings: A word by itself is easy to guess.

         Suggestions: Add another word or two. Uncommon words are better.

         Time it took to calculate the above (in seconds): 0.0354738

         Your password has appeared a total of 2046 times in data breach reports.

         If the above number is greater than zero please consider discarding this password.
         Visit https://haveibeenpwned.com/Passwords for more information.
        */

        /* Wipe password-related information from memory */
        passwordCritic.wipe();

        System.exit(0);
    }
}

A  => examples/DemoGUI.java +37 -0
@@ 1,37 @@
import xyz.jorgecastro.passwordcritic.PasswordCritic;

import javax.swing.*;

public class DemoGUI extends JFrame {
    PasswordCritic passwordCritic;

    public DemoGUI() {
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));

        JTextArea textArea = new JTextArea();
        textArea.setEditable(false);

        JPasswordField passwordField = new JPasswordField(10);
        passwordField.addActionListener(
            e -> {
                passwordCritic = new PasswordCritic(passwordField.getPassword());
                textArea.append(passwordCritic.critique() + "\n");
                passwordCritic.wipe();
                passwordField.setEnabled(false);
            }
        );

        panel.add(textArea);
        panel.add(new JLabel("Type in your password below and hit ENTER to assess its strength"));
        panel.add(passwordField);

        add(panel);
        setTitle("Password Critic");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(850, 450);
        setVisible(true);
    }

    public static void main(String[] args) { new DemoGUI(); }
}

A  => gradle/wrapper/gradle-wrapper.jar +0 -0
A  => gradle/wrapper/gradle-wrapper.properties +5 -0
@@ 1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

A  => gradlew +240 -0
@@ 1,240 @@
#!/bin/sh

#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

##############################################################################
#
#   Gradle start up script for POSIX generated by Gradle.
#
#   Important for running:
#
#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
#       noncompliant, but you have some other compliant shell such as ksh or
#       bash, then to run this script, type that shell name before the whole
#       command line, like:
#
#           ksh Gradle
#
#       Busybox and similar reduced shells will NOT work, because this script
#       requires all of these POSIX shell features:
#         * functions;
#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
#         * compound commands having a testable exit status, especially «case»;
#         * various built-in commands including «command», «set», and «ulimit».
#
#   Important for patching:
#
#   (2) This script targets any POSIX shell, so it avoids extensions provided
#       by Bash, Ksh, etc; in particular arrays are avoided.
#
#       The "traditional" practice of packing multiple parameters into a
#       space-separated string is a well documented source of bugs and security
#       problems, so this is (mostly) avoided, by progressively accumulating
#       options in "$@", and eventually passing that to Java.
#
#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
#       see the in-line comments for details.
#
#       There are tweaks for specific operating systems such as AIX, CygWin,
#       Darwin, MinGW, and NonStop.
#
#   (3) This script is generated from the Groovy template
#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
#       within the Gradle project.
#
#       You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################

# Attempt to set APP_HOME

# Resolve links: $0 may be a link
app_path=$0

# Need this for daisy-chained symlinks.
while
    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
    [ -h "$app_path" ]
do
    ls=$( ls -ld "$app_path" )
    link=${ls#*' -> '}
    case $link in             #(
      /*)   app_path=$link ;; #(
      *)    app_path=$APP_HOME$link ;;
    esac
done

APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit

APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

warn () {
    echo "$*"
} >&2

die () {
    echo
    echo "$*"
    echo
    exit 1
} >&2

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD=java
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
    case $MAX_FD in #(
      max*)
        MAX_FD=$( ulimit -H -n ) ||
            warn "Could not query maximum file descriptor limit"
    esac
    case $MAX_FD in  #(
      '' | soft) :;; #(
      *)
        ulimit -n "$MAX_FD" ||
            warn "Could not set maximum file descriptor limit to $MAX_FD"
    esac
fi

# Collect all arguments for the java command, stacking in reverse order:
#   * args from the command line
#   * the main class name
#   * -classpath
#   * -D...appname settings
#   * --module-path (only if needed)
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.

# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )

    JAVACMD=$( cygpath --unix "$JAVACMD" )

    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    for arg do
        if
            case $arg in                                #(
              -*)   false ;;                            # don't mess with options #(
              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
                    [ -e "$t" ] ;;                      #(
              *)    false ;;
            esac
        then
            arg=$( cygpath --path --ignore --mixed "$arg" )
        fi
        # Roll the args list around exactly as many times as the number of
        # args, so each arg winds up back in the position where it started, but
        # possibly modified.
        #
        # NB: a `for` loop captures its iteration list before it begins, so
        # changing the positional parameters here affects neither the number of
        # iterations, nor the values presented in `arg`.
        shift                   # remove old arg
        set -- "$@" "$arg"      # push replacement arg
    done
fi

# Collect all arguments for the java command;
#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
#     shell script including quotes and variable substitutions, so put them in
#     double quotes to make sure that they get re-expanded; and
#   * put everything else in single quotes, so that it's not re-expanded.

set -- \
        "-Dorg.gradle.appname=$APP_BASE_NAME" \
        -classpath "$CLASSPATH" \
        org.gradle.wrapper.GradleWrapperMain \
        "$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
    die "xargs is not available"
fi

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
#   set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval "set -- $(
        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
        xargs -n1 |
        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
        tr '\n' ' '
    )" '"$@"'

exec "$JAVACMD" "$@"

A  => gradlew.bat +91 -0
@@ 1,91 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega

A  => lib/build.gradle +88 -0
@@ 1,88 @@
plugins {
    id 'java-library'
    id 'java-library-distribution'
    id 'maven-publish'
    id 'signing'
}

group 'xyz.jorgecastro'
version '1.0.0'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2'

    api 'com.nulab-inc:zxcvbn:1.7.0'

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
    implementation "com.google.guava:guava:31.1-jre"
}

tasks.named('test') {
    useJUnitPlatform()
}

tasks.named('jar') {
    manifest {
        attributes('Implementation-Title': project.name, 'Implementation-Version': project.version)
    }
}

java {
    withSourcesJar()
    withJavadocJar()
}

distributions {
    main {
        distributionBaseName = 'password-critic'
    }
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            artifactId = 'password-critic'
            from components.java
            pom {
                name = 'Password Critic'
                description = 'A small Java library providing functionality to assess password strength and present the results in a user-friendly format'
                url = 'https://sr.ht/~jorgecastro/password-critic/'
                licenses {
                    license {
                        name = 'MIT License'
                        url = 'https://git.sr.ht/~jorgecastro/password-critic/blob/master/LICENSE.md'
                    }
                }
                developers {
                    developer {
                        id = 'jorgecastro'
                        name = 'Jorge Castro'
                        email = 'me@jorgecastro.xyz'
                    }
                }
                scm {
                    connection = 'scm:git:https://git.sr.ht/~jorgecastro/password-critic'
                    developerConnection = 'scm:git:ssh://git@git.sr.ht/~jorgecastro/password-critic'
                    url = 'https://git.sr.ht/~jorgecastro/password-critic'
                }
            }
        }
    }
    repositories {
        maven {
            name = 'ossrh'
            url = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/'
            credentials(PasswordCredentials)
        }
    }
}

signing {
    useGpgCmd()
    sign publishing.publications.mavenJava
}

A  => lib/src/main/java/xyz/jorgecastro/passwordcritic/PasswordCritic.java +374 -0
@@ 1,374 @@
package xyz.jorgecastro.passwordcritic;

import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.nulabinc.zxcvbn.AttackTimes.CrackTimesDisplay;
import com.nulabinc.zxcvbn.Feedback;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.WipeableString;
import com.nulabinc.zxcvbn.Zxcvbn;
import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.converter.scalars.ScalarsConverterFactory;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.stream.Stream;

/**
 * The PasswordCritic class provides various methods to assess password strength and present the results in a
 * user-friendly format.
 *
 * <p>It extends {@link Zxcvbn}.
 *
 * @see WipeableString
 * @see Strength
 * @see HashFunction
 * @see Retrofit
 */
public class PasswordCritic extends Zxcvbn {
    private WipeableString password;
    private Strength passwordStrength;
    private final HashFunction sha1;
    private final PwnedPasswordsService pwnedPasswordsService;

    /**
     * Construct an instance of {@link PasswordCritic} ready to operate on the given password. This constructor
     * automatically zeroes out the passed {@code char[]} to increase security.
     *
     * @param password the {@code char[]} representing the password to be tested.
     */
    public PasswordCritic(char[] password) {
        // Create a WipeableString to hold our password and assign it to field
        this.password = new WipeableString(password);
        //Zero out the char[] originally containing the password for security
        Arrays.fill(password, '0');
        // Get Strength object with information on password strength and assign it to field
        passwordStrength = super.measure(this.password);

        // Instantiate function to generate SHA-1 hashes and assign it to field
        sha1 = Hashing.sha1();

        // Create Retrofit object and generate an implementation of our interface to interact with the Pwned Passwords
        // API
        Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://api.pwnedpasswords.com/")
            .addConverterFactory(ScalarsConverterFactory.create())
            .build();

        pwnedPasswordsService = retrofit.create(PwnedPasswordsService.class);
    }

    /**
     * Measures the strength of a password and returns a {@link Strength} object representing the results. Only use this
     * method if you wish to manage passwords and work with {@link Strength} objects directly, otherwise just pass
     * passwords using {@link #PasswordCritic(char[])} and {@link #setPassword(char[])}.
     *
     * @param password the {@code char[]} representing the password to be tested.
     * @return a {@link Strength} object representing the password's strength.
     */
    public static Strength measure(char[] password) { return new Zxcvbn().measure(new WipeableString(password)); }

    /**
     * Get a reference to the {@link WipeableString} containing the password associated with this instance. Please note
     * that this {@link WipeableString} will be wiped if a new password is set using {@link #setPassword(char[])} or if
     * the {@link #wipe()} method is called on this object.
     *
     * @return a reference to the {@link WipeableString} containing the password.
     */
    public WipeableString getPassword() { return password; }

    /**
     * Set a new password for this instance to operate on. This method automatically zeroes out the passed
     * {@code char[]} and calls {@link #wipe()} to remove previous password-related information for security.
     *
     * @param password the {@code char[]} representing the password to be tested.
     */
    public void setPassword(char[] password) {
        //Wipe the information for the previous password and associated Strength from memory
        this.wipe();
        // Create a WipeableString to hold our password and assign it to field
        this.password = new WipeableString(password);
        //Zero out the char[] originally containing the password for security
        Arrays.fill(password, '0');
        // Get Strength object with information on password strength
        passwordStrength = measure(this.password);
    }

    /**
     * Return the estimated number of guesses that would be needed to crack a password with this {@link Strength}.
     *
     * @param strength the {@link Strength} of the password in question.
     * @return a {@code double} representing the guess estimate.
     */
    public static double getGuesses(Strength strength) { return strength.getGuesses(); }

    /**
     * Calls {@link #getGuesses(Strength)} and passes the {@link Strength} object derived from this instance's
     * associated password.
     *
     * @return a {@code double} representing the guess estimate.
     */
    public double getGuesses() { return getGuesses(passwordStrength); }

    /**
     * Return an integer from 0 to 4 representing the strength score of a password with this {@link Strength}. The
     * scores are given as follows:
     * <ul>
     *     <li>0 (Weak): {@link #getGuesses(Strength)} {@literal < 10^3 + 5}</li>
     *     <li>1 (Fair): {@link #getGuesses(Strength)} {@literal < 10^6 + 5}</li>
     *     <li>2 (Good): {@link #getGuesses(Strength)} {@literal < 10^8 + 5}</li>
     *     <li>3 (Strong): {@link #getGuesses(Strength)} {@literal < 10^10 + 5}</li>
     *     <li>4 (Very strong): {@link #getGuesses(Strength)} {@literal >= 10^10 + 5}</li>
     * </ul>
     *
     * @param strength the {@link Strength} of the password in question.
     * @return an {@code int} representing the strength score.
     */
    public static int getScore(Strength strength) {return strength.getScore(); }

    /**
     * Calls {@link #getScore(Strength)} and passes the {@link Strength} object derived from this instance's associated
     * password.
     *
     * @return an {@code int} representing the strength score.
     */
    public int getScore() {return getScore(passwordStrength); }

    /**
     * Return a double representing the time (in seconds) that it took to calculate a password's {@link Strength}.
     *
     * @param strength the {@link Strength} of the password in question.
     * @return a {@code double} representing the {@link Strength}'s calculation time.
     */
    public static double getCalcTime(Strength strength) { return (double) strength.getCalcTime() / 1000000000; }

    /**
     * Calls {@link #getCalcTime(Strength)} and passes the {@link Strength} object derived from this instance's
     * associated password.
     *
     * @return a {@code double} representing the {@link Strength}'s calculation time.
     */
    public double getCalcTime() { return getCalcTime(passwordStrength); }

    /**
     * Return a nicely formatted {@link String} with estimates on how long it'd take to crack a password with this
     * {@link Strength}.
     *
     * @param strength the {@link Strength} of the password in question.
     * @return a pretty {@link String} with crack time estimates.
     */
    public static String getCrackTimesEstimate(Strength strength) {
        // Get crack times estimate
        CrackTimesDisplay crackTimes = strength.getCrackTimesDisplay();

        // Simulates online attack on a service that rate limits password auth attempts
        String onlineThrottling = crackTimes.getOnlineThrottling100perHour();

        // Simulates online attack on a service that doesn't rate limit or where rate limiting has been bypassed
        String onlineNoThrottling = crackTimes.getOnlineNoThrottling10perSecond();

        // Simulates an offline attack. assumes multiple attackers, proper user-unique salting, and a slow hash function
        // with moderate work factor, such as bcrypt, scrypt, PBKDF2.
        String offlineSlowHashing = crackTimes.getOfflineSlowHashing1e4perSecond();

        // Simulates an offline attack with user-unique salting but a fast hash function like SHA-1, SHA-256 or MD5.
        // A wide range of reasonable numbers anywhere from one billion - one trillion guesses per second,
        // depending on number of cores and machines.
        String offlineFastHashing = crackTimes.getOfflineFastHashing1e10PerSecond();

        return String.format(
            """
                Online attack on a rate-limited service (~100 guesses/hour): %s
                Online attack on a service with absent or bypassed rate limiting (~10 guesses/second): %s
                Offline attack using a slow hash function and many cores (~10K guesses/second): %s
                Offline attack using a fast hash function and many cores (~10B guesses/second): %s""",
            onlineThrottling,
            onlineNoThrottling,
            offlineSlowHashing,
            offlineFastHashing
        );
    }

    /**
     * Calls {@link #getCrackTimesEstimate(Strength)} and passes the {@link Strength} object derived from this
     * instance's associated password.
     *
     * @return a pretty {@link String} with crack time estimates.
     */
    public String getCrackTimesEstimate() { return getCrackTimesEstimate(passwordStrength); }

    /**
     * Parse the content of the given {@link Feedback} instance into a nicely formatted {@link String} showing relevant
     * warnings and suggestions.
     *
     * @param feedback the {@link Feedback} associated to the password in question.
     * @return a pretty {@link String} with feedback, i.e. warnings and suggestions, or an empty one if there's none.
     */
    public static String parseFeedback(Feedback feedback) {
        StringBuilder parsedFeedback = new StringBuilder();

        // Get warnings from feedback (if any)
        if (!feedback.getWarning().isBlank()) {
            parsedFeedback.append("Warnings: ").append(feedback.getWarning()).append("\n\n");
        }

        // Get suggestions from feedback (if any)
        if (!feedback.getSuggestions().isEmpty()) {
            String suggestions = String.join(" ", feedback.getSuggestions());
            parsedFeedback.append("Suggestions: ").append(suggestions);
        }

        // If feedback could be found, return it
        if (!parsedFeedback.isEmpty()) {
            return parsedFeedback.toString();
        }

        return "No specific feedback has been computed for your password.\nIt seems like it might be a good one!";
    }

    /**
     * Calls {@link #parseFeedback(Feedback)} and passes the {@link Feedback} object generated from the {@link Strength}
     * object derived from this instance's associated password.
     *
     * @return a pretty {@link String} with feedback, i.e. warnings and suggestions, or an empty one if there's none.
     */
    public String getFeedback() { return parseFeedback(passwordStrength.getFeedback()); }

    /**
     * Analyze a {@link Strength} object and provide a summary with relevant metrics.
     *
     * @param strength the {@link Strength} object to analyze.
     * @return a pretty {@link String} with information on a password's associated {@link Strength}.
     */
    public static String getStrengthSummary(Strength strength) {
        // Estimated number of guesses needed to crack password
        double guesses = getGuesses(strength);

        // Get crack times estimate
        String crackTimesEstimate = getCrackTimesEstimate(strength);

        // Integer from 0-4: 0 (Weak), 1 (Fair), 2 (Good), 3 (Strong), 4 (Very strong)
        int score = getScore(strength);

        // Get feedback on our password
        String feedback = parseFeedback(strength.getFeedback());

        // Get the time it took to compute this information (in seconds)
        double calcTime = getCalcTime(strength);

        return String.format(
            """
                Estimated number of guesses it would take to crack this password: %g

                Here's how long it would take to crack your password in different scenarios:

                %s

                Score on a 0-4 scale; 0 (Weak), 1 (Fair), 2 (Good), 3 (Strong), 4 (Very strong): %d

                %s

                Time it took to calculate the above (in seconds): %g""",
            guesses,
            crackTimesEstimate,
            score,
            feedback,
            calcTime
        );
    }

    /**
     * Calls {@link #getStrengthSummary(Strength)} and passes the {@link Strength} object derived from this
     * instance's associated password.
     *
     * @return a pretty {@link String} with information on the password's associated Strength.
     */
    public String getStrengthSummary() { return getStrengthSummary(passwordStrength); }

    /**
     * Consult the Pwned Passwords API to find if this instance's associated password has been exposed in a reported
     * data breach.
     *
     * @return the number of occurrences of this password in data breach reports.
     */
    public int search() {
        // Refer to https://haveibeenpwned.com/API/v3#PwnedPasswords for API details

        // Hash our password and get the hash prefix (first 5 characters) and suffix (rest of the hash)
        HashCode hashedPassword = sha1.hashString(password, Charsets.UTF_8);
        String hashPrefix = hashedPassword.toString().substring(0, 5).toUpperCase();
        String hashSuffix = hashedPassword.toString().substring(5).toUpperCase();

        // Call object will make a request to our endpoint when executed
        Call<String> request = pwnedPasswordsService.searchPassword(hashPrefix);

        // Declare variable to hold our response's body as a String
        String responseBody;
        try {
            // Execute request and get the body of the response
            responseBody = request.execute().body();
        } catch (IOException e) {
            e.printStackTrace();
            return 0;
        }

        // If no potential matches are found then password has not been exposed in a data breach
        if (responseBody == null || responseBody.isBlank()) {
            return 0;
        }

        // Break up the response body line-by-line
        Stream<String> lines = responseBody.lines();

        // HashMap passwordRange will contain the hash suffixes of the potential matches as keys and their occurrence
        // count as the corresponding value
        HashMap<String, Integer> passwordRange = new HashMap<>();
        for (Iterator<String> it = lines.iterator(); it.hasNext(); ) {
            String line = it.next();
            passwordRange.put(
                line.substring(0, 35),
                Integer.valueOf(line.substring(36))
            );
        }

        // If our password suffix is a valid key return the corresponding count, otherwise 0 as that means our password
        // hash was not found in the dataset
        return passwordRange.getOrDefault(hashSuffix, 0);
    }

    /**
     * Provide an assessment of this instance's associated password using all the available information. Presents the
     * strength summary provided by {@link #getStrengthSummary()} and the number of occurrences of this password in data
     * breach reports provided by {@link #search()}.
     *
     * @return a pretty {@link String} with a fully-detailed assessment of this password.
     */
    public String critique() {
        return String.format(
            """
                %s

                Your password has appeared a total of %d times in data breach reports.

                If the above number is greater than zero please consider discarding this password.
                Visit https://haveibeenpwned.com/Passwords for more information.""",
            getStrengthSummary(),
            search()
        );
    }

    /**
     * Wipe sensitive password-related information from memory by calling {@link Strength#wipe()} on the
     * {@link Strength} object generated from the password associated to this instance.
     */
    public void wipe() {
        // This will wipe the WipeableString with our password and other information that may be stored in the Strength
        // object
        passwordStrength.wipe();
    }
}

A  => lib/src/main/java/xyz/jorgecastro/passwordcritic/PwnedPasswordsService.java +20 -0
@@ 1,20 @@
package xyz.jorgecastro.passwordcritic;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;

/** Interface to be adapted by {@link retrofit2.Retrofit} to interact with the Pwned Passwords API. */
public interface PwnedPasswordsService {
    // Refer to https://haveibeenpwned.com/API/v3#PwnedPasswords for API details

    /**
     * Generate a {@link Call} object that will query the Pwned Passwords API to search for a password by range (using
     * its partial hash) when executed.
     *
     * @param hashPrefix the first 5 characters of a SHA-1 password hash.
     * @return a {@link Call} object to search for password by range.
     */
    @GET("range/{hashPrefix}")
    Call<String> searchPassword(@Path("hashPrefix") String hashPrefix);
}

A  => lib/src/main/java/xyz/jorgecastro/passwordcritic/package-info.java +4 -0
@@ 1,4 @@
/**
 * Provides a set of classes to assess password strength and present the results in a user-friendly format.
 */
package xyz.jorgecastro.passwordcritic;

A  => lib/src/test/java/xyz/jorgecastro/passwordcritic/PasswordCriticTest.java +95 -0
@@ 1,95 @@
package xyz.jorgecastro.passwordcritic;

import com.nulabinc.zxcvbn.WipeableString;
import org.junit.jupiter.api.Test;

import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.*;

class PasswordCriticTest {
    @Test
    void getPassword() {
        char[] password = {'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        PasswordCritic passwordCritic = new PasswordCritic(password);
        assertEquals(passwordCritic.getPassword(), new WipeableString("password"));
    }

    @Test
    void setPassword() {
        char[] password = {'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        PasswordCritic passwordCritic = new PasswordCritic(password);

        char[] otherPassword = {'o', 't', 'h', 'e', 'r', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        passwordCritic.setPassword(otherPassword);

        assertEquals(passwordCritic.getPassword(), new WipeableString("otherpassword"));
        char[] charArray = new char[otherPassword.length];
        Arrays.fill(charArray, '0');
        assertArrayEquals(otherPassword, charArray);
    }

    @Test
    void getGuesses() {
        char[] password = {'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        PasswordCritic passwordCritic = new PasswordCritic(password);
        assertTrue(passwordCritic.getGuesses() > 0.0);
    }

    @Test
    void getScore() {
        char[] password = {'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        PasswordCritic passwordCritic = new PasswordCritic(password);
        int score = passwordCritic.getScore();
        assertTrue(score <= 4 && score >= 0);
    }

    @Test
    void getCalcTime() {
        char[] password = {'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        PasswordCritic passwordCritic = new PasswordCritic(password);
        assertTrue(passwordCritic.getCalcTime() > 0);
    }

    @Test
    void getCrackTimesEstimate() {
        char[] password = {'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        PasswordCritic passwordCritic = new PasswordCritic(password);
        assertEquals(
            passwordCritic.getCrackTimesEstimate(),
            """
                Online attack on a rate-limited service (~100 guesses/hour): 2 minutes
                Online attack on a service with absent or bypassed rate limiting (~10 guesses/second): less than a second
                Offline attack using a slow hash function and many cores (~10K guesses/second): less than a second
                Offline attack using a fast hash function and many cores (~10B guesses/second): less than a second"""
        );
    }

    @Test
    void getFeedback() {
        char[] password = {'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        PasswordCritic passwordCritic = new PasswordCritic(password);
        assertEquals(
            passwordCritic.getFeedback(),
            """
                Warnings: This is a top-10 common password.

                Suggestions: Add another word or two. Uncommon words are better."""
        );
    }

    @Test
    void search() {
        char[] password = {'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        PasswordCritic passwordCritic = new PasswordCritic(password);
        assertTrue(passwordCritic.search() >= 0);
    }

    @Test
    void wipe() {
        char[] password = {'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        PasswordCritic passwordCritic = new PasswordCritic(password);
        passwordCritic.wipe();
        assertTrue(String.valueOf(passwordCritic.getPassword()).isEmpty());
    }
}

A  => settings.gradle +2 -0
@@ 1,2 @@
rootProject.name = 'password-critic'
include 'lib'